Skip to content

Feat/boost#197

Open
nekochanfood wants to merge 34 commits into
developmentfrom
feat/boost
Open

Feat/boost#197
nekochanfood wants to merge 34 commits into
developmentfrom
feat/boost

Conversation

@nekochanfood

Copy link
Copy Markdown
Member

No description provided.

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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, embedded reference, and boostCount.
  • 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 {
@nekochanfood

Copy link
Copy Markdown
Member Author

TODO: ブーストのキャンセルができるようにする

@nekochanfood

Copy link
Copy Markdown
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
- 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.
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.

3 participants