Repository navigation
Expire a peer's typing state when its stop signal never arrives #342
Copy link
Copy link
Open
Labels
ChatRelated to chat featuresRelated to chat featuresbugSomething isn't workingSomething isn't working
Description
Parent
#328.
What to build
The typing bounce on a face has no keepalive and no expiry. If the stop signal is lost, the bounce stays. It clears only when another presence event rewrites that person, and nothing bounds that time. The emoji and voice activity chips already solve this with a resend and an expiry. Typing should follow the same rule, so a stale bounce clears within seconds.
Acceptance criteria
- While a person keeps typing, their client resends the typing start at most once every 3 s.
- A peer drops the typing bounce 8 s after the last typing start it received, if no stop arrived.
- The expiry never overwrites a newer state. A face that went OFFLINE or ONLINE in between stays as it is.
- A normal stop still clears the bounce at once, as today: 1 s after the last key, or on send.
- The typing bounce still hands over to an emoji or voice chip as today.
- The existing sender test file has one new case for the resend, and it passes.
- The chatroom
CLAUDE.mddescribes the typing resend and expiry next to the "Activity chips" bullet.
Blocked by
None — can start now.
Agent brief
Type: AFK — an agent can finish this alone.
Category: bug
Current behavior:
- The sender sends
startTypingonce per burst, guarded byhasStartedTyping(apps/webapp/src/components/chatroom/components/MessageComposer/helpers/handleTypingIndicator.ts:50-58). - It sends
stopTyping1 s after the last key (:45-48), or at once on send (:61-64). sendTypingIndicatorignores the send result (:35-42). It also sends nothing whenbroadcasterorprofileis missing (:25-27). A lost stop is never resent.- The peer sets
TYPINGonstartTypingandONLINEonstopTyping, with no timer (apps/webapp/src/hooks/useBroadcastListener.ts:35-38). - Activity chips differ. The sender resends every 3 s (
handleTypingIndicator.ts:15,:87-89). The peer expires the chip 8 s after the last start (useBroadcastListener.ts:6,:39-50). - Presence events rewrite the status. A presence
syncsets every tracked face toONLINE(apps/webapp/src/services/workspacePresenceSync.ts:78-89,:255-257). AjoinsetsONLINE(:258-266) and aleavesetsOFFLINE(:267-271). AvatarStackshows the bounce whenstatus === 'TYPING'(apps/webapp/src/components/ui/AvatarStack.tsx:74).- Evidence: code-traced on 2026-09-28. Not reproduced in a browser.
Desired behavior:
- Sender: in the
StartTypingbranch, whenhasStartedTypingis true, resendstartTypingif 3 s have passed since the last start. Keep the time of the last start in module state. Reset it whereverhasStartedTypinggoes back to false. No interval is needed: typing ends 1 s after the last key. - Peer: keep a separate expiry map for typing. Do not reuse
activityExpiries. OnstartTyping, setTYPINGand arm an 8 s timer. OnstopTyping, clear the timer. When the timer fires, read the face fromuseStore.getState().usersPresence. SetONLINEonly if itsstatusis stillTYPING. Clear the map in the effect cleanup. - A presence
syncstill resets the face toONLINE. The next resend brings the bounce back within 3 s. - A client from before this change never resends. Its bounce drops after 8 s of steady typing, then returns on its next burst. Accept this.
- Use the same 3 s and 8 s values as the activity chips. 8 s stays above two resends, so one lost resend keeps the bounce.
Where to start:
handleTypingIndicator.ts:handleTypingIndicator,debouncedStopTypingIndicator,startComposerActivity.useBroadcastListener.ts: thetypingIndicatorhandler and the effect cleanup.handleTypingIndicator.test.tsbeside the sender: it already uses fake timers and a fresh module per test.
Line numbers are hints as of 2026-09-28; the agent searches by symbol.
Rules that apply:
apps/webapp/src/components/chatroom/CLAUDE.md§MessageComposer, the "Activity chips" bullet.- AGENTS.md §Test Policy, case (c): this is a timing rule that is hard to check by hand. Add one case to the existing sender test file. Add no test for the peer.
- AGENTS.md §Documentation And Comments: comments are 4 lines at most, and say why.
- The
CLAUDE.mdedit is prose. Write it in Simplified English:.cursor/skills/tech-writer/SKILL.md§Simplified English (house standard).
Verify:
bun run --filter @docs.plus/webapp test handleTypingIndicatorpasses.- Prove it by sabotage. Remove the resend, and the new case must fail. Put the resend back.
bun run --filter @docs.plus/webapp typecheckpasses.bun run lintandbun run check:agent-docspass.- Open one pad in two browsers, signed in as two members, with the same heading chat open. Check light and dark, desktop and phone.
- Type for 10 s without a pause in browser A. Browser B keeps the bounce the whole time, in the chat participants stack and on the TOC row.
- Stop typing in A. B clears the bounce about 1 s later.
- Open DevTools in A. In its console, run
setTimeout(() => { debugger }, 2000). Type without a break until the pause hits. Keep A paused. The 1 s stop is now frozen. - B drops the bounce about 8 s later. Before this change, B keeps it well past 8 s, until A's socket times out and B gets a leave (unverified).
Out of scope
- Any change to the activity chips, their timings, or
Profile.activity. - Presence tracking, the heartbeat, and the
statuscolumn in Postgres. - The look of the typing bounce.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
ChatRelated to chat featuresRelated to chat featuresbugSomething isn't workingSomething isn't working
Activity