This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / web / src / lib / components / comment / CommentCard.svelte
6.0 kB 203 lines
1<script lang="ts"> 2 import { resolve } from "$app/paths"; 3 import Pencil from "$icon/pencil"; 4 import Trash2 from "$icon/trash-2"; 5 import { deleteComment } from "$lib/api/comment"; 6 import { getAuth } from "$lib/auth.svelte"; 7 import { type MarkupContext } from "$lib/markup"; 8 import ErrorAlert from "$lib/components/ui/Error.svelte"; 9 import Comment from "./Comment.svelte"; 10 import CommentBox from "./CommentBox.svelte"; 11 import CommentEditor from "./CommentEditor.svelte"; 12 import type { CommentThread, CommentView, ThreadInput } from "./comments"; 13 14 interface Props { 15 thread: CommentThread; 16 subjectUri: string; 17 subjectCid?: string; 18 markup: MarkupContext; 19 onsubmitted?: (submitted: ThreadInput) => void; 20 onedited?: (edited: ThreadInput) => void; 21 ondeleted?: (uri: string) => void; 22 } 23 24 let { thread, subjectUri, subjectCid, markup, onsubmitted, onedited, ondeleted }: Props = 25 $props(); 26 27 const auth = getAuth(); 28 const currentUser = $derived(auth?.currentUser ?? null); 29 30 let replying = $state(false); 31 let editingUri = $state<string | null>(null); 32 let deletingUri = $state<string | null>(null); 33 let deleteError = $state<string | null>(null); 34 35 const handleDelete = async (comment: CommentView) => { 36 const agent = auth?.agent; 37 if (!agent || deletingUri) return; 38 if (!confirm("Delete this comment? This cannot be undone.")) return; 39 deletingUri = comment.uri; 40 deleteError = null; 41 try { 42 await deleteComment(agent, comment.rkey); 43 ondeleted?.(comment.uri); 44 } catch (err) { 45 deleteError = err instanceof Error ? err.message : "Failed to delete comment"; 46 } finally { 47 deletingUri = null; 48 } 49 }; 50</script> 51 52{#snippet commentActions(comment: CommentView)} 53 {#if currentUser?.did === comment.authorDid} 54 <div 55 class="absolute top-4 right-4 flex items-center gap-2 opacity-0 transition group-hover:opacity-100 focus-within:opacity-100" 56 > 57 <button 58 type="button" 59 aria-label="Edit comment" 60 class="cursor-pointer text-foreground-subtle hover:text-foreground-default" 61 onclick={() => (editingUri = comment.uri)} 62 > 63 <Pencil class="size-3" /> 64 </button> 65 <button 66 type="button" 67 aria-label="Delete comment" 68 disabled={deletingUri === comment.uri} 69 class="cursor-pointer text-foreground-danger hover:text-foreground-danger-strong disabled:opacity-50" 70 onclick={() => handleDelete(comment)} 71 > 72 <Trash2 class="size-3" /> 73 </button> 74 </div> 75 {/if} 76{/snippet} 77 78{#snippet commentEditor( 79 comment: CommentView, 80 replyUri: string | undefined, 81 replyCid: string | undefined 82)} 83 <CommentEditor 84 {subjectUri} 85 {subjectCid} 86 replyToUri={replyUri} 87 replyToCid={replyCid} 88 authorDid={comment.authorDid} 89 authorHandle={comment.authorHandle} 90 rkey={comment.rkey} 91 createdAt={comment.createdAt} 92 body={comment.body} 93 {markup} 94 rows={4} 95 placeholder="Edit your comment. Markdown is supported." 96 submitLabel="Save" 97 submitIcon={Pencil} 98 autofocus 99 onsubmitted={(edited) => { 100 editingUri = null; 101 onedited?.(edited); 102 }} 103 oncancel={() => (editingUri = null)} 104 /> 105{/snippet} 106 107<div 108 class="overflow-hidden rounded border border-border-default bg-background-canvas bg-background-default/50 drop-shadow-xs" 109> 110 {#snippet selfEditor()} 111 {@render commentEditor(thread.self, undefined, undefined)} 112 {/snippet} 113 <div class="group relative"> 114 <Comment 115 authorHandle={thread.self.authorHandle} 116 authorDid={thread.self.authorDid} 117 createdAt={thread.self.createdAt} 118 body={thread.self.body} 119 bodyHtml={thread.self.bodyHtml} 120 variant="top" 121 deleted={thread.self.deleted} 122 editor={editingUri === thread.self.uri ? selfEditor : undefined} 123 /> 124 {#if editingUri !== thread.self.uri && !thread.self.deleted} 125 {@render commentActions(thread.self)} 126 {/if} 127 </div> 128 129 {#if thread.replies.length} 130 <div class="ml-10"> 131 {#each thread.replies as reply, i (reply.uri)} 132 {#snippet replyEditor()} 133 {@render commentEditor(reply, thread.self.uri, thread.self.cid)} 134 {/snippet} 135 <div class="group relative isolate -ml-4"> 136 <!-- thread connector; on the last reply it stops at the avatar's centre (h-8) --> 137 <div 138 class={`pointer-events-none absolute top-0 left-4 -z-10 w-0.5 bg-border-default ${i === thread.replies.length - 1 ? "h-8" : "bottom-0"}`} 139 ></div> 140 <Comment 141 authorHandle={reply.authorHandle} 142 authorDid={reply.authorDid} 143 createdAt={reply.createdAt} 144 body={reply.body} 145 bodyHtml={reply.bodyHtml} 146 variant="reply" 147 editor={editingUri === reply.uri ? replyEditor : undefined} 148 /> 149 {#if editingUri !== reply.uri} 150 {@render commentActions(reply)} 151 {/if} 152 </div> 153 {/each} 154 </div> 155 {/if} 156 157 {#if deleteError} 158 <div class="border-t border-border-default px-6 py-2"> 159 <ErrorAlert label={deleteError} /> 160 </div> 161 {/if} 162 163 {#if thread.self.deleted && !thread.self.cid} 164 <!-- deleted parent with no recoverable strongRef; nothing to reply against --> 165 {:else if replying && currentUser} 166 <div class={thread.replies.length ? "border-t border-border-default" : ""}> 167 <CommentBox 168 variant="thread" 169 {subjectUri} 170 {subjectCid} 171 replyToUri={thread.self.uri} 172 replyToCid={thread.self.cid} 173 authorDid={currentUser.did} 174 authorHandle={currentUser.handle} 175 {markup} 176 autofocus 177 onsubmitted={(submitted) => { 178 replying = false; 179 onsubmitted?.(submitted); 180 }} 181 oncancel={() => (replying = false)} 182 /> 183 </div> 184 {:else} 185 <div 186 class={`flex items-center gap-2 bg-background-default/50 px-6 py-2 ${thread.replies.length ? "border-t border-border-default" : ""}`} 187 > 188 {#if currentUser} 189 <button 190 type="button" 191 class="w-full cursor-text text-left text-foreground-subtle focus:outline-none" 192 onclick={() => (replying = true)} 193 > 194 Leave a reply... 195 </button> 196 {:else} 197 <span class="text-foreground-subtle"> 198 <a href={resolve("/login")} class="underline">Login</a> to leave a reply 199 </span> 200 {/if} 201 </div> 202 {/if} 203</div>