Feat/boost#197
Open
nekochanfood wants to merge 34 commits into
Open
Conversation
Add reference_id column to posts table to support boosts (pure reposts) and quotes (reposts with commentary). A boost is internally a post that references another post via reference_id. - Add migration with reference_id column, self-reference check constraint, boost count index, and partial unique index preventing duplicate pure boosts - Extend OpenAPI schema with referenceId, reference, and boostCount fields on Post, and referenceId on CreatePostRequest - Update all SQL queries to include reference_id in SELECT/INSERT - Add CountBoostsByPostIDs query for batch boost counting - Implement attachBoostCountsToPosts and attachReferencesToPosts in service layer with one-level-deep reference expansion - Validate referenced post existence, handle duplicate pure boost as 409 - Wire boost enrichment into all post fetch paths (Get, GetContext, GetThread, ListByUsername, ListReplies, Timeline)
When a post's content contains a URL pointing to another post (e.g. https://example.com/posts/{uuid}), automatically treat it as a quote reference. Explicit referenceId in the request takes priority. If the URL points to a non-existent or deleted post, the reference is silently ignored and the post is created normally.
Match any https?:// URL with a /posts/{uuid} path instead of
requiring the backend's PUBLIC_BASE_URL. This ensures detection
works when frontend and backend are on different domains.
Add reference_id column to posts table to support boosts (pure reposts) and quotes (reposts with commentary). A boost is internally a post that references another post via reference_id. - Add migration with reference_id column, self-reference check constraint, boost count index, and partial unique index preventing duplicate pure boosts - Extend OpenAPI schema with referenceId, reference, and boostCount fields on Post, and referenceId on CreatePostRequest - Update all SQL queries to include reference_id in SELECT/INSERT - Add CountBoostsByPostIDs query for batch boost counting - Implement attachBoostCountsToPosts and attachReferencesToPosts in service layer with one-level-deep reference expansion - Validate referenced post existence, handle duplicate pure boost as 409 - Wire boost enrichment into all post fetch paths (Get, GetContext, GetThread, ListByUsername, ListReplies, Timeline)
When a post's content contains a URL pointing to another post (e.g. https://example.com/posts/{uuid}), automatically treat it as a quote reference. Explicit referenceId in the request takes priority. If the URL points to a non-existent or deleted post, the reference is silently ignored and the post is created normally.
Match any https?:// URL with a /posts/{uuid} path instead of
requiring the backend's PUBLIC_BASE_URL. This ensures detection
works when frontend and backend are on different domains.
^ Conflicts: ^ apps/frontend/components/PostCard.tsx
Add "embedded" PostCard variant for rendering quoted/boosted posts inline. The embedded card shows only user identity (small 24px avatar), content, timestamp, and media — no action buttons or menus. - Add "embedded" variant and showMoreMenu config to post-card-display - Render reference as nested PostCard in media area (one level deep) - Suppress OGP extraction when post has a referenceId - Add border/rounded styling to distinguish embedded cards
Keep HEAD's action area layout (Reply, Boost, ReactionPicker + Share) and Rocket icon. Discard remote's flat layout and Repeat2/Clipboard icons.
Replace the placeholder boost button with a dropdown (desktop) / bottom sheet (mobile) showing "Boost" and "Quote" actions, with boost count display matching the reply counter style.
Wire the "Boost" dropdown menu item in PostCard to create a pure boost (empty content + referenceId) via the API, with 409 duplicate detection and toast feedback. Display pure boosts in the timeline and user profile as the referenced post with a "X boosted" indicator on top, fetching the full referenced post via usePost to ensure nested references are loaded.
…on embedded PostCard The full-card overlay link on embedded reference cards conflicted with interactive elements (avatar, display name, MFM links, collapse toggle). Remove it and rely on the existing body-text and timestamp links for detail navigation, matching the timeline PostCard pattern. Also pass onUserClick to enable avatar/display-name → user profile navigation.
Wire up the Quote option in PostCard's Boost dropdown to open a composer dialog with the referenced post displayed as a non-interactive embedded preview. Users write text and submit to create a quote post (content + referenceId). - Add CreateQuoteDialog following the CreateReplyDialog pattern - Extend useComposePost with referenceId support (content required) - Add quotedPost prop to PostComposerContent for embedded preview - Invalidate quoted post query on success to refresh boostCount
There was a problem hiding this comment.
Pull request overview
Adds “boost” (no-content repost) and “quote” (post referencing another post) support across the API contract, backend storage/services, and frontend UI rendering.
Changes:
- Extend the Post/CreatePost schemas with
referenceId, embeddedreference, andboostCount. - Backend: add
posts.reference_id, counting/query support for boosts, and attach referenced post data to timeline/posts. - Frontend: add boost/quote actions, quote composer dialog, embedded referenced-post rendering, and localization strings.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/api/schemas/post.yml | Adds boostCount, referenceId, and embedded reference to API schemas |
| apps/frontend/messages/ja.json | Adds JA strings for boost/quote UI |
| apps/frontend/messages/en.json | Adds EN strings for boost/quote UI |
| apps/frontend/components/PostCard.tsx | Adds boost/quote actions + renders referenced posts and indicators |
| apps/frontend/components/post-composer/useComposePost.ts | Adds referenceId support and quote-specific posting rules |
| apps/frontend/components/post-composer/PostComposerContent.tsx | Adds quoted-post preview rendering in composer |
| apps/frontend/components/post-card-display.ts | Adds embedded PostCard variant + showMoreMenu flag |
| apps/frontend/components/CreateQuoteDialog.tsx | New dialog for composing quote posts |
| apps/frontend/app/users/[username]/UserProfileContent.tsx | Renders pure boosts as referenced posts with “boosted by” indicator |
| apps/frontend/app/HomePage.tsx | Renders pure boosts in timeline with indicator |
| apps/backend/tests/unit/service/timeline_get_test.go | Updates mocked row shape to include reference_id |
| apps/backend/tests/unit/service/realtime_publish_test.go | Updates expectations for reference_id and boost counting |
| apps/backend/main.go | Wires TimelineService to PostsService for boost/reference attachment |
| apps/backend/internal/service/timeline.go | Attaches boost counts and references when serving timeline |
| apps/backend/internal/service/sqlc_maps.go | Maps reference_id into API Post models |
| apps/backend/internal/service/posts.go | Implements create/attach logic for boosts/quotes + boost counting + reference embedding |
| apps/backend/internal/service/mentions.go | Adds URL-based post reference extraction helper |
| apps/backend/internal/service/mentions_test.go | Adds tests for post-reference extraction |
| apps/backend/db/schema.sql | Adds reference_id column + indexes/constraints |
| apps/backend/db/queries.sql | Adds reference_id to relevant selects + boost counting query |
| apps/backend/db/migrations/20260608_add_post_boosts.up.sql | Migration to add boosts/quotes support |
| apps/backend/db/migrations/20260608_add_post_boosts.down.sql | Rollback for boosts/quotes migration |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+460
to
+469
| const handleBoost = useCallback(async () => { | ||
| if (!auth.user) { | ||
| toast.error(t("actions.boostError")); | ||
| return; | ||
| } | ||
| if (isBoosting) return; | ||
| setIsBoosting(true); | ||
| try { | ||
| const result = await api.createPost({ content: "", referenceId: post.id }); | ||
| if (!result.ok) { |
Comment on lines
+172
to
+175
| referenceId: | ||
| allOf: | ||
| - $ref: ./common.yml#/components/schemas/PostId | ||
| description: When set, the new post is a boost (no content) or quote (with content) of the referenced post. Returns 404 if the referenced post does not exist or has been deleted. |
Comment on lines
64
to
141
| @@ -75,9 +77,18 @@ func (s *PostsService) Create(ctx context.Context, user auth.User, req api.Creat | |||
|
|
|||
| mentionNames := ExtractMentions(content, MaxMentionsPerPost) | |||
|
|
|||
| autoDetectedReference := false | |||
| if req.ReferenceId == nil && content != "" { | |||
| if refID := ExtractPostReference(content); refID != nil { | |||
| postId := api.PostId(*refID) | |||
| req.ReferenceId = &postId | |||
| autoDetectedReference = true | |||
| } | |||
| } | |||
|
|
|||
| var created sqlc.CreatePostRow | |||
| if err := s.store.WithTx(ctx, func(q *sqlc.Queries) error { | |||
| var parentID, rootID uuid.NullUUID | |||
| var parentID, rootID, referenceID uuid.NullUUID | |||
| if req.ParentId != nil { | |||
| parent, err := q.GetPostThreadInfoByID(ctx, *req.ParentId) | |||
| if err != nil { | |||
| @@ -97,13 +108,36 @@ func (s *PostsService) Create(ctx context.Context, user auth.User, req api.Creat | |||
| } | |||
| } | |||
|
|
|||
| if req.ReferenceId != nil { | |||
| ref, err := q.GetPostThreadInfoByID(ctx, *req.ReferenceId) | |||
| if err != nil { | |||
| if err == sql.ErrNoRows { | |||
| if !autoDetectedReference { | |||
| return NewError(http.StatusNotFound, "not_found", "referenced post not found") | |||
| } | |||
| } else { | |||
| return err | |||
| } | |||
| } else if ref.DeletedAt.Valid { | |||
| if !autoDetectedReference { | |||
| return NewError(http.StatusNotFound, "not_found", "referenced post not found") | |||
| } | |||
| } else { | |||
| referenceID = uuid.NullUUID{UUID: ref.ID, Valid: true} | |||
| } | |||
| } | |||
|
|
|||
| c, err := q.CreatePost(ctx, sqlc.CreatePostParams{ | |||
| UserID: user.ID, | |||
| Content: content, | |||
| ParentID: parentID, | |||
| RootID: rootID, | |||
| UserID: user.ID, | |||
| Content: content, | |||
| ParentID: parentID, | |||
| RootID: rootID, | |||
| ReferenceID: referenceID, | |||
| }) | |||
| if err != nil { | |||
| if isUniqueViolation(err) && req.ReferenceId != nil && content == "" { | |||
| return NewError(http.StatusConflict, "already_boosted", "you have already boosted this post") | |||
| } | |||
| return err | |||
Comment on lines
+58
to
62
| const pureBoost = isPureBoost(post); | ||
| const boostReferenceId = pureBoost ? post.referenceId! : undefined; | ||
| const parentId = pureBoost ? undefined : (post.parentId ?? undefined); | ||
| const { data: boostedPost } = usePost(boostReferenceId); | ||
| const { |
Comment on lines
+91
to
95
| const pureBoost = isPureBoost(post); | ||
| const boostReferenceId = pureBoost ? post.referenceId! : undefined; | ||
| const parentId = pureBoost ? undefined : (post.parentId ?? undefined); | ||
| const { data: boostedPost } = usePost(boostReferenceId); | ||
| const { |
Member
Author
|
TODO: ブーストのキャンセルができるようにする |
Member
Author
|
TODO: ブースト元が消滅したときの挙動を確認 |
Extend the PostCard indicator row with a relative timestamp and a "..." menu on the right side, vertically aligned with the post header's own timestamp and menu button. The menu provides copy actions and an "undo boost" option visible only to the user who boosted.
…lumns Restructure the boost indicator row to use a flex layout matching the avatar + content columns, so the icon sits right-aligned in the avatar column and the label starts at the username column.
When a boosted or quoted post is deleted, display a DeletedPostCard placeholder instead of leaving a broken empty state. The placeholder shows a default avatar, generic username, centered trash icon with "Deleted" label, and a menu for copying the post ID or undoing boosts. Extract PostCardIndicatorRow into a shared component to avoid duplicating indicator rendering between PostCard and DeletedPostCard. Nullify cached references in real-time when a post_deleted WebSocket event is received so the placeholder appears immediately.
- Switch runtime base from debian:bookworm-slim to alpine:3.22 - Remove bundled ffmpeg (video processing gracefully returns 503 when ffmpeg is unavailable; mount from host if needed) - Add -ldflags="-s -w" to migrate binary build
This reverts commit d70b18f.
- Embedded: remove avatar, username, and menu; show only centered trash icon + label - Timeline: align username text style with PostCard identity stack
Deleted posts lack the "Copy" parent submenu that regular posts have, so the bare "Post ID" label was unclear. Add a dedicated full label.
Embedded PostCard does not render action buttons (reactions, more menu), so the bottom margins meant for spacing above those buttons were creating empty whitespace.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.