Skip to content

fix(desktop): channel topic and membership metadata cleanup - #3642

Merged
delkc merged 4 commits into
mainfrom
claydelk/system-message-copy
Jul 30, 2026
Merged

fix(desktop): channel topic and membership metadata cleanup#3642
delkc merged 4 commits into
mainfrom
claydelk/system-message-copy

Conversation

@delkc

@delkc delkc commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

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_changed event carrying an empty string — there's no separate "cleared" event type. So the timeline printed:

Alice
changed the topic to “”

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. title and action render 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

  • Blank, missing, or whitespace-only topic/purpose now reads "cleared the channel topic" / "cleared the channel purpose".
  • Membership captions drop "was": "added by Alice Chen", matching "joined the channel" and "left the channel". Rationale below.
  • Self-references inside a caption are lowercase: "added by you", "removed you from the channel", "along with you". The row title still says "You" — that's a name standing on its own next to the avatar, same as any other person's.
  • The wording moves to lib/systemEventCopy.ts as pure functions, so it's assertable in a unit test instead of only reachable through the DOM. That also removes two JSX fragments from SystemMessageRow.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_added and member_joined where 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_removed exactly 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.

  1. mentions.spec.ts:1245 asserted a button was un-underlined while the mouse was still parked from a previous hover(). 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.
  2. mentions.spec.ts:1253 used a bare role=tooltip lookup. 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

  • Timestamps. The day divider, per-message clock times, the Inbox thread pane, and the inbox list have three divergent date implementations and none fully match the writing standard's Today/Yesterday/weekday/date progression. That's its own slice of Desktop content polish (tracking) #2216.
  • Whose avatar and name lead the row. See the active-voice discussion above — it's a design question, not copy.
  • "Added by You" in the persona catalog. PersonaAddedBy.tsx has the same capitalization mismatch, but it's the agents feature rather than channel system messages.
  • the channel vs this 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
  • Unit: 3784/3784, including 6 tests covering set/blank/undefined/null/whitespace for both fields, a guard that no variant can emit empty quotes, and 3 for the inline-name rule (including that "Youssef" and "You Know Who" are left alone)
  • CI green on all 20 checks, including all 4 Smoke E2E shards and both Desktop E2E Integration shards
  • The previously fragile assertions run locally with --repeat-each=5: 5/5

I 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.

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>
@delkc
delkc requested a review from a team as a code owner July 29, 2026 23:19
@delkc delkc mentioned this pull request Jul 30, 2026
6 tasks
@delkc

delkc commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

This also fixes that You was added bug.
Screenshot 2026-07-23 at 9 34 54 AM

@delkc delkc changed the title fix(desktop): say when a channel topic or purpose is cleared fix(desktop): channel topic and membership metadata cleanup Jul 30, 2026
@delkc
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>
@tellaho

tellaho commented Jul 30, 2026

Copy link
Copy Markdown
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, toInlineName(label === "You") would also lowercase another user whose display name is literally You. Comparing the relevant pubkey with currentPubkey using the existing normalization pattern would keep the behavior precise; the pure helper could take an isSelf boolean if useful.

Non-blocking follow-ups

  • Mobile still shows the empty-topic and was added by variants, so tracking desktop/mobile copy parity on Desktop content polish (tracking) #2216 would be worthwhile.
  • The copy varies between topic / channel topic and the channel / this channel; those nouns could be normalized in a follow-up.
  • An earlier E2E assertion still uses an unscoped getByRole("tooltip"). It is safe with the current flow, but may be worth tightening if that test gains another hover.

With the identity-based self-label fix, this looks good to me.

delkc and others added 2 commits July 30, 2026 13:15
…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>
@delkc
delkc merged commit 9e8fcfd into main Jul 30, 2026
68 of 72 checks passed
@delkc
delkc deleted the claydelk/system-message-copy branch July 30, 2026 22:53
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants