chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
import { useQuery } from '@tanstack/react-query';
import { CircleDotIcon, MessageSquareIcon } from 'lucide-react';
import { ButtonLink } from '#/components/atoms/button-link.jsx';
import { EmptyState } from '#/components/molecules/empty-state.jsx';
import { Handle } from '#/components/molecules/handle.jsx';
import { issueLook } from '#/components/molecules/issue-state.jsx';
import { ListBox } from '#/components/molecules/list-box.jsx';
import { Dot, ListCount, ListRow } from '#/components/molecules/list-row.jsx';
import { ListSkeleton } from '#/components/molecules/list-skeleton.jsx';
import { PageBar } from '#/components/molecules/page-bar.jsx';
import { PageNotice } from '#/components/molecules/page-notice.jsx';
import { Screen } from '#/components/molecules/screen.jsx';
import { TabBar } from '#/components/molecules/tab-bar.jsx';
import { timeAgo } from '#/lib/format.js';
import { account, repoRecord } from '#/lib/git.js';
import { issueThreads, loadIssueStatements } from '#/lib/issues.js';
/**
* The two questions a reader has: what is live, and what was answered. A tab
* is a filter rather than a state, so closed holds both of the words the
* owner can settle a thread with, and the mark on each row says which one
* they used.
*/
const TABS = [
{
key: 'open',
label: 'Open',
holds: ['open'],
nothing: 'Every issue anyone has filed has been answered.',
},
{
key: 'closed',
label: 'Closed',
holds: ['resolved', 'declined'],
nothing: 'The owner has settled none of these.',
},
];
/**
* The repository's issues: every thread, newest activity first, each with
* the state the owner's latest disposition gives it.
*
* Anyone signed in files one, into their own repo. Listed voices show at
* once; anybody else's arrives through the runners' index, so a stranger's
* issue appears when a runner has seen it.
*
* One read fills both tabs, so moving between them costs nothing.
*/
export function IssuesPage({ repo, tab, space }) {
const record = repoRecord(repo, space);
const { data: statements } = useQuery({
queryKey: ['issues', repo, space ?? ''],
enabled: Boolean(record),
queryFn: () => loadIssueStatements(repo, space),
});
if (!record) {
return <PageNotice>No such repository.</PageNotice>;
}
const threads = statements ? issueThreads(statements, account.did) : null;
const active = TABS.find((one) => one.key === tab) ?? TABS[0];
const at = `/${encodeURIComponent(repo)}/issues`;
const held = (entry) =>
(threads ?? []).filter((one) => entry.holds.includes(one.state.state));
const shown = threads ? held(active) : [];
return (
<Screen
bar={
<PageBar
tabs
// Filing is a page of its own: an issue is somebody's account of
// what went wrong, and it is markdown wherever it is read.
actions={
<ButtonLink href={`${at}/new`} variant="default">
New issue
</ButtonLink>
}
>
<TabBar
className="border-b-0"
active={active.key}
tabs={TABS.map((entry) => ({
key: entry.key,
label: entry.label,
href: entry.key === 'open' ? at : `${at}/${entry.key}`,
count: threads ? held(entry).length : undefined,
}))}
/>
</PageBar>
}
>
<ListBox>
{!threads ? (
<ListSkeleton />
) : shown.length === 0 ? (
<EmptyState
icon={<CircleDotIcon />}
title={
threads.length === 0
? 'No issues'
: `Nothing ${active.label.toLowerCase()}`
}
description={
threads.length === 0
? 'No one has filed an issue here yet.'
: active.nothing
}
/>
) : (
<div>
{shown.map(({ root, thread, state }) => {
const look = issueLook(state.state);
const rkey = root.uri?.split('/').pop();
return (
<ListRow
key={root.uri ?? root.createdAt}
Icon={look.Icon}
iconClassName={look.ink}
title={root.title || '(untitled)'}
href={`${at}/${encodeURIComponent(root.by?.did ?? '')}/${encodeURIComponent(rkey ?? '')}`}
marks={
<ListCount
Icon={MessageSquareIcon}
count={thread.length - 1}
className="text-faint"
title="Replies"
/>
}
>
{/* A sentence rather than a row of states. The mark and the
tab both say the state, so the words say when the owner
said it, which nothing else on the row can. */}
<Handle who={root.by} className="text-[12.5px]" />
<Dot />
<span>opened {timeAgo(root.createdAt)}</span>
{state.said && (
<>
<Dot />
<span>
{state.said.disposition} {timeAgo(state.said.createdAt)}
</span>
</>
)}
</ListRow>
);
})}
</div>
)}
</ListBox>
</Screen>
);
}