Read-it-later social network

implement subscribe button

authored by zeu.dev and committed by tangled.org 663a6e98 0b4c47ae

+79 -17
+71 -16
src/lib/components/PublicationCard.svelte
··· 1 1 <script lang="ts"> 2 2 import { getContext } from "svelte"; 3 3 import { createQuery } from "@tanstack/svelte-query"; 4 - import { resolveHandle, type MiniDoc, type PublicationNode, type StandardSiteThemeColorRGB } from "$lib/utils"; 5 4 import type { QuicksliceClient } from "quickslice-client-js"; 5 + import { parseAtUri, resolveHandle, type MiniDoc, type PublicationNode } from "$lib/utils"; 6 6 7 7 const user = getContext("user") as MiniDoc; 8 8 const atclient = getContext("atclient") as QuicksliceClient; 9 9 10 10 let { publication, showEmpty = false }: { publication: PublicationNode, showEmpty?: boolean } = $props(); 11 11 12 + let disableSubscribeButton = $state(false); 12 13 let isSubscribeButtonHovered = $state(false); 13 14 14 15 const miniDocQuery = createQuery(() => ({ ··· 40 41 const subscribers = Number(data.links["site.standard.graph.subscription"]?.[".publication"]?.records) || 0; 41 42 42 43 return { documents, subscribers } 43 - } 44 + }, 44 45 })); 45 46 46 - const isSubscribedQuery = createQuery(() => ({ 47 + const subscriptionQuery = createQuery(() => ({ 47 48 queryKey: ["isSubscribed", publication.uri, user.did], 48 49 queryFn: async () => { 49 50 const constellationUrl = new URL("https://constellation.microcosm.blue/xrpc/blue.microcosm.links.getBacklinks"); ··· 57 58 }); 58 59 59 60 60 - const json = await response.json() as { total: number }; 61 + const json = await response.json() as { records: { did: string, collection: string, rkey: string }[] }; 61 62 return json; 62 63 }, 64 + select: (data) => data.records[0] && data.records[0].rkey 63 65 })); 64 66 65 67 let documents = $derived(countQuery.data?.documents || 0); 66 68 let subscribers = $derived(countQuery.data?.subscribers || 0); 67 - let isSubscribed = $derived((isSubscribedQuery.data?.total ?? 0) === 1); 69 + let subscriptionRkey = $derived(subscriptionQuery.data); 68 70 let blobSyncUrl = $derived((`${miniDocQuery.data?.pds}/xrpc/com.atproto.sync.getBlob?did=${publication.did}&cid=${publication.value.icon?.ref.$link}`)); 69 71 const theme = publication.value.basicTheme || { 70 72 $type: "site.standard.theme.basic", ··· 94 96 }, 95 97 }; 96 98 97 - // TODO: update with `site.standard.graph.subscription` create or delete on click with auth 98 - function toggleSubscribe() { 99 - const past = isSubscribed; 100 - isSubscribed = !isSubscribed; 101 - if (subscribers) { 102 - if (past) { 103 - subscribers--; 99 + async function toggleSubscribe() { 100 + disableSubscribeButton = true; 101 + 102 + const pastRkey = subscriptionRkey; 103 + if (pastRkey) { 104 + subscribers--; 105 + subscriptionRkey = undefined; 106 + } 107 + else { 108 + subscribers++; 109 + subscriptionRkey = "placeholder_rkey"; 110 + } 111 + 112 + try { 113 + if (pastRkey) { 114 + const mutation = ` 115 + mutation { 116 + deleteSiteStandardGraphSubscription(rkey: "${pastRkey}") { 117 + uri 118 + } 119 + } 120 + `; 121 + await atclient.mutate(mutation) as { createSiteStandardGraphSubscription: { uri: string }}; 122 + subscriptionRkey = undefined; 104 123 } 105 124 else { 125 + const mutation = ` 126 + mutation { 127 + createSiteStandardGraphSubscription(input: { 128 + publication: "${publication.uri}" 129 + }) { 130 + uri 131 + } 132 + } 133 + `; 134 + const result = await atclient.mutate(mutation) as { createSiteStandardGraphSubscription: { uri: string }}; 135 + const { rkey } = parseAtUri(result.createSiteStandardGraphSubscription.uri); 136 + subscriptionRkey = rkey; 137 + } 138 + 139 + disableSubscribeButton = false; 140 + } 141 + catch (e) { 142 + console.log(e); 143 + // rollback initial changes 144 + if (pastRkey) { 106 145 subscribers++; 146 + subscriptionRkey = pastRkey; 107 147 } 148 + else { 149 + subscribers--; 150 + subscriptionRkey = undefined; 151 + } 152 + 153 + disableSubscribeButton = false; 108 154 } 109 155 } 110 156 </script> ··· 159 205 </div> 160 206 <button 161 207 onclick={toggleSubscribe} 208 + disabled={disableSubscribeButton} 162 209 onmouseenter={() => isSubscribeButtonHovered = true} 163 210 onmouseleave={() => isSubscribeButtonHovered = false} 164 - class={["flex flex-1 flex-col items-center justify-center gap-1 p-4 hover:cursor-pointer transition-all duration-150 hover:bg-green-500", isSubscribed && "bg-green-500 hover:bg-red-400"]}> 165 - <span class="text-2xl font-bold"> 211 + class={[ 212 + "flex flex-1 flex-col items-center justify-center gap-1 p-4 hover:cursor-pointer transition-all duration-150 hover:bg-green-500", 213 + subscriptionRkey && "bg-green-500 hover:bg-red-400" 214 + ]} 215 + > 216 + <span class="gap-[0.5rem] text-2xl font-bold"> 166 217 {subscribers} 167 218 </span> 168 - <span class="text-xs uppercase tracking-wide"> 169 - {#if isSubscribed} 219 + <span class="text-xs uppercase tracking-wide flex"> 220 + {#if subscriptionRkey} 170 221 {#if isSubscribeButtonHovered} 171 222 Unsubscribe? 172 223 {:else} ··· 178 229 {:else} 179 230 Subscribers 180 231 {/if} 232 + {/if} 233 + 234 + {#if disableSubscribeButton} 235 + <p class="animate-spin">◝</p> 181 236 {/if} 182 237 </span> 183 238 </button>
+7
src/lib/utils.ts
··· 9 9 } 10 10 } 11 11 12 + export function createAtUri({ did, collection, rkey }: { did: string, collection: string, rkey: string }) { 13 + if (did && collection && rkey) { 14 + return `at://${did}/${collection}/${rkey}`; 15 + } 16 + return undefined; 17 + } 18 + 12 19 export type MiniDoc = { 13 20 did: string; 14 21 handle: string;
+1 -1
src/routes/+page.svelte
··· 1 1 <script lang="ts"> 2 - import PublicationCard from '$lib/components/PublicationCard.svelte'; 3 2 import type { PublicationNode } from '$lib/utils'; 4 3 import { createInfiniteQuery } from '@tanstack/svelte-query'; 4 + import PublicationCard from '$lib/components/PublicationCard.svelte'; 5 5 6 6 let { data } = $props(); 7 7 let { atclient, user } = data;