~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
123 lines · 4.3 KB Raw History
<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>