Skip to content

Add community features (Phase 1 - API skeleton + client UI)#9

Closed
Eoic wants to merge 17 commits intomasterfrom
community-features-phase1
Closed

Add community features (Phase 1 - API skeleton + client UI)#9
Eoic wants to merge 17 commits intomasterfrom
community-features-phase1

Conversation

@Eoic
Copy link
Member

@Eoic Eoic commented Feb 23, 2026

Summary

  • Server: Add Alembic migrations setup, 9 SQLAlchemy ORM models (User, Follow, Block, CatalogBook, UserBook, Rating, Review, ReviewReaction, Activity), 7 Pydantic schema modules, and 7 route modules (profiles, social, catalog, ratings, reviews, user-books, feed) — all returning stub data for now
  • Client: Add API client with Firebase JWT auth, 4 community data models, 2 providers (CommunityProvider, SocialProvider), Community tab in bottom nav with Feed/Discover pages, user profile page, community book page, and write review page
  • Tests: 32 new server tests (140 total), 40 new client tests (510 total)

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.md

Test plan

  • Server: ruff format --check . clean
  • Server: ruff check . clean
  • Server: pytest — 140 passed
  • Client: dart format clean
  • Client: dart analyze — no issues
  • Client: flutter test — 510 passed

🤖 Generated with Claude Code

Eoic and others added 17 commits February 23, 2026 02:16
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>
Copilot AI review requested due to automatic review settings February 23, 2026 01:22
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +48 to +50
isbn: str | None = Field(None, max_length=13)
cover_url: str | None = Field(None, max_length=500)
description: str | None = None
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +24
title: str
author: str
cover_url: str | None = None

Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +26
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()
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +80 to +82
@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."""
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +84 to +87
_viewedProfile = _viewedProfile!.copyWith(
isFollowing: false,
followerCount: _viewedProfile!.followerCount - 1,
);
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +42 to +46
author: authorStr,
authors: authors?.cast<String>(),
coverImageUrl: json['cover_url'] as String?,
description: json['description'] as String?,
pageCount: json['page_count'] as int?,
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
catalogBookId: json['catalog_book_id'] as String,
title: json['title'] as String,
author: json['author'] as String,
coverImageUrl: json['cover_url'] as String?,
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
coverImageUrl: json['cover_url'] as String?,
coverImageUrl: (json['cover_image_url'] ?? json['cover_url']) as String?,

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +16
catalog_book_id: UUID
title: str
author: str
cover_url: str | None = None
average_rating: float | None = None
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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',

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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(),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@Eoic Eoic self-assigned this Feb 23, 2026
@Eoic Eoic closed this Feb 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants