136 lines
3.8 KiB
Svelte
136 lines
3.8 KiB
Svelte
<script lang="ts">
|
|
import {
|
|
threadStore,
|
|
loadThreads,
|
|
loadMore,
|
|
type ThreadData,
|
|
type SortMode,
|
|
} from "$lib/threads.svelte";
|
|
import ThreadItem, {
|
|
type ThreadRow,
|
|
type Author,
|
|
} from "$lib/components/ThreadItem.svelte";
|
|
import SortToggle from "$lib/components/SortToggle.svelte";
|
|
import type { NostrUser } from "@nostr/gadgets/metadata";
|
|
import { auth, openLogin } from "$lib/auth.svelte";
|
|
import { openDraft } from "$lib/draft.svelte";
|
|
import { sortPref } from "$lib/sort.svelte";
|
|
import { page } from "$app/state";
|
|
|
|
type Props = {
|
|
groupId: string;
|
|
title: string;
|
|
};
|
|
|
|
let { groupId, title }: Props = $props();
|
|
|
|
function parseSort(v: string | null): SortMode | null {
|
|
return v === "new" ? "new" : v === "active" ? "active" : null;
|
|
}
|
|
|
|
// URL param is the explicit override; otherwise fall back to the saved
|
|
// preference so the sort survives Home/room navigation.
|
|
const urlSort = $derived(parseSort(page.url.searchParams.get("sort")));
|
|
const sort = $derived<SortMode>(urlSort ?? sortPref.value);
|
|
|
|
// Remember any explicit choice that arrives via the URL.
|
|
$effect(() => {
|
|
if (urlSort) sortPref.value = urlSort;
|
|
});
|
|
|
|
// Re-runs on mount and whenever the group or effective sort changes.
|
|
$effect(() => {
|
|
loadThreads(groupId, sort);
|
|
});
|
|
|
|
function onNewTopic() {
|
|
if (!auth.user) {
|
|
openLogin();
|
|
return;
|
|
}
|
|
openDraft();
|
|
}
|
|
|
|
function relativeTime(ts: number): string {
|
|
const diff = Math.floor(Date.now() / 1000) - ts;
|
|
if (diff < 3600) return `${Math.floor(diff / 60)}m`;
|
|
if (diff < 86400) return `${Math.floor(diff / 3600)}h`;
|
|
return `${Math.floor(diff / 86400)}d`;
|
|
}
|
|
|
|
function resolveAuthor(
|
|
pubkey: string,
|
|
profiles: Record<string, NostrUser>,
|
|
): Author {
|
|
const user = profiles[pubkey];
|
|
return {
|
|
pubkey,
|
|
name: user?.shortName ?? pubkey.slice(0, 8),
|
|
picture: user?.metadata.picture,
|
|
};
|
|
}
|
|
|
|
function toRow(
|
|
t: ThreadData,
|
|
profiles: Record<string, NostrUser>,
|
|
): ThreadRow {
|
|
return {
|
|
id: t.id,
|
|
title: t.title,
|
|
labels: t.labels,
|
|
author: resolveAuthor(t.authorPubkey, profiles),
|
|
replyCount: t.replyCount,
|
|
repliers: t.replierPubkeys.map((pk) => resolveAuthor(pk, profiles)),
|
|
lastActiveAuthor: resolveAuthor(t.latestPubkey, profiles),
|
|
lastActivity: relativeTime(t.latestAt),
|
|
};
|
|
}
|
|
|
|
const rows = $derived(
|
|
threadStore.threads.map((t) => toRow(t, threadStore.profiles)),
|
|
);
|
|
</script>
|
|
|
|
<svelte:head>
|
|
<title>{title}</title>
|
|
</svelte:head>
|
|
|
|
<div class="mx-auto max-w-6xl">
|
|
<div class="flex flex-wrap items-center justify-between gap-2 py-2">
|
|
<h1 class="text-brand text-[1.65rem] leading-7">{title}</h1>
|
|
<div class="flex items-center gap-2">
|
|
<button
|
|
onclick={onNewTopic}
|
|
class="bg-brand hover:bg-brand-hover rounded px-4 py-1.5 font-medium text-white md:px-6 md:text-sm"
|
|
>
|
|
New discussion
|
|
</button>
|
|
<SortToggle {sort} />
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
{#each rows as thread}
|
|
<ThreadItem {thread} />
|
|
{/each}
|
|
</div>
|
|
|
|
{#if threadStore.loading && rows.length === 0}
|
|
<p class="py-6 text-center text-sm text-neutral-400">
|
|
Loading discussions…
|
|
</p>
|
|
{:else if !threadStore.exhausted}
|
|
<div class="flex justify-center py-6">
|
|
<button
|
|
onclick={() => loadMore(groupId)}
|
|
disabled={threadStore.loadingMore}
|
|
aria-busy={threadStore.loadingMore}
|
|
class="rounded border border-neutral-200 px-6 py-1.5 text-sm font-medium text-neutral-700 hover:bg-neutral-50 disabled:opacity-50"
|
|
>
|
|
{threadStore.loadingMore ? "Loading…" : "Show more"}
|
|
</button>
|
|
</div>
|
|
{:else if rows.length > 0}
|
|
<p class="py-6 text-center text-sm text-neutral-400">No more discussions</p>
|
|
{/if}
|
|
</div>
|