Skip to content

fix(direct-messages): show verified identity in new message recipient search - #2585

Merged
kalvinnchau merged 5 commits into
mainfrom
fix/dm-recipient-identity
Nov 5, 2026
Merged

kalvinnchau merged 5 commits into
mainfrom
fix/dm-recipient-identity

Conversation

@kalvinnchau

@kalvinnchau kalvinnchau commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

New message recipient search now names people the way Command-K does: by their community-verified (NIP-FI) identity when one exists, and by their profile name otherwise.

  • Searched rows show the verified name. The verified full name and @username appear as a second line. Rows without verified identity show only the profile name, with an npub suffix to tell namesakes apart. An editable handle is never presented as identity.
  • Search also finds people by verified display name, username or full name. This covers the shared verified profiles and people verified for an earlier page, matching Command-K. Space completion treats a verified person found under an editable name as that name's namesake.
  • Recipients keep the facts they were chosen with: pubkey and profile name. Chips, the composer label and avatars derive their names from the current verification. When verification is withdrawn, the label falls back to the current unverified profile, then the raw row seen in the search, then the key. A verified name is never stored as a fallback.
  • Relabeling never edits the recipient list, so send and upload errors and a prepared conversation survive a late verification answer.
  • Selection and routing remain keyed by pubkey.
  • Relays without verified identity (no authority and no verification read) keep plain profile-name search and labels.

Testing

New NewMessage.test.tsx cases (component, modeled session):

  1. Rows show verified identity and never an editable handle: namesakes get npub keys, a verified handle equal to the name still shows, Space stays ambiguous for a namesake, and sending opens the chosen pubkey.
  2. Verified people are found by verified name, handle or full name, from the shared profiles and from an earlier page.
  3. A recipient whose verification answers late is relabeled without clearing a failed send's error.
  4. After withdrawal, a chosen recipient falls back to the browsed profile name or the key, then follows a current unverified profile.
  5. Without verified identity (no authority, no read), search and labels use profile names, namesake keys and no handle.

vitest run src/features/direct-messages src/features/profiles src/bundled/channels src/app/shell, tsc and biome check pass. Not exercised against a real relay with or without verified identity.

kalvinnchau and others added 4 commits November 4, 2026 21:45
label directory rows by community-verified display name with the verified
full name and handle beneath, reusing useVerifiedNames/withVerifiedNames as
command-k and mentions do. find people the shared profiles verified by a
matching verified name, handle or full name. editable profile names stay
hidden match keys and block space completion for namesakes. unverified
rows keep profile names and npub namesake suffixes; selection stays keyed
by pubkey.

Co-authored-by: peon <9ac6794b000690b7e814eb1805ad32405d0bec7d52838de3a86cf967565dacc0@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Kalvin Chau <[email protected]>
…n recipients

match people this picker already discovered by their page-verified name,
handle or full name through verifiedName, not only the shared profiles.
verify chosen recipients independently of the directory and send the
verified label up so chips, header, composer label and saved view follow.
always show the verified handle, even when it equals the display name.

Co-authored-by: peon <9ac6794b000690b7e814eb1805ad32405d0bec7d52838de3a86cf967565dacc0@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Kalvin Chau <[email protected]>
recipients keep the facts they were chosen with. new message names them
from the current verification for the chips, composer label and avatars,
falling back to those facts when verification is withdrawn. the picker
hands up unnamed rows and remembers unnamed people, so a late relabel
never edits recipients (send errors and the prepared conversation stay)
and removed optional fields such as a full name do not linger.

Co-authored-by: peon <9ac6794b000690b7e814eb1805ad32405d0bec7d52838de3a86cf967565dacc0@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Kalvin Chau <[email protected]>
…t fallback

a person found through a shared verified head is stored with the profile
seen in this search, or their key, instead of the verified name the shared
projection put in place of the profile name. new message follows the
shared profiles, so an unverified current profile names a recipient once
verification is withdrawn. covers relays with and without verified
identity.

Co-authored-by: peon <9ac6794b000690b7e814eb1805ad32405d0bec7d52838de3a86cf967565dacc0@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Kalvin Chau <[email protected]>
@kalvinnchau
kalvinnchau marked this pull request as ready for review November 5, 2026 05:47
@kalvinnchau
kalvinnchau requested a review from a team as a code owner November 5, 2026 05:47
…viors

merge overlapping cases, share one verification fixture and keep the
send-and-route assertion in a single test.

Co-authored-by: peon <9ac6794b000690b7e814eb1805ad32405d0bec7d52838de3a86cf967565dacc0@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Kalvin Chau <[email protected]>

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Star Lord’s automated source review via Wes’s account, integrating independent Rocket, Mantis and Groot reviews.

No actionable findings at head d886a6c720ae1d1a9a3a8d169725997fce27fcb5 against base 2c3efec16b305b914d34230994c042c074306f26.

Reviewed verified-name search and authority/cache precedence, namesake handling, raw recipient storage and pubkey routing, withdrawal fallbacks, accessible labels, and separation of display updates from composer/send state. Also inspected the revised component tests and public PR description.

Source-only review: no tests, app, live relay or CI checks were run or verified. The current component cases do not directly assert full-name removal or successful retry after relabel; prepared-conversation and upload-state preservation remain source inference. Runtime, visual and screen-reader behavior remain unverified.

@kalvinnchau
kalvinnchau merged commit f06754e into main Nov 5, 2026
81 of 83 checks passed
damienrj added a commit to damienrj/buzz-app that referenced this pull request Nov 5, 2026
…ux-8fe4a6

* commit 'bac7a4fccad5fbf5cc31d949738b28264be1705a':
  feat(desktop): add Settings to the macOS app menu (block#2594)
  Preserve and renew external app approval (block#2464)
  fix(direct-messages): show verified identity in new message recipient search (block#2585)
  Offer plugin updates from the Buzz shell (block#2491)
  fix(messages): keep decoded images across virtualized remounts (block#2474)

# Conflicts:
#	src/app/PluginImport.tsx
wpfleger96 added a commit that referenced this pull request Nov 5, 2026
…-service

* origin/main:
  feat(settings): follow the system clock and let people hide message times (#2440)
  feat(plugins): repository access rules for Git imports (#2484)
  feat(desktop): add Settings to the macOS app menu (#2594)
  Preserve and renew external app approval (#2464)
  fix(direct-messages): show verified identity in new message recipient search (#2585)
  Offer plugin updates from the Buzz shell (#2491)

# Conflicts:
#	src-tauri/src/host_mcp.rs
wpfleger96 added a commit that referenced this pull request Nov 5, 2026
…2633)

#2585 made `RecipientPicker`'s `labels` prop required. The forward
dialog from #2167 renders it without `labels`, so `tsc` fails on `main`
with TS2741 and every PR's JavaScript lane is red.

The forward dialog now derives `labels` the same way New message does:
`useVerifiedNames` plus `withVerifiedNames`, falling back to the current
profile name. `pnpm typecheck` passes.

Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
johnmatthewtennant added a commit that referenced this pull request Nov 5, 2026
Signed-off-by: John Tennant <[email protected]>

* origin/main: (47 commits)
  fix(messages): pass verified labels to the forward recipient picker (#2633)
  feat(messages): forward a message to a person (#2167)
  docs(agents): design relay-facing paths for the whole fleet at once (#2620)
  Route debug sessions through the credential helper (#2541)
  perf(search): skip no-op slow-search resets on each keystroke (#2605)
  perf(agents): dedupe channel members only for dm connection setup (#2603)
  perf(search): keep the search tab row across palette keystrokes (#2588)
  perf(sidebar): skip the hidden actions menu in solo and team headers (#2582)
  perf(search): rank each palette name once per render (#2606)
  perf(sidebar): skip re-rendering unchanged animated channel rows (#2572)
  feat(settings): follow the system clock and let people hide message times (#2440)
  feat(plugins): repository access rules for Git imports (#2484)
  feat(desktop): add Settings to the macOS app menu (#2594)
  Preserve and renew external app approval (#2464)
  fix(direct-messages): show verified identity in new message recipient search (#2585)
  Offer plugin updates from the Buzz shell (#2491)
  fix(messages): keep decoded images across virtualized remounts (#2474)
  Keep /leave commands out of channel history (#2476)
  fix(agents): ask before switching or closing drops unsaved edits (#2503)
  fix(agents): keep the Model settings row full width (#2577)
  ...
agude-sq added a commit to agude-sq/buzz-app that referenced this pull request Nov 5, 2026
* origin/main: (43 commits)
  feat(settings): follow the system clock and let people hide message times (block#2440)
  feat(plugins): repository access rules for Git imports (block#2484)
  feat(desktop): add Settings to the macOS app menu (block#2594)
  Preserve and renew external app approval (block#2464)
  fix(direct-messages): show verified identity in new message recipient search (block#2585)
  Offer plugin updates from the Buzz shell (block#2491)
  fix(messages): keep decoded images across virtualized remounts (block#2474)
  Keep /leave commands out of channel history (block#2476)
  fix(agents): ask before switching or closing drops unsaved edits (block#2503)
  fix(agents): keep the Model settings row full width (block#2577)
  feat(github): show a pull request's changes beside its summary (block#2492)
  fix(search): open an existing DM from a ⌘K person row without a relay open; cover hidden-DM previews (block#2519)
  feat(search): show channel type icon in search result subtitles (block#2521)
  test(browser): bind port-0 fixture servers where the OS assigns (block#2564)
  test(browser): fix six journey sync flakes from the 2026-11-04 sweep (block#2566)
  fix(channels): ask before Escape, Close or an outside click drops a template draft (block#2501)
  fix(channels): render the channel details description as linked Markdown with a label-only Edit target (block#2506)
  fix(agents): let Back and Forward shortcuts traverse provider dialogs (block#2509)
  Fix Inbox recovery after a delayed Retry (block#2496)
  feat(composer): optional ⌘/Ctrl+Enter to send (block#2331)
  ...

# Conflicts:
#	docs/buzz-help.md
kalvinnchau added a commit that referenced this pull request Nov 6, 2026
…2726)

This PR combines two changes that cut main-thread work in large
channels. The conversation header no longer re-renders its member
avatars and members button on traffic in other channels, and matching
people by name folds each name once instead of on every keystroke. Both
costs grow with the member count.

## Problem

**Header.** #2626 moved `ChannelMemberAvatars` into every conversation
header through `ChannelHeaderIdentity`. It also gave
`ChannelMembersButton` its own subscription to the whole channel list.
Both components re-render on every list publish, which means every live
message in any channel.

On each of those renders, both components run a `list.channels.find`
over every channel. The avatars also join, filter and sort every member
of the open channel. With a 5,080-member channel open, other channels'
traffic keeps re-rendering a header whose content has not changed.

**Name matching.** Typing in the Manage members filter of a large
channel re-scans every member on every keystroke. #2356 raised the
per-member cost: the filter now checks the verified display name,
username and full name, which is about 3 `matchPerson` calls per member
where v0.7.10 made 1. #2585 added a similar scan of every cached profile
to the New message recipient picker.

For each name it checks, `matchPerson` folds every code point through
`normalize("NFKD")`, an accent regex and `toLowerCase`, and folds the
query again on every call. The filter also bech32-encodes each member's
key on every keystroke to match npubs. In a 5,080-member channel this
adds up to seconds of main-thread work while the user types a short
query.

## Change

**Header.**
- `useHeaderChannel` is a new helper in `listed-channel.ts`. It is
`useListedChannel` plus the existing `channels.get` fallback for
channels the list does not hold. Each header component selects only what
it renders through it: members, the Solo flag, or the access summary.
All of these are referentially stable, so unrelated list publishes skip
the render.
- `memberAccess` returns one of four shared frozen objects instead of a
fresh object.
- `ChannelMemberAvatars` memoizes the member key, the agent map and the
preview.

**Name matching.**
- `foldPoint` handles ASCII with a plain `toLowerCase`. ASCII has no
accents, decompositions or final sigma, so the fold is the same.
- `ChannelMembersDialog` folds the query once per render and matches
each name with `matchTier`, which reuses the folded-name cache #2570
added. It also keeps one npub map for as long as it is open, so each
member's npub is encoded at most once.
- Rebased onto #2570: the earlier per-dialog `folds` map in
`matchPerson` was dropped, because #2570's cache already keeps each name
folded across keystrokes.

## No behavior changes

**Header.** The lookup order is the same as before: the list entry
first, then `channels.get`. A membership change still produces a new
`members` array, so the header updates. Previews and activity reuse the
channel summary's `members`, so they no longer re-render the header.

New tests cover three cases:
- An unrelated list publish commits nothing.
- A membership change updates both the button and the avatars.
- A channel missing from the list renders through the fallback, and
still skips renders on unrelated publishes.

**Name matching.** The ASCII path gives the same fold as before.
`matchTier` gives the same tier as `matchPerson` (the #2570 test checks
this), and an empty query still matches at tier 0.

The npub map is dropped when the dialog closes and holds one entry per
member.

## Evidence

The table compares main (`8c0cb0c1e`) with this PR as a whole. All runs
are interleaved, with the same two builds on both sides.
- **Browser:** production Chromium at 4× CPU throttling with 128 DMs,
1,001 participants, read state and a 5,080-member channel, while another
channel receives a message every 200 ms. **Idle** is 3 s with no input
while the 5k channel is open. **Switch** opens Cmd+K, types the channel
name and presses Enter. **Members dialog** opens Manage members for the
5k channel and types a 6-character query one key at a time; 1,000
members have profiles. Two Linux and two macOS workstations, 36 samples
per cell; values are the mean of the per-workstation medians.
- **Typing latency:** the same app with 5,080 cached member profiles
that share name prefixes, a deliberate worst case. Each value is the
median Event Timing duration from keydown to the next paint, over 6
interleaved pairs on one Linux and one macOS workstation. Event Timing
quantizes to 8 ms and only reports events of 16 ms or more. **Mention**
types `@Member004` in the composer, **palette miss** types `zzzzzz` in
Cmd+K, and **prose** types `ordinarytext`. Each key waits for the next
rendering opportunity before the following key.
- **Header micro-benchmark:** renders the header with 400 channels and
5,080 members, then delivers 50 live messages to another channel.
- **Picker micro-benchmark:** runs the New message recipient picker's
profile scan, typing 6 keys.
- Micro-benchmark cells are the mean of two workstations per OS, each
the median of 6 runs.

| metric | Linux: main | Linux: this PR | Linux Δ % | macOS: main |
macOS: this PR | macOS Δ % |
|---|---|---|---|---|---|---|
| idle 5k, script | 226.0 ms | 182.5 ms | −19.2% | 150.8 ms | 126.7 ms |
−15.9% |
| idle 5k, task | 367.5 ms | 323.1 ms | −12.1% | 266.6 ms | 240.5 ms |
−9.8% |
| switch 5k, script | 580.5 ms | 563.1 ms | −3.0% | 325.5 ms | 312.9 ms
| −3.9% |
| switch 5k, blocking | 279.1 ms | 264.3 ms | −5.3% | 116.8 ms | 115.6
ms | −1.0% (noise) |
| members dialog, script | 3,272.1 ms | 1,834.2 ms | −43.9% | 2,643.0 ms
| 1,173.2 ms | −55.6% |
| members dialog, task | 3,455.5 ms | 2,055.1 ms | −40.5% | 2,863.3 ms |
1,321.1 ms | −53.9% |
| members dialog, blocking | 2,764.1 ms | 1,327.0 ms | −52.0% | 2,039.2
ms | 650.7 ms | −68.1% |
| mention typing, keydown to paint | 448 ms | 200 ms | −55.4% | 272 ms |
112 ms | −58.8% |
| palette miss, keydown to paint | 136 ms | 104 ms | −23.5% | 88 ms | 72
ms | −18.2% |
| prose typing, keydown to paint | 24 ms | 24 ms | 0% | 16 ms | 16 ms |
0% |
| header renders for 50 messages | 50 | 0 | −100% | 50 | 0 | −100% |
| header time for 50 messages | 58.2 ms | 1.5 ms | −97.4% | 32.8 ms |
1.1 ms | −96.6% |
| picker scan, 1,000 profiles | 42.4 ms | 17.3 ms | −59.2% | 24.3 ms |
9.8 ms | −59.5% |
| picker scan, 5,000 profiles | 213.3 ms | 86.8 ms | −59.3% | 119.1 ms |
48.8 ms | −59.0% |
| picker scan, 10,000 profiles | 431.0 ms | 178.0 ms | −58.7% | 240.5 ms
| 98.9 ms | −58.9% |

Lower is better. These numbers were measured with the earlier per-dialog
`folds` map, before the rebase onto #2570. Blocking is
long-animation-frame blocking time. Typing rows have 162 mention keys,
108 palette keys and 216 prose keys per build; one Linux palette key of
this PR had no Event Timing entry, so that cell covers 107 keys. The
browser harness runs the built frontend with modeled HTTP, not the
native app.

**Larger rosters.** A standalone Node bench of the whole member
predicate over a typing session takes 0.22–0.40× of the old time. It
covers 5k, 9k, 20k and 50k members, with and without names. The first
keystroke after the dialog opens pays the one-time fold and encode: on
large rosters without names it is up to about 1.2× the old cost, and
later keystrokes are 10× faster or more.

Checks:
- The channels, search, mentions, direct-messages and channel-members
unit suites pass.
- `tsc` and `biome` pass.
- After the rebase: the search and channels suites pass (1,163 tests).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Kalvin Chau <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
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.

3 participants