Skip to content

fix(search): open an existing DM from a ⌘K person row without a relay open; cover hidden-DM previews - #2519

Merged
matt2e merged 4 commits into
mainfrom
go-back-to-hidden-dms
Nov 5, 2026
Merged

matt2e merged 4 commits into
mainfrom
go-back-to-hidden-dms

Conversation

@matt2e

@matt2e matt2e commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

Summary

The ⌘K person row looks for the viewer's existing DM with that person before deciding how to open it. The lookup excluded channel.hidden, but hidden is the NIP-29 tag the relay puts on every DM's metadata, so no real DM ever matched. Every pick took the confirmed 41010 open, even for a DM that was already visible in the sidebar.

SearchResults.tsx now drops that exclusion. A person row plain-navigates to an existing DM only when the relay's DM visibility snapshot is ready and the DM is not hidden from the sidebar. A locally or relay-hidden DM still takes the confirmed open, and so does any DM while visibility is idle, loading or error, because the DM might be hidden. Unhiding works as it does on main.

Why it matters

  • Opening a visible DM from a person row (for example, one found by username) navigates straight to it with no relay round trip.
  • One fewer error path: on a connection that cannot start DMs, the row opens the existing DM instead of reporting an error.
  • The "existing DM" branch never ran before because no real DM could match. Now it runs.
  • The ⌘K and New message test fixtures now carry the relay's ["hidden", ""] and ["private", ""] tags, so a filter on channel.hidden fails the tests instead of passing against untagged fixtures.

Not changed

New message already loads a hidden DM's history inline once the chosen recipients exactly match its participants, on main. That covers 1:1 and group DMs, and both local hides (Remove from Team) and relay hides (Hide conversation). This PR only adds tests and docs for that behaviour. Previewing still never opens, unhides, marks read or publishes. Only Send does.

Tests

src/app/shell/SearchResults.test.tsx

  • New: a DM found by username while visibility is idle opens through directMessages.open with the person's pubkey.
  • New: it.each(["loading", "error"]) asserts the same confirmed open while visibility is refreshing or has failed.
  • New: a sidebar-hidden DM opens directly from a person row when this connection cannot start DMs.
  • Changed: the relay-hidden DM (Scooby) is also clicked through its person row and asserts the confirmed open. The visible-DM test loads visibility first, since it asserts a plain open. DM fixtures carry the relay's hidden tag.

src/features/direct-messages/NewMessage.recovery.test.tsx (real relay session)

  • Locally and relay-hidden DMs both preview without restoring the hide, opening or publishing. Send still takes the confirmed open and clears the local hide.
  • Only an exact participant set matches, in any order. A subset or superset of a group does not.
  • A held history read for an earlier match never takes over the preview after an edit.
  • Recipients edited while the DM roster loads match the recipients chosen at load time.
  • With directMessages.available false, history still previews and Send stays disabled.
  • The harness answers history reads per #h channel. The 1:1 and group 39000 fixtures carry hidden and private tags.

Fail-before checks:

  • Restoring the !channel.hidden exclusion fails the existing visible-DM person-row test.
  • The previous person-row condition fails the idle test.
  • Changing the guard to !== "idle" fails both loading and error cases.
  • Matching only local hides fails the Scooby assertion.
  • In New message: excluding hidden channels fails 10 of 32 tests, and subset matching, gating on available or ignoring recipient edits each fail one new test.

Validation: typecheck and lint pass. The SearchResults suite (115 tests) passes, as do the direct-messages, PageSearch, BestieNavigation, ShortcutSettings and identity-names consumer suites. No browser journey clicks a search person row.

Not checked here: whether a relay-hidden DM's sidebar row comes back without a refresh after Send. The browser fixture does not model 41010 unhiding.

Docs

  • docs/channels.md: Starting a direct message now says how New message previews an existing DM, including a hidden one. The DM visibility section no longer says an in-app unhide flow is deferred. Any confirmed 41010 open, including New message Send, unhides the DM.
  • docs/buzz-help.md: a new Hidden DMs entry covers getting a hidden DM back through New message or search. It also covers what happens when the connection can't start DMs.

🤖 Generated with Claude Code

@matt2e
matt2e requested a review from a team as a code owner November 4, 2026 23:02
@matt2e
matt2e force-pushed the go-back-to-hidden-dms branch from 078f8c7 to cc626e1 Compare November 5, 2026 01:07
matt2e and others added 4 commits November 5, 2026 15:31
… DMs

New message already loads an existing DM's history inline once the chosen
recipients match it. It matches against the full session channel roster,
while Remove from Team and Hide conversation only filter the sidebar and
search. So hidden DMs of both kinds, and DMs missing from the sidebar,
already preview without any production change. A probe in the browser
fixture confirmed this for a locally hidden DM. This commit locks the
behaviour in with Vitest coverage and documents it, including in the help
guide, which had nothing on getting a hidden DM back.

Tricky cases covered (real relay session, NewMessage.recovery.test.tsx):
- Locally hidden (useHiddenDms mounted beside the page, as in Messages)
  and relay-hidden (30622 snapshot) DMs both preview. Previewing never
  restores either hide, calls the 41010 open or publishes. Send still
  takes the confirmed open and clears the local hide.
- Exact participant set, in any order, for a 3-person group. A subset
  (Zoe only, or Avery only, which matches the 1:1 instead) and a superset
  (adding Kai) do not match. The preview swaps straight to the avatars.
- A held history read for the group, outlived by an edit back to the
  1:1, never takes over the preview. The group shows "Loading messages…"
  in place of the 1:1 timeline, then its own history when matched again.
- Recipients edited while the DM roster is still loading match the
  recipients chosen when it lands, not the earlier ones.
- With directMessages.available false, history still previews, the
  existing unavailable notice shows and Send stays disabled.

The test harness now answers history reads per `#h` channel, so more than
one DM can have history.

Fail-before: excluding hidden DMs from the match, subset matching, gating
the preview on `available`, and a lookup that ignores recipient edits each
fail exactly one new test. Removing `key={existing.id}` does not, because
useChannelWindow already switches snapshots by id.

Behaviour kept deliberately (all pre-existing):
- Previewing does not mark anything read. The preview mounts with
  `active={false}` and `marksRead={false}`. jsdom cannot establish
  reading, so this is not asserted here.
- Previewing records no ⌘K Recent visit, since only navigation opens count.
- The preview stays when the connection cannot start DMs, because reading
  history needs no open capability.
- Typing never opens, creates or unhides a DM. Only Send does.

Deferred: whether the sidebar row returns without a refresh after Send
for a relay-hidden DM. The browser fixture does not model 41010 unhiding.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
Relay-signed DM metadata carries the NIP-29 `hidden` tag, and the
channel summary exposes it as `hidden`. The ⌘K person row excluded
hidden channels when looking for the viewer's existing DM, so no real
DM ever matched: choosing a person whose visible DM was not already
listed (for example, found by username) asked the relay to open the DM
again instead of navigating to it, and on a connection that cannot
start DMs the row reported an error instead of opening the hidden DM
directly. Only the fixtures without the tag kept the existing test
green.

Drop the `hidden` exclusion; DMs are hidden from directories by design,
and the sidebar-hidden check next to it already decides between a plain
open and the confirmed open that restores the row.

Tests: the ⌘K DM fixtures now carry the relay's `hidden` tag, which
makes the existing person-row test fail before this change, and a new
test covers opening a sidebar-hidden DM from a person row when this
connection cannot start DMs.

Entry-point audit against a local relay (block/buzz at 49fcab332):
New menu > New DM and the DMs sidebar + both land in New message and
preview a hidden 1:1 and hidden group with no relay writes, at 1280 px
and in the 390 px layout. ⌘K person rows, the profile hover card
Message action, Members > Send message and the agent Message buttons
are confirmed opens that navigate to the DM and unhide it on the relay,
as documented; none creates or unhides while recipients are typed.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
…oads

The relay's DM visibility snapshot starts `idle` with nothing hidden, and
only the sidebar's ChannelInventory refreshes it once the roster is
ready. Until the first `ready` snapshot, a relay-hidden DM looked visible
to the ⌘K person row, which plain-navigated to it and never sent the
41010 open that asks the relay to unhide it. When the snapshot landed,
the sidebar hid the row while the user was reading the DM.

The person row now plain-opens an existing DM only once visibility is
`ready` and the DM is not hidden from the sidebar. While it is not ready,
the row takes the confirmed open whenever this connection can start
DMs, as every DM did before 73c54bdf2. A connection that cannot start
DMs still opens the DM directly.

Tests (SearchResults.test.tsx):
- New: a DM found by username while visibility is still `idle` opens
  through `directMessages.open` with the person's pubkey.
- The relay-hidden DM (Scooby) is now clicked through its person row
  too, asserting the confirmed open with Scooby's pubkey.
- The existing visible-DM person-row test loads visibility first, since
  it asserts a plain open.

NewMessage.recovery.test.tsx: the 1:1 and group 39000 fixtures carry
the relay's `["hidden", ""]` and `["private", ""]` tags, so a future
`!channel.hidden` in New message's match fails the preview tests. That
was the fixture gap that hid the ⌘K bug fixed in 73c54bdf2.

Fail-before: the old person-row condition fails the new idle-window
test. Matching the person row against local hides only fails the
Scooby assertion. Excluding hidden channels from New message's match
fails 10 of 32 NewMessage.recovery tests.

Docs: channels.md no longer says an in-app DM unhide flow is deferred.
Any confirmed 41010 open, including New message Send, unhides the DM.
The help guide adds that choosing the person in search is a quicker way
back for a 1:1. It also says that when the connection can't start DMs,
New message previews the history with Send disabled, and search opens
the DM without restoring it.

Validation: typecheck and lint pass. SearchResults, direct-messages,
PageSearch, BestieNavigation, ShortcutSettings and identity-names
consumer suites pass (241 tests). No browser journey clicks a search
person row.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
…efreshes

The ⌘K person row plain-opens an existing DM only once the relay's DM
visibility snapshot is `ready`. A `loading` refresh keeps the last hidden
set, which may be stale, and a failed refresh keeps it too, so either can
show a relay-hidden DM as visible. Only `idle` was pinned, so changing
the guard to `!== "idle"` passed every person-row test while plain-opening
a hidden DM during the first `loading` read or after a failed load.

A new `it.each` over `loading` and `error` stubs the visibility snapshot
with nothing hidden, as the hidden-DM test does, and asserts the row
calls `directMessages.open` with Alice's pubkey. The guard comment now
says the guard covers every refresh and failed refresh, not only the
first load.

Fail-before: with the guard changed to `!== "idle"`, both new cases fail
and the other person-row tests pass.

Validation: the SearchResults suite (115 tests), typecheck and lint pass.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Matt Toohey <[email protected]>
@matt2e
matt2e force-pushed the go-back-to-hidden-dms branch from cc626e1 to 348573d Compare November 5, 2026 04:34

@kazemek21 kazemek21 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Kyle reviewed this PR and the Nexus review-tracker notes. Approved with no comments.

Posted by Codex (gpt-6.1-sol) on behalf of kylez.

@matt2e
matt2e merged commit 49bdc82 into main Nov 5, 2026
43 checks passed
@matt2e
matt2e deleted the go-back-to-hidden-dms branch November 5, 2026 06:18
ljknight added a commit to ljknight/buzz-app that referenced this pull request Nov 5, 2026
* origin/main:
  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)

# Conflicts:
#	docs/buzz-help.md
wpfleger96 added a commit that referenced this pull request Nov 5, 2026
…-service

* origin/main: (68 commits)
  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)
  feat(github): show a pull request's changes beside its summary (#2492)
  fix(search): open an existing DM from a ⌘K person row without a relay open; cover hidden-DM previews (#2519)
  feat(search): show channel type icon in search result subtitles (#2521)
  test(browser): bind port-0 fixture servers where the OS assigns (#2564)
  test(browser): fix six journey sync flakes from the 2026-11-04 sweep (#2566)
  fix(channels): ask before Escape, Close or an outside click drops a template draft (#2501)
  fix(channels): render the channel details description as linked Markdown with a label-only Edit target (#2506)
  fix(agents): let Back and Forward shortcuts traverse provider dialogs (#2509)
  Fix Inbox recovery after a delayed Retry (#2496)
  feat(composer): optional ⌘/Ctrl+Enter to send (#2331)
  test: use keyboard activation for reply caret setup (#2557)
  test: settle member search before invitation expansion (#2532)
  test(agent-command): isolate the copied executable writer (#2537)
  test(sidebar): wait for the rolled-back row to leave Starred before collapsing Work (#2574)
  test(browser): click timeline rows only after the timeline settles (#2560)
  ci: split Vitest across six shards so 3/4 stops hitting the job cap (#2571)
  ...

# Conflicts:
#	src/app/Settings.tsx
#	src/bundled/goose/sessions.test.ts
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
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