Read-it-later social network
12
fork

Configure Feed

Select the types of activity you want to include in your feed.

update queries with authed viewer subscription

authored by

zeudev and committed by tangled.org 6c0aa99f ac157f47

+106 -69
+16 -34
src/lib/components/PublicationCard.svelte
··· 2 2 import { getContext } from "svelte"; 3 3 import { createQuery } from "@tanstack/svelte-query"; 4 4 import type { QuicksliceClient } from "quickslice-client-js"; 5 - import { parseAtUri, resolveHandle, type MiniDoc, type PublicationNode } from "$lib/utils"; 5 + import { parseAtUri, resolveHandle, type MiniDoc, type PublicationNode, type SubscriptionNode } from "$lib/utils"; 6 6 7 7 const user = getContext("user") as MiniDoc; 8 8 const atclient = getContext("atclient") as QuicksliceClient; 9 9 10 - let { publication, showEmpty = false }: { publication: PublicationNode, showEmpty?: boolean } = $props(); 10 + let { publication, showEmpty = false }: { 11 + publication: PublicationNode & { viewerSiteStandardGraphSubscriptionViaPublication?: SubscriptionNode | null }, showEmpty?: boolean 12 + } = $props(); 13 + 14 + const { rkey: pubRkey } = parseAtUri(publication.uri); 11 15 12 16 let disableSubscribeButton = $state(false); 13 17 let isSubscribeButtonHovered = $state(false); ··· 44 48 }, 45 49 })); 46 50 47 - const subscriptionQuery = createQuery(() => ({ 48 - queryKey: ["isSubscribed", publication.uri, user && user.did], 49 - queryFn: async () => { 50 - if (!user.did) { 51 - return { records: [] } 52 - } 53 - const constellationUrl = new URL("https://constellation.microcosm.blue/xrpc/blue.microcosm.links.getBacklinks"); 54 - constellationUrl.searchParams.set("subject", publication.uri); 55 - constellationUrl.searchParams.set("source", "site.standard.graph.subscription:publication"); 56 - constellationUrl.searchParams.set("did", user.did); 57 - const response = await fetch(constellationUrl, { 58 - headers: { 59 - "Accept": "application/json" 60 - } 61 - }); 62 - 63 - 64 - const json = await response.json() as { records: { did: string, collection: string, rkey: string }[] }; 65 - return json; 66 - }, 67 - select: (data) => data.records[0] && data.records[0].rkey 68 - })); 69 - 70 51 let documents = $derived(countQuery.data?.documents || 0); 71 52 let subscribers = $derived(countQuery.data?.subscribers || 0); 72 - let subscriptionRkey = $derived(subscriptionQuery.data); 73 - let blobSyncUrl = $derived((`${miniDocQuery.data?.pds}/xrpc/com.atproto.sync.getBlob?did=${publication.did}&cid=${publication.value.icon?.ref.$link}`)); 74 - const theme = publication.value.basicTheme || { 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 || { 75 56 $type: "site.standard.theme.basic", 76 57 background: { 77 58 $type: "site.standard.theme.color#rgb", ··· 167 148 `} 168 149 > 169 150 <div class="flex flex-1 flex-col items-center justify-center gap-3 p-8"> 170 - {#if publication.value.icon} 151 + {#if publication.icon} 171 152 <img 172 153 src={blobSyncUrl.toString()} 173 - alt={publication.value.name} 154 + alt={publication.name} 174 155 class="size-24 rounded-xl hover:-rotate-15 transition-transform duration-150" 175 156 /> 176 157 {/if} 177 158 <h3 class="text-xl font-semibold text-center text-balance"> 178 - {publication.value.name} 159 + {publication.name} 179 160 </h3> 180 161 <a 181 162 href={`https://bsky.app/profile/${publication.actorHandle}`} ··· 187 168 by @{publication.actorHandle} 188 169 </a> 189 170 <p class="text-xs text-center max-w-md leading-relaxed font-neco"> 190 - {publication.value.description} 171 + {publication.description} 191 172 </p> 192 173 </div> 193 174 194 175 <div class="flex w-full lg:w-32 border-t lg:flex-col lg:border-t-0 lg:border-l bg-muted/50"> 195 - <div 176 + <a 177 + href={`/${miniDocQuery.data?.handle}/${pubRkey}`} 196 178 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" 197 179 style={` 198 180 background-color: rgb(${theme.accent.r},${theme.accent.g},${theme.accent.b}); ··· 206 188 Documents 207 189 <span class="group-hover:rotate-45 transition-transform duration-150">↗</span> 208 190 </span> 209 - </div> 191 + </a> 210 192 <button 211 193 onclick={toggleSubscribe} 212 194 disabled={disableSubscribeButton}
+7 -3
src/lib/utils.ts
··· 39 39 40 40 export type ATBlob = { 41 41 $type: string; 42 - ref: { $link: string; }; 42 + ref: string; 43 43 mimeType: string; 44 44 size: number; 45 45 } ··· 51 51 r: number; 52 52 } 53 53 54 - export type PublicationNode = Node & { value: { 54 + export type PublicationNode = Node & { 55 55 url: string; 56 56 name: string; 57 57 description: string; ··· 67 67 accent: StandardSiteThemeColorRGB; 68 68 accentForeground: StandardSiteThemeColorRGB; 69 69 }; 70 - }} 70 + } 71 + 72 + export type SubscriptionNode = Node & { 73 + publication: string; 74 + } 71 75 72 76 export type DocumentNode = Node & { value: { 73 77 title: string;
+5
src/routes/+error.svelte
··· 1 + <script lang="ts"> 2 + import { page } from "$app/state"; 3 + </script> 4 + 5 + <p>{page.error.message}</p>
+3 -12
src/routes/+layout.svelte
··· 1 1 <script lang="ts"> 2 2 import '../app.css'; 3 3 import { page } from '$app/state'; 4 - import { onMount, setContext } from 'svelte'; 4 + import { setContext } from 'svelte'; 5 5 import { QueryClient, QueryClientProvider } from "@tanstack/svelte-query"; 6 6 import { SvelteQueryDevtools } from "@tanstack/svelte-query-devtools"; 7 - import { goto } from '$app/navigation'; 8 7 9 8 let { data, children } = $props(); 10 9 const { atclient, user } = data; ··· 32 31 } 33 32 } 34 33 }); 35 - 36 - /** 37 - onMount(() => { 38 - if (user) { 39 - goto("/home"); 40 - } 41 - }); 42 - **/ 43 34 </script> 44 35 45 36 <QueryClientProvider client={queryClient}> ··· 50 41 51 42 <div class="flex gap-4 items-center flex-wrap"> 52 43 <nav class="flex gap-4 flex-wrap items-center px-3 py-1.5"> 53 - <a href="/explore" class="hover:text-shadow-sm" title="explore" aria-label="explore">🛰️ explore</a> 44 + <a href="/explore" class="hover:text-shadow-sm" title="explore" aria-label="explore">🛰️ Explore</a> 54 45 {#if user} 55 - <a href="/home" class="hover:text-shadow-sm" title="explore" aria-label="explore">🏠 {user.handle}</a> 46 + <a href="/home" class="hover:text-shadow-sm" title="explore" aria-label="explore">🏠 Home</a> 56 47 {/if} 57 48 </nav> 58 49 {#if user}
+1 -1
src/routes/+layout.ts
··· 13 13 14 14 if (url.searchParams.has("code")) { 15 15 await atclient.handleRedirectCallback(); 16 - redirect(302, "/"); 16 + redirect(302, "/home"); 17 17 } 18 18 19 19 const isAuthed = await atclient.isAuthenticated();
+24 -2
src/routes/+page.svelte
··· 1 1 <script lang="ts"> 2 + import { getContext } from "svelte"; 2 3 import LeafletIcon from "$lib/components/LeafletIcon.svelte"; 3 4 import OffprintIcon from "$lib/components/OffprintIcon.svelte"; 4 5 import PcktIcon from "$lib/components/PcktIcon.svelte"; 5 - </script> 6 + import type { QuicksliceClient } from "quickslice-client-js"; 6 7 8 + const user = getContext("user"); 9 + const atclient = getContext("atclient") as QuicksliceClient; 10 + let handleInput = $state(""); 7 11 12 + async function login() { 13 + if (handleInput) { 14 + await atclient.loginWithRedirect({ handle: handleInput }); 15 + } 16 + } 17 + </script> 8 18 9 19 <section class="flex flex-col gap-4 my-8"> 10 20 <h2 class="text-amber-400 text-2xl font-bold font-neco">Talk about what everyone's reading today</h2> ··· 39 49 <h2 class="text-center text-amber-400 text-3xl font-bold font-neco">Find your next read on potatonet</h2> 40 50 <div class="flex gap-4"> 41 51 <a href="/explore" class="bg-amber-400 text-black hover:cursor-pointer hover:bg-amber-500 hover:text-white px-4 py-2">🛰️ Explore</a> 42 - <button class="bg-amber-400 text-black hover:cursor-pointer hover:bg-amber-500 hover:text-white px-4 py-2">Login</button> 52 + {#if user} 53 + <a href="/home" class="bg-amber-400 text-black hover:cursor-pointer hover:bg-amber-500 hover:text-white px-4 py-2">🏠 Home</a> 54 + {:else} 55 + <input 56 + type="text" 57 + bind:value={handleInput} 58 + placeholder="Handle (eg: zeu.dev)" 59 + class="border border-black border-dashed text-sm px-3 py-2 hover:shadow-lg focus:shadow-lg" 60 + /> 61 + <button onclick={login} class="bg-amber-400 text-black hover:cursor-pointer hover:bg-amber-500 hover:text-white px-4 py-2"> 62 + Login 63 + </button> 64 + {/if} 43 65 </div> 44 66 <pre class="text-xs tracking-widest"> 45 67
-7
src/routes/[handle]/[pubRkey]/+page.svelte
··· 1 - <script lang="ts"> 2 - import { page } from "$app/state"; 3 - 4 - const { handle, pubRkey } = page.params; 5 - </script> 6 - 7 - <p>{handle} {pubRkey}</p>
+31 -5
src/routes/explore/+page.svelte
··· 1 1 <script lang="ts"> 2 2 import { Debounced } from "runed"; 3 - import type { PublicationNode } from '$lib/utils'; 3 + import type { PublicationNode, SubscriptionNode } from '$lib/utils'; 4 4 import { createInfiniteQuery } from '@tanstack/svelte-query'; 5 5 import PublicationCard from '$lib/components/PublicationCard.svelte'; 6 6 ··· 23 23 actorHandle: { contains: "${debouncedSearchTerm.current}" } 24 24 }] 25 25 }`}) { 26 - edges {} 26 + edges { 27 + node { 28 + viewerSiteStandardGraphSubscriptionViaPublication {} 29 + uri 30 + indexedAt 31 + cid 32 + did 33 + url 34 + name 35 + description 36 + icon {} 37 + actorHandle 38 + preferences { 39 + showInDiscover 40 + } 41 + basicTheme {} 42 + } 43 + } 27 44 pageInfo { 28 45 hasNextPage 29 46 endCursor ··· 31 48 } 32 49 } 33 50 `; 34 - const data = await atclient.publicQuery(query); 51 + let data; 52 + if (user) { 53 + data = await atclient.query(query); 54 + } 55 + else { 56 + data = await atclient.publicQuery(query); 57 + } 35 58 return data as { 36 59 siteStandardPublication: { 37 - edges: { node: PublicationNode, cursor: string }[], 60 + edges: { 61 + node: PublicationNode & { viewerSiteStandardGraphSubscriptionViaPublication: SubscriptionNode | null}, 62 + cursor: string 63 + }[], 38 64 pageInfo: { 39 65 hasNextPage: boolean; 40 66 endCursor: string; ··· 98 124 {#if currentPage?.length === 0} 99 125 There are no publications based onb the current filters 100 126 {/if} 101 - {#each currentPage as publication (publication.uri)} 127 + {#each currentPage as publication, i (i)} 102 128 <PublicationCard {publication} /> 103 129 {/each} 104 130 {/if}
+19 -5
src/routes/home/+page.svelte
··· 1 1 <script lang="ts"> 2 - import { Debounced } from "runed"; 3 2 import { goto } from "$app/navigation"; 4 3 import { getContext, onMount } from "svelte"; 5 4 import type { MiniDoc, PublicationNode } from "$lib/utils"; ··· 28 27 }) { 29 28 edges { 30 29 node { 31 - publicationResolved {} 30 + uri 31 + publicationResolved { 32 + uri 33 + indexedAt 34 + cid 35 + did 36 + url 37 + name 38 + description 39 + icon {} 40 + actorHandle 41 + preferences { 42 + showInDiscover 43 + } 44 + basicTheme {} 45 + } 32 46 } 33 47 } 34 48 pageInfo { ··· 39 53 } 40 54 `; 41 55 42 - const data = await atclient.publicQuery(query); 56 + const data = await atclient.query(query); 43 57 return data as { 44 58 siteStandardGraphSubscription: { 45 - edges: { node: { publicationResolved: PublicationNode }}[], 59 + edges: { node: { uri: string, publicationResolved: PublicationNode }}[], 46 60 pageInfo: { 47 61 hasNextPage: boolean; 48 62 endCursor: string; ··· 54 68 getNextPageParam: (lastPage) => lastPage.siteStandardGraphSubscription.pageInfo.endCursor, 55 69 select: (data) => { 56 70 const items = data.pages.map((page) => page.siteStandardGraphSubscription.edges).flat(); 57 - const nodes = items.map((i) => i.node.publicationResolved); 71 + const nodes = items.map((i) => { return { ...(i.node.publicationResolved), viewerSiteStandardGraphSubscriptionViaPublication: { uri: i.node.uri }} }); 58 72 return nodes; 59 73 } 60 74 }));