You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

241 lines
6.3 KiB

<script module lang="ts">
export interface Comment {
id: string;
content: string;
author: string;
createdAt: number;
kind: number;
pubkey: string;
replies?: Comment[];
}
</script>
<script lang="ts">
import UserBadge from '$lib/components/UserBadge.svelte';
import EventCopyButton from '$lib/components/EventCopyButton.svelte';
import {
processContentWithNostrLinks,
getReferencedEventWithTagType,
formatDiscussionTime,
type ProcessedContentPart
} from '$lib/utils/nostr-links.js';
import type { NostrEvent } from '$lib/types/nostr.js';
import CommentRendererSelf from './CommentRenderer.svelte';
interface Props {
comment: Comment;
commentEvent?: NostrEvent; // Full event for getting referenced events
eventCache: Map<string, NostrEvent>;
profileCache: Map<string, string>;
userPubkey?: string | null;
onReply?: (commentId: string) => void;
nested?: boolean;
}
let {
comment,
commentEvent,
eventCache,
profileCache,
userPubkey,
onReply,
nested = false
}: Props = $props();
const referencedEventWithTag = $derived(commentEvent
? getReferencedEventWithTagType(commentEvent, eventCache)
: undefined);
const referencedEvent = $derived(referencedEventWithTag?.event);
const referencedTagType = $derived(referencedEventWithTag?.tagType);
const referencedLabel = $derived(
referencedTagType === 'q' ? 'Quoting:' : 'Reply-To:'
);
const contentParts = $derived(processContentWithNostrLinks(comment.content, eventCache, profileCache));
</script>
<div class="comment-item" class:nested-comment={nested}>
<div class="comment-meta">
<UserBadge pubkey={comment.author} />
<span>{new Date(comment.createdAt * 1000).toLocaleString()}</span>
<EventCopyButton eventId={comment.id} kind={comment.kind} pubkey={comment.pubkey} />
{#if userPubkey && onReply}
<button
class="create-reply-button"
onclick={() => onReply(comment.id)}
title="Reply to comment"
>
<img src="/icons/plus.svg" alt="Reply" class="icon" />
</button>
{/if}
</div>
<div class="comment-content">
{#if referencedEvent}
<div class="referenced-event">
<div class="referenced-event-header">
<span class="referenced-event-label">{referencedLabel}</span>
<UserBadge pubkey={referencedEvent.pubkey} disableLink={true} inline={true} />
<span class="referenced-event-time">{formatDiscussionTime(referencedEvent.created_at)}</span>
</div>
<div class="referenced-event-content">{referencedEvent.content || '(No content)'}</div>
</div>
{/if}
<div>
{#each contentParts as part}
{#if part.type === 'text'}
<span>{part.value}</span>
{:else if part.type === 'event' && part.event}
<div class="nostr-link-event">
<div class="nostr-link-event-header">
<UserBadge pubkey={part.event.pubkey} disableLink={true} />
<span class="nostr-link-event-time">{formatDiscussionTime(part.event.created_at)}</span>
</div>
<div class="nostr-link-event-content">{part.event.content || '(No content)'}</div>
</div>
{:else if part.type === 'profile' && part.pubkey}
<UserBadge pubkey={part.pubkey} />
{:else}
<span class="nostr-link-placeholder">{part.value}</span>
{/if}
{/each}
</div>
</div>
{#if comment.replies && comment.replies.length > 0}
<div class="nested-replies">
{#each comment.replies as reply}
<CommentRendererSelf
comment={reply}
commentEvent={eventCache.get(reply.id)}
{eventCache}
{profileCache}
{userPubkey}
{onReply}
nested={true}
/>
{/each}
</div>
{/if}
</div>
<style>
.comment-item {
margin-bottom: 1rem;
padding: 0.75rem;
border-left: 2px solid var(--border-color);
background: var(--bg-tertiary, var(--bg-secondary));
color: var(--text-primary);
}
.nested-comment {
margin-left: 1.5rem;
margin-top: 0.5rem;
border-left-color: var(--border-color);
background: var(--bg-secondary, var(--bg-primary));
}
.comment-meta {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
font-size: 0.875rem;
color: var(--text-secondary);
}
.comment-content {
margin-top: 0.5rem;
color: var(--text-primary);
}
.create-reply-button {
margin-left: auto;
background: none;
border: none;
cursor: pointer;
padding: 0.25rem;
display: flex;
align-items: center;
}
.create-reply-button:hover {
opacity: 0.7;
}
.create-reply-button .icon {
width: 16px;
height: 16px;
}
.nested-replies {
margin-top: 0.75rem;
padding-left: 0.5rem;
}
.referenced-event {
margin-bottom: 0.75rem;
padding: 0.5rem;
background: var(--bg-secondary, var(--bg-primary));
color: var(--text-muted, var(--text-secondary));
border-radius: 4px;
border-left: 2px solid var(--border-light, var(--border-color));
opacity: 0.8;
}
.referenced-event-header {
display: flex;
align-items: center;
gap: 0.375rem;
margin-bottom: 0.25rem;
font-size: 0.75rem;
color: var(--text-muted, var(--text-secondary));
}
.referenced-event-label {
font-weight: 500;
color: var(--text-muted, var(--text-secondary));
}
.referenced-event-time {
color: var(--text-muted, var(--text-secondary));
font-size: 0.7rem;
}
.referenced-event-content {
font-size: 0.8rem;
color: var(--text-muted, var(--text-secondary));
line-height: 1.4;
}
.nostr-link-event {
margin: 0.5rem 0;
padding: 0.5rem;
background: var(--bg-secondary, var(--bg-primary));
color: var(--text-primary);
border-radius: 4px;
border-left: 2px solid var(--border-color);
}
.nostr-link-event-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.25rem;
font-size: 0.875rem;
}
.nostr-link-event-time {
color: var(--text-secondary);
}
.nostr-link-event-content {
font-size: 0.9rem;
color: var(--text-primary);
}
.nostr-link-placeholder {
color: var(--accent-color, var(--button-primary));
text-decoration: underline;
}
</style>