johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
<script lang="ts">
import type { CommentView, LineView } from '#lib/lexicons/dev/knotless/defs.ts';
import type { Snippet } from 'svelte';
import { untrack } from 'svelte';
import type { AtUriString } from '@atproto/lex';
import { ago } from '#lib/format.ts';
import { fences } from '#lib/highlight.ts';
import { diagrams } from '#lib/mermaid.ts';
import Avatar from '#lib/atoms/Avatar.svelte';
import Icon from '#lib/atoms/Icon.svelte';
import Time from '#lib/atoms/Time.svelte';
import LineQuote from '#lib/molecules/LineQuote.svelte';
import MarkdownField from '#lib/molecules/MarkdownField.svelte';
import Person from '#lib/molecules/Person.svelte';
/**
* A thread on a pull's lines, as GitHub's review comments are: the
* comment that starts it and the answers to it, oldest first, each with
* `menu` of what can be done with it; with `quote`, the lines it is on
* first. Whoever is `in` may answer it with `reply`, and whoever `may`
* resolve it, or open it again, with `resolve`; each answers why it
* failed, or nothing. Resolved, it shows folded until opened.
*/
let {
root,
replies,
quote = false,
in: logged,
may = false,
repo,
reply,
resolve,
menu
}: {
root: CommentView & { line: LineView };
replies: CommentView[];
quote?: boolean;
in: boolean;
may?: boolean;
repo?: AtUriString;
reply: (body: string) => Promise<string | undefined>;
resolve: (resolved: boolean) => Promise<string | undefined>;
menu?: Snippet<[CommentView]>;
} = $props();
let open = $state(untrack(() => !root.resolved));
let answering = $state(false);
let body = $state('');
// Which is under way: 'reply' or 'resolve'.
let busy = $state('');
let problem = $state('');
const at = $derived(root.line);
const range = $derived(at.startLine ? `lines ${at.startLine} to ${at.line}` : `line ${at.line}`);
const anchor = (c: CommentView) => (c.uri ? `comment-${c.uri.split('/').pop()}` : undefined);
async function act(action: string, write: () => Promise<string | undefined>) {
busy = action;
problem = (await write()) ?? '';
busy = '';
return !problem;
}
async function send(e: SubmitEvent) {
e.preventDefault();
if (await act('reply', () => reply(body))) {
body = '';
answering = false;
}
}
</script>
<details class="box thread" class:resolved={root.resolved} bind:open>
<summary class="thread-head"
><Icon name="caret" /><span class="path">{at.path}</span><span class="mute"
>{at.side === 'old' ? `${range}, as it was` : range}</span
>{#if at.outdated}<span class="tag">Outdated</span>{/if}{#if root.resolved}<span class="tag"
>Resolved</span
>{/if}</summary
>
{#if quote}<LineQuote {at} />{/if}
{#each [root, ...replies] as c (c.uri)}
<div class="thread-said" id={anchor(c)}>
<div class="thread-by">
<Avatar name={c.author.handle} did={c.author.did} src={c.author.avatar} />
<Person handle={c.author.handle} bot={c.author.bot} />
<span class="mute"
><Time at={c.createdAt} show={(t) => ago(t, 'on ')} />{c.updatedAt
? ', edited'
: ''}</span
>
{#if menu}<span class="end">{@render menu(c)}</span>{/if}
</div>
<div class="prose" use:diagrams={c.html} use:fences={c.html}>{@html c.html}</div>
</div>
{/each}
{#if answering}
<form class="thread-end composer" onsubmit={send}>
<MarkdownField label="Reply" placeholder="Reply" rows={3} {repo} bind:value={body} />
{#if problem}<p class="problem" role="alert">{problem}</p>{/if}
<div class="composer-end">
<button class="btn" type="button" onclick={() => (answering = false)}>Cancel</button>
<button
class="btn primary"
type="submit"
disabled={!!busy || !body.trim()}
aria-busy={busy === 'reply'}>{busy === 'reply' ? 'Replying…' : 'Reply'}</button
>
</div>
</form>
{:else if logged || may}
<div class="thread-end">
{#if logged}<button class="reply-prompt" type="button" onclick={() => (answering = true)}
>Reply…</button
>{/if}
{#if may}<button
class="btn"
type="button"
disabled={!!busy}
aria-busy={busy === 'resolve'}
onclick={() => act('resolve', () => resolve(!root.resolved))}
>{root.resolved ? 'Unresolve conversation' : 'Resolve conversation'}</button
>{/if}
{#if problem}<p class="problem" role="alert">{problem}</p>{/if}
</div>
{/if}
</details>