Clean successor to V3 (forge: lilith/atlilith). Seeded from local Mac working tree at ~/Code/@projects/@cocottetech/. node_modules and build artifacts excluded via .gitignore. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
17 KiB
peer-directory.screen
Implementation breakdown of Brief AE §AE5 — the opt-in, platform-wide peer-discoverable directory. Mobile-narrow list with a filter bar at the top, searchable by surface, language, region, and endorsement scope-claim text. Rows reveal handle, bio excerpt, surface chips, and endorsement-count badge; tap routes to peer-profile.screen.md. The drawer is reached from chat-home's top-bar overflow ("Peers") and from the coop drawer's peer-roster sub-tab when a coop member opts in to platform-wide discoverability.
Voice register: working by default (per 00-system-voice.md §V2b — peers are colleagues, decisions are deliberate); plain on empty / blocked / posture-related states (per §V2c — the cost of leaking that someone is incognito is non-trivial). Hearth lexicon stays out — this is a directory, not a hearth.
Layout (iPhone 17 logical 393×852)
┌─────────────────────────────────────────────────┐
│ ◄ status bar (system) │ 47pt
├─────────────────────────────────────────────────┤
│ [◄] Peers [🎤] [⌥] │ 56pt — top bar
│ │ ⌥ = posture + filters sheet
├─────────────────────────────────────────────────┤
│ ╭─────────────────────────────────────────╮ │ 44pt — searchbar
│ │ 🔍 Search handle, bio, scope-claim… │ │ voice mic on right of bar
│ ╰─────────────────────────────────────────╯ │
├─────────────────────────────────────────────────┤
│ [ surface ▾ ] [ language ▾ ] [ region ▾ ] │ 36pt — filter chip row
│ [ endorses ▾ ] [ reset ] │ horizontal scroll
├─────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────┐ │ result row
│ │ @sarah-k ★ 12 │ │ handle · endorsement badge
│ │ Berlin · DE/EN · she/her │ │ region + langs line
│ │ "Two years on Tryst, three on OF. │ │ bio excerpt (2 lines max)
│ │ Pricing-study group regular." │ │
│ │ [tryst] [of] [threads] [ Connect ] │ │ surface chips · primary CTA
│ └───────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────┐ │
│ │ @q-berlin ★ 31 │ │
│ │ Berlin · DE/EN/中文 │ │
│ │ "Mandarin-speaking subject handling." │ │
│ │ [tryst] [of] [ Requested ] │ │ pending-state CTA
│ └───────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────┐ │
│ │ @mira-pdx ★ 4 │ │
│ │ Portland · EN │ │
│ │ "PPV pricing for content-onlyfans." │ │
│ │ [of] [ Connected ] │ │ post-accept state
│ └───────────────────────────────────────────┘ │
│ … │
│ [ load more · 23 results ] │
│ │
├─────────────────────────────────────────────────┤
│ │ 34pt — home indicator
└─────────────────────────────────────────────────┘
Components
| Component | Brief ref | Notes |
|---|---|---|
| Top bar | AE §AE5 | Back to chat. [🎤] = voice query (routes through ai-copilot → directory filter set; see Interactions). [⌥] opens posture + advanced-filters sheet (see Interactions). |
| Searchbar | AE §AE5 | Full-width, debounced 250ms. Searches handle, bio substring, and endorsement.scope_claim text. Never searches region literal beyond chip filter (avoids precise-location leak per K3f-2). |
| Filter chip row | AE §AE5 + F §F5 | Four canonical chips: surface (multi-select per O surface_kind), language (ISO 639-1 multi), region (country/metro coarse only — never lat/long or city precision), endorses (free-text scope-claim picker). [ reset ] clears all. Active chips show count badge. |
| Result row | AE §AE5 + AE §AE7 | Handle line + endorsement-count badge (★N — count of accepted public endorsements per AE7). Region + langs line (coarse only). Bio excerpt (2 lines, ellipsis). Surface chips per O. Right-aligned connection CTA whose label reflects current peer_connections.state for this pair (per AE2). |
| Connection CTA | AE §AE2 | Polymorphic per state: none → "Connect", requested-by-me → "Requested" (disabled), requested-by-them → "Accept ▾" (drop = accept/decline), connected → "Connected" (tap → row context menu), muted → "Muted", blocked → row hidden. |
| Posture banner | AE §AE11 | Renders above results when viewer's posture ≠ open and they navigated here directly. Plain register: "Your posture is discoverable. You can browse, but you won't appear in this directory until you switch to open." Tap → settings posture sheet. |
| Voice query mic | A §multimodal + AD §AD3 | Push-to-talk; produces a structured filter set ("show me Berlin peers who endorse Tryst optimization") which surfaces as auto-filled chips + searchbar; never auto-executes a connection request. |
| PII-filter receipt | AE §AE5 + K §K3 | Invisible-by-default footer line at very bottom of list: "Directory hides government names + precise location." Plain register. Tap → AE11 posture sheet. |
States
- Browse, populated (default) — opt-in posture is
discoverableoropen; ≥1 result rows matching filters. Default sort: endorsement-count desc, thenlast_active_atdesc. - Searching / debouncing — searchbar non-empty, results re-rendering. Subtle top progress bar (1.5pt) under the searchbar; existing rows dim to 50%. No skeleton — too noisy.
- No matches — filters narrow to zero. Working register: "No peers match these filters. Try widening region or removing a surface chip." Includes a single-tap "reset filters" button.
- Empty — incognito posture — viewer's posture is
incognito(AE11 default). Plain register full-screen takeover: "You're incognito. The directory is hidden until you opt in." Single CTA "Open posture sheet" → AE11 sheet. No peer rows render; this is the privacy floor — incognito viewers should not even see the directory exists is populated. - Empty — posture
discoverablebut no platform-wide rows opted in to surface to you — populated but coop-only; banner: "Coop-mediated peers only. Switch toopenfor the platform-wide directory." Result rows that exist via shared-coop opt-in still render. - Voice query active —
[🎤]long-pressed. Composer-like mic-pulse replaces filter bar; on release, the recognized intent fills chips + searchbar with a 2s "review filters" toast before applying. Mishears: tap the toast to undo. - Filtered to one endorsement scope-claim — chip "endorses:
bookings-trystlisting optimization" pinned. Result rows reorder by endorsements-on-this-claim-desc. Subtle helper line below filter row: "Sorted by endorsements on this claim." - Degraded (M §M2b) — directory backend unreachable. Top banner (yellow, plain): "Peer directory can't reach the server. Showing cached rows from {time}." Cached rows are read-only; Connect CTAs are disabled with hint "back online to send a request."
- Offline (M §M2c) — no cached data: empty state with grey banner "Offline. Directory needs a connection." If cache exists: behave as state 8.
- Rate-limited / posture-rate-cap (AE §AE10) — viewer has sent too many connection requests in the rolling window (anti-spam per AE10). Connect buttons globally disabled with plain-register tooltip: "You've sent 12 requests today. Cooldown until tomorrow morning." Browsing remains live.
- Viewer is shadowbanned from directory (AE §AE10 sanction) — viewer can browse but their own posture is forcibly demoted; banner: "You can't appear in the directory while a moderation review is open." Browsing is unaffected; appearing is.
Interactions / gestures
- Tap result row → routes to
peer-profile.screen.md. - Tap Connect CTA → opens a one-line "context note" sheet (per AE1b referral pattern but for self-introductions); confirm to write a
peer_connectionsrow inrequestedstate. Haptic on commit. CTA flips toRequested(disabled). - Long-press result row → row context menu: "View profile", "Mute", "Hide from this directory view" (per-viewer hide; doesn't block), "Report". Block is intentionally NOT here — block lives on the profile screen so the viewer reads the bio first.
- Tap surface chip in row → adds that surface to the filter set; brief flash on the chip row to show the new filter.
- Pull-to-refresh → re-pulls directory page. Surfaces newly-opt-in peers at the top with a "new" pip for 2s.
- Swipe-down on searchbar from idle → dismisses keyboard.
[🎤]long-press → voice query mode (state 6).[⌥]tap → posture + advanced-filters sheet (toggle posture, save filter preset, manage muted-peers list).- VoiceOver order per row — handle → endorsement-count → region → langs → bio excerpt → surfaces → connection CTA. Most-identifying first.
In-the-wild copy
- (working, empty-no-matches) "No peers match these filters. Try widening region or removing a surface chip."
- (plain, incognito empty) "You're incognito. The directory is hidden until you opt in."
- (plain, posture-discoverable) "Your posture is
discoverable. You can browse, but you won't appear in this directory until you switch to open." - (working, voice-query toast) "Filtering: Berlin · Tryst · endorsing listing optimization. Tap to undo."
- (plain, degraded) "Peer directory can't reach the server. Showing cached rows from 14:02."
- (plain, rate-limited) "You've sent 12 requests today. Cooldown until tomorrow morning."
- (working, PII filter footer) "Directory hides government names + precise location."
- (plain, shadowbanned) "You can't appear in the directory while a moderation review is open."
- (working, connect-context-sheet placeholder) "One line so they know why you're reaching out. Optional."
Privacy invariants
- No government name field ever rendered.
peer_profiles.handleis the only identity; bio passes K3 PII gate at publish time, never at render. - No precise location. Region chip is country / metro at coarsest; the renderer ignores any free-text region beyond a known coarse vocabulary.
- No "viewed by" telemetry. Tapping a row does not write a visible-to-target event. (Audit row exists per I, but the target peer is never notified of directory views — only of connection requests.)
- Blocked peers are invisible to the blocker and the blocked. A row that exists for the blocker's view will not render for the blocked viewer, and vice versa. Hash-based row filter at query time, not at render — never round-trip blocked rows to client.
- Incognito peers never appear, even with shared coops. The directory honors
peer_profiles.posturestrictly. - Voice query never echoes raw recognized text to other peers. The structured filter set is what travels; the spoken bytes stay on-device + the audit row.
Edge cases
- Filter chips combine to a single-peer result who happens to be the viewer → viewer-self row is suppressed; helper line: "Showing 0 matches (excluding you)."
- Bio contains a flagged phrase post-publish (K3 list updated after publish) → row renders with bio replaced by plain stub: "Bio under review." Peer is notified separately via chat-home receipt.
- Endorsement count includes withdrawn endorsements? → No. Badge counts only
public=true AND withdrawn_at IS NULL. Withdrawal updates the count within ~1 vigil per AE9 aggregator cadence. - Two peers with the same handle → schema-enforced unique; this state cannot occur. If it ever does (data corruption), render both with a
(disputed)chip and block any connection action. - Searchbar query that K3-PII-trips (viewer types a govt name as query) → query is dropped client-side with a plain-register inline notice: "We don't search by names like that. Try a handle or scope-claim."
- Reduced motion — replace top progress bar with a static dim of the result list during debounce.
- Dynamic Type XXL — result row reflows: surface chips wrap to a second line below bio; CTA stays right-aligned but full-width on row when wrapped.
- VoiceOver + voice query — voice query goes through VoiceOver's mic path, not the directory mic, to avoid double-narration.
- Right-to-left locale (per AD-Q4) — searchbar mic-icon flips to left; chip row scroll direction mirrors; CTA stays at the row's trailing edge.
- Peer just-opted-in mid-scroll → "new" pip on insertion; list does not jump-scroll.
- Peer in shared coop has opt-in to
discoverablebut notopen→ renders only when viewer is also in that coop and viewer's posture ≥discoverable. Filter chip "in coop ▾" is conditionally available if viewer is in ≥1 shared coop. [open]Bio length limit on the row excerpt — currently 2 lines truncated. Open whether tap-to-expand-inline is worth the affordance vs. routing to profile. Lean: route to profile; an excerpt is a teaser.[open]Default sort tie-breakers — endorsement-count-desc, thenlast_active_atdesc. Open whether mutual-coop count should outranklast_active_at.
Related
- Brief AE §AE5 — directory parent + opt-in posture.
- Brief AE §AE2 — connection state machine driving CTA labels.
- Brief AE §AE7 — endorsement-count badge source.
- Brief AE §AE10 — rate-limit + shadowban states.
- Brief AE §AE11 — posture floor + privacy invariants.
peer-profile.screen.md— destination of row tap.chat-home.screen.md— top-bar overflow entrypoint.coop-drawer.screen.md— sibling drawer; complementary plane.- Brief K §K3 — PII gate on bio + search query.
- Brief AD — locale-invariant filter labels + voice query.
- Brief X — VoiceOver row order + reduced motion.
- Brief V — erasure redacts profile + endorsements without deleting connection-state history.
- Brief O —
surface_kindvocabulary for chips. - Brief F §F5 — surface-chip iconography.
00-system-voice.md§V2b working / §V2c plain.
Out of scope
- The posture + advanced-filters sheet interior (its own
.screen.mdif it grows beyond toggles). - Coop-mediated peer-roster sub-tab (lives inside
coop-drawer.screen.md). - The introduction flow from AE1b referral (separate screen — a referral-introduce sheet).
- Cross-org directory rows for org-attributed peer connections (deferred to AE-Q6 P5+).
- iPad / web variants.
Open questions
- Default sort tie-breaker — endorsement-count-desc →
last_active_atdesc → mutual-coop count? Or mutual-coop count second?[nice-to-have](lean:last_active_atsecond; mutual-coop count is its own filter chip). - Voice query review-toast window — 2s feels short for non-native-language voice input.
[exploratory](lean: 2s for confident parses, 4s for any chip below 0.75 confidence per AD3 threshold). [open]Can endorsement-count badge be tapped to surface which scope-claims drive the count?[exploratory](lean: yes, but on the profile screen, not the directory row — keep rows scannable).