<script lang="ts">
import { page } from "$app/state";
import Eye from "$icon/eye";
import EyeFilled from "#lib/components/icons/EyeFilled.svelte";
import TriangleAlert from "$icon/triangle-alert";
import { getAuth } from "#lib/auth.svelte.ts";
import { loginHrefFor } from "#lib/auth/guards.ts";
import { createSubscription, deleteSubscription } from "#lib/api/graph.ts";
import { createBobbinClient } from "#lib/api/client.ts";
import { awaitDeletedRecord, awaitIndexedOrSaved } from "#lib/api/awaitRecord.ts";
import Button from "#lib/components/ui/Button.svelte";
import ButtonGroup from "#lib/components/ui/ButtonGroup.svelte";
import ToggleFaces from "#lib/components/ui/ToggleFaces.svelte";
import Tooltip from "#lib/components/ui/Tooltip.svelte";
import {
createOptimisticRelation,
resetFailureSoon,
seededValue
} from "#lib/optimistic.svelte.ts";
interface Props {
repoDid: string;
initialRkey?: string | null | Promise<string | null>;
class?: string;
}
let { repoDid, initialRkey, class: className }: Props = $props();
const auth = getAuth();
const signedIn = $derived(Boolean(auth.currentDid));
const loginHref = $derived(loginHrefFor(page.url));
const rkeySeed = seededValue(
() => initialRkey,
(rkey) => rkey,
null
);
const relation = createOptimisticRelation({
key: () => `${auth.currentDid ?? ""}:${repoDid}`,
loadedRkey: () => rkeySeed.value
});
const subscribed = $derived(relation.active);
const error = $derived(relation.failed ? "Failed to update subscription" : null);
resetFailureSoon(
() => relation.failed,
() => relation.resetFailure()
);
const toggle = async () => {
const agent = auth.agent;
if (!agent || !repoDid || !relation.known || relation.loading) return;
const wasSubscribed = relation.active;
const existingRkey = relation.rkey;
relation.begin(!wasSubscribed);
const bobbinUrl = page.data.publicConfig?.bobbinUrl;
const ctx = bobbinUrl ? createBobbinClient({ serviceUrl: bobbinUrl }) : null;
try {
if (wasSubscribed && existingRkey) {
const { uri } = await deleteSubscription(agent, existingRkey);
relation.deleted();
if (ctx) {
awaitDeletedRecord(ctx, uri).catch((cause) =>
console.error("failed to wait for unsubscribe index", cause)
);
}
} else {
const written = await createSubscription(agent, repoDid);
relation.created(written.rkey);
if (ctx) {
// todo: needs a design pass for surfacing index wait failures
awaitIndexedOrSaved(ctx, written).catch((cause) =>
console.error("failed to wait for subscribe index", cause)
);
}
}
} catch {
relation.fail();
}
};
</script>
<Tooltip content={error ?? ""} disabled={!error} class={className}>
<ButtonGroup class="w-full">
<Button
data-testid="watch"
data-subscribed={subscribed}
variant={error ? "danger" : "default"}
href={signedIn ? undefined : loginHref}
disabled={signedIn && !relation.known}
aria-busy={relation.loading}
class="flex-1 max-sm:px-xs! sm:min-w-24"
onclick={signedIn ? toggle : undefined}
>
<ToggleFaces
on={subscribed}
failed={Boolean(error)}
offLabel="Watch"
onLabel="Watching"
>
{#snippet off(cls)}
<Eye class={cls} />
{/snippet}
{#snippet onIcon(cls)}
<EyeFilled class={cls} />
{/snippet}
{#snippet err(cls)}
<TriangleAlert class={cls} />
{/snippet}
</ToggleFaces>
</Button>
</ButtonGroup>
</Tooltip>
<span role="status" aria-live="polite" class="sr-only">{error ?? ""}</span>