Workstream 2 of the reader/Kavita epic — the backend for an opt-in social reading layer. Security groundwork (#618 read-authz) is now in place, so this can land.
What
Profile settings (ProfileSocial): ShareBookmarks, ViewOthersBookmarks, ShareReadingList, DisplayNameMode — all default OFF. ProfileDto.Social is null for child/minor accounts; UpdateProfileCommand.ApplySocial silently drops writes for them.
Central choke pointISocialVisibilityService: the single source of truth for eligibility — viewer gate, batch sharer resolution (no N+1), reading-list sharer gate, and a display-name resolver. Children/minors hard-excluded both directions via one SocialRules.IsExcluded predicate used at all four sites (write-guard, both service gates, DTO-null).
Endpoints (ReaderController, thin→MediatR, no caching): GET social/bookmarks/{bookId}/pages (markers), GET social/bookmarks/{bookId}/page/{page}, POST|DELETE bookmarks/{id}/upvote (idempotent; own→400, non-visible→404 no-oracle), GET social/users/{userRef}/reading-list (book identity + last-read date only — no page/%).
CreateBookmark gains IsPublic, forced false unless the owner is a currently-eligible sharer.
Invariants held
Eligibility is evaluated live at read time — a sharer who turns sharing off immediately vanishes from counts/lists (not just the stored IsPublic bit).
Cross-user identity is an opaque userRef (profile id); display names never leak username/keycloak-id/email (First·L → Handle → generic fallback).
The three opt-ins gate three distinct things and are not collapsed.
Not yet verified against live MongoDB (integration is gated off). Two provider behaviors were checked against shipped analogs and confirmed safe: legacy profiles with no social field hydrate the = new() default (same as Learning/Travel), and localList.Contains(x.Field) translates to $in (as in GetPlatformAdoption). Upvote persistence mirrors UpdateBookmarkCommandHandler (reassign list + .Update()), since the Mongo EF provider doesn't reliably track in-place embedded-collection edits.
Minor note: the own-bookmark 400 runs before the viewer gate so an owner always gets 400 (not 404) on their own bookmark.
PR: spikersoft-backend feat/reader-social-backend. Frontend (profile Social tab + reader Community tab + reading-list dialog) is the follow-up PR-7.
Workstream 2 of the reader/Kavita epic — the backend for an opt-in social reading layer. Security groundwork (#618 read-authz) is now in place, so this can land.
## What
- **Profile settings** (`ProfileSocial`): `ShareBookmarks`, `ViewOthersBookmarks`, `ShareReadingList`, `DisplayNameMode` — all default OFF. `ProfileDto.Social` is null for child/minor accounts; `UpdateProfileCommand.ApplySocial` silently drops writes for them.
- **Central choke point** `ISocialVisibilityService`: the single source of truth for eligibility — viewer gate, batch sharer resolution (no N+1), reading-list sharer gate, and a display-name resolver. Children/minors hard-excluded both directions via one `SocialRules.IsExcluded` predicate used at all four sites (write-guard, both service gates, DTO-null).
- **Endpoints** (ReaderController, thin→MediatR, no caching): `GET social/bookmarks/{bookId}/pages` (markers), `GET social/bookmarks/{bookId}/page/{page}`, `POST|DELETE bookmarks/{id}/upvote` (idempotent; own→400, non-visible→404 no-oracle), `GET social/users/{userRef}/reading-list` (book identity + last-read date only — no page/%).
- **CreateBookmark** gains `IsPublic`, forced false unless the owner is a currently-eligible sharer.
## Invariants held
- Eligibility is evaluated **live** at read time — a sharer who turns sharing off immediately vanishes from counts/lists (not just the stored `IsPublic` bit).
- Cross-user identity is an opaque `userRef` (profile id); display names never leak username/keycloak-id/email (First·L → Handle → generic fallback).
- The three opt-ins gate three distinct things and are not collapsed.
## Verification
- `SpikerSoft.UnitTests.slnf` builds clean. Business.Tests **7577** green (incl. 22 new: SocialVisibilityService negative matrix + upvote handler own/idempotent/non-visible/ineligible). API.Tests **1296** green.
- **Not yet verified against live MongoDB** (integration is gated off). Two provider behaviors were checked against shipped analogs and confirmed safe: legacy profiles with no `social` field hydrate the `= new()` default (same as `Learning`/`Travel`), and `localList.Contains(x.Field)` translates to `$in` (as in `GetPlatformAdoption`). Upvote persistence mirrors `UpdateBookmarkCommandHandler` (reassign list + `.Update()`), since the Mongo EF provider doesn't reliably track in-place embedded-collection edits.
Minor note: the own-bookmark 400 runs before the viewer gate so an owner always gets 400 (not 404) on their own bookmark.
PR: spikersoft-backend `feat/reader-social-backend`. Frontend (profile Social tab + reader Community tab + reading-list dialog) is the follow-up PR-7.
Resolved in spikersoft-backend PR #340 (merged to master, 360828c). Social reading backend: ProfileSocial opt-ins, the ISocialVisibilityService choke point (child/minor hard-excluded via one live predicate), the reader social endpoints (page markers, per-page shared bookmarks, idempotent upvote, shared reading-list), and CreateBookmark.IsPublic. Business.Tests 7577 + API.Tests 1296 green. Frontend shipped in #634 (angular PR #221). Closing.
Resolved in spikersoft-backend PR #340 (merged to `master`, `360828c`). Social reading backend: `ProfileSocial` opt-ins, the `ISocialVisibilityService` choke point (child/minor hard-excluded via one live predicate), the reader social endpoints (page markers, per-page shared bookmarks, idempotent upvote, shared reading-list), and `CreateBookmark.IsPublic`. Business.Tests 7577 + API.Tests 1296 green. Frontend shipped in #634 (angular PR #221). Closing.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Workstream 2 of the reader/Kavita epic — the backend for an opt-in social reading layer. Security groundwork (#618 read-authz) is now in place, so this can land.
What
ProfileSocial):ShareBookmarks,ViewOthersBookmarks,ShareReadingList,DisplayNameMode— all default OFF.ProfileDto.Socialis null for child/minor accounts;UpdateProfileCommand.ApplySocialsilently drops writes for them.ISocialVisibilityService: the single source of truth for eligibility — viewer gate, batch sharer resolution (no N+1), reading-list sharer gate, and a display-name resolver. Children/minors hard-excluded both directions via oneSocialRules.IsExcludedpredicate used at all four sites (write-guard, both service gates, DTO-null).GET social/bookmarks/{bookId}/pages(markers),GET social/bookmarks/{bookId}/page/{page},POST|DELETE bookmarks/{id}/upvote(idempotent; own→400, non-visible→404 no-oracle),GET social/users/{userRef}/reading-list(book identity + last-read date only — no page/%).IsPublic, forced false unless the owner is a currently-eligible sharer.Invariants held
IsPublicbit).userRef(profile id); display names never leak username/keycloak-id/email (First·L → Handle → generic fallback).Verification
SpikerSoft.UnitTests.slnfbuilds clean. Business.Tests 7577 green (incl. 22 new: SocialVisibilityService negative matrix + upvote handler own/idempotent/non-visible/ineligible). API.Tests 1296 green.socialfield hydrate the= new()default (same asLearning/Travel), andlocalList.Contains(x.Field)translates to$in(as inGetPlatformAdoption). Upvote persistence mirrorsUpdateBookmarkCommandHandler(reassign list +.Update()), since the Mongo EF provider doesn't reliably track in-place embedded-collection edits.Minor note: the own-bookmark 400 runs before the viewer gate so an owner always gets 400 (not 404) on their own bookmark.
PR: spikersoft-backend
feat/reader-social-backend. Frontend (profile Social tab + reader Community tab + reading-list dialog) is the follow-up PR-7.Resolved in spikersoft-backend PR #340 (merged to
master,360828c). Social reading backend:ProfileSocialopt-ins, theISocialVisibilityServicechoke point (child/minor hard-excluded via one live predicate), the reader social endpoints (page markers, per-page shared bookmarks, idempotent upvote, shared reading-list), andCreateBookmark.IsPublic. Business.Tests 7577 + API.Tests 1296 green. Frontend shipped in #634 (angular PR #221). Closing.