Read-it-later social network
at feat/hide-empty-publications 204 lines 6.7 kB view raw
1<script lang="ts"> 2 import { getContext } from "svelte"; 3 import { createQuery } from "@tanstack/svelte-query"; 4 import type { QuicksliceClient } from "quickslice-client-js"; 5 import { defaultTheme, parseAtUri, resolveHandle, type MiniDoc, type PublicationNode, type SubscriptionNode } from "$lib/utils"; 6 7 const user = getContext("user") as MiniDoc; 8 const atclient = getContext("atclient") as QuicksliceClient; 9 10 let { publication, hideEmptyPublications = false }: { 11 publication: PublicationNode & { viewerSiteStandardGraphSubscriptionViaPublication?: SubscriptionNode | null }, hideEmptyPublications?: boolean 12 } = $props(); 13 14 const { rkey: pubRkey } = parseAtUri(publication.uri); 15 16 let disableSubscribeButton = $state(false); 17 let isSubscribeButtonHovered = $state(false); 18 19 const miniDocQuery = createQuery(() => ({ 20 queryKey: ["miniDoc", publication.did], 21 queryFn: async () => { 22 const miniDoc = await resolveHandle(publication.did); 23 return miniDoc; 24 }, 25 staleTime: "static" 26 })); 27 28 const countQuery = createQuery(() => ({ 29 queryKey: ["counts", publication.uri], 30 queryFn: async () => { 31 const constellationUrl = new URL("https://constellation.microcosm.blue/links/all"); 32 constellationUrl.searchParams.set("target", publication.uri); 33 const response = await fetch(constellationUrl, { 34 headers: { 35 "Accept": "application/json" 36 } 37 }); 38 39 40 const json = await response.json() as { links: Record<string, any> }; 41 return json; 42 }, 43 select: (data) => { 44 const documents = Number(data.links["site.standard.document"]?.[".site"]?.records) || 0; 45 const subscribers = Number(data.links["site.standard.graph.subscription"]?.[".publication"]?.records) || 0; 46 47 return { documents, subscribers } 48 }, 49 })); 50 51 let documents = $derived(countQuery.data?.documents || 0); 52 let subscribers = $derived(countQuery.data?.subscribers || 0); 53 let subscriptionRkey = $derived(parseAtUri(publication.viewerSiteStandardGraphSubscriptionViaPublication?.uri || "").rkey); 54 let blobSyncUrl = $derived(`${miniDocQuery.data?.pds}/xrpc/com.atproto.sync.getBlob?did=${publication.did}&cid=${publication.icon?.ref}`); 55 const theme = publication.basicTheme || defaultTheme; 56 57 async function toggleSubscribe() { 58 if (!user) { throw Error() } 59 disableSubscribeButton = true; 60 61 const pastRkey = subscriptionRkey; 62 if (pastRkey) { 63 subscribers--; 64 subscriptionRkey = undefined; 65 } 66 else { 67 subscribers++; 68 subscriptionRkey = "placeholder_rkey"; 69 } 70 71 try { 72 if (pastRkey) { 73 const mutation = ` 74 mutation { 75 deleteSiteStandardGraphSubscription(rkey: "${pastRkey}") { 76 uri 77 } 78 } 79 `; 80 await atclient.mutate(mutation) as { createSiteStandardGraphSubscription: { uri: string }}; 81 subscriptionRkey = undefined; 82 } 83 else { 84 const mutation = ` 85 mutation { 86 createSiteStandardGraphSubscription(input: { 87 publication: "${publication.uri}" 88 }) { 89 uri 90 } 91 } 92 `; 93 const result = await atclient.mutate(mutation) as { createSiteStandardGraphSubscription: { uri: string }}; 94 const { rkey } = parseAtUri(result.createSiteStandardGraphSubscription.uri); 95 subscriptionRkey = rkey; 96 } 97 98 disableSubscribeButton = false; 99 } 100 catch (e) { 101 console.error(e); 102 // rollback initial changes 103 if (pastRkey) { 104 subscribers++; 105 subscriptionRkey = pastRkey; 106 } 107 else { 108 subscribers--; 109 subscriptionRkey = undefined; 110 } 111 112 disableSubscribeButton = false; 113 } 114 } 115</script> 116 117{#if (hideEmptyPublications && documents > 0) || !hideEmptyPublications} 118 119<div 120 class="flex flex-col lg:flex-row overflow-hidden rounded border shadow-sm" 121 style={` 122 background-color: rgb(${theme.background.r},${theme.background.g},${theme.background.b}); 123 color: rgb(${theme.foreground.r},${theme.foreground.g},${theme.foreground.b}); 124 `} 125> 126 <div class="flex flex-1 flex-col items-center justify-center gap-3 p-8"> 127 {#if publication.icon} 128 <img 129 src={blobSyncUrl.toString()} 130 alt={publication.name} 131 class="size-24 rounded-xl hover:-rotate-15 transition-transform duration-150" 132 /> 133 {/if} 134 <h3 class="text-xl font-semibold text-center text-balance"> 135 {publication.name} 136 </h3> 137 <a 138 href={`/${publication.actorHandle}`} 139 style={` 140 color: rgb(${theme.foreground.r},${theme.foreground.g},${theme.foreground.b}); 141 `} 142 class="hover:!text-blue-500" 143 > 144 by @{publication.actorHandle} 145 </a> 146 <p class="text-xs text-center max-w-md leading-relaxed font-neco"> 147 {publication.description} 148 </p> 149 </div> 150 151 <div class="flex w-full lg:w-32 border-t lg:flex-col lg:border-t-0 lg:border-l bg-muted/50"> 152 <a 153 href={`/${miniDocQuery.data?.handle}/${pubRkey}`} 154 class="group flex flex-1 flex-col items-center justify-center gap-1 border-r lg:border-r-0 lg:border-b border-border p-4 hover:cursor-pointer" 155 style={` 156 background-color: rgb(${theme.accent.r},${theme.accent.g},${theme.accent.b}); 157 color: rgb(${theme.accentForeground.r},${theme.accentForeground.g},${theme.accentForeground.b}); 158 `} 159 > 160 <span class="text-2xl font-bold text-card-foreground"> 161 {documents} 162 </span> 163 <span class="flex gap-1 text-xs uppercase tracking-wide"> 164 Documents 165 <span class="group-hover:rotate-45 transition-transform duration-150"></span> 166 </span> 167 </a> 168 <button 169 onclick={toggleSubscribe} 170 disabled={disableSubscribeButton} 171 onmouseenter={() => isSubscribeButtonHovered = true} 172 onmouseleave={() => isSubscribeButtonHovered = false} 173 class={[ 174 "flex flex-1 flex-col items-center justify-center gap-1 p-4 hover:cursor-pointer transition-all duration-150 hover:bg-green-500", 175 subscriptionRkey && "bg-green-500 hover:bg-red-400" 176 ]} 177 > 178 <span class="gap-[0.5rem] text-2xl font-bold"> 179 {subscribers} 180 </span> 181 <span class="text-xs uppercase tracking-wide flex"> 182 {#if subscriptionRkey} 183 {#if isSubscribeButtonHovered} 184 Unsubscribe? 185 {:else} 186 Subscribed 187 {/if} 188 {:else} 189 {#if isSubscribeButtonHovered} 190 Subscribe? 191 {:else} 192 Subscribers 193 {/if} 194 {/if} 195 196 {#if disableSubscribeButton} 197 <p class="animate-spin"></p> 198 {/if} 199 </span> 200 </button> 201 </div> 202</div> 203 204{/if}