Replaces the earlier 404-for-private model with Mastodon-style locked accounts. A private profile now returns 200 with a stub layout and gates content behind follow approval. Default for new users flips from 'public' to 'private' to align with trails.cool's privacy-first content defaults. Schema: - users.profile_visibility default flipped to 'private'. Existing rows remain 'public' (backfill on first migration handled them). Follow API (follow.server.ts): - followUser now creates Pending (accepted_at = NULL) against private targets and Accepted against public targets — no more refusal. - New: countPendingFollowRequests, listPendingFollowRequests, approveFollowRequest, rejectFollowRequest. Approve/reject are owner-bound: only the followed user can act on their own incoming requests. - countFollowers / countFollowing / listFollowers / listFollowing now filter to accepted-only relations. Loader (users.$username.tsx): - Drops the 404 paths. New canSeeContent flag = isOwn || profile_visibility='public' || (followState.following === true). - When canSeeContent=false, render a stub: header + 🔒 badge + body copy + Request-to-follow / sign-in CTA. Routes/activities sections are not rendered. UI: - FollowButton gains a "Request to follow" / "Requested" state for private targets via a new isPrivateTarget prop. Cancel-request reuses the unfollow endpoint. - New /follows/requests page lists incoming Pending requests with Approve / Reject buttons. - New API routes: POST /api/follows/:id/approve and /reject. - Navbar shows a count badge linking to /follows/requests when pending > 0. Privacy manifest already documents the follows relation; no changes needed (the locked-account semantics don't add new data — same row, different lifecycle). Specs / design (social-feed change): - public-profiles delta rewritten around the four-mode locked model (public, private+anon, private+pending, private+accepted) with scenarios for each. - social-follows delta gains Pending lifecycle requirements (auto vs. manual accept, approve/reject endpoints, pending request management, Pending follows do not contribute to feed). - design.md decision section reflects the new model and rationale for default-private; non-goal "locked-local-accounts as a follow-up" is removed since this change ships it. Tests: - follow.integration.test.ts: pending-against-private, approve flips to accepted, reject deletes, owner-bound enforcement. - e2e/social.test.ts: full Request → Pending → Approve → full-view flow, plus stub-for-anonymous and /follows/requests auth gate. Supersedes PR #309 (closed): the empty-public-profile 200 is now a side-effect of the new render path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4.9 KiB
ADDED Requirements
Requirement: Profile visibility setting (locked accounts)
Every user SHALL have an explicit profile_visibility of public or private. New accounts SHALL default to private (locked: profile is reachable but content is gated behind follow approval). Users SHALL be able to change their profile visibility from account settings at any time. private is functionally Mastodon's "locked" / "manually approves followers" — visitors see a stub with a Request-to-follow button, follows land in a Pending state, and content is only revealed to accepted followers.
Scenario: Default for a new account
- WHEN a user registers
- THEN their
profile_visibilityisprivate
Scenario: Existing user backfilled to public
- WHEN the migration that introduces
profile_visibilityruns against pre-existing rows - THEN every existing user's
profile_visibilityis set topublic, preserving current effective behavior (no surprise lock-down at deploy)
Scenario: User toggles profile to private
- WHEN a user changes their profile visibility to
privatein settings and saves - THEN subsequent visitor requests to
/users/:usernamereturn HTTP 200 but render a stub page (header + Request-to-follow button) with no content; existing follow rows are unaffected; new follows from anyone other than already-accepted followers are recorded as Pending
Scenario: User toggles profile back to public
- WHEN a previously-private user switches
profile_visibilitytopublicand saves - THEN their
/users/:usernamerenders the full profile to anyone again, and any incoming follows auto-accept going forward
MODIFIED Requirements
Requirement: Public profile page
The Journal SHALL serve a profile page at /users/:username for any user who exists. The render SHALL depend on the viewer relationship to the profile owner:
- If the username does not exist, return HTTP 404.
- If the owner's
profile_visibility = 'public', render the full profile (display name, handle, follower/following counts, public routes, public activities) regardless of who is viewing. - If the owner's
profile_visibility = 'private', render a stub page (header + handle + counts + lock badge) for non-owner viewers who do NOT have an accepted follow relation. Render the full profile for the owner themselves and for accepted followers.
The page SHALL display follower and following counts (always, regardless of stub vs. full). For signed-in viewers other than the owner, the page SHALL render a Follow button whose label depends on the relation: "Follow" against a public profile with no relation, "Request to follow" against a private profile with no relation, "Requested" (cancellable) when a Pending request exists, "Unfollow" when an accepted relation exists.
Scenario: Logged-out visitor views a public profile
- WHEN an unauthenticated visitor navigates to
/users/:usernamefor a user whoseprofile_visibilityispublic - THEN the page renders the full profile (display name, handle, counts, public routes, public activities)
Scenario: Logged-out visitor views a private profile
- WHEN an unauthenticated visitor navigates to
/users/:usernamefor a user whoseprofile_visibilityisprivate - THEN the page returns HTTP 200 and renders the stub layout — header with display name, handle, and counts; a 🔒 "Private" badge; a body block with "This profile is private" and a sign-in prompt; no routes or activities are rendered
Scenario: Signed-in visitor views a private profile they don't follow
- WHEN a signed-in user other than the owner navigates to
/users/:usernamefor a private profile, with no follow row OR a Pending follow row - THEN the page returns 200 and renders the stub layout, plus a Follow button (or Pending indicator) so the viewer can request access
Scenario: Signed-in viewer with accepted follow sees full private profile
- WHEN a signed-in user with an accepted follow against a private user navigates to that user's profile
- THEN the page returns 200 and renders the full profile — same as a public profile would render — plus an Unfollow button
Scenario: Owner sees their own profile
- WHEN a user navigates to their own
/users/:usernamewhile logged in - THEN the full profile renders regardless of
profile_visibility, plus a small owner-only banner (blue "ownNote" for public profiles, amber "private/locked" explanation for private profiles), plus a link to settings; no Follow button is shown
Scenario: Profile 404 only for nonexistent users
- WHEN a visitor navigates to
/users/:usernamefor a username that does not exist - THEN the server responds with HTTP 404
Scenario: Profile page emits social-share metadata
- WHEN any visitor loads a populated
/users/:username(full or stub) - THEN the page emits Open Graph tags (
og:title,og:site_name,og:type="profile") so links shared on social platforms render a meaningful preview