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
5.8 kB 200 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 editor={editingUri === thread.self.uri ? selfEditor : undefined} 122 /> 123 {#if editingUri !== thread.self.uri} 124 {@render commentActions(thread.self)} 125 {/if} 126 </div> 127 128 {#if thread.replies.length} 129 <div class="ml-10"> 130 {#each thread.replies as reply, i (reply.uri)} 131 {#snippet replyEditor()} 132 {@render commentEditor(reply, thread.self.uri, thread.self.cid)} 133 {/snippet} 134 <div class="group relative isolate -ml-4"> 135 <!-- thread connector; on the last reply it stops at the avatar's centre (h-8) --> 136 <div 137 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"}`} 138 ></div> 139 <Comment 140 authorHandle={reply.authorHandle} 141 authorDid={reply.authorDid} 142 createdAt={reply.createdAt} 143 body={reply.body} 144 bodyHtml={reply.bodyHtml} 145 variant="reply" 146 editor={editingUri === reply.uri ? replyEditor : undefined} 147 /> 148 {#if editingUri !== reply.uri} 149 {@render commentActions(reply)} 150 {/if} 151 </div> 152 {/each} 153 </div> 154 {/if} 155 156 {#if deleteError} 157 <div class="border-t border-border-default px-6 py-2"> 158 <ErrorAlert label={deleteError} /> 159 </div> 160 {/if} 161 162 {#if replying && currentUser} 163 <div class={thread.replies.length ? "border-t border-border-default" : ""}> 164 <CommentBox 165 variant="thread" 166 {subjectUri} 167 {subjectCid} 168 replyToUri={thread.self.uri} 169 replyToCid={thread.self.cid} 170 authorDid={currentUser.did} 171 authorHandle={currentUser.handle} 172 {markup} 173 autofocus 174 onsubmitted={(submitted) => { 175 replying = false; 176 onsubmitted?.(submitted); 177 }} 178 oncancel={() => (replying = false)} 179 /> 180 </div> 181 {:else} 182 <div 183 class={`flex items-center gap-2 bg-background-default/50 px-6 py-2 ${thread.replies.length ? "border-t border-border-default" : ""}`} 184 > 185 {#if currentUser} 186 <button 187 type="button" 188 class="w-full cursor-text text-left text-foreground-subtle focus:outline-none" 189 onclick={() => (replying = true)} 190 > 191 Leave a reply... 192 </button> 193 {:else} 194 <span class="text-foreground-subtle"> 195 <a href={resolve("/login")} class="underline">Login</a> to leave a reply 196 </span> 197 {/if} 198 </div> 199 {/if} 200</div>