Skip to content

feat(desktop): hide archived identities from discovery surfaces (NIP-IA)#734

Merged
tlongwell-block merged 4 commits into
mainfrom
feat/nip-ia-hide
May 23, 2026
Merged

feat(desktop): hide archived identities from discovery surfaces (NIP-IA)#734
tlongwell-block merged 4 commits into
mainfrom
feat/nip-ia-hide

Conversation

@tlongwell-block
Copy link
Copy Markdown
Collaborator

Summary

NIP-IA follow-up: hide archived identities from forward-looking discovery surfaces in the desktop GUI. Never hides messages in channels or DMs — history stays fully intact; an archived person's past posts render normally with their name. They simply stop appearing in "who's here / who can I add / who can I mention" surfaces.

Builds on #733 (the relay backend + kind:13535 archived-set client plumbing). No backend change.

Behavior

  • Members sidebar → foldable "Archived (N)" section below active members. Collapsed by default, expandable; click-through to profile + unarchive preserved.
  • Hard-filtered (no visibility): mention autocomplete (chat + forum), DM recipient picker, channel member-adder, agent allowlist picker.
  • Left alone (history / not discovery): message timeline, members count pill, extractMentionPubkeys (resolves archived authors' past @-mentions), internal membership predicates.

Key design decisions

  • One predicate, self-exemption baked in. useIsArchivedPredicate() reads the archived set + the current identity internally — the current user is never hidden or folded from their own client (NIP-IA §Self Requests anti-shadowban property). Self-exemption is structural, not a caller parameter, so it can't be forgotten.
  • Archived wins over bot in the sidebar partition — a zombie agent folds into Archived, not Bots (NIP-IA's headline use case).
  • Fail-open while loading: predicate is a no-op until the snapshot resolves, so a cold start never briefly hides everyone.
  • Filter at the consumer, never at the query — keeps history-resolution paths whole.

Verification

  • New identity-archive-hide.spec.ts: 7 cases (fold, no-section-when-empty, autocomplete filter + positive, DM filter + positive, history invariant, 2× self-exemption).
  • pnpm exec playwright test --project=smoke: 112 passed (was 105 on main; +7, 0 regressions).
  • pnpm typecheck + pnpm check clean; full pre-push gate green.

Implements Dawn's discovery-sweep contract (v2, stamped by Eva): every
forward-looking discovery surface in the desktop hides archived
identities, while history and audit surfaces leave them visible.

The shape:
- One predicate, applied uniformly. `useIsArchivedPredicate` wraps the
  `kind:13535` snapshot in a Set for O(1) lookup, fails open while
  loading (cold-start can't briefly hide everyone), no `currentPubkey`
  carve-out (NIP-IA's anti-shadowban property surfaces in the UI; the
  profile pane's flair and `selfMember` role lookup are independent of
  bucket — self folds where archive state puts it).
- `useClassifiedMembers` peels archived FIRST, then splits remainder
  into people/bots. Archived wins over bot — a zombie agent under
  "Bots" defeats NIP-IA's headline use case.
- Filter at consumers, never at the query — `extractMentionPubkeys`
  must still resolve archived authors' historical @-mentions.

Surfaces touched (Dawn's table):
- MembersSidebar.tsx: foldable "Archived (N)" `<details>` section
  below People + Bots, only rendered when archived.length > 0.
- useMentions.ts: `.filter` pre-scoring in `suggestions` memo only —
  `members` source stays unfiltered for history resolution.
- NewDirectMessageDialog.tsx, ChannelMemberInviteCard.tsx,
  RespondToField.tsx: one `!isArchivedDiscovery(user.pubkey)` clause
  added to each existing search-results `.filter`.

Surfaces deliberately left alone (per Dawn's table): channel-members
count pill, ChannelManagementSheet internal lookups, ChannelScreen
timeline/typing enrichment, extractMentionPubkeys, AddAgentToChannel
"already in channel" predicate. All consume `useChannelMembersQuery`
but render history / role state / internal-only — Tyler's "never hide
messages" rule.

Tests (tests/e2e/identity-archive-hide.spec.ts, in smoke project):
1. Archived member folds under "Archived (N)", not active People list.
2. No Archived section when no archived members.
3. Mention autocomplete filters archived; non-archived still
   suggestable (proves autocomplete is functional, not always-empty).
4. DM picker hides archived from search; non-archived still searchable.
5. History invariant: archived user's existing message still renders
   with their display name in the timeline.

Bonus: `mention-suggestion-${pubkey}` testid added in
MentionAutocomplete.tsx so the autocomplete spec can assert
presence/absence by pubkey rather than text matching.

Verification:
- `pnpm exec playwright test --project=smoke`: 110 passed (was 105;
  +5 new, 0 regressions).
- `pnpm typecheck`, `pnpm check`: clean.
- Pre-commit (rust-fmt, desktop-tauri-fmt, desktop-check, web-check,
  mobile-check): all green.

Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
ff1e561 shipped Shape B — predicate identity-only, no `currentPubkey`
carve-out, self folds in own Archived section. Eva's final stamp in
the thread (msg [10], superseding her earlier [8]) is Shape A: self
is *never* filtered or folded from their own client, NIP-IA §Self
Requests anti-shadowban property. I built from Dawn's intermediate
[9] message before the final stamp landed.

Two design points worth recording:

1. **Where self-exemption lives.** Dawn's locked v2 helper takes
   `currentPubkey` as an arg and threads it through every caller.
   Reading identity inside the predicate via `useIdentityQuery`
   (global, infinite-staleTime) is materially better:
   - No prop drilling through useMentions, RespondToField, etc.
   - Impossible for a future caller to forget the self-exemption.
   - One source of truth for "what is self".
   Net diff vs. arg-threading: ~zero callers change, one helper
   gains 3 lines.

2. **Why this is a real bug, not a polish.** Without self-exemption,
   a self-archived user loses their own seat in the members sidebar
   AND drops from their own @-autocomplete — both reproducible with
   the new tests. That's exactly the shadowban NIP-IA §Self Requests
   exists to prevent.

Tests added:
- self-exemption: archived current user still appears in their own
  People list (not folded). Asserts "You" + "deadbeef" prefix in
  members-sidebar-people AND members-sidebar-archived has count 0.
- self-exemption: archived current user can still self-mention in
  their own autocomplete. Confirms the predicate's self-exemption
  fires at the suggestion filter, not just the panel partition.

Verification:
- `pnpm exec playwright test --project=smoke`: 112 passed (was 110,
  +2 new self-exemption cases). Pre-existing 5 cases unchanged.
- `pnpm typecheck`, `pnpm check`: clean.

Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
@tlongwell-block tlongwell-block requested a review from a team as a code owner May 23, 2026 16:31
Adds the channel member-adder (ChannelMemberInviteCard) to the hide
e2e matrix — the one hard-filter surface Max's review flagged as
code-correct but untested. Tested in #agents (where Alice is a
non-member) with a paired control run so the assertion proves the
archive filter drops her, not member-exclusion.
@tlongwell-block tlongwell-block merged commit 59f51a8 into main May 23, 2026
15 checks passed
@tlongwell-block tlongwell-block deleted the feat/nip-ia-hide branch May 23, 2026 17:58
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.

1 participant