Something went wrong. Try again.
Monorepo for Tangled
Something went wrong. Try again.
Svelte
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228<script module lang="ts"> import type { ReactionGroup } from "$lib/components/reaction/reactions";
export interface IssueData { uri: string; cid?: string; rkey: string; title: string; body: string; bodyHtml: string | null; state: "open" | "closed"; authorDid: string; authorHandle: string; createdAt: string; reactions: ReactionGroup[]; }</script>
<script lang="ts"> import { untrack } from "svelte"; import { goto } from "$app/navigation"; import { resolve } from "$app/paths"; import Pencil from "$icon/pencil"; import Trash2 from "$icon/trash-2"; import { createAction } from "$lib/action.svelte"; import { deleteIssue, type IssueRecord } from "$lib/api/issue"; import type { RecordView } from "$lib/api/records"; import { getAuth } from "$lib/auth.svelte"; import CommentBox from "$lib/components/comment/CommentBox.svelte"; import CommentList from "$lib/components/comment/CommentList.svelte"; import type { CommentThread, ThreadInput } from "$lib/components/comment/comments"; import ReportMenu from "$lib/components/moderation/ReportMenu.svelte"; import Reactions from "$lib/components/reaction/Reactions.svelte"; import IssueBody from "$lib/components/repo/issues/IssueBody.svelte"; import IssueForm from "$lib/components/repo/issues/IssueForm.svelte"; import IssueHeader from "$lib/components/repo/issues/IssueHeader.svelte"; import IssueInfoBar from "$lib/components/repo/issues/IssueInfoBar.svelte"; import IssueLabelPanel from "$lib/components/repo/issues/IssueLabelPanel.svelte"; import type { RepoInfo } from "$lib/components/repo/types"; import ErrorAlert from "$lib/components/ui/Error.svelte"; import SignupPrompt from "$lib/components/ui/SignupPrompt.svelte"; import TabPanel from "$lib/components/ui/TabPanel.svelte"; import { renderMarkup } from "$lib/markup"; import type { MarkupContext } from "$lib/markup/paths"; import type { Did } from "@atcute/lexicons/syntax";
interface Props { issue: IssueData; comments: CommentThread[]; markup: MarkupContext; repo: RepoInfo; }
let props: Props = $props();
const auth = getAuth();
// local state for optimistic replies, re-synced when streamed props land let issue = $state<IssueData>(untrack(() => props.issue)); let comments = $state<CommentThread[]>(untrack(() => props.comments)); $effect(() => { issue = props.issue; comments = props.comments; });
let editing = $state(false);
const isAuthor = $derived(auth.hasAccount(issue.authorDid)); const issuesBase = $derived(`/${props.repo.ownerHandle}/${props.repo.name}/issues`);
const insertComment = ({ comment, replyTo }: ThreadInput) => { if (replyTo === null) { comments = [...comments, { self: comment, replies: [] }]; } else { comments = comments.map((thread) => thread.self.uri === replyTo ? { ...thread, replies: [...thread.replies, comment] } : thread ); } };
const updateComment = ({ comment }: ThreadInput) => { comments = comments.map((thread) => ({ self: thread.self.uri === comment.uri ? comment : thread.self, replies: thread.replies.map((reply) => (reply.uri === comment.uri ? comment : reply)) })); };
const removeComment = (uri: string) => { comments = comments .map((thread) => { if (thread.self.uri === uri) { return thread.replies.length ? { ...thread, self: { ...thread.self, body: "", bodyHtml: null, deleted: true } } : null; } return { ...thread, replies: thread.replies.filter((reply) => reply.uri !== uri) }; }) .filter((thread) => thread !== null); };
const handleSaved = async (saved: RecordView<IssueRecord>) => { const { title, body = "" } = saved.value; const bodyHtml = body ? await renderMarkup(body, props.markup).catch(() => null) : null; issue = { ...issue, title, body, bodyHtml }; editing = false; };
const removeIssue = createAction(async () => { const agent = await auth.agentFor(issue.authorDid as Did); await deleteIssue(agent, issue.rkey); await goto(resolve(issuesBase as "/")); });
const handleDelete = () => { if (!auth.currentDid || removeIssue.loading) return; if (!confirm("Delete this issue? This cannot be undone.")) return; void removeIssue.run(); };</script>
<svelte:head> <title >{issue.title} · Issue #{issue.rkey} · {props.repo.ownerHandle}/{props.repo .name} · Tangled</title ></svelte:head>
{#snippet issueActions()} <button type="button" aria-label="Edit issue" class="cursor-pointer text-foreground-subtle hover:text-foreground-default" onclick={() => (editing = true)} > <Pencil class="size-3" /> </button> <button type="button" aria-label="Delete issue" disabled={removeIssue.loading} class="cursor-pointer text-foreground-danger hover:text-foreground-danger-strong disabled:opacity-50" onclick={handleDelete} > <Trash2 class="size-3" /> </button>{/snippet}
<div class="grid grid-cols-1 gap-4 lg:grid-cols-10"> <div class="lg:col-span-8"> <TabPanel> {#if editing} <IssueForm mode="edit" authorDid={issue.authorDid} repoDid={props.repo.repoDid ?? ""} markup={props.markup} rkey={issue.rkey} createdAt={issue.createdAt} title={issue.title} body={issue.body} onsaved={handleSaved} oncancel={() => (editing = false)} /> {:else} <IssueHeader title={issue.title} rkey={issue.rkey} /> <IssueInfoBar state={issue.state} authorHandle={issue.authorHandle} authorDid={issue.authorDid} createdAt={issue.createdAt} actions={isAuthor ? issueActions : undefined} /> <IssueBody body={issue.body} bodyHtml={issue.bodyHtml} /> <div class="mt-2 flex items-center gap-2"> <Reactions reactions={issue.reactions} subjectUri={issue.uri} alwaysShow /> <ReportMenu class="ml-auto" subjectUri={issue.uri} subjectCid={issue.cid} subjectNoun="issue" authorDid={issue.authorDid} authorHandle={issue.authorHandle} /> </div> {#if removeIssue.error} <div class="mt-3"> <ErrorAlert label={removeIssue.error} /> </div> {/if} {/if} </TabPanel>
<div class="mt-4 flex flex-col gap-4"> <CommentList threads={comments} subjectUri={issue.uri} subjectCid={issue.cid} markup={props.markup} onsubmitted={insertComment} onedited={updateComment} ondeleted={removeComment} />
{#if auth.currentUser} <CommentBox subjectUri={issue.uri} subjectCid={issue.cid} authorDid={auth.currentUser.did} authorHandle={auth.currentUser.handle} markup={props.markup} onsubmitted={insertComment} /> {:else} <SignupPrompt /> {/if} </div> </div>
<div class="lg:col-span-2"> <IssueLabelPanel /> </div></div>