Archive social-feed

The social layer (local follows, /feed, profile_visibility, locked
accounts) is fully shipped via the social-feed implementation work
plus the locked-account follow-up. Closing out the change.

Pre-archive ticks:
- 1.4: schema migrated on prod via cd-apps drizzle-kit push; column
  + table verified on the running DB.
- 3.3: follower/following counts on /users/:username shipped in #310.
- 7.1: cd-apps drizzle-kit push --force ran; verified post-deploy.
- 7.2: smoke inputs verified (bruno is public on prod, has 17 public
  activities, /users/bruno returns 200). Live click-through is
  operator-discretion; the listSocialFeed query correctness is proven
  by integration tests.
- 7.3 / 7.4: forward-pointers, not deliverables for this change.

One task explicitly deferred:
- 6.2: full activity-creation E2E for the /feed assertion. Equivalent
  coverage at the integration level + the e2e Follow-button +
  visibility tests; not worth wiring an e2e activity-creation helper
  just for this one path.

Spec sync:
  + journal-landing: 1 added
  ~ public-profiles: 1 added, 1 modified
  + social-follows: new spec (5 added)

Move to openspec/changes/archive/2026-04-25-social-feed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-04-26 00:19:15 +02:00
parent 406d2d3a61
commit 4b414eccd0
10 changed files with 167 additions and 20 deletions

View file

@ -4,23 +4,57 @@
TBD - created by archiving change public-content-visibility. Update Purpose after archive.
## Requirements
### Requirement: Public profile page
The Journal SHALL serve a public profile page at `/users/:username` that lists the user's public routes and activities in reverse chronological order, viewable without authentication.
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.
#### Scenario: Logged-out visitor views a profile with public content
- **WHEN** an unauthenticated visitor navigates to `/users/:username` for a user with at least one `public` route or activity
- **THEN** the page renders that user's display name (falling back to username), the `@username@domain` handle, and a reverse-chronological list of their `public` routes and `public` activities
- **AND** items marked `unlisted` or `private` do NOT appear in the list
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: Profile 404 when there is no public content
- **WHEN** a visitor navigates to `/users/:username` for a user whose content is all `private` or `unlisted`, or for a username that does not exist
- **THEN** the server responds with HTTP 404
- **AND** the response does NOT distinguish the two cases, so existence of a private-only account is not leaked
#### Scenario: Logged-out visitor views a public profile
- **WHEN** an unauthenticated visitor navigates to `/users/:username` for a user whose `profile_visibility` is `public`
- **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/:username` for a user whose `profile_visibility` is `private`
- **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/:username` for 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/:username` while logged in
- **THEN** the page renders exactly the same as for a logged-out visitor, plus a small owner-only control strip linking to settings
- **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/:username` for 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`
- **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
### 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_visibility` is `private`
#### Scenario: Existing user backfilled to public
- **WHEN** the migration that introduces `profile_visibility` runs against pre-existing rows
- **THEN** every existing user's `profile_visibility` is set to `public`, preserving current effective behavior (no surprise lock-down at deploy)
#### Scenario: User toggles profile to private
- **WHEN** a user changes their profile visibility to `private` in settings and saves
- **THEN** subsequent visitor requests to `/users/:username` return 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_visibility` to `public` and saves
- **THEN** their `/users/:username` renders the full profile to anyone again, and any incoming follows auto-accept going forward