170 lines
20 KiB
Markdown
170 lines
20 KiB
Markdown
|
|
# 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).
|