diff --git a/CHANGES.md b/CHANGES.md index 389d5729..39e07f78 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. [[#490]] + - 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 @@ -371,6 +382,7 @@ To be released. [#487]: https://github.com/fedify-dev/hollo/pull/487 [#488]: https://github.com/fedify-dev/hollo/issues/488 [#489]: https://github.com/fedify-dev/hollo/issues/489 +[#490]: https://github.com/fedify-dev/hollo/pull/490 Version 0.8.4 diff --git a/src/components/Post.tsx b/src/components/Post.tsx index 44e60170..90e9f3fb 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,41 +12,45 @@ import type { Reaction, } from "../schema"; +export type PostAccount = Account & { owner?: AccountOwner | null }; + +export type PostForView = DbPost & { + account: PostAccount; + media: DbMedium[]; + poll: (DbPoll & { options: PollOption[] }) | null; + sharing: + | (DbPost & { + account: PostAccount; + media: DbMedium[]; + poll: (DbPoll & { options: PollOption[] }) | null; + replyTarget: (DbPost & { account: PostAccount }) | null; + quoteTarget: + | (DbPost & { + account: PostAccount; + media: DbMedium[]; + poll: (DbPoll & { options: PollOption[] }) | null; + replyTarget: (DbPost & { account: PostAccount }) | null; + reactions: Reaction[]; + }) + | null; + reactions: Reaction[]; + }) + | null; + replyTarget: (DbPost & { account: PostAccount }) | null; + quoteTarget: + | (DbPost & { + account: PostAccount; + media: DbMedium[]; + poll: (DbPoll & { options: PollOption[] }) | null; + replyTarget: (DbPost & { account: PostAccount }) | null; + reactions: Reaction[]; + }) + | null; + reactions: Reaction[]; +}; + export interface PostProps { - readonly post: DbPost & { - account: Account; - media: DbMedium[]; - poll: (DbPoll & { options: PollOption[] }) | null; - sharing: - | (DbPost & { - account: Account; - media: DbMedium[]; - poll: (DbPoll & { options: PollOption[] }) | null; - replyTarget: (DbPost & { account: Account }) | null; - quoteTarget: - | (DbPost & { - account: Account; - media: DbMedium[]; - poll: (DbPoll & { options: PollOption[] }) | null; - replyTarget: (DbPost & { account: Account }) | null; - reactions: Reaction[]; - }) - | null; - reactions: Reaction[]; - }) - | null; - replyTarget: (DbPost & { account: Account }) | null; - quoteTarget: - | (DbPost & { - account: Account; - media: DbMedium[]; - poll: (DbPoll & { options: PollOption[] }) | null; - replyTarget: (DbPost & { account: Account }) | null; - reactions: Reaction[]; - }) - | null; - reactions: Reaction[]; - }; + readonly post: PostForView; readonly shared?: Date; readonly pinned?: boolean; readonly quoted?: boolean; @@ -85,6 +90,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 +195,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 +233,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 +266,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 = ( + <> +