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

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
175 lines · 5.8 KB Raw History
<script lang="ts">
	import type * as listNotifications from '#lib/lexicons/dev/knotless/listNotifications.ts';
	import type * as setOrgMembership from '#lib/lexicons/dev/knotless/setOrgMembership.ts';
	import type * as endOrgMembership from '#lib/lexicons/dev/knotless/endOrgMembership.ts';
	import type * as putSubscription from '#lib/lexicons/dev/knotless/putSubscription.ts';
	import type { AtUriString, DidString } from '@atproto/lex';
	import { call } from '#lib/data.ts';
	import { ago, excerpt, itemHref, repoHref, urlPath } from '#lib/format.ts';
	import {
		byRunner,
		failedOf,
		itemOf,
		leadOf,
		markOf,
		tailOf,
		type Group
	} from '#lib/notifications.svelte.ts';
	import Avatar from '#lib/atoms/Avatar.svelte';
	import Icon from '#lib/atoms/Icon.svelte';
	import Crowd from './Crowd.svelte';
	import Time from '#lib/atoms/Time.svelte';
	import RepoLink from './RepoLink.svelte';
	/**
	 * Something one or more accounts did that concerns the one looking: a
	 * mark of what, who, a sentence, the start of what they said, and
	 * when. An invitation to an organization is answered here, as on its
	 * Members tab, and a pull or an issue muted, as on its page.
	 */
	let { group }: { group: Group } = $props();
	const n: listNotifications.Notification = $derived(group.first);
	const others = $derived(group.authors.length - 1);
	const mark = $derived(markOf(n));
	const repo = $derived(n.repo && repoHref(n.repo.handle, n.repo.name));
	// The pull or issue it is, or is on.
	const item = $derived(itemOf(n));
	const lead = $derived(leadOf(n));
	// For checks, those that failed.
	const failed = $derived(failedOf(n));
	// Which answer is under way: 'public', 'hidden' or 'decline'.
	let busy = $state('');
	let said = $state('');
	let problem = $state('');
	const org = $derived(n.author.did as DidString);
	// The pull or issue it may be muted as: not for a review asked or an
	// assignment, which muting leaves heard of.
	const thread = $derived(
		n.reason === 'review-requested' || n.reason === 'assigned' ? undefined : item
	);
	const noun = $derived(thread?.includes('/dev.knotless.pull/') ? 'pull' : 'issue');
	let muted = $state(false);
	let muting = $state(false);
	async function mute(on: boolean) {
		muting = true;
		problem = '';
		const level = on ? 'muted' : 'participating';
		try {
			await call<typeof putSubscription.main>('dev.knotless.putSubscription', {
				subject: thread as AtUriString,
				level
			});
			muted = on;
		} catch (err) {
			problem = (err as Error).message;
		}
		muting = false;
	}
	async function answer(visibility?: 'public' | 'hidden') {
		busy = visibility ?? 'decline';
		problem = '';
		try {
			if (visibility) {
				await call<typeof setOrgMembership.main>('dev.knotless.setOrgMembership', {
					org,
					visibility
				});
				said =
					visibility === 'hidden'
						? `You're a hidden member of ${n.author.handle}. Only its members see it.`
						: `You're a member of ${n.author.handle}, and anyone can see it.`;
			} else {
				await call<typeof endOrgMembership.main>('dev.knotless.endOrgMembership', { org });
				said = `You declined the invitation to ${n.author.handle}.`;
			}
		} catch (err) {
			problem = (err as Error).message;
		}
		busy = '';
	}
</script>

<li class:unread={!n.isRead} class:invite={n.reason === 'invite'}>
	<span class="feed-node" class:own={byRunner(n)}
		>{#if !byRunner(n)}<Avatar
				name={n.author.handle}
				did={n.author.did}
				src={n.author.avatar}
			/>{/if}<span class="feed-kind {mark[1]}"><Icon name={mark[0]} /></span></span
	>
	<div class="grow">
		{#if others}<Crowd actors={group.authors.slice(1)} />{/if}
		<p>
			<a class="who" href="/{urlPath(n.author.handle)}">{n.author.handle}</a
			>{#if others === 1}{' and '}<a class="who" href="/{urlPath(group.authors[1].handle)}"
					>{group.authors[1].handle}</a
				>{:else if others > 1}{` and ${others} others`}{/if}{lead}{#if item && repo}<a
					href={itemHref(repo, item)}>{n.title ?? 'a thread'}</a
				>{`${tailOf(n)} on `}{/if}{#if n.repo && n.reason !== 'follow'}<RepoLink
					owner={n.repo.handle}
					name={n.repo.name}
				/>{/if}
		</p>
		{#if failed}<p class="failed">{failed}</p>{/if}
		{#if n.text && !others}<p class="feed-quote">“{excerpt(n.text, 280)}”</p>{/if}
		{#if muted}<p class="row-said" role="status">
				You muted this {noun}.
				<button
					class="text-button"
					type="button"
					disabled={muting}
					aria-busy={muting}
					onclick={() => mute(false)}>Undo</button
				>
			</p>{/if}
		{#if problem && n.reason !== 'invite'}<p class="problem" role="alert">{problem}</p>{/if}
		<p class="below">
			<Time at={n.createdAt} show={ago} />{#if thread && !muted}{' · '}<button
					class="text-button"
					type="button"
					disabled={muting}
					aria-busy={muting}
					onclick={() => mute(true)}>Mute</button
				>{/if}
		</p>
	</div>
	<span class="mute beside"
		>{#if thread && !muted}<button
				class="text-button row-mute"
				type="button"
				disabled={muting}
				aria-busy={muting}
				onclick={() => mute(true)}>Mute</button
			>{/if}<Time at={n.createdAt} show={ago} /></span
	>
	{#if n.reason === 'invite'}
		<div class="invite-answer">
			{#if said}
				<p class="invite-said" role="status">{said}</p>
			{:else}
				<button
					class="btn primary"
					type="button"
					disabled={!!busy}
					aria-busy={busy === 'public'}
					onclick={() => answer('public')}
					>{busy === 'public' ? 'Joining…' : 'Join publicly'}</button
				>
				<button
					class="btn"
					type="button"
					disabled={!!busy}
					aria-busy={busy === 'hidden'}
					onclick={() => answer('hidden')}>{busy === 'hidden' ? 'Joining…' : 'Join hidden'}</button
				>
				<button
					class="text-button"
					type="button"
					disabled={!!busy}
					aria-busy={busy === 'decline'}
					onclick={() => answer()}>{busy === 'decline' ? 'Declining…' : 'Decline'}</button
				>
			{/if}
			{#if problem}<p class="problem" role="alert">{problem}</p>{/if}
		</div>
	{/if}
</li>