Skip to content

Make hold-to-record respond to release, lock, cancel, and the 5-minute cap #265

Description

@HMarzban

Problem

On a phone, a voice note is recorded by holding the mic button. The gesture handlers read a stale copy of the recorder state, so most of the gesture does nothing.

ComposerPrimaryAction binds window pointermove, pointerup, and pointercancel listeners once, at press time (apps/webapp/src/components/chatroom/components/MessageComposer/components/Actions/ActionButtons/ComposerPrimaryAction.tsx:23-42, :44-53). The listeners keep the voice object from that render, when phase was 'idle'. Nothing binds the listeners again when voice changes. listeningRef only blocks a second bind while a hold is active.

useVoiceRecorder guards its handlers on the phase of the render that created them (apps/webapp/src/components/chatroom/components/MessageComposer/hooks/useVoiceRecorder.ts):

  • moveHold returns early unless phase === 'recording' (:253).
  • endHold does the same (:275).
  • stopRecording does the same (:123).
  • startHold arms the 5-minute cap with the stopRecording of its own render (:226).

Measured on 14ab7f9c1 with the real useVoiceRecorder and React 19.2.8. The run used happy-dom. It faked getUserMedia, MediaRecorder, AudioContext, requestAnimationFrame, and the object-URL calls. It caught the 5-minute cap callback by patching window.setTimeout, then fired it by hand. The harness calls the handlers that the window listeners hold, from the press-time render.

Step Result
Press: startHold phase becomes recording
Slide up 200 px: moveHold (lock needs 80) isLocked stays false
Slide left 200 px: moveHold (cancel needs 80) isCancelArmed stays false
Release: endHold phase stays recording
The 5-minute cap fires phase stays recording
Control: endHold from the latest render phase becomes preview
Desktop: startLockedFromMenu, then the 5-minute cap fires phase stays recording

What a phone user sees:

  • The recording keeps running after release. No preview appears.
  • The input row stops responding. While an unlocked recording runs, the row gets pointer-events-none (apps/webapp/src/components/chatroom/components/MessageComposer/components/layouts/ComposerBar.tsx:63, :103). Lock never arms, so the row stays blocked until the recording ends.
  • The bar shows only Cancel, because Stop appears only when locked (apps/webapp/src/components/chatroom/components/MessageComposer/components/VoiceRecordingBar.tsx:98-110).
  • The hints "slide up to lock" and "slide left to cancel" are hidden below the sm breakpoint (VoiceRecordingBar.tsx:91). On a phone in portrait, the user never sees them.

On desktop, the mic button and Record voice in the insert menu both call startLockedFromMenu. The Stop button works there, because the bar reads the latest stopRecording. Only the 5-minute cap is broken on desktop.

On a quick tap, or at the microphone prompt, the finger can lift before getUserMedia resolves (useVoiceRecorder.ts:172). A second harness run released during that wait, and even endHold from the latest render returned early. When getUserMedia resolved, the recording started, unlocked, with no finger down.

Steps to reproduce

  1. Sign in on a phone. Open a channel with an empty composer.
  2. Press and hold the mic. Allow the microphone.
  3. Release.
  4. See the recording continue. No preview appears, and the input row does not respond.
  5. Tap Cancel to stop it. The note is lost.

Acceptance criteria

  • Releasing the mic on a phone stops the recording and shows the preview.
  • If the finger lifts before the recording starts, no recording runs and no preview appears.
  • Sliding left past the cancel distance, then releasing, discards the recording.
  • Sliding up past the lock distance locks the recording. Release does not stop it, and Stop appears.
  • A recording stops by itself at 5 minutes, on phone and on desktop.
  • While holding on a phone, the recording bar shows the lock hint and the cancel hint.
  • On desktop, the mic button and Record voice in the insert menu still start a locked recording, and Stop still works.

Agent Brief

Category: bug
Summary: Hold-to-record handlers and the 5-minute cap must act on the live recorder state, not the state of the press-time render.

Current behavior:
Window listeners bound at press time call recorder handlers that captured phase === 'idle'. Release, slide-to-lock, slide-to-cancel, and the 5-minute cap all return early. A phone recording never reaches the preview. Only a discard ends it, such as Cancel or leaving the chat.

Desired behavior:
Every gesture handler and the 5-minute cap act on the current phase. Release stops the recording unless it is locked or cancel-armed. A release before the recording starts leaves no recording and no preview. The cap stops any recording at 5 minutes. The lock and cancel hints are visible on phones.

Key interfaces:

  • useVoiceRecorder() — startHold, moveHold, endHold, stopRecording, startLockedFromMenu; isLockedRef and isCancelArmedRef already show the ref pattern.
  • UseVoiceRecorderReturn — the object that ComposerPrimaryAction keeps in its window listeners.
  • VoiceRecordingBar — shows Stop only when locked, and hides the gesture hints on small screens.

Out of scope

Notes

A working pattern is already in the tree. ComposerBar reads stopAndCleanup through a ref, so a stale closure cannot run (apps/webapp/src/components/chatroom/components/MessageComposer/components/layouts/ComposerBar.tsx:47-48).

The handlers came in with 3e08d6a71 (2026-07-01).

Final sign-off needs a real phone, in iOS Safari and Android Chrome.

Activity

  1. added a commit that references this issue on Sep 22, 2026
    a939c0a
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    ChatRelated to chat featuresDesktopMobilebugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions