-
-
Notifications
You must be signed in to change notification settings - Fork 1
feat: リノート/引用ができるようにした #553
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- SVGアイコンを🔁絵文字に変更 - リノートしたユーザーのアバターを追加 - UI言語を英語に変更(renoted/quoted) - 引用時は"quoted"、通常リノート時は"renoted"と表示 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- リノートのID、reactionsをリノート自体のものを使用(オリジナルノートではなく) - 元ノート情報を取得するAPI関数を追加 - タイムラインとアカウントページでリノート表示を改善 - リノートヘッダーコンポーネントを統合 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- コメント入力フォームを追加 - エラーハンドリングを実装 - CSS Modulesを使用したスタイリング - プレビュー機能を追加 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR implements renote (retweet) and quote functionality for the social media application, allowing users to share posts with or without additional comments.
Changes:
- Added renote and quote buttons with dropdown menu UI
- Implemented API routes and handlers for creating renotes/quotes
- Enhanced timeline and account views to display renoted posts with author information
- Added utility functions to fetch individual notes by ID
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
| app/styles/renote.module.css | Styling for the quote renote page form and layout |
| app/routes/timeline.tsx | Modified to fetch and display renoted posts with original note data |
| app/routes/notes.$id.renote.tsx | New route for creating quote renotes with preview |
| app/routes/api.renote.ts | API endpoint for creating renotes without navigation |
| app/routes/accounts.$id._index.tsx | Modified to fetch and display renoted posts on account timelines |
| app/lib/api/timeline.ts | Extended types to support renote fields and original note data |
| app/lib/api/note.ts | New utility for fetching individual notes by ID |
| app/components/renoteMenu.tsx | Dropdown menu component for renote/quote actions |
| app/components/renoteMenu.module.css | Styling for the renote menu dropdown |
| app/components/renoteHeader.tsx | Header component showing who renoted/quoted a post |
| app/components/renoteHeader.module.css | Styling for the renote header |
| app/components/note.tsx | Enhanced to display renote information and quote comments |
| app/components/note.module.css | Added styling for action buttons layout |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| return ( | ||
| <div key={note.id}> | ||
| <Note | ||
| id={note.id} |
Copilot
AI
Jan 14, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The note ID passed to the Note component should be the renote ID (note.id), not the original note ID. This allows users to interact with the specific renote instance. The current implementation in accounts.$id._index.tsx incorrectly passes note.originalNote.id, which would make all renotes of the same original note appear identical and share interactions.
|
|
||
| if (isRenote) { | ||
| return { | ||
| id: note.originalNote.id, |
Copilot
AI
Jan 14, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The note ID should be note.id (the renote ID), not note.originalNote.id. Using the original note ID means all renotes of the same original note will have the same ID, which breaks the ability to distinguish between different renote instances and could cause React key conflicts.
| if (fetcher.state === "loading") { | ||
| if ("error" in fetcher.data) { | ||
| setIsRenoted(false); | ||
| } | ||
| } |
Copilot
AI
Jan 14, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error handling logic is incorrect. The condition checks if state is "loading", but errors should be handled when the fetcher has completed (state is "idle"). Change the condition to check if fetcher.state === "idle" and if there's an error in the response data, then reset isRenoted to false.
| // リノートの元ノート情報を取得 | ||
| const notesWithOriginal = await Promise.all( | ||
| res.notes.map(async (note) => { | ||
| if (note.original_note_id) { | ||
| const originalNote = await fetchNoteById( | ||
| note.original_note_id, | ||
| cookie, | ||
| basePath | ||
| ); | ||
| if ("error" in originalNote) { | ||
| return note; | ||
| } | ||
| return { | ||
| ...note, | ||
| originalNote: { | ||
| id: originalNote.id, | ||
| content: originalNote.content, | ||
| contents_warning_comment: originalNote.contents_warning_comment, | ||
| author: { | ||
| id: originalNote.author.id, | ||
| name: originalNote.author.name, | ||
| display_name: originalNote.author.display_name, | ||
| avatar: originalNote.author.avatar, | ||
| }, | ||
| reactions: originalNote.reactions, | ||
| }, | ||
| }; | ||
| } | ||
| return note; | ||
| }) | ||
| ); |
Copilot
AI
Jan 14, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This implementation creates an N+1 query problem. For each note with original_note_id, a separate API call is made to fetch the original note. If the timeline contains many renotes, this will result in many sequential API requests, significantly impacting performance. Consider implementing a batch endpoint on the backend to fetch multiple notes in a single request, or have the timeline API include the original note data directly.
| a:hover { | ||
| text-decoration: underline; | ||
| } | ||
| } |
Copilot
AI
Jan 14, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The quoteComment CSS class is used in the note.tsx component at line 92, but this class is not defined in the module CSS file. This will cause the styling to not be applied to the quote comment element.
| setIsRenoted(false); | ||
| } | ||
| } | ||
| }, [fetcher.state]); |
Copilot
AI
Jan 14, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The useEffect hook is missing fetcher.data in its dependency array. This could cause the effect to not run when fetcher.data changes. Add fetcher.data to the dependency array.
| // リノートの元ノート情報を取得 | ||
| const timelineWithOriginal = await Promise.all( | ||
| timelineRes.map(async (note) => { | ||
| if (note.original_note_id) { | ||
| const originalNote = await fetchNoteById( | ||
| note.original_note_id, | ||
| token, | ||
| basePath | ||
| ); | ||
| if ("error" in originalNote) { | ||
| return note; | ||
| } | ||
| return { | ||
| ...note, | ||
| originalNote: { | ||
| id: originalNote.id, | ||
| content: originalNote.content, | ||
| contents_warning_comment: originalNote.contents_warning_comment, | ||
| author: { | ||
| id: originalNote.author.id, | ||
| name: originalNote.author.name, | ||
| display_name: originalNote.author.display_name, | ||
| avatar: originalNote.author.avatar, | ||
| }, | ||
| reactions: originalNote.reactions, | ||
| }, | ||
| }; | ||
| } | ||
| return note; | ||
| }) | ||
| ); |
Copilot
AI
Jan 14, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This implementation creates an N+1 query problem. For each note with original_note_id, a separate API call is made to fetch the original note. If the account timeline contains many renotes, this will result in many sequential API requests, significantly impacting performance. Consider implementing a batch endpoint on the backend to fetch multiple notes in a single request, or have the timeline API include the original note data directly.
|
|
||
| if (!res.ok) { | ||
| const errorRes = (await res.json()) as { error: string }; | ||
| console.log("[notes.renote] error:", errorRes.error); |
Copilot
AI
Jan 14, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Console.log statement should be removed from production code or replaced with a proper logging system. Debug logs in production can expose sensitive information and clutter server logs.
| const noteID = params.id; | ||
|
|
||
| const basePath = (context.cloudflare.env as Env).API_BASE_URL; | ||
| const res = await fetch(new URL(`/v0/notes/${noteID}/renote`, basePath), { |
Copilot
AI
Jan 14, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing input validation for noteID parameter. If params.id is undefined, it could cause unexpected API behavior or errors. Add validation to ensure noteID exists before making the API call.
| const noteID = params.id; | ||
| const basePath = (context.cloudflare.env as Env).API_BASE_URL; | ||
|
|
||
| const res = await fetch(new URL(`/v0/notes/${noteID}`, basePath), { |
Copilot
AI
Jan 14, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing input validation for noteID parameter. If params.id is undefined, it could cause unexpected API behavior or errors. Add validation to ensure noteID exists before making the API call.
close #476
やったこと
スクリーンショット:
リノートボタンの様子

リノートボタンを押したときの挙動

リノート/引用の表示
