~james/core

core/web/src/lib/components/repo/SubscribeButton.svelte -rw-r--r-- 3.5 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 16 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
<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>