From 718e620d8a6222dbb595ff52aecc3932df0860e2 Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Sun, 17 May 2026 03:28:59 +0900 Subject: [PATCH 01/11] Add public reaction list pages for each post Four new SSR pages let visitors see who reacted to a local post: /@:handle/:id/likes lists likers, /shares lists boosters, /reactions/:emoji lists accounts that used that emoji, and /quotes lists posts that quote it. Each page features the original post above its list, with ?page-based pagination at 100 entries per page. The per-post like, share, quote, and reaction-emoji indicators on the profile feed and post permalink page now link into these pages for local posts; remote posts keep displaying the counts as plain text. The /shares and /quotes lists filter to public/unlisted entries so private boosts and private accepted quotes aren't exposed on the public HTML. Assisted-by: Codex:gpt-5.5 --- CHANGES.md | 11 + src/components/Post.tsx | 127 ++++++-- src/components/PublicAccountList.tsx | 79 +++++ src/pages/profile/index.tsx | 90 +++--- src/pages/profile/postReactions.tsx | 459 +++++++++++++++++++++++++++ src/pages/profile/profilePost.tsx | 75 +++-- src/pages/tags/index.tsx | 35 +- 7 files changed, 755 insertions(+), 121 deletions(-) create mode 100644 src/components/PublicAccountList.tsx create mode 100644 src/pages/profile/postReactions.tsx diff --git a/CHANGES.md b/CHANGES.md index 389d5729..13b7abe2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -235,6 +235,17 @@ To be released. are removed; UnoCSS emits a single _src/public/uno.css_ whose URL is cache-busted by file mtime. + - Added public reaction list pages anchored to each local post: + `/@:handle/:id/likes` lists the accounts that liked the post, + `/@:handle/:id/shares` lists the accounts that boosted it, + `/@:handle/:id/reactions/:emoji` lists the accounts that reacted with + a specific emoji, and `/@:handle/:id/quotes` lists the posts that + quote it. Each page features the original post above the list. On + the profile feed and post permalink page, the per-post like, share, + quote, and reaction-emoji indicators now link into these pages for + local posts; remote posts continue to display the counts as plain + text. + - Added avatar and header image upload to the admin account creation and editing forms, with drag-and-drop support and in-page image preview. Files are stored using the same storage backend as the Mastodon-compatible diff --git a/src/components/Post.tsx b/src/components/Post.tsx index 44e60170..5aba649c 100644 --- a/src/components/Post.tsx +++ b/src/components/Post.tsx @@ -4,6 +4,7 @@ import { proxyUrl } from "../media-proxy"; import type { PreviewCard } from "../previewcard"; import type { Account, + AccountOwner, Medium as DbMedium, Poll as DbPoll, Post as DbPost, @@ -11,36 +12,38 @@ import type { Reaction, } from "../schema"; +export type PostAccount = Account & { owner?: AccountOwner | null }; + export interface PostProps { readonly post: DbPost & { - account: Account; + account: PostAccount; media: DbMedium[]; poll: (DbPoll & { options: PollOption[] }) | null; sharing: | (DbPost & { - account: Account; + account: PostAccount; media: DbMedium[]; poll: (DbPoll & { options: PollOption[] }) | null; - replyTarget: (DbPost & { account: Account }) | null; + replyTarget: (DbPost & { account: PostAccount }) | null; quoteTarget: | (DbPost & { - account: Account; + account: PostAccount; media: DbMedium[]; poll: (DbPoll & { options: PollOption[] }) | null; - replyTarget: (DbPost & { account: Account }) | null; + replyTarget: (DbPost & { account: PostAccount }) | null; reactions: Reaction[]; }) | null; reactions: Reaction[]; }) | null; - replyTarget: (DbPost & { account: Account }) | null; + replyTarget: (DbPost & { account: PostAccount }) | null; quoteTarget: | (DbPost & { - account: Account; + account: PostAccount; media: DbMedium[]; poll: (DbPoll & { options: PollOption[] }) | null; - replyTarget: (DbPost & { account: Account }) | null; + replyTarget: (DbPost & { account: PostAccount }) | null; reactions: Reaction[]; }) | null; @@ -85,6 +88,8 @@ export function Post({ ); const authorUrl = account.url ?? account.iri; const avatar = proxyUrl(account.avatarUrl, baseUrl); + const localPermalink = + account.owner == null ? null : `/@${account.owner.handle}/${post.id}`; const wrapperClass = quoted ? "rounded-lg border border-neutral-200 bg-neutral-50 p-4 dark:border-neutral-800 dark:bg-neutral-900/60" : featured @@ -188,19 +193,37 @@ export function Post({ {post.likesCount != null && post.likesCount > 0 && ( <> - - + )} {post.sharesCount != null && post.sharesCount > 0 && ( <> - - + + + )} + {post.quotesCount != null && post.quotesCount > 0 && ( + <> + + )} {post.reactions.length > 0 && ( @@ -208,17 +231,30 @@ export function Post({ {Object.entries(groupByEmojis(post.reactions, baseUrl)).map( - ([emoji, { src, count }]) => - src == null ? ( - {emoji} + ([emoji, { src, count }]) => { + const inner = + src == null ? ( + {emoji} + ) : ( + {emoji} + ); + const title = `${emoji} × ${count}`; + return localPermalink == null ? ( + {inner} ) : ( - {emoji} - ), + + {inner} + + ); + }, )} @@ -228,6 +264,41 @@ export function Post({ ); } +interface CountLinkProps { + readonly localPermalink: string | null; + readonly path: string; + readonly icon: string; + readonly count: number; + readonly label: string; +} + +function CountLink({ + localPermalink, + path, + icon, + count, + label, +}: CountLinkProps) { + const inner = ( + <> +