tangled
alpha
login
or
join now
zeu.dev
/
potatonet-app
12
fork
atom
Read-it-later social network
12
fork
atom
overview
issues
pulls
pipelines
update queries with authed viewer subscription
zeu.dev
1 month ago
793c0e4d
cf0f4aff
+106
-69
9 changed files
expand all
collapse all
unified
split
src
lib
components
PublicationCard.svelte
utils.ts
routes
+error.svelte
+layout.svelte
+layout.ts
+page.svelte
[handle]
[pubRkey]
+page.svelte
explore
+page.svelte
home
+page.svelte
+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
5
-
import { parseAtUri, resolveHandle, type MiniDoc, type PublicationNode } from "$lib/utils";
5
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
10
-
let { publication, showEmpty = false }: { publication: PublicationNode, showEmpty?: boolean } = $props();
10
10
+
let { publication, showEmpty = false }: {
11
11
+
publication: PublicationNode & { viewerSiteStandardGraphSubscriptionViaPublication?: SubscriptionNode | null }, showEmpty?: boolean
12
12
+
} = $props();
13
13
+
14
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
47
-
const subscriptionQuery = createQuery(() => ({
48
48
-
queryKey: ["isSubscribed", publication.uri, user && user.did],
49
49
-
queryFn: async () => {
50
50
-
if (!user.did) {
51
51
-
return { records: [] }
52
52
-
}
53
53
-
const constellationUrl = new URL("https://constellation.microcosm.blue/xrpc/blue.microcosm.links.getBacklinks");
54
54
-
constellationUrl.searchParams.set("subject", publication.uri);
55
55
-
constellationUrl.searchParams.set("source", "site.standard.graph.subscription:publication");
56
56
-
constellationUrl.searchParams.set("did", user.did);
57
57
-
const response = await fetch(constellationUrl, {
58
58
-
headers: {
59
59
-
"Accept": "application/json"
60
60
-
}
61
61
-
});
62
62
-
63
63
-
64
64
-
const json = await response.json() as { records: { did: string, collection: string, rkey: string }[] };
65
65
-
return json;
66
66
-
},
67
67
-
select: (data) => data.records[0] && data.records[0].rkey
68
68
-
}));
69
69
-
70
51
let documents = $derived(countQuery.data?.documents || 0);
71
52
let subscribers = $derived(countQuery.data?.subscribers || 0);
72
72
-
let subscriptionRkey = $derived(subscriptionQuery.data);
73
73
-
let blobSyncUrl = $derived((`${miniDocQuery.data?.pds}/xrpc/com.atproto.sync.getBlob?did=${publication.did}&cid=${publication.value.icon?.ref.$link}`));
74
74
-
const theme = publication.value.basicTheme || {
53
53
+
let subscriptionRkey = $derived(parseAtUri(publication.viewerSiteStandardGraphSubscriptionViaPublication?.uri || "").rkey);
54
54
+
let blobSyncUrl = $derived(`${miniDocQuery.data?.pds}/xrpc/com.atproto.sync.getBlob?did=${publication.did}&cid=${publication.icon?.ref}`);
55
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
170
-
{#if publication.value.icon}
151
151
+
{#if publication.icon}
171
152
<img
172
153
src={blobSyncUrl.toString()}
173
173
-
alt={publication.value.name}
154
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
178
-
{publication.value.name}
159
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
190
-
{publication.value.description}
171
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
195
-
<div
176
176
+
<a
177
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
209
-
</div>
191
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
42
-
ref: { $link: string; };
42
42
+
ref: string;
43
43
mimeType: string;
44
44
size: number;
45
45
}
···
51
51
r: number;
52
52
}
53
53
54
54
-
export type PublicationNode = Node & { value: {
54
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
-
}}
70
70
+
}
71
71
+
72
72
+
export type SubscriptionNode = Node & {
73
73
+
publication: string;
74
74
+
}
71
75
72
76
export type DocumentNode = Node & { value: {
73
77
title: string;
+5
src/routes/+error.svelte
···
1
1
+
<script lang="ts">
2
2
+
import { page } from "$app/state";
3
3
+
</script>
4
4
+
5
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
4
-
import { onMount, setContext } from 'svelte';
4
4
+
import { setContext } from 'svelte';
5
5
import { QueryClient, QueryClientProvider } from "@tanstack/svelte-query";
6
6
import { SvelteQueryDevtools } from "@tanstack/svelte-query-devtools";
7
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
35
-
36
36
-
/**
37
37
-
onMount(() => {
38
38
-
if (user) {
39
39
-
goto("/home");
40
40
-
}
41
41
-
});
42
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
53
-
<a href="/explore" class="hover:text-shadow-sm" title="explore" aria-label="explore">🛰️ explore</a>
44
44
+
<a href="/explore" class="hover:text-shadow-sm" title="explore" aria-label="explore">🛰️ Explore</a>
54
45
{#if user}
55
55
-
<a href="/home" class="hover:text-shadow-sm" title="explore" aria-label="explore">🏠 {user.handle}</a>
46
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
16
-
redirect(302, "/");
16
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
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
5
-
</script>
6
6
+
import type { QuicksliceClient } from "quickslice-client-js";
6
7
8
8
+
const user = getContext("user");
9
9
+
const atclient = getContext("atclient") as QuicksliceClient;
10
10
+
let handleInput = $state("");
7
11
12
12
+
async function login() {
13
13
+
if (handleInput) {
14
14
+
await atclient.loginWithRedirect({ handle: handleInput });
15
15
+
}
16
16
+
}
17
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
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
52
+
{#if user}
53
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
54
+
{:else}
55
55
+
<input
56
56
+
type="text"
57
57
+
bind:value={handleInput}
58
58
+
placeholder="Handle (eg: zeu.dev)"
59
59
+
class="border border-black border-dashed text-sm px-3 py-2 hover:shadow-lg focus:shadow-lg"
60
60
+
/>
61
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
62
+
Login
63
63
+
</button>
64
64
+
{/if}
43
65
</div>
44
66
<pre class="text-xs tracking-widest">
45
67
-7
src/routes/[handle]/[pubRkey]/+page.svelte
···
1
1
-
<script lang="ts">
2
2
-
import { page } from "$app/state";
3
3
-
4
4
-
const { handle, pubRkey } = page.params;
5
5
-
</script>
6
6
-
7
7
-
<p>{handle} {pubRkey}</p>
+31
-5
src/routes/explore/+page.svelte
···
1
1
<script lang="ts">
2
2
import { Debounced } from "runed";
3
3
-
import type { PublicationNode } from '$lib/utils';
3
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
26
-
edges {}
26
26
+
edges {
27
27
+
node {
28
28
+
viewerSiteStandardGraphSubscriptionViaPublication {}
29
29
+
uri
30
30
+
indexedAt
31
31
+
cid
32
32
+
did
33
33
+
url
34
34
+
name
35
35
+
description
36
36
+
icon {}
37
37
+
actorHandle
38
38
+
preferences {
39
39
+
showInDiscover
40
40
+
}
41
41
+
basicTheme {}
42
42
+
}
43
43
+
}
27
44
pageInfo {
28
45
hasNextPage
29
46
endCursor
···
31
48
}
32
49
}
33
50
`;
34
34
-
const data = await atclient.publicQuery(query);
51
51
+
let data;
52
52
+
if (user) {
53
53
+
data = await atclient.query(query);
54
54
+
}
55
55
+
else {
56
56
+
data = await atclient.publicQuery(query);
57
57
+
}
35
58
return data as {
36
59
siteStandardPublication: {
37
37
-
edges: { node: PublicationNode, cursor: string }[],
60
60
+
edges: {
61
61
+
node: PublicationNode & { viewerSiteStandardGraphSubscriptionViaPublication: SubscriptionNode | null},
62
62
+
cursor: string
63
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
101
-
{#each currentPage as publication (publication.uri)}
127
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
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
31
-
publicationResolved {}
30
30
+
uri
31
31
+
publicationResolved {
32
32
+
uri
33
33
+
indexedAt
34
34
+
cid
35
35
+
did
36
36
+
url
37
37
+
name
38
38
+
description
39
39
+
icon {}
40
40
+
actorHandle
41
41
+
preferences {
42
42
+
showInDiscover
43
43
+
}
44
44
+
basicTheme {}
45
45
+
}
32
46
}
33
47
}
34
48
pageInfo {
···
39
53
}
40
54
`;
41
55
42
42
-
const data = await atclient.publicQuery(query);
56
56
+
const data = await atclient.query(query);
43
57
return data as {
44
58
siteStandardGraphSubscription: {
45
45
-
edges: { node: { publicationResolved: PublicationNode }}[],
59
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
57
-
const nodes = items.map((i) => i.node.publicationResolved);
71
71
+
const nodes = items.map((i) => { return { ...(i.node.publicationResolved), viewerSiteStandardGraphSubscriptionViaPublication: { uri: i.node.uri }} });
58
72
return nodes;
59
73
}
60
74
}));