fix(desktop): channel topic and membership metadata cleanup - #3642
Merged
Conversation
Clearing a channel topic arrives as a topic_changed event carrying an empty string, not as its own event type, so the timeline rendered `changed the topic to ""` — which reads as if the topic were set to two quote marks. Same for purpose. Both now read "cleared the channel topic" / "cleared the channel purpose", and whitespace-only values count as cleared for the same reason. The wording moves to lib/systemEventCopy.ts as a pure function so it can be asserted directly; that also drops two JSX fragments from SystemMessageRow, which shrinks 911 -> 900 lines. Membership captions lose "was": the caption renders on its own line under the member's name, so "added by Alice" reads as the metadata line it is and matches its siblings "joined the channel" and "left the channel". Two E2E assertions in mentions.spec.ts were measuring the wrong thing and this copy change exposed both: - The un-hovered style check ran with the mouse still parked from the previous hover. Any reflow — new rows, scroll-to-bottom, a different text wrap — could slide the button under that fixed pointer, so it asserted where the mouse happened to be rather than the resting style. It now parks the pointer off-target first. - A bare role=tooltip lookup matched two elements once the first tooltip began animating out while the second opened. It now scopes to the open tooltip. Part of #2216. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Clay Delk <clay.delk@gmail.com>
6 tasks
Contributor
Author
delkc
enabled auto-merge (squash)
July 30, 2026 13:18
An agent's metadata reads "managed by you" while the activity line right under it read "added by You" — the same word, two capitalizations, on the same row. `resolveUserLabel` returns "You", which is right where it stands alone as a name (the row title) and wrong once it lands mid-phrase. `toInlineName` adjusts it for the sentence positions: "added by you", "removed you from the channel", "along with you". Every other name is a proper noun and is returned untouched, matched exactly so "Youssef" is unaffected. This is the rule `formatOwnerLabel` already applies from the other side — it returns lowercase because it is only ever read as "managed by you". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Clay Delk <clay.delk@gmail.com>
Contributor
|
🤖 Nice, focused improvement overall. The pure copy extraction is a good shape, the empty-topic guard is well tested, and the two E2E adjustments address the underlying tooltip flake mechanics cleanly. One blocking fix Could we make the inline self-label depend on identity rather than the resolved display string? As written, Non-blocking follow-ups
With the identity-based self-label fix, this looks good to me. |
…string Review catch on #3642: `toInlineName` compared the resolved label against "You", so a different person whose display name is literally "You" would have been rewritten as if they were the reader. The label is user-controlled; identity is not. The helper now takes an `isSelf` boolean the caller derives by comparing pubkeys through the existing `normalizePubkey` path. Also normalizes the noun: "cleared the channel topic" against "changed the topic to …" used two different nouns for the same field. Both now say "the topic" / "the purpose". This row only renders in a channel timeline, under that channel's own header, so naming the channel again is redundant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Clay Delk <clay.delk@gmail.com>
Two of the three desktop fixes in this PR apply to the Flutter timeline. **Cleared topic rendered as empty quotes.** `SystemEvent.describe` printed `changed the topic to ""` when the relay reported a clear, same as desktop did — a clear arrives as a `topic_changed` event carrying an empty string, not as its own event type. Now reads "cleared the topic" / "cleared the purpose", and the value is trimmed before it goes inside the quotes. **"was added by" in the two-line membership layout.** Every `member_joined` event renders through `_MembershipSystemMessageContent`, which puts the member's name and timestamp on one line via `MessageAuthorMeta` and the caption beneath — the same structure as desktop's `SystemMessageRow`. So the caption drops "was" for the same reason. `SystemEvent.describe` keeps "$target was added by $actor". That path builds subject and predicate into a single string, so dropping "was" there would read "Bob added by Alice". It is also only a fallback: the membership layout handles every well-formed member_joined event. Left alone: mobile's date formatters have the same divergence desktop just resolved (#3769) — `formatDayHeading` has no weekday band and always prints the year, and `formatThreadSummaryLastReplyTime` still emits ordinals like "on May 19th". Tracked separately rather than folded into a copy change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Clay Delk <clay.delk@gmail.com>
tellaho
approved these changes
Jul 30, 2026
brow
pushed a commit
that referenced
this pull request
Jul 31, 2026
* origin/main: (70 commits) fix(catalog): update Amp tagline (#3806) fix(desktop): channel topic and membership metadata cleanup (#3642) fix(desktop): align data deletion labels (#2230) fix(relay): align NIP-11 max_limit with REQ ceiling (#3635) fix(desktop): allow linux-only media items as dead code off-linux (#3811) fix(desktop): report authenticated relay recovery (#3812) fix(desktop): don't gate hover affordances on the hover media query (#3657) feat(relay): gate kind 30178 team-catalog reads behind the shared tag (#3358) test(desktop): click visible thread collapse guide (#3800) feat(desktop): raise the install ceiling and make installs observable (#3368) fix(db): isolate usage metrics advisory-lock test on scratch DB (#3670) Add Devin as a preset ACP harness (#3225) feat(desktop): improve agent activity header ui (#3321) perf(presence): reduce heartbeat frequency (#3783) Tighten continuation message rows (#3724) Fix video reviews in thread replies (#3719) feat(release): make desktop releases immutable (#3568) Make relay reconnect backoff authoritative (#3774) feat(desktop): add password-protected backups in settings (#3701) fix(desktop): reuse profiles when joining communities (#2155) ... Signed-off-by: npub15w828kxsxu2684ynste0uah2jwkgatd99flt7ds4523hzm8ju6cshdr8hh <a38ea3d8d03715a3d49382f2fe76ea93ac8eada52a7ebf3615a2a3716cf2e6b1@buzz.block.builderlab.xyz>
wpfleger96
pushed a commit
that referenced
this pull request
Jul 31, 2026
…evert-fix * origin/main: fix(desktop): open profiles from avatars (#3751) refactor(voice): extract reusable Pocket primitives + Pocket voice settings (relands #2467 + #3208) (#3910) docs: add VISION_REMOTE_AGENTS.md (#3924) feat(desktop): auto-enable huddle transcription for agents (#3180) feat(agent): optional reply guard reminds a silent turn to publish (#3763) feat(desktop): upgrade Pocket TTS model (#3266) feat(desktop): delete a message by clearing its edit to empty (#3813) feat(relay): raise hosted community limit to five (#3829) feat(desktop): locally stored NIP-49 encrypted key backup (#2937) fix(catalog): update Amp tagline (#3806) fix(desktop): channel topic and membership metadata cleanup (#3642) fix(desktop): align data deletion labels (#2230) fix(relay): align NIP-11 max_limit with REQ ceiling (#3635) Signed-off-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
wpfleger96
pushed a commit
that referenced
this pull request
Jul 31, 2026
…chive * origin/main: (25 commits) feat(desktop): import local Pocket voices (#3259) fix(desktop): open profiles from avatars (#3751) refactor(voice): extract reusable Pocket primitives + Pocket voice settings (relands #2467 + #3208) (#3910) docs: add VISION_REMOTE_AGENTS.md (#3924) feat(desktop): auto-enable huddle transcription for agents (#3180) feat(agent): optional reply guard reminds a silent turn to publish (#3763) feat(desktop): upgrade Pocket TTS model (#3266) feat(desktop): delete a message by clearing its edit to empty (#3813) feat(relay): raise hosted community limit to five (#3829) feat(desktop): locally stored NIP-49 encrypted key backup (#2937) fix(catalog): update Amp tagline (#3806) fix(desktop): channel topic and membership metadata cleanup (#3642) fix(desktop): align data deletion labels (#2230) fix(relay): align NIP-11 max_limit with REQ ceiling (#3635) fix(desktop): allow linux-only media items as dead code off-linux (#3811) fix(desktop): report authenticated relay recovery (#3812) fix(desktop): don't gate hover affordances on the hover media query (#3657) feat(relay): gate kind 30178 team-catalog reads behind the shared tag (#3358) test(desktop): click visible thread collapse guide (#3800) feat(desktop): raise the install ceiling and make installs observable (#3368) ... Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> # Conflicts: # desktop/src/testing/e2eBridge.ts # desktop/tests/helpers/bridge.ts
wpfleger96
pushed a commit
that referenced
this pull request
Jul 31, 2026
…chive * origin/main: (25 commits) feat(desktop): import local Pocket voices (#3259) fix(desktop): open profiles from avatars (#3751) refactor(voice): extract reusable Pocket primitives + Pocket voice settings (relands #2467 + #3208) (#3910) docs: add VISION_REMOTE_AGENTS.md (#3924) feat(desktop): auto-enable huddle transcription for agents (#3180) feat(agent): optional reply guard reminds a silent turn to publish (#3763) feat(desktop): upgrade Pocket TTS model (#3266) feat(desktop): delete a message by clearing its edit to empty (#3813) feat(relay): raise hosted community limit to five (#3829) feat(desktop): locally stored NIP-49 encrypted key backup (#2937) fix(catalog): update Amp tagline (#3806) fix(desktop): channel topic and membership metadata cleanup (#3642) fix(desktop): align data deletion labels (#2230) fix(relay): align NIP-11 max_limit with REQ ceiling (#3635) fix(desktop): allow linux-only media items as dead code off-linux (#3811) fix(desktop): report authenticated relay recovery (#3812) fix(desktop): don't gate hover affordances on the hover media query (#3657) feat(relay): gate kind 30178 team-catalog reads behind the shared tag (#3358) test(desktop): click visible thread collapse guide (#3800) feat(desktop): raise the install ceiling and make installs observable (#3368) ... Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> # Conflicts: # desktop/src/testing/e2eBridge.ts # desktop/tests/helpers/bridge.ts
joahg
added a commit
to joahg/buzz-dev-mode
that referenced
this pull request
Jul 31, 2026
…-style * origin/main: (22 commits) feat(desktop): import local Pocket voices (block#3259) fix(desktop): open profiles from avatars (block#3751) refactor(voice): extract reusable Pocket primitives + Pocket voice settings (relands block#2467 + block#3208) (block#3910) docs: add VISION_REMOTE_AGENTS.md (block#3924) feat(desktop): auto-enable huddle transcription for agents (block#3180) feat(agent): optional reply guard reminds a silent turn to publish (block#3763) feat(desktop): upgrade Pocket TTS model (block#3266) feat(desktop): delete a message by clearing its edit to empty (block#3813) feat(relay): raise hosted community limit to five (block#3829) feat(desktop): locally stored NIP-49 encrypted key backup (block#2937) fix(catalog): update Amp tagline (block#3806) fix(desktop): channel topic and membership metadata cleanup (block#3642) fix(desktop): align data deletion labels (block#2230) fix(relay): align NIP-11 max_limit with REQ ceiling (block#3635) fix(desktop): allow linux-only media items as dead code off-linux (block#3811) fix(desktop): report authenticated relay recovery (block#3812) fix(desktop): don't gate hover affordances on the hover media query (block#3657) feat(relay): gate kind 30178 team-catalog reads behind the shared tag (block#3358) test(desktop): click visible thread collapse guide (block#3800) feat(desktop): raise the install ceiling and make installs observable (block#3368) ... Amp-Thread-ID: https://ampcode.com/threads/T-019fb8e1-6ece-72a7-8808-9b12e0f7e833 Co-authored-by: Amp <amp@ampcode.com> Signed-off-by: Joah Gerstenberg <joah@squareup.com> # Conflicts: # desktop/src-tauri/src/linux_media.rs # desktop/src/app/AppShell.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.

First slice of #2216, scoped to the system/status lines in the chat timeline.
Why
Three problems on the same surface.
Clearing a channel topic renders as empty quotes. The relay reports a clear as a
topic_changedevent carrying an empty string — there's no separate "cleared" event type. So the timeline printed:which reads as if the topic were set to two quote marks. Same for purpose.
The membership caption reads like a headline, not a metadata line.
titleandactionrender on separate lines — the member's name sits in the header row with the avatar and timestamp, and the caption sits beneath it. So the caption was "was added by Alice Chen" standing alone under a name, while its siblings on that same line are "joined the channel" and "left the channel"."You" was capitalized two different ways on one row. An agent's metadata reads "managed by you" while the activity line directly under it read "added by You" — the same word, two capitalizations, a few pixels apart.
What
lib/systemEventCopy.tsas pure functions, so it's assertable in a unit test instead of only reachable through the DOM. That also removes two JSX fragments fromSystemMessageRow.tsx.Why "was" is gone rather than dynamic
Restoring "was" means agreeing with the subject, and the subject can be "You" — "You was added by Alice" is broken.
label === "You" ? "were" : "was"is a one-line fix, so this was a judgment call, not a cost one. Three reasons it went the other way:The verb would be agreeing with a subject on a different line. The row is
flex-col: the name sits in the header with the avatar, the timestamp, a middot, and sometimes a "managed by" chip; the caption is a separate<p>beneath it. "was added by Alice Chen" is a sentence fragment reaching back across a metadata row for its subject. The UI isn't presenting it as a sentence, so it shouldn't depend on being read as one.Agreement keys on user-controlled data, and fails loudly. The rule can only special-case the strings it knows. A display name like "Design Team" or any user-set plural reads wrong, and every future passive line ("blocked by", "invited by", "removed by") has to re-derive the rule. When capitalization is slightly off it's a polish nit; when agreement is off the user sees visibly broken grammar.
Consistent active voice isn't reachable without a design change. 10 of the 12 cases are already active. The 2 passive ones are exactly
members_addedandmember_joinedwhere actor ≠ target — and those rows are deliberately titled with the person who arrived, not the person who added them (it's why they get the dual avatar with the new member's face first). Self-joins in the same slot read active only because actor and target are the same person. So the passive voice is a consequence of the title choice, not a copy slip.Removing the passive voice properly means re-titling those rows with the actor and going active — "Alice added Marcia to the channel, along with Peter, Jordan, and 2 others." That matches
member_removedexactly and is what Slack does, but it changes whose avatar leads the row. Filed as a follow-up on #2216 rather than smuggled into a copy PR.Two E2E assertions this exposed
Both were measuring something other than what they claimed, and the copy change tipped them over. Neither is a product bug, but both would have failed the next person too.
mentions.spec.ts:1245asserted a button was un-underlined while the mouse was still parked from a previoushover(). Any reflow — new rows, scroll-to-bottom, a different text wrap — can slide that button under the stationary pointer, so the assertion measured where the mouse happened to be rather than the resting style. Dropping four characters changed the text wrap, changed the row height, changed the scroll offset, and the pointer landed on it. Now parks the pointer off-target first.mentions.spec.ts:1253used a barerole=tooltiplookup. Once the first tooltip animates out while the second opens, two elements match and strict mode trips. Now scopes to the open tooltip via:not([data-state="closed"]).Deliberately out of scope
PersonaAddedBy.tsxhas the same capitalization mismatch, but it's the agents feature rather than channel system messages.the channelvsthis channel. joined/left/removed say "the channel"; created/archived/unarchived say "this channel". Worth normalizing, but it touches lines this PR otherwise leaves alone.Validation
pnpm check,pnpm typecheck— clean--repeat-each=5: 5/5I could not produce a system row with the current user as actor in mock data, so the lowercase-"you" path is covered by unit tests and typecheck rather than a screenshot.