diff --git a/web/src/lib/components/comment/CommentCard.svelte b/web/src/lib/components/comment/CommentCard.svelte --- a/web/src/lib/components/comment/CommentCard.svelte +++ b/web/src/lib/components/comment/CommentCard.svelte @@ -6,6 +6,13 @@ import { getAuth } from "$lib/auth.svelte"; import { type MarkupContext } from "$lib/markup"; import ErrorAlert from "$lib/components/ui/Error.svelte"; + import ReactionPicker from "$lib/components/reaction/ReactionPicker.svelte"; + import { + upsertViewerReaction, + withoutViewerReaction, + type ReactionGroup, + type ReactionKind + } from "$lib/components/reaction/reactions"; import Comment from "./Comment.svelte"; import CommentBox from "./CommentBox.svelte"; import CommentEditor from "./CommentEditor.svelte"; @@ -32,6 +39,42 @@ let deletingUri = $state(null); let deleteError = $state(null); + let reactionsByUri = $derived.by(() => { + const map: Record = {}; + map[thread.self.uri] = thread.self.reactions ?? []; + for (const reply of thread.replies) map[reply.uri] = reply.reactions ?? []; + return map; + }); + + const setReactions = (uri: string, next: ReactionGroup[]) => { + reactionsByUri = { ...reactionsByUri, [uri]: next }; + }; + + // kinds the viewer reacted with, mapped to their record's rkey so the picker can toggle + const reactedMap = (uri: string): Map => + new Map( + (reactionsByUri[uri] ?? []) + .filter((g) => g.isReacted && g.viewerRkey) + .map((g) => [g.kind, g.viewerRkey!] as [ReactionKind, string]) + ); + + const onReacted = (uri: string) => (kind: ReactionKind, rkey: string) => { + const viewer = currentUser; + if (!viewer) return; + setReactions(uri, upsertViewerReaction(reactionsByUri[uri] ?? [], kind, viewer.handle, rkey)); + }; + + const onUnreacted = (uri: string) => (kind: ReactionKind) => { + const viewer = currentUser; + if (!viewer) return; + setReactions( + uri, + (reactionsByUri[uri] ?? []) + .map((g) => (g.kind === kind ? withoutViewerReaction(g, viewer.handle) : g)) + .filter((g) => g.count > 0) + ); + }; + const handleDelete = async (comment: CommentView) => { const agent = auth?.agent; if (!agent || deletingUri) return; @@ -50,27 +93,35 @@ {#snippet commentActions(comment: CommentView)} - {#if currentUser?.did === comment.authorDid} + {#if currentUser}
- - + + {#if currentUser.did === comment.authorDid} + + + {/if}
{/if} {/snippet} @@ -119,6 +170,9 @@ bodyHtml={thread.self.bodyHtml} variant="top" deleted={thread.self.deleted} + reactions={reactionsByUri[thread.self.uri]} + subjectUri={thread.self.uri} + onreactionschange={(next) => setReactions(thread.self.uri, next)} editor={editingUri === thread.self.uri ? selfEditor : undefined} /> {#if editingUri !== thread.self.uri && !thread.self.deleted} @@ -144,6 +198,9 @@ body={reply.body} bodyHtml={reply.bodyHtml} variant="reply" + reactions={reactionsByUri[reply.uri]} + subjectUri={reply.uri} + onreactionschange={(next) => setReactions(reply.uri, next)} editor={editingUri === reply.uri ? replyEditor : undefined} /> {#if editingUri !== reply.uri} diff --git a/web/src/lib/components/comment/comments.ts b/web/src/lib/components/comment/comments.ts --- a/web/src/lib/components/comment/comments.ts +++ b/web/src/lib/components/comment/comments.ts @@ -1,3 +1,5 @@ +import type { ReactionGroup } from "$lib/components/reaction/reactions"; + export interface CommentView { uri: string; cid?: string; @@ -7,7 +9,7 @@ createdAt: string; body: string; bodyHtml: string | null; - // the record is gone but we keep it to anchor its replies + reactions?: ReactionGroup[]; deleted?: boolean; } @@ -16,7 +18,6 @@ replies: CommentView[]; } -// pairs a rendered view with the parent uri it replies to export interface ThreadInput { comment: CommentView; replyTo: string | null;