Repository navigation
fix(messages): keep a playing row mounted in the channel timeline - #2695
Merged
Merged
Conversation
Voice notes, audio files and inline videos stopped and restarted from 0:00 when their row left the channel timeline's virtualizer buffer, unless focus happened to be inside that row. MessageRow now tracks whether any <audio> or <video> in its attachment list is playing and feeds that into the existing keepAlive pin, so the row holds its keepMounted pin while media plays and releases it on pause or ended. Media events don't bubble in the DOM, but React passes them up its tree, so the list's handlers also see plugin players such as the voice-notes card. The flag is recomputed from the list on each event, so pausing one player while another plays keeps the pin. ChannelTimeline.report.test.tsx covers a plain audio file, a voice note and an inline video, plus a row with two players. All four fail without the MessageRow change. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: mtoohey <[email protected]>
…ently The playback pin from the previous commit cleared only when React delivered a pause or ended event from a player still in the attachment list. Some paths never deliver one, so the row stayed in keptIndices for the rest of the visit. A player is now counted as playing only while it is not paused and has no error. The list also recomputes on emptied, which load(), media read recovery and src swaps fire instead of pause. It also recomputes on error, which reaches the list before AudioAttachment or MediaAttachment swaps in its unavailable state. React drops the pause a removed player fires. A player is removed without one when it is edited out, claimed by a message renderer, swapped by its plugin's ContributionBoundary, or unmounted with the whole list once no attachment groups remain. While a player plays, a MutationObserver on the row rechecks the list whenever the row's tree changes, and it disconnects when playback stops. It watches the row, not the list, so the list unmounting counts as a change. A recheck after MessageRow commits would miss boundary swaps, which re-render only the boundary. The comment now says that only players inside the list's DOM count, so a plugin player portalled elsewhere never pins the row. ChannelTimeline.report.test.tsx adds three tests: a playing a.mp3 that fails, a playing player edited out, and one claimed by a message renderer, which empties the list. All three fail on f2889a1, with the row still pinned after eviction. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: mtoohey <[email protected]>
…unmounts The observer from the previous commit watched the row div, so it missed that div or an ancestor being removed while MessageRowContent stayed mounted. An agents-audience row renders the message inside Collapsible.Panel, which Base UI 1.9 unmounts on close by default. Collapsing a row while its audio played detached the player without a mutation record inside the row, and React dropped the browser's pause, so the row stayed pinned for the rest of the visit. The effect also reran only when mediaPlaying changed, so after the row reopened it kept watching the detached div. A stable callback ref on the attachment list now owns the observer. It watches the list's subtree, rechecks only while a player plays, and its cleanup disconnects and clears the flag. React 19 runs ref cleanups whenever the node is deleted, whichever ancestor is removed, so that covers a claim of the last attachment group as well as the fold. The callback also keeps the ref the media handlers read in sync. The review also named MessageActionBar's wrapper swap. That can't happen today: no production code has provided ConversationPresentation since 89bc099, so it is always true. The ref cleanup would cover it too. Test changes in ChannelTimeline.report.test.tsx: - A new test plays audio in an agents row, collapses it and evicts it. It fails on the previous commit. - The error test now uses a voice note with a decode error. Recovery retries only network errors, and the card keeps its failed <audio> mounted, so only onError and the error check can release the row. - The playback it.each gains a voice note stopped by emptied. - That it.each now flushes microtasks after play. The list observer is attached for the list's whole life, so records from the play's own re-render otherwise arrived after the test set paused, and released the row whatever the stop event was. Each of these fails when its code is removed: onError with the error check fails the error test, onEmptied fails the emptied case, and dropping setMediaPlaying(false) from the cleanup fails the agents and claim tests. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: mtoohey <[email protected]>
trackMedia read the attachment list through a ref that observeAttachments set on attach and cleared in its cleanup. A React media event's currentTarget is already that list div, including events that reach it from a portalled player. trackMedia now takes the event and rechecks event.currentTarget, so the attachmentList ref and the two lines that kept it in sync are gone. playingIn always gets a list now, so it takes a non-null Element. No behaviour change. The ref was set before the list could receive events and cleared only once it was detached, so it always matched currentTarget when a handler ran. No test changes. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: mtoohey <[email protected]>
kazemek21
approved these changes
Nov 6, 2026
kazemek21
left a comment
Contributor
There was a problem hiding this comment.
Kyle reviewed this PR and the Nexus review-tracker notes and approved with no comments.
Posted by Codex (gpt-6.1-sol) on behalf of kylez
tlongwell-block
added a commit
that referenced
this pull request
Nov 6, 2026
* eva/huddle-audio-fixes: (29 commits) fix(huddle): keep calls flowing under stalled sends and multiple devices fix(channels): wrap unread conversation stepping around the sidebar (#2682) fix(navigation): let bare buzz:// open Buzz without a broken-link notice (#2718) fix(inbox): open DM thread rows as the thread (#2587) test(channels): complete the context navigation fixture (#2783) fix(channels): open a thread's channel at its root message from the #channel link (#2377) fix(people): find people by a pasted npub or hex key (#2396) test(bestie): open the narrow long-history thread with clickRow (#2723) Account-first onboarding and sign-in recovery (#2579) fix(agents): scroll the avatar dock with a mouse wheel (#2567) fix(avatars): keep Done in view when the avatar popup is short (#2706) feat(messages): highlight reactions you've made (#2709) feat(usage): move Usage from the sidebar into Settings (#2742) fix(messages): keep a playing row mounted in the channel timeline (#2695) Fix plugin status declaration closure for author consumers (#2643) fix(navigation): keep empty new chats out of Back history (#1617) [buzz-app] Recover remote agent message outcomes (#2542) feat(messages): name background send destinations and attach the files that fit (#2406) feat(drafts): clear a draft from its row (#2725) Authorize external application identities in Buzz (#2685) ... Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: Tyler Longwell <[email protected]> Signed-off-by: Tyler Longwell <[email protected]> # Conflicts: # docs/huddles.md # src-tauri/src/relay/huddle/audio.rs
wpfleger96
added a commit
that referenced
this pull request
Nov 6, 2026
…leanup * origin/main: (84 commits) Restore macOS Cmd-W window closing when no tab handles it (#2443) Make agent transcripts quieter and link finished replies to activity (#2424) fix(channels): wrap unread conversation stepping around the sidebar (#2682) fix(navigation): let bare buzz:// open Buzz without a broken-link notice (#2718) fix(inbox): open DM thread rows as the thread (#2587) test(channels): complete the context navigation fixture (#2783) fix(channels): open a thread's channel at its root message from the #channel link (#2377) fix(people): find people by a pasted npub or hex key (#2396) test(bestie): open the narrow long-history thread with clickRow (#2723) Account-first onboarding and sign-in recovery (#2579) fix(agents): scroll the avatar dock with a mouse wheel (#2567) fix(avatars): keep Done in view when the avatar popup is short (#2706) feat(messages): highlight reactions you've made (#2709) feat(usage): move Usage from the sidebar into Settings (#2742) fix(messages): keep a playing row mounted in the channel timeline (#2695) Fix plugin status declaration closure for author consumers (#2643) fix(navigation): keep empty new chats out of Back history (#1617) [buzz-app] Recover remote agent message outcomes (#2542) feat(messages): name background send destinations and attach the files that fit (#2406) feat(drafts): clear a draft from its row (#2725) ... # Conflicts: # src/bundled/builderlab/adapter-type/Activity.test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Voice notes, audio files and inline videos stopped and restarted from 0:00 when their row left the channel timeline's virtualizer buffer (about 1,600 px), unless focus happened to be inside that row. In the Mac app a pointer click doesn't focus the Play button, so this likely affected every play started with the trackpad.
MessageRownow tracks whether any<audio>or<video>in its attachment list is playing and feeds that into the existingkeepAlive→keepMountedpin, the one the report dialog and action menus already use. The row stays mounted while media plays and is released when it pauses, ends, errors, or leaves the row. Media events don't bubble in the DOM, but React passes them up its tree, so the list's handlers also see plugin players such as the voice-notes card. No plugin API change.Before and after
Left: main. Right: this branch. Play a 2-minute voice note, click the composer so focus leaves the row, scroll up 3,600 px, then scroll back. On main the audio stops and the note is back at 0:00; on the branch it keeps playing and is further along on return. Pausing then releases the row as before.
voice-note-sbs.mp4
Details
!paused && !error.emptied(fromload(), read recovery andsrcswaps) anderroralso trigger a recheck.MutationObserveron the attachment list rechecks when a player leaves the list (edited out, claimed by a message renderer, swapped by a plugin boundary). The callback ref's cleanup clears the flag when the list itself unmounts, e.g. an agents-audience row collapsing.Tests
ChannelTimeline.report.test.tsxgains 9 tests: plain audio, voice note, voice note reloaded viaemptied, inline video, two players in one row, a voice note with a decode error, a player edited out, a player claimed by a renderer, and an agents row collapsing mid-play. Each fails with the code it covers removed.MessageRow,AudioAttachment,VoiceNoteCard,MediaAttachment.videoandChannelTimelinesuites pass unchanged;tscandbiomeare clean. Rebased onto currentmain(#2590).🤖 Generated with Claude Code