Something went wrong. Try again.
Monorepo for Tangled forked from tangled.org/core
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192<script module lang="ts"> import { tv, type VariantProps } from "tailwind-variants";
export const comment = tv({ slots: { root: "grid grid-cols-[auto_minmax(0,1fr)] gap-x-1.5", avatar: "row-span-2 self-start", meta: "flex flex-wrap items-center gap-xs text-foreground-muted", actions: "ml-auto shrink-0", content: "col-start-2", editor: "mt-2xs", markup: "markup mt-2xs", plain: "mt-2xs wrap-anywhere whitespace-pre-wrap text-foreground-default", reactions: "mt-xs" }, variants: { variant: { top: { root: "border-b border-border-default bg-background-default p-sm pb-lg" }, reply: { root: "pt-sm pr-md pb-md" } }, deleted: { true: { root: "block text-foreground-muted italic" } }, masked: { true: {} } }, compoundVariants: [{ variant: "top", masked: true, class: { root: "pb-sm" } }], defaultVariants: { variant: "top" } });
export type CommentVariants = VariantProps<typeof comment>;</script>
<script lang="ts"> import type { Snippet } from "svelte"; import type { Attachment } from "svelte/attachments"; import type { Moderation } from "#lib/api/moderationLabels.ts"; import type { KnotOrigin } from "#lib/api/syntax.ts"; import TriangleAlert from "$icon/triangle-alert"; import ModerationNotice from "#lib/components/moderation/ModerationNotice.svelte"; import { createAuthorGate, type ModerationGate } from "#lib/components/moderation/gate.svelte.ts"; import VouchAvatar from "#lib/components/profile/VouchAvatar.svelte"; import { avatar } from "#lib/components/ui/Avatar.svelte"; import ProfilePopover from "#lib/components/profile/ProfilePopover.svelte"; import User from "#lib/components/ui/User.svelte"; import TimeAgo from "#lib/components/ui/TimeAgo.svelte"; import Link from "#lib/components/ui/Link.svelte"; import Reactions from "#lib/components/reaction/Reactions.svelte"; import Markup from "#lib/components/ui/Markup.svelte"; import type { MarkupContext } from "#lib/markup/paths.ts"; import type { ReactionGroup } from "#lib/components/reaction/reactions.ts";
interface Props { authorHandle: string; authorDid: string; moderation?: Moderation; gate?: ModerationGate; createdAt: string; anchor?: string; body: string; markup: MarkupContext; variant?: CommentVariants["variant"]; deleted?: boolean; unloaded?: boolean; reactions?: ReactionGroup[]; subjectUri: string; subjectCid?: string; knot?: KnotOrigin; onreactionschange?: (next: ReactionGroup[]) => void; editor?: Snippet; actions?: Snippet; }
let { authorHandle, authorDid, moderation, gate: sharedGate, createdAt, anchor, body, markup, variant = "top", deleted = false, unloaded = false, reactions = [], subjectUri, subjectCid, knot, onreactionschange, editor, actions }: Props = $props();
const classes = $derived(comment({ variant, deleted })); const anchorId = $derived(anchor ? `comment-${anchor}` : undefined);
const ownGate = createAuthorGate( () => moderation, () => authorDid ); const gate = $derived(sharedGate ?? ownGate);
let content = $state<HTMLElement>(); // the rendered comment's height, kept from before the editor swapped in let restingHeight = 0; const measure = (height: number) => { if (!editor) restingHeight = height; }; // grow the editor's field so the comment keeps its height instead of jumping const fillRestingHeight: Attachment<HTMLElement> = (node) => { const field = node.querySelector("textarea"); const missing = restingHeight - (content?.offsetHeight ?? 0); if (field && missing > 0) field.style.height = `${field.offsetHeight + missing}px`; };</script>
{#if deleted && unloaded} <div id={anchorId} data-testid="unloadedCommentNotice" class={classes.root()}> In reply to an earlier comment. Load older comments to see it. </div>{:else if deleted} <div id={anchorId} data-testid="deletedCommentNotice" class={classes.root()}> This comment was deleted. </div>{:else if gate.masked} <div id={anchorId} class={comment({ variant, masked: true }).root()}> <span class={avatar({ size: "large", class: classes.avatar() })}> <TriangleAlert class="size-1/2" aria-hidden="true" /> </span> <ModerationNotice {gate} icon={false} /> </div>{:else} <div id={anchorId} class={classes.root()}> <div class={classes.avatar()}> <ProfilePopover identifier={authorDid}> <VouchAvatar did={authorDid} handle={authorHandle} size="large" /> </ProfilePopover> </div> <div class={classes.meta()}> <ProfilePopover identifier={authorDid}> <User handle={authorHandle} did={authorDid} showImage={false} link handleClass="font-semibold" /> </ProfilePopover> {#if anchorId} <Link fragment={anchorId} variant="inherit"><TimeAgo value={createdAt} /></Link> {:else} <TimeAgo value={createdAt} /> {/if} {#if actions} <!-- kept mounted while editing so the row keeps its height (no header shift) --> <div class={classes.actions()} class:invisible={!!editor}>{@render actions()}</div> {/if} </div> <div bind:this={content} bind:offsetHeight={null, measure} class={classes.content()}> {#if editor} <div class={classes.editor()} {@attach fillRestingHeight}>{@render editor()}</div> {:else} <Markup source={body} ctx={markup} tag="article" data-testid="commentBody" class={classes.markup()} plainClass={classes.plain()} /> {/if} {#if !editor} <Reactions {reactions} {subjectUri} {subjectCid} {knot} onchange={onreactionschange} class={classes.reactions()} /> {/if} </div> </div>{/if}