Show full threads with replies

This commit is contained in:
dtonon 2026-04-06 17:19:38 +01:00
parent 75d1d16cf1
commit 994e76313e
3 changed files with 187 additions and 139 deletions

View file

@ -1,9 +1,9 @@
<script lang="ts">
import { onMount } from "svelte";
import type { Post } from "$lib/mock";
type PostLike = { createdAt: number };
type Props = {
posts: Post[];
posts: PostLike[];
postEls: (HTMLElement | null)[];
topOffset: number;
};
@ -52,15 +52,15 @@
: "",
);
function formatShortDate(iso: string) {
return new Date(iso).toLocaleDateString("en-US", {
function formatShortDate(ts: number) {
return new Date(ts * 1000).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
});
}
function formatDate(iso: string) {
return new Date(iso).toLocaleDateString("en-US", {
function formatDate(ts: number) {
return new Date(ts * 1000).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",

97
src/lib/thread.svelte.ts Normal file
View file

@ -0,0 +1,97 @@
import { SimplePool } from "@nostr/tools";
import { loadNostrUser, type NostrUser } from "@nostr/gadgets/metadata";
import { RELAY_URL } from "$lib/config";
import { threads as mockThreads } from "$lib/mock";
const isNostrId = (id: string) => /^[0-9a-f]{64}$/.test(id);
export type PostData = {
id: string;
pubkey: string;
createdAt: number;
content: string;
};
type ThreadDetail = {
id: string;
title: string;
op: PostData;
replies: PostData[];
};
let detail = $state<ThreadDetail | null>(null);
let profiles = $state<Record<string, NostrUser>>({});
export const threadDetailStore = {
get detail() { return detail; },
get profiles() { return profiles; },
};
async function loadProfile(pubkey: string) {
if (profiles[pubkey]) return;
const user = await loadNostrUser(pubkey);
profiles[pubkey] = user;
}
function loadMockThread(id: string) {
const t = mockThreads.find((t) => t.id === id);
if (!t) return;
const toUnix = (iso: string) => Math.floor(new Date(iso).getTime() / 1000);
detail = {
id: t.id,
title: t.title,
op: { id: t.op.id, pubkey: t.op.author.pubkey, createdAt: toUnix(t.op.createdAt), content: t.op.content },
replies: (t.op.replies ?? []).map((r) => ({
id: r.id, pubkey: r.author.pubkey, createdAt: toUnix(r.createdAt), content: r.content,
})),
};
const allAuthors = [t.op.author, ...(t.op.replies ?? []).map((r) => r.author)];
for (const a of allAuthors) {
profiles[a.pubkey] = {
pubkey: a.pubkey, npub: a.pubkey, shortName: a.name, image: a.picture,
metadata: { name: a.name, picture: a.picture },
lastUpdated: 0,
} as NostrUser;
}
}
export async function loadThread(id: string) {
detail = null;
profiles = {};
if (!isNostrId(id)) { await Promise.resolve(); loadMockThread(id); return; }
const pool = new SimplePool();
try {
const [threadEvents, replyEvents] = await Promise.all([
pool.querySync([RELAY_URL], { kinds: [11], ids: [id] }),
pool.querySync([RELAY_URL], { kinds: [1111], "#E": [id] }),
]);
const event = threadEvents[0];
if (!event) return;
const replies = replyEvents.sort((a, b) => a.created_at - b.created_at);
detail = {
id: event.id,
title: event.tags.find((t) => t[0] === "title")?.[1] ?? "(untitled)",
op: {
id: event.id,
pubkey: event.pubkey,
createdAt: event.created_at,
content: event.content,
},
replies: replies.map((r) => ({
id: r.id,
pubkey: r.pubkey,
createdAt: r.created_at,
content: r.content,
})),
};
[event.pubkey, ...replies.map((r) => r.pubkey)].forEach(loadProfile);
} finally {
pool.close([RELAY_URL]);
}
}