diff --git a/backend/app/controllers/posts_controller.rb b/backend/app/controllers/posts_controller.rb index cfb0cbd..310c449 100644 --- a/backend/app/controllers/posts_controller.rb +++ b/backend/app/controllers/posts_controller.rb @@ -109,7 +109,7 @@ class PostsController < ApplicationController render json: PostRepr.base(post, current_user) .merge(tags: build_tag_tree_for(post.tags), - related: post.related(limit: 20)) + related: PostRepr.many(post.related(limit: 20))) end def create diff --git a/backend/app/models/post.rb b/backend/app/models/post.rb index 63c23d2..02b4c08 100644 --- a/backend/app/models/post.rb +++ b/backend/app/models/post.rb @@ -37,18 +37,26 @@ class Post < ApplicationRecord def parent_posts = parents + def child_posts = children + + def sibling_posts + parent_post_ids = parent_posts.order(:id).pluck(:id) + + parent_post_ids.to_h { [_1, PostImplication.where(parent_post: _1).map(&:post)] } + end + def as_json options = { } - super(options).merge({ thumbnail: thumbnail.attached? ? - Rails.application.routes.url_helpers.rails_blob_url( - thumbnail, only_path: false) : - nil }) + super(options).merge(thumbnail: thumbnail.attached? ? + Rails.application.routes.url_helpers.rails_blob_url( + thumbnail, only_path: false) : + nil) rescue super(options).merge(thumbnail: nil) end def snapshot_tag_names = tags.joins(:tag_name).order('tag_names.name').pluck('tag_names.name') - def snapshot_parent_post_ids = parents.order(:parent_post_id).pulck(:parent_post_id) + def snapshot_parent_post_ids = parents.order(:parent_post_id).pluck(:parent_post_id) def related limit: nil ids = post_similarities.order(cos: :desc) diff --git a/backend/app/representations/post_repr.rb b/backend/app/representations/post_repr.rb index b77d0b8..87f59f9 100644 --- a/backend/app/representations/post_repr.rb +++ b/backend/app/representations/post_repr.rb @@ -3,7 +3,7 @@ module PostRepr BASE = { include: { tags: TagRepr::BASE, uploaded_user: UserRepr::BASE }, - methods: [:parent_posts] }.freeze + methods: [:parent_posts, :child_posts, :sibling_posts] }.freeze module_function diff --git a/frontend/src/components/PostEditForm.tsx b/frontend/src/components/PostEditForm.tsx index 8c3411b..5558e69 100644 --- a/frontend/src/components/PostEditForm.tsx +++ b/frontend/src/components/PostEditForm.tsx @@ -4,6 +4,7 @@ import PostFormTagsArea from '@/components/PostFormTagsArea' import PostOriginalCreatedTimeField from '@/components/PostOriginalCreatedTimeField' import Label from '@/components/common/Label' import { Button } from '@/components/ui/button' +import { toast } from '@/components/ui/use-toast' import { apiPut } from '@/lib/api' import type { FC } from 'react' @@ -35,20 +36,30 @@ export default (({ post, onSave }: Props) => { useState (post.originalCreatedBefore) const [originalCreatedFrom, setOriginalCreatedFrom] = useState (post.originalCreatedFrom) - const [title, setTitle] = useState (post.title) + const [parentPostIds, setParentPostIds] = useState (post.parentPosts!.map (p => p.id).join (' ')) const [tags, setTags] = useState ('') + const [title, setTitle] = useState (post.title) const handleSubmit = async () => { - const data = await apiPut ( - `/posts/${ post.id }`, - { title, tags, original_created_from: originalCreatedFrom, - original_created_before: originalCreatedBefore }, - { headers: { 'Content-Type': 'multipart/form-data' } }) - onSave ({ ...post, - title: data.title, - tags: data.tags, - originalCreatedFrom: data.originalCreatedFrom, - originalCreatedBefore: data.originalCreatedBefore } as Post) + try + { + const data = await apiPut ( + `/posts/${ post.id }`, + { title, tags, parent_post_ids: parentPostIds, + original_created_from: originalCreatedFrom, + original_created_before: originalCreatedBefore }, + { headers: { 'Content-Type': 'multipart/form-data' } }) + onSave ({ ...post, + title: data.title, + tags: data.tags, + originalCreatedFrom: data.originalCreatedFrom, + originalCreatedBefore: data.originalCreatedBefore } as Post) + toast ({ description: '更新しました.' }) + } + catch + { + toast ({ description: '更新はできなかったよ……' }) + } } useEffect (() => { @@ -66,6 +77,16 @@ export default (({ post, onSave }: Props) => { onChange={ev => setTitle (ev.target.value)}/> + {/* 親投稿 */} +
+ + setParentPostIds (e.target.value)} + className="w-full border p-2 rounded"/> +
+ {/* タグ */} diff --git a/frontend/src/components/PostList.tsx b/frontend/src/components/PostList.tsx index c072154..582762e 100644 --- a/frontend/src/components/PostList.tsx +++ b/frontend/src/components/PostList.tsx @@ -3,6 +3,7 @@ import { useRef } from 'react' import { useLocation } from 'react-router-dom' import PrefetchLink from '@/components/PrefetchLink' +import { cn } from '@/lib/utils' import { useSharedTransitionStore } from '@/stores/sharedTransitionStore' import type { FC, MouseEvent } from 'react' @@ -39,8 +40,10 @@ export default (({ posts, onClick }: Props) => { 0 && 'border-4 border-green-500', + (post.parentPosts ?? []).length > 0 && 'border-4 border-yellow-500')} whileHover={{ scale: 1.02 }} onLayoutAnimationStart={() => { if (!(cardRef.current)) diff --git a/frontend/src/pages/posts/PostDetailPage.tsx b/frontend/src/pages/posts/PostDetailPage.tsx index 433d312..5cefce7 100644 --- a/frontend/src/pages/posts/PostDetailPage.tsx +++ b/frontend/src/pages/posts/PostDetailPage.tsx @@ -21,7 +21,7 @@ import ServiceUnavailable from '@/pages/ServiceUnavailable' import type { FC } from 'react' -import type { NiconicoViewerHandle, User } from '@/types' +import type { NiconicoViewerHandle, Post, User } from '@/types' type Props = { user: User | null } @@ -108,7 +108,34 @@ export default (({ user }: Props) => { {post ? ( <> - {/* TODO: 親投稿リスト */} + {(post.childPosts ?? []).length > 0 && ( +
+

この投稿には {post.childPosts!.length} 件の子投稿があります.

+ ({ + ...p, parentPosts: [{ } as Post] }))]}/> +
+ )} + {(post.parentPosts ?? []).map (pp => { + const siblings = post.siblingPosts?.[String (pp.id) as `${ number }`] + if (!(siblings)) + return + + return ( +
+

+ この投稿には 1 件の親投稿{ + siblings.length > 1 + && `と ${ siblings.length - 1 } 件の姉妹投稿`}があります. +

+ ({ + ...p, parentPosts: [{ } as Post] }))]}/> +
) + })} + {(post.thumbnail || post.thumbnailBase) && ( { (prev: any) => newPost ?? prev) qc.invalidateQueries ({ queryKey: postsKeys.root }) qc.invalidateQueries ({ queryKey: tagsKeys.root }) - toast ({ description: '更新しました.' }) }}/> )} diff --git a/frontend/src/pages/posts/PostHistoryPage.tsx b/frontend/src/pages/posts/PostHistoryPage.tsx index fb6b27e..564ed79 100644 --- a/frontend/src/pages/posts/PostHistoryPage.tsx +++ b/frontend/src/pages/posts/PostHistoryPage.tsx @@ -95,6 +95,8 @@ export default (() => { {/* タグ */} + {/* 親投稿 */} + {/* オリジナルの投稿日時 */} {/* 更新日時 */} @@ -110,6 +112,7 @@ export default (() => { タイトル URL タグ + 親投稿 オリジナルの投稿日時 更新日時 @@ -180,6 +183,28 @@ export default (() => { {tag.name} ))))} + + {change.parentPosts.map ((pp, i) => ( + pp.type === 'added' + ? ( + + {pp.title} + ) + : ( + pp.type === 'removed' + ? ( + + {pp.title} + ) + : ( + + {pp.title} + ))))} + {change.versionNo === 1 ? originalCreatedAtString (change.originalCreatedFrom.current, @@ -225,6 +250,8 @@ export default (() => { .map (t => t.name) .filter (t => t.slice (0, 5) !== 'nico:') .join (' '), + parent_post_ids: + change.parentPosts.map (p => p.id).join (' '), original_created_from: change.originalCreatedFrom.current, original_created_before: diff --git a/frontend/src/pages/posts/PostNewPage.tsx b/frontend/src/pages/posts/PostNewPage.tsx index 5a2f77b..a5bcbf3 100644 --- a/frontend/src/pages/posts/PostNewPage.tsx +++ b/frontend/src/pages/posts/PostNewPage.tsx @@ -29,6 +29,7 @@ export default (({ user }: Props) => { const [originalCreatedBefore, setOriginalCreatedBefore] = useState (null) const [originalCreatedFrom, setOriginalCreatedFrom] = useState (null) + const [parentPostIds, setParentPostIds] = useState ('') const [tags, setTags] = useState ('') const [thumbnailAutoFlg, setThumbnailAutoFlg] = useState (true) const [thumbnailFile, setThumbnailFile] = useState (null) @@ -46,6 +47,7 @@ export default (({ user }: Props) => { formData.append ('title', title) formData.append ('url', url) formData.append ('tags', tags) + formData.append ('parent_post_ids', parentPostIds) if (thumbnailFile) formData.append ('thumbnail', thumbnailFile) if (originalCreatedFrom) @@ -177,6 +179,16 @@ export default (({ user }: Props) => { className="mt-2 max-h-48 rounded border"/>)} + {/* 親投稿 */} +
+ + setParentPostIds (e.target.value)} + className="w-full border p-2 rounded"/> +
+ {/* タグ */} diff --git a/frontend/src/types.ts b/frontend/src/types.ts index d5eb53e..51363c3 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -121,6 +121,9 @@ export type Post = { thumbnail: string | null thumbnailBase: string | null tags: Tag[] + parentPosts?: Post[] + childPosts?: Post[] + siblingPosts?: Record<`${ number }`, Post[]> viewed: boolean related: Post[] originalCreatedFrom: string | null @@ -144,7 +147,11 @@ export type PostVersion = { url: { current: string; prev: string | null } thumbnail: { current: string | null; prev: string | null } thumbnailBase: { current: string | null; prev: string | null } - tags: { name: string; type: 'context' | 'added' | 'removed' }[] + tags: { name: string + type: 'context' | 'added' | 'removed' }[] + parentPosts: { id: number + title: string + type: 'context' | 'added' | 'removed' }[] originalCreatedFrom: { current: string | null; prev: string | null } originalCreatedBefore: { current: string | null; prev: string | null } createdAt: string