Locked-account profiles: private = stub + Pending follow flow
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>
This commit is contained in:
parent
ede68712a3
commit
5da7ffa037
17 changed files with 656 additions and 181 deletions
|
|
@ -34,11 +34,14 @@ export const users = journalSchema.table("users", {
|
|||
displayName: text("display_name"),
|
||||
bio: text("bio"),
|
||||
domain: text("domain").notNull(),
|
||||
// Whether the user is discoverable on this instance and (later) over
|
||||
// ActivityPub. `private` 404s the profile and disables follows; `public`
|
||||
// means /users/:username renders when the user has any public content.
|
||||
// See spec: journal-landing + public-profiles + social-follows.
|
||||
profileVisibility: text("profile_visibility").$type<ProfileVisibility>().notNull().default("public"),
|
||||
// Profile visibility / lock setting. `public` means anyone can view
|
||||
// the profile and follows auto-accept. `private` is Mastodon-style
|
||||
// locked: the profile renders a stub for non-followers, and follows
|
||||
// require manual approval (Pending → Accepted via /follows/requests).
|
||||
// New users default to `private` to match trails.cool's privacy-first
|
||||
// content defaults; existing users were backfilled to `public` by an
|
||||
// earlier migration so behavior didn't change for them.
|
||||
profileVisibility: text("profile_visibility").$type<ProfileVisibility>().notNull().default("private"),
|
||||
termsAcceptedAt: timestamp("terms_accepted_at", { withTimezone: true }),
|
||||
termsVersion: text("terms_version"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue