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.
 
 
 
 
 

146 lines
4.2 KiB

<script lang="ts">
import ProfileBadge from '../../components/layout/ProfileBadge.svelte';
import MarkdownRenderer from '../../components/content/MarkdownRenderer.svelte';
import MediaAttachments from '../../components/content/MediaAttachments.svelte';
import CommentThread from '../comments/CommentThread.svelte';
import ReactionButtons from '../reactions/ReactionButtons.svelte';
import ZapButton from '../zaps/ZapButton.svelte';
import ZapReceipt from '../zaps/ZapReceipt.svelte';
import { nostrClient } from '../../services/nostr/nostr-client.js';
import { relayManager } from '../../services/nostr/relay-manager.js';
import { onMount } from 'svelte';
import type { NostrEvent } from '../../types/nostr.js';
interface Props {
threadId: string;
}
let { threadId }: Props = $props();
let thread = $state<NostrEvent | null>(null);
let loading = $state(true);
onMount(async () => {
await nostrClient.initialize();
loadThread();
});
$effect(() => {
if (threadId) {
loadThread();
}
});
async function loadThread() {
loading = true;
try {
const relays = relayManager.getThreadReadRelays();
const event = await nostrClient.getEventById(threadId, relays);
thread = event;
} catch (error) {
console.error('Error loading thread:', error);
} finally {
loading = false;
}
}
function getTitle(): string {
if (!thread) return '';
const titleTag = thread.tags.find((t) => t[0] === 'title');
return titleTag?.[1] || 'Untitled';
}
function getTopics(): string[] {
if (!thread) return [];
return thread.tags.filter((t) => t[0] === 't').map((t) => t[1]).slice(0, 3);
}
function getClientName(): string | null {
if (!thread) return null;
const clientTag = thread.tags.find((t) => t[0] === 'client');
return clientTag?.[1] || null;
}
function getRelativeTime(): string {
if (!thread) return '';
const now = Math.floor(Date.now() / 1000);
const diff = now - thread.created_at;
const hours = Math.floor(diff / 3600);
const days = Math.floor(diff / 86400);
if (days > 0) return `${days}d ago`;
if (hours > 0) return `${hours}h ago`;
return 'just now';
}
</script>
{#if loading}
<p class="text-fog-text dark:text-fog-dark-text">Loading thread...</p>
{:else if thread}
<article class="thread-view">
<div class="thread-header mb-4">
<h1 class="text-2xl font-bold mb-2">{getTitle()}</h1>
<div class="flex items-center gap-2 mb-2">
<ProfileBadge pubkey={thread.pubkey} />
<span class="text-sm text-fog-text-light dark:text-fog-dark-text-light">{getRelativeTime()}</span>
{#if getClientName()}
<span class="text-xs text-fog-text-light dark:text-fog-dark-text-light">via {getClientName()}</span>
{/if}
</div>
{#if getTopics().length > 0}
<div class="flex gap-2 mb-2">
{#each getTopics() as topic}
<span class="text-xs bg-fog-highlight dark:bg-fog-dark-highlight text-fog-text dark:text-fog-dark-text px-2 py-1 rounded">{topic}</span>
{/each}
</div>
{/if}
</div>
<div class="thread-content mb-4">
<MediaAttachments event={thread} />
<MarkdownRenderer content={thread.content} />
</div>
<div class="thread-actions flex items-center gap-4 mb-6">
<ReactionButtons event={thread} />
<ZapButton event={thread} />
<ZapReceipt eventId={thread.id} pubkey={thread.pubkey} />
</div>
<div class="comments-section">
<CommentThread threadId={thread.id} />
</div>
</article>
{:else}
<p class="text-fog-text dark:text-fog-dark-text">Thread not found</p>
{/if}
<style>
.thread-view {
max-width: var(--content-width);
margin: 0 auto;
}
.thread-content {
line-height: 1.6;
}
.thread-actions {
padding-top: 1rem;
border-top: 1px solid var(--fog-border, #e5e7eb);
}
:global(.dark) .thread-actions {
border-top-color: var(--fog-dark-border, #374151);
}
.comments-section {
margin-top: 2rem;
padding-top: 2rem;
border-top: 1px solid var(--fog-border, #e5e7eb);
}
:global(.dark) .comments-section {
border-top-color: var(--fog-dark-border, #374151);
}
</style>