Add community features (Phase 1 - API skeleton + client UI)#9
Add community features (Phase 1 - API skeleton + client UI)#9
Conversation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add CommunityProvider for feed, catalog search, book details, ratings, and reviews. Add SocialProvider for user profiles, follow/unfollow, block/unblock, follow lists, and user search. Both are online-only with TODO stubs for API integration. Includes 31 tests covering initial state, loading transitions, and listener notifications. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Import models in Alembic env.py so autogenerate detects tables - Make ApiResponse.json/jsonList nullable for 204 No Content responses - Add reaction_type path param to remove_reaction endpoint Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Phase 1 of “community features” adds a stubbed community API surface on the server (schemas/models/routes + Alembic scaffolding) and a matching client UI shell (models/providers/pages + API client), with tests covering the new endpoints/state.
Changes:
- Server: introduce community route modules (profiles/social/catalog/ratings/reviews/user-books/feed) returning stub responses, plus new schemas/models and Alembic setup.
- Client: add Community tab + pages/widgets, community models/providers, and an authenticated API client.
- Tests: add new server and client tests for the community surface.
Reviewed changes
Copilot reviewed 57 out of 58 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| server/tests/test_user_books.py | Tests for user-books endpoints |
| server/tests/test_social.py | Tests for social endpoints |
| server/tests/test_reviews.py | Tests for review endpoints |
| server/tests/test_ratings.py | Tests for rating endpoints |
| server/tests/test_profiles.py | Tests for profile endpoints |
| server/tests/test_feed.py | Tests for feed endpoints |
| server/tests/test_catalog.py | Tests for catalog endpoints |
| server/src/papyrus/schemas/social.py | Social response schemas |
| server/src/papyrus/schemas/community_user_book.py | User-book schemas |
| server/src/papyrus/schemas/community_review.py | Review schemas |
| server/src/papyrus/schemas/community_rating.py | Rating schemas |
| server/src/papyrus/schemas/community_profile.py | Profile schemas |
| server/src/papyrus/schemas/community_activity.py | Feed activity schemas |
| server/src/papyrus/schemas/catalog.py | Community catalog schemas |
| server/src/papyrus/models/user_book.py | ORM model: user book status |
| server/src/papyrus/models/user.py | ORM model: community user |
| server/src/papyrus/models/review_reaction.py | ORM model: review reactions |
| server/src/papyrus/models/review.py | ORM model: reviews |
| server/src/papyrus/models/rating.py | ORM model: ratings |
| server/src/papyrus/models/follow.py | ORM model: follows |
| server/src/papyrus/models/catalog_book.py | ORM model: catalog book |
| server/src/papyrus/models/block.py | ORM model: blocks |
| server/src/papyrus/models/activity.py | ORM model: activity feed |
| server/src/papyrus/models/init.py | Model registration exports |
| server/src/papyrus/api/routes/user_books.py | Stub user-books routes |
| server/src/papyrus/api/routes/social.py | Stub social routes |
| server/src/papyrus/api/routes/reviews.py | Stub review routes |
| server/src/papyrus/api/routes/ratings.py | Stub rating routes |
| server/src/papyrus/api/routes/profiles.py | Stub profile routes |
| server/src/papyrus/api/routes/feed.py | Stub feed routes |
| server/src/papyrus/api/routes/catalog.py | Stub catalog routes |
| server/src/papyrus/api/routes/init.py | Registers new routers |
| server/alembic/script.py.mako | Alembic revision template |
| server/alembic/env.py | Alembic env (async) |
| server/alembic/README | Alembic readme |
| server/alembic.ini | Alembic configuration |
| client/test/services/api_client_test.dart | ApiClient unit tests |
| client/test/providers/social_provider_test.dart | SocialProvider state tests |
| client/test/providers/community_provider_test.dart | CommunityProvider state tests |
| client/test/models/community_models_test.dart | Community model JSON tests |
| client/lib/widgets/shell/adaptive_app_shell.dart | Adds Community nav item |
| client/lib/widgets/community/discover_content.dart | Discover tab placeholder UI |
| client/lib/widgets/community/activity_feed_list.dart | Feed list UI shell |
| client/lib/widgets/community/activity_card.dart | Feed card UI shell |
| client/lib/services/api_client.dart | Firebase-auth API client |
| client/lib/providers/social_provider.dart | Social state management |
| client/lib/providers/community_provider.dart | Community state management |
| client/lib/pages/write_review_page.dart | Write review UI shell |
| client/lib/pages/user_profile_page.dart | Profile page UI shell |
| client/lib/pages/community_page.dart | Community Feed/Discover tabs |
| client/lib/pages/community_book_page.dart | Community book page UI shell |
| client/lib/models/community_user.dart | Community user model |
| client/lib/models/community_review.dart | Community review model |
| client/lib/models/catalog_book.dart | Community catalog book model |
| client/lib/models/activity_item.dart | Feed activity model |
| client/lib/main.dart | Registers new providers |
| client/lib/config/app_router.dart | Adds community routes |
| .gitignore | Adds .worktrees ignore |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| isbn: str | None = Field(None, max_length=13) | ||
| cover_url: str | None = Field(None, max_length=500) | ||
| description: str | None = None |
There was a problem hiding this comment.
CreateCatalogBookRequest exposes cover_url, which diverges from the repo’s existing cover_image_url naming used by the main book API. Aligning request/response field names now will avoid needing client-side branching later.
| title: str | ||
| author: str | ||
| cover_url: str | None = None | ||
|
|
There was a problem hiding this comment.
ActivityBook uses cover_url, but the rest of the API uses cover_image_url for cover fields. To keep the JSON contract consistent (and reduce client model duplication), consider renaming this to cover_image_url across the community feed schemas and routes.
| authors: Mapped[dict | None] = mapped_column(JSONB) | ||
| cover_url: Mapped[str | None] = mapped_column(sa.String(500)) | ||
| description: Mapped[str | None] = mapped_column(sa.Text) | ||
| page_count: Mapped[int | None] = mapped_column() | ||
| published_date: Mapped[date | None] = mapped_column() |
There was a problem hiding this comment.
authors and genres are typed as dict | None, but the corresponding API schema uses list[str] (papyrus/schemas/catalog.py). This mismatch will cause confusion and likely type issues once the ORM is wired. Consider changing these column Python types to list[str] | None (still backed by JSONB) to match the API contract.
| @router.delete("/{review_id}/react/{reaction_type}", status_code=status.HTTP_204_NO_CONTENT) | ||
| async def remove_reaction(user_id: CurrentUserId, review_id: UUID, reaction_type: str) -> Response: | ||
| """Remove a specific reaction from a review.""" |
There was a problem hiding this comment.
reaction_type is a free-form str path parameter, so invalid values will be accepted. Since the request schema constrains reactions to like|helpful, consider typing this path param as an enum/Literal (or applying a regex constraint) to get consistent validation and OpenAPI docs.
| _viewedProfile = _viewedProfile!.copyWith( | ||
| isFollowing: false, | ||
| followerCount: _viewedProfile!.followerCount - 1, | ||
| ); |
There was a problem hiding this comment.
followerCount is decremented optimistically without clamping, which can produce a negative count (e.g., when the initial count is 0). Consider using max(0, followerCount - 1) to keep the local state valid.
| author: authorStr, | ||
| authors: authors?.cast<String>(), | ||
| coverImageUrl: json['cover_url'] as String?, | ||
| description: json['description'] as String?, | ||
| pageCount: json['page_count'] as int?, |
There was a problem hiding this comment.
This model reads/writes the community catalog cover field as cover_url, which is inconsistent with the existing cover_image_url key used by the main Book model (client/lib/models/book.dart). Consider aligning the JSON key to cover_image_url to avoid needing two cover-field conventions in the client.
| catalogBookId: json['catalog_book_id'] as String, | ||
| title: json['title'] as String, | ||
| author: json['author'] as String, | ||
| coverImageUrl: json['cover_url'] as String?, |
There was a problem hiding this comment.
This feed model expects cover_url, but elsewhere in the client/API the cover field is cover_image_url. Aligning the key name across APIs would reduce client-side branching and model duplication.
| coverImageUrl: json['cover_url'] as String?, | |
| coverImageUrl: (json['cover_image_url'] ?? json['cover_url']) as String?, |
| catalog_book_id: UUID | ||
| title: str | ||
| author: str | ||
| cover_url: str | None = None | ||
| average_rating: float | None = None |
There was a problem hiding this comment.
The community catalog schemas use cover_url, but the existing book API uses cover_image_url (e.g., papyrus/schemas/book.py). Having two different JSON keys for the same concept will make the client/API inconsistent and harder to integrate. Consider renaming this to cover_image_url (and updating related schemas/routes) to match the established convention.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 21f1040a06
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| routes: [ | ||
| GoRoute( | ||
| name: 'USER_PROFILE', | ||
| path: 'user/:userId', |
There was a problem hiding this comment.
Unify profile identifier across client and server routes
The client routes community profiles by userId (/community/user/:userId), while the server only exposes GET /v1/profiles/{username} (server/src/papyrus/api/routes/profiles.py). This identifier mismatch means the profile flow will break as soon as SocialProvider.loadUserProfile is wired to the API, because UUID-based navigation and username-based lookup are not interchangeable. Pick one canonical identifier (username or user_id) and use it consistently in both route contracts.
Useful? React with 👍 / 👎.
| if (_viewedProfile != null && _viewedProfile!.userId == targetUserId) { | ||
| _viewedProfile = _viewedProfile!.copyWith( | ||
| isFollowing: false, | ||
| followerCount: _viewedProfile!.followerCount - 1, |
There was a problem hiding this comment.
Clamp optimistic unfollow count at zero
unfollowUser always subtracts 1 from followerCount for the viewed profile, even if the user is already not followed or the count is already zero. In repeated taps/retries this drives the UI state negative and out of sync with server truth. Guarding on isFollowing and clamping the floor to 0 avoids invalid follower counts.
Useful? React with 👍 / 👎.
| async def get_profile_by_username(user_id: CurrentUserId, username: str) -> CommunityProfile: | ||
| """Return a user's public community profile.""" | ||
| return CommunityProfile( | ||
| user_id=uuid4(), |
There was a problem hiding this comment.
Return deterministic IDs from username profile lookup
get_profile_by_username generates user_id with uuid4() on every request, so the same username receives a different ID each time. Any client behavior keyed by user_id (caching, follow/block actions, optimistic updates) becomes unstable across refreshes. Even for stubbed responses, this endpoint should return a stable ID for a given username.
Useful? React with 👍 / 👎.
Summary
Phase 1 establishes the full API contract and client UI shell. All routes return hardcoded stub responses — database wiring comes in Phase 2.
Design doc:
docs/plans/2026-02-23-community-features-design.mdTest plan
ruff format --check .cleanruff check .cleanpytest— 140 passeddart formatcleandart analyze— no issuesflutter test— 510 passed🤖 Generated with Claude Code