# peer-profile.screen Implementation breakdown of [Brief AE §AE5](./AE-provider-social-network.brief.md) profile rendering + [Brief AE §AE2](./AE-provider-social-network.brief.md) connection-state controls — what a provider sees when viewing **another** peer. Reached from [`peer-directory.screen.md`](./peer-directory.screen.md), from coop-drawer peer-roster rows, from a referral-introduction sheet (AE1b), from peer-DM thread headers, and from endorsement / mentorship notification rows in chat-home. Voice register: **working** for the bio, surfaces, endorsements, mutual-coop chips, and connection controls (per [`00-system-voice.md`](./00-system-voice.md) §V2b — peer relationships are deliberate). **Plain** for moderation actions, blocked / 404 states, and posture-derived empty views (per §V2c — block + moderation cannot be softened). ## Layout (iPhone 17 logical 393×852) ``` ┌─────────────────────────────────────────────────┐ │ ◄ status bar (system) │ 47pt ├─────────────────────────────────────────────────┤ │ [◄] Profile [⋯] │ 56pt — top bar │ │ ⋯ = moderation menu (long-press anywhere) ├─────────────────────────────────────────────────┤ │ │ │ @sarah-k ★ 12 │ header band │ Berlin · DE / EN · she/her │ region · langs · pronouns │ posture: open · last active 2h ago │ posture badge + freshness │ │ │ ──── Bio ──────────────────────────────── │ │ "Two years on Tryst, three on OF. Pricing- │ bio body — passes K3 + V6 gate │ study group regular. Berlin-area tours │ │ monthly. Mandarin OK, German native." │ │ │ │ ──── Surfaces operated ────────────────── │ │ [tryst] [of] [threads] │ surface chips per O │ │ │ ──── Endorsements · 12 ────────────────── │ │ "bookings-tryst listing optimization" · 5 │ scope-claim · endorser count │ endorsed by @q-berlin · @mira-pdx · +3 │ small-text endorser sample │ "Mandarin-speaking subject handling" · 4 │ │ endorsed by @q-berlin · @lin-sf · +2 │ │ "Tour pricing for Berlin" · 3 │ │ endorsed by @mira-pdx · @rosa-de · +1 │ │ [ See all 12 → ] │ │ │ │ ──── Mutual coops · 2 ─────────────────── │ │ [Berlin escort coop] [Pricing study] │ chip row; tap → coop drawer │ │ │ │ (scroll padding) │ │ ├─────────────────────────────────────────────────┤ │ [ Follow ▾ ] [ Request colleague ] │ 64pt — footer control bar └─────────────────────────────────────────────────┘ │ 34pt — home indicator ``` ## Components | Component | Brief ref | Notes | |---|---|---| | Top bar | AE §AE5 | Back to source surface. `[⋯]` opens the moderation + utility menu: "Mute", "Block", "Report to coop moderators", "Report to platform-admin", "Share profile via referral" (AE1b), "Copy handle" (handle only, never any other field). | | Header band | AE §AE5 + AE7 | Handle · endorsement-count badge (★N). Second line: region (coarse) · languages · pronouns. Third line: posture badge + freshness (`last active ~Nh ago`, rounded to vigil; never precise). | | Posture badge | AE §AE11 | One of `open` / `discoverable` / `incognito`. Color-neutral; this is information, not status. Only rendered if the viewer has visibility into the posture (i.e. the peer is in `discoverable` for a shared-coop view, or `open` for a directory view). | | Bio section | AE §AE5 | Free-text, passed through K3 + voice §V6 gate at publish-time. Renders as quoted text in working register. Long bios expand with "See more"; never auto-truncate mid-sentence. | | Surfaces operated chips | AE §AE5 + O | One chip per `peer_profiles.surfaces_operated` entry. Claim-only at P0 (no verification — per AE5). Tappable: opens a sheet describing the surface kind, NOT a deep-link into the peer's content (cross-surface content links across providers are out-of-scope at P0). | | Endorsements list | AE §AE7 | Grouped by `scope_claim`; per group: claim text + endorser count + first 2–3 endorser handles + overflow. "See all" routes to a full endorsements sheet. Renders only `public=true AND withdrawn_at IS NULL`. | | Mutual coops chip row | AE §AE5 + N | Coops the viewer and the peer both belong to (intersection per N membership). Tap → coop drawer for that coop. If 0: section hidden entirely (don't render an empty mutual-coops zone — signals nothing useful). | | Connection control footer | AE §AE2 | Polymorphic per `peer_connections.(kind, state)` for the viewer↔peer pair. Two slots: a `follow` control (lightweight) and a `colleague` control (gated). Labels per State table below. | | Moderation menu | AE §AE10 | Long-press anywhere on the profile body (or `[⋯]`) opens it. Items in plain register: "Mute (per AE2)", "Block (per AE2)", "Report to coop moderators", "Report to platform-admin", separated visually from utility items. | ## States 1. **Viewing a directory-discoverable peer, `none` connection (default)** — full layout as drawn. Footer: `[ Follow ▾ ]` + `[ Request colleague ]`. Both actions live. 2. **Viewing a follow-only peer (you follow them, they don't follow back)** — footer: `[ Following ▾ ]` (tap = unfollow / request colleague) + `[ Request colleague ]`. Endorsements + bio fully rendered. 3. **Viewing a connected colleague** — footer: `[ Following ▾ ]` + `[ Connected ▾ ]` (drop: disconnect / mute / start peer-DM). New section appears above the footer: "Threads" — 1–3 recent peer-DM thread snippets (rendered per chat-home thread row pattern). Endorsement-offer affordance: `[ Endorse @sarah-k ]` inline at the bottom of the endorsements section. 4. **Connection requested by viewer** — footer: `[ Follow ▾ ]` + `[ Requested ]` (disabled). Header has small plain-register stamp: "Requested 2h ago. They'll see this when they next open Peers." 5. **Connection requested by peer (incoming)** — footer: `[ Follow ▾ ]` + `[ Accept ▾ ]` (drop: accept / decline / decline-with-note). Header has working-register banner: "Sarah-K asked to be colleagues. They endorse `bookings-tryst` listing optimization." (Sample-endorsement line is generated from peer's accepted endorsements, not from the request itself.) 6. **Muted by viewer** — footer: `[ Following ▾ ]` + `[ Muted ▾ ]` (drop: unmute / block / disconnect). Endorsements + bio render normally; this is muting, not blocking. Header has working-register chip: "You muted Sarah-K. They don't know." 7. **Blocked by viewer** — full-screen plain-register takeover: "You blocked Sarah-K. They won't see your profile, feed, or any AE surface." Single CTA `[ Unblock ]`. No bio / endorsements / mutual-coops visible — blocker has chosen to not see, honor that. 8. **Viewed-by-blocked-peer (404-style)** — viewer was blocked BY this peer. Plain register full-screen takeover: "This profile isn't available." No bio, no endorsements, no posture, no last-active, no mutual coops, no footer controls — only `[ ← Back ]`. The copy is intentionally ambiguous: does not confirm the peer exists, does not confirm a block. Mirrors HTTP 404 semantics so blocking is non-distinguishable from "deleted profile" or "never existed". Per AE §AE10: a block must not leak its existence to the blocked peer. 9. **Viewed-by-non-discoverable-peer (mutual-coop-only view)** — the peer's posture is `incognito` or `discoverable` and the viewer has no platform-wide visibility, but ≥1 shared coop exists. Renders a stripped layout: handle + posture badge (`incognito` rendered as "—" / "private") + mutual coops chip row + footer with `[ Request colleague ]` *only if* the peer is `discoverable` AND a shared-coop opt-in exists (AE1a); otherwise footer is suppressed. Bio, endorsements, surfaces all hidden. Plain register helper line: "This peer keeps their profile to coops you share." 10. **Viewing your own profile** — redirects to the self-edit profile screen (`peer-profile-edit.screen.md`, out-of-scope here). If somehow rendered: read-only with "This is you — edit in Settings". 11. **Peer is under moderation review** (AE §AE10) — header has plain-register banner: "Profile under review. Endorsements paused." Endorsement section renders with a "paused" overlay; bio + surfaces render normally. Connection actions still live — moderation is on their profile claims, not on contactability. 12. **Peer just dissolved their account / erased (Brief V)** — full-screen plain takeover: "This profile has been erased." Identical visual to state 8 (no leakage about whether it's erasure vs block). Past peer-DM threads still visible in viewer's chat-home audit; this screen only governs profile rendering. 13. **Degraded (M §M2b)** — backend unreachable. Top banner (yellow, plain): "Peer profile can't refresh. Showing cached data from {time}." Connection actions disabled with hint "back online to act." 14. **Offline (M §M2c)** — no cached data: plain register full-screen "Offline. This profile needs a connection." Cached: behave as state 13. ## Interactions / gestures - **Tap surface chip** → surface-kind info sheet (not a deep link; AE doesn't bridge content across providers at P0). - **Tap endorsement claim row** → full endorsement detail sheet (claim text, evidence if any, endorser handles, accepted-at timestamps). - **Tap mutual-coop chip** → opens `coop-drawer.screen.md` for that coop. - **Long-press anywhere on profile body** → moderation menu (same as `[⋯]`). - **Tap footer `Follow` / `Request colleague`** → confirmation sheet ("one-line context" for request; follow is one-tap with subtle haptic). - **Tap `Connected ▾` drop** → context menu: "Start peer-DM", "Endorse @handle", "Disconnect", "Mute", "Block". - **Swipe-down** → dismiss to source surface. - **VoiceOver order** — handle → endorsement-count → posture → freshness → bio → surfaces → endorsements (claim-by-claim) → mutual coops → footer controls. Identity-and-posture-first, action-last. - **Pull-to-refresh** → re-pulls profile; surfaces any state changes (new endorsements, posture changes, connection state advances) with a subtle "updated" pip. - **Tap header endorsement-count badge ★N** → scrolls to endorsements section + expands all groups. ## In-the-wild copy - (working, default empty bio) "No bio yet." — never invent one. - (working, connection request received header banner) "Sarah-K asked to be colleagues. They endorse `bookings-tryst` listing optimization." - (plain, blocked-by-viewer) "You blocked Sarah-K. They won't see your profile, feed, or any AE surface." - (plain, 404 / blocked-by-peer / erased) "This profile isn't available." - (plain, non-discoverable mutual-coop-only) "This peer keeps their profile to coops you share." - (plain, under-review banner) "Profile under review. Endorsements paused." - (working, endorse affordance) "Endorse @sarah-k for…" — sheet opens with scope-claim entry. - (plain, degraded) "Peer profile can't refresh. Showing cached data from 14:02." - (working, requested-stamp) "Requested 2h ago. They'll see this when they next open Peers." - (plain, mute-receipt) "You muted Sarah-K. They don't know." - (plain, moderation menu — block confirmation) "Block Sarah-K? They won't see your profile, your feed, or that you exist on this platform. You can unblock later." ## Privacy invariants - **Blocked peers see state-8 takeover — never anything else.** No leakage of bio, endorsements, posture, or mutual coops. The 404 copy is identical to erased-profile to remove the block signal. - **`last_active_at` is rounded to vigil (~6h buckets) — never precise.** Per AE11 + I append-only audit, precise activity is internal-only. - **No screenshot affordance + iOS screen-capture detection** raises a plain banner (per `coop-drawer.screen.md` privacy pattern): "Screenshots of peer profiles are discouraged. This is a private directory." - **Handle is the only copyable identifier.** Bio, endorsement text, region, langs — none are tap-to-copy. Long-press shows a privacy reminder, not a copy menu. - **Endorser handles in the endorsement list respect the endorser's posture.** If `@q-berlin` is `incognito`, their endorsement still counts toward the public count but their handle renders as `(private peer)` — the endorsement is real, the endorser is hidden. - **Mutual-coop chips never reveal coops the viewer is not in.** The intersection is computed server-side; rows that fail RLS never reach the client. - **No "viewed your profile" telemetry to the peer.** Profile view is audit-only (I); never push-notified to the peer. Only connection requests, endorsement offers, and DM sends generate peer-visible events. - **PII filter applies at render time too**, not only at publish. If K3 list updates post-publish, the bio renders as "Bio under review" stub (per directory screen state 7 pattern). ## Edge cases - **Peer changes posture from `open` → `incognito` while viewer is on the profile** → screen live-transitions to state 9 (or state 8 if no shared coop) with a subtle fade; no error. Connection state (if any) persists per AE11 — past data doesn't disappear, surfacing stops. - **Peer-DM thread exists but peer just blocked viewer** → profile renders as state 8 (404), BUT the past thread in chat-home remains visible to viewer (it's audit-preserved). Viewer cannot send new messages; composer disabled in that thread with plain copy "Thread paused." - **Endorsement count includes an endorsement from a peer who has since been suspended (AE10)** → count holds (it was valid when made); the suspended endorser's handle renders as `(peer suspended)` in the endorser sample. - **Mutual-coop chip for a coop the viewer just left mid-session** → chip removed on next refresh; if tapped mid-stale, opens the coop drawer in "left" state (per `coop-drawer.screen.md` state 6). - **Peer's handle changed since the viewer last loaded** → new handle renders; small footer hint on first load post-change: "@old-handle is now @new-handle." (One-time, not persistent.) Handle history is audit-only. - **Request-colleague with a peer who has rate-limit-capped incoming requests** (AE10 anti-spam) → request still submits; peer's queue holds it; viewer sees `Requested` state. No leakage that the peer is at cap. - **Long-press moderation menu opened on viewer's own self-profile** → menu hides Mute / Block / Report items (those are not self-actionable); shows "Edit profile" + "Posture settings". - **VoiceOver + endorser handle that includes punctuation** → handles render character-by-character on first encounter, then by token; matches handle-screen convention. - **Reduced motion** — replace section-expand animations on "See more" / "See all" with instant reveal. - **Dynamic Type XXL** — header band wraps to 4 lines; endorsements list renders one claim per visible viewport at a time; footer controls stack vertically (Follow on top, Request colleague below). - **Right-to-left locale** (per AD-Q4) — header alignment mirrors; endorsement counts stay numerically rendered; footer button order flips per RTL action precedence. - **Endorsement from a peer in a coop the viewer is NOT in** → endorser handle renders normally; mutual coops section is unaffected. The endorsement's existence is per-peer profile data, not per-coop data. - **Counter-action target** (per I) — viewer who blocked-by-mistake can unblock; unblock writes a counter-action audit row but does NOT notify the formerly-blocked peer. Block + unblock both stay silent from the peer's side. - **`[open]` Endorsement freshness decay (AE-Q5)** — should the endorsement count visually weight recent endorsements higher (e.g. ★ 12 with subtle dim on >6mo-old)? Open. Lean: dim, but keep the count integer. - **`[open]` Peer-DM "Threads" section in state 3** — currently sketched as 1–3 recent thread snippets inline. Open whether this is too aggressive a surface (might leak thread context to a shoulder-surfer). Lean: behind a tap-to-reveal toggle off by default. ## Related - [Brief AE §AE5](./AE-provider-social-network.brief.md) — profile schema source. - [Brief AE §AE2](./AE-provider-social-network.brief.md) — connection state machine driving footer controls. - [Brief AE §AE7](./AE-provider-social-network.brief.md) — endorsements section + endorse affordance. - [Brief AE §AE10](./AE-provider-social-network.brief.md) — moderation menu, under-review state, block invariants. - [Brief AE §AE11](./AE-provider-social-network.brief.md) — posture-derived rendering (states 7, 8, 9, 12). - [`peer-directory.screen.md`](./peer-directory.screen.md) — primary entrypoint. - [`chat-home.screen.md`](./chat-home.screen.md) — peer-DM thread row pattern reused in state 3. - [`coop-drawer.screen.md`](./coop-drawer.screen.md) — destination of mutual-coop chip tap. - [Brief N](./N-provider-coop.brief.md) — mutual-coop intersection source. - [Brief K §K3](./K-safety-blocklist.brief.md) — PII gate on bio + render-time filter. - [Brief AD](./AD-multilingual-opaque.brief.md) — bio render in viewer's locale (per AD3 re-authoring). - [Brief I](./I-audit-trust-replay.brief.md) — every connection action + endorsement + block is an `agent_actions` row. - [Brief V](./V-data-portability-erasure.brief.md) — erasure-vs-block indistinguishability in state 8 + 12. - [Brief X](./X-accessibility.brief.md) — VoiceOver order + reduced motion. - [Brief O](./O-surface-kinds.brief.md) — surface_kind vocabulary. - [`00-system-voice.md`](./00-system-voice.md) §V2b working / §V2c plain. ## Out of scope - The self-edit profile screen (`peer-profile-edit.screen.md` — separate file). - The full endorsements sheet (state 3 "See all" destination). - The endorse compose sheet (own `.screen.md` if it grows). - Peer-DM thread interior (lives in chat-home thread variant). - The referral-introduction compose sheet (AE1b — own `.screen.md`). - Org-attributed peer profiles (deferred to AE-Q6 P5+). - iPad / web variants. ## Open questions - **Endorsement freshness decay surface treatment** (AE-Q5) — dim old endorser handles vs. visually re-rank vs. footnote a "freshness" line. `[exploratory]` (lean: dim handles older than 6mo; keep count integer; surface decay only on tap-to-detail). - **State 3 Threads section default visibility** — show inline (current sketch) or hide behind tap-to-reveal? `[blocking]` (lean: hide-by-default to avoid shoulder-surf leak; show on tap with subtle haptic). - **`[open]` Posture badge rendering for `incognito` peers in state 9** — currently "—" / "private". Open whether posture should be hidden entirely from incognito-peer views (so even the existence of a posture is not signaled). `[nice-to-have]` (lean: hide entirely; the mutual-coop-only stripped layout is already a posture signal in itself).