<script lang="ts">
import BellMinus from "$icon/bell-minus";
import BellPlus from "$icon/bell-plus";
import TriangleAlert from "$icon/triangle-alert";
import { getAuth } from "#lib/auth.svelte.ts";
import { createBobbinClient } from "#lib/api/client.ts";
import {
ENTITY_SUBSCRIBED,
getEntitySubscription,
setEntitySubscribed
} from "#lib/api/graph.ts";
import Button from "#lib/components/ui/Button.svelte";
import DropdownItem from "#lib/components/ui/DropdownItem.svelte";
import ToggleFaces from "#lib/components/ui/ToggleFaces.svelte";
import Tooltip from "#lib/components/ui/Tooltip.svelte";
import { resetFailureSoon } from "#lib/optimistic.svelte.ts";
interface Props {
entityUri: string;
bobbinUrl?: string;
variant?: "button" | "block" | "menuitem";
// true for the ticket's author and the repo's owner, who get notices without a subscription record
notifiedByDefault?: boolean;
}
let { entityUri, bobbinUrl, variant = "button", notifiedByDefault = false }: Props = $props();
const auth = getAuth();
let subscribed = $state(false);
let known = $state(false);
let toggling = $state(false);
let rkey = $state<string | null>(null);
let error = $state<string | null>(null);
$effect(() => {
const did = auth.currentDid;
if (!bobbinUrl || !did) {
subscribed = notifiedByDefault;
rkey = null;
known = true;
return;
}
let cancelled = false;
known = false;
error = null;
getEntitySubscription(createBobbinClient({ serviceUrl: bobbinUrl }), did, entityUri)
.then((found) => {
if (cancelled) return;
rkey = found?.rkey ?? null;
subscribed = found ? !found.ignore : notifiedByDefault;
known = true;
})
.catch(() => {
if (cancelled) return;
// subscribing re-checks before it writes, so the button can stay usable
known = true;
error = "Could not load subscription state";
});
return () => {
cancelled = true;
};
});
// a subscribe or unmute from elsewhere on the page comes in as this event
$effect(() => {
const onSubscribed = (event: Event) => {
const detail = (event as CustomEvent<{ entityUri: string; rkey: string }>).detail;
if (detail.entityUri !== entityUri) return;
subscribed = true;
rkey = detail.rkey;
};
window.addEventListener(ENTITY_SUBSCRIBED, onSubscribed);
return () => window.removeEventListener(ENTITY_SUBSCRIBED, onSubscribed);
});
resetFailureSoon(
() => error !== null,
() => (error = null)
);
// unsubscribing sets ignore on the record, it never deletes it
const toggle = async () => {
const agent = auth.agent;
const did = auth.currentDid;
if (!agent || !did || !bobbinUrl || !known || toggling) return;
const wasSubscribed = subscribed;
const wasRkey = rkey;
error = null;
toggling = true;
subscribed = !wasSubscribed;
try {
const ctx = createBobbinClient({ serviceUrl: bobbinUrl });
rkey = await setEntitySubscribed(agent, ctx, did, entityUri, !wasSubscribed);
} catch {
subscribed = wasSubscribed;
rkey = wasRkey;
error = "Failed to update subscription";
} finally {
toggling = false;
}
};
</script>
{#if auth.currentDid && variant === "menuitem"}
<DropdownItem
icon={error ? TriangleAlert : subscribed ? BellMinus : BellPlus}
danger={Boolean(error)}
disabled={!known || toggling}
onclick={toggle}
data-testid="toggleSubscription"
data-subscribed={subscribed}
data-pending={toggling}
>
{error ?? (subscribed ? "Unsubscribe" : "Subscribe")}
</DropdownItem>
{:else if auth.currentDid}
<Tooltip
content={error ?? ""}
disabled={!error}
class={variant === "block" ? "w-full" : undefined}
>
<Button
variant={error ? "danger" : "default"}
class={variant === "block" ? "w-full" : "max-sm:w-8 max-sm:px-0 sm:min-w-30"}
disabled={!known}
aria-busy={toggling}
onclick={toggle}
data-testid="toggleSubscription"
data-subscribed={subscribed}
data-pending={toggling}
>
<ToggleFaces
on={subscribed}
failed={Boolean(error)}
offLabel="Subscribe"
onLabel="Unsubscribe"
labelClass="max-sm:hidden"
>
{#snippet off(cls)}
<BellPlus class={cls} />
{/snippet}
{#snippet onIcon(cls)}
<BellMinus class={cls} />
{/snippet}
{#snippet err(cls)}
<TriangleAlert class={cls} />
{/snippet}
</ToggleFaces>
</Button>
</Tooltip>
<span role="status" aria-live="polite" class="sr-only">{error ?? ""}</span>
{/if}