johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
<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>