Coves frontend - a photon fork
at main 208 lines 5.9 kB view raw
1<script lang="ts"> 2 import { browser } from '$app/environment' 3 import { goto } from '$app/navigation' 4 import { coves } from '$lib/api/client.svelte' 5 import type { StrongRef } from '$lib/api/coves/types' 6 import type { DID } from '$lib/types/atproto' 7 import { errorMessage } from '$lib/app/error' 8 import { t } from '$lib/app/i18n' 9 import { Button, toast } from 'mono-svelte' 10 import { ArrowDownCircle } from 'svelte-hero-icons/dist' 11 import Comment from './Comment.svelte' 12 import { 13 type CommentNodeI, 14 buildCommentsTree, 15 searchCommentTree, 16 } from './comments.svelte' 17 import CommentTree from './CommentTree.svelte' 18 19 interface Props { 20 nodes: CommentNodeI[] 21 postRef: StrongRef 22 postAuthorDid?: DID 23 } 24 25 let { nodes = $bindable(), postRef, postAuthorDid }: Props = $props() 26 27 function adjustDepths(nodes: CommentNodeI[], depth: number): void { 28 for (const n of nodes) { 29 n.depth = depth 30 adjustDepths(n.children, depth + 1) 31 } 32 } 33 34 async function fetchChildren(parent: CommentNodeI) { 35 if ( 36 !(parent.comment.stats.replyCount > 0 && parent.children.length === 0) 37 ) { 38 return 39 } 40 41 try { 42 parent.loading = true 43 44 // TODO: The API does not yet support a `parent` param to scope to a subtree. 45 // Once supported, pass `parent: parent.comment.uri` to avoid fetching all comments. 46 const response = await coves().getComments({ 47 post: postRef.uri, 48 }) 49 50 // TODO: response.cursor is currently ignored — use it for pagination support. 51 52 if (response.comments.length === 0) { 53 toast({ 54 content: $t('toast.noComments'), 55 type: 'error', 56 }) 57 return 58 } 59 60 // The server returns ThreadViewComment[] for the full post tree. 61 // Build with baseDepth=0 since the server returns the complete tree from root. 62 const fullTree = buildCommentsTree(response.comments, 0) 63 const matchedNode = searchCommentTree(fullTree, parent.comment.uri) 64 if (matchedNode) { 65 parent.children = matchedNode.children 66 adjustDepths(parent.children, parent.depth + 1) 67 } else { 68 console.warn( 69 `[comments] Could not find parent node ${parent.comment.uri as string} in fetched comment tree. Children not loaded.`, 70 ) 71 toast({ 72 content: $t('toast.failedToLoadComments'), 73 type: 'error', 74 }) 75 parent.children = [] 76 } 77 } catch (err) { 78 console.error(err) 79 toast({ 80 content: errorMessage(err), 81 type: 'error', 82 }) 83 } finally { 84 parent.loading = false 85 } 86 } 87</script> 88 89<ul> 90 {#each nodes as node, index (node.comment.uri)} 91 <Comment 92 bind:node={nodes[index]} 93 {postRef} 94 {postAuthorDid} 95 contentClass={[ 96 (node.children.length > 0 || node.comment.stats.replyCount > 0) && 97 'border-l', 98 'ml-2.5 pl-3 sm:pl-4 lg:pl-5', 99 'comment-border', 100 ]} 101 bind:open={nodes[index].expanded} 102 > 103 <button 104 class="expand-btn" 105 onclick={() => (nodes[index].expanded = !nodes[index].expanded)} 106 aria-label={$t('comment.expand')} 107 ></button> 108 <div class={['comment-corner', node.depth == 0 && 'hidden']}></div> 109 {#if node.children?.length > 0} 110 <CommentTree 111 {postRef} 112 {postAuthorDid} 113 bind:nodes={nodes[index].children} 114 /> 115 {/if} 116 </Comment> 117 {#if node.comment.stats.replyCount > 0 && node.children.length == 0} 118 <svelte:element 119 this={browser ? 'div' : 'a'} 120 class="w-full h-10 -mt-2 -ml-2.5" 121 href="/comment/{encodeURIComponent(node.comment.uri as string)}" 122 > 123 <Button 124 loading={nodes[index].loading} 125 disabled={nodes[index].loading} 126 rounding="pill" 127 color="tertiary" 128 class="font-normal text-slate-600 dark:text-zinc-400" 129 shadow="none" 130 loaderWidth={16} 131 onclick={() => { 132 if (nodes[index].depth > 4) { 133 goto( 134 `/comment/${encodeURIComponent(nodes[index].comment.uri as string)}#comments`, 135 ) 136 } else { 137 fetchChildren(nodes[index]) 138 } 139 }} 140 icon={ArrowDownCircle} 141 > 142 {$t('comment.more', { 143 comments: node.comment.stats.replyCount, 144 })} 145 </Button> 146 </svelte:element> 147 {/if} 148 {/each} 149</ul> 150 151<style> 152 @reference '../../../app.css'; 153 154 :global(.comment-border) { 155 border-color: var(--color-slate-200); 156 @variant dark { 157 border-color: var(--color-zinc-800); 158 } 159 160 transition: border-color 600ms cubic-bezier(0.075, 0.82, 0.165, 1); 161 162 &:has(:global(> * > .expand-btn:hover:not(:active))) { 163 border-color: color-mix( 164 in oklab, 165 var(--color-primary-900), 166 var(--color-slate-500) 167 ); 168 @variant dark { 169 border-color: color-mix( 170 in oklab, 171 var(--color-primary-100), 172 var(--color-zinc-500) 173 ); 174 } 175 } 176 } 177 178 .expand-btn { 179 width: calc(var(--spacing) * 4); 180 position: absolute; 181 top: 0; 182 left: calc(var(--spacing) * 0.5); 183 height: 100%; 184 cursor: pointer; 185 } 186 187 .comment-corner { 188 position: absolute; 189 top: calc(var(--spacing) * 2); 190 left: calc(var(--spacing) * -3); 191 border-bottom-left-radius: calc(infinity * 1px); 192 border-left-width: 1px; 193 border-bottom-width: 1px; 194 border-color: var(--color-slate-200); 195 @variant dark { 196 border-color: var(--color-zinc-800); 197 } 198 width: calc(var(--spacing) * 3); 199 height: calc(var(--spacing) * 3); 200 201 @variant sm { 202 top: calc(var(--spacing) * 1); 203 left: calc(var(--spacing) * -5.5); 204 width: calc(var(--spacing) * 5); 205 height: calc(var(--spacing) * 5); 206 } 207 } 208</style>