+19
-1
src/lib/components/page/page-header.svelte
+19
-1
src/lib/components/page/page-header.svelte
···
1
1
<script lang="ts">
2
+
import type { Snippet } from 'svelte';
3
+
2
4
interface Props {
3
5
title: string;
6
+
children?: Snippet;
4
7
}
5
8
6
-
const { title }: Props = $props();
9
+
const { title, children }: Props = $props();
7
10
</script>
8
11
9
12
<div class="page-header">
10
13
<h2 class="title">{title}</h2>
14
+
15
+
{#if children}
16
+
<div class="actions">
17
+
{@render children()}
18
+
</div>
19
+
{/if}
11
20
</div>
12
21
13
22
<style>
14
23
.page-header {
24
+
display: flex;
15
25
position: sticky;
16
26
top: 0;
27
+
justify-content: space-between;
28
+
align-items: center;
17
29
z-index: 1;
18
30
border-bottom: 1px solid var(--divider-sm);
19
31
background: var(--bg-primary);
···
24
36
font-weight: 600;
25
37
font-size: 1rem;
26
38
line-height: 1.5rem;
39
+
}
40
+
41
+
.actions {
42
+
display: flex;
43
+
align-items: center;
44
+
gap: 4px;
27
45
}
28
46
</style>
+75
src/lib/queries/constellation.ts
+75
src/lib/queries/constellation.ts
···
63
63
64
64
return json as LinkResponse<K>;
65
65
};
66
+
67
+
// due to the way Bluesky has designed its embeds, quotes can be in two
68
+
// different paths, `.embed.record.uri` and `.embed.record.record.uri`.
69
+
// Since Constellation can only support one path at a time, here's a function
70
+
// that will make this happen really nicely
71
+
const MP_CURSOR_RE = /^mp:(\d+)(?::(.+))?$/;
72
+
73
+
export const getLinksMultiPath = async <K extends keyof Records>({
74
+
uri,
75
+
collection,
76
+
paths,
77
+
limit = 10,
78
+
cursor = null,
79
+
}: {
80
+
uri: string;
81
+
collection: K;
82
+
paths: [string, string, ...string[]];
83
+
limit?: number;
84
+
cursor?: string | null;
85
+
}): Promise<LinkResponse<K>> => {
86
+
let index = 0;
87
+
let curs: string | null = null;
88
+
89
+
const result: LinkResponse<K> = {
90
+
// this will never be anything other than 0 unfortunately,
91
+
// can't make it work across different paths
92
+
total: 0,
93
+
cursor: null,
94
+
linking_records: [],
95
+
};
96
+
97
+
if (cursor !== null) {
98
+
const match = MP_CURSOR_RE.exec(cursor);
99
+
if (match === null) {
100
+
return result;
101
+
}
102
+
103
+
index = parseInt(match[1], 10);
104
+
curs = match[2] ?? null;
105
+
106
+
if (index >= paths.length) {
107
+
return result;
108
+
}
109
+
}
110
+
111
+
while (index < paths.length) {
112
+
const data = await getLinks({
113
+
uri: uri,
114
+
collection: collection,
115
+
path: paths[index],
116
+
limit: limit - result.linking_records.length,
117
+
cursor: curs,
118
+
});
119
+
120
+
result.linking_records = [...result.linking_records, ...data.linking_records];
121
+
122
+
// response returned a cursor, so we're breaking early
123
+
if (data.cursor !== null) {
124
+
result.cursor = `mp:${index}:${data.cursor}`;
125
+
break;
126
+
}
127
+
128
+
// we've reached the limit
129
+
if (result.linking_records.length >= limit) {
130
+
break;
131
+
}
132
+
133
+
// move to the next path
134
+
index++;
135
+
curs = null;
136
+
result.cursor = index < paths.length ? `mp:${index}` : null;
137
+
}
138
+
139
+
return result;
140
+
};
+3
-3
src/routes/(app)/[actor=did]/[rkey=tid]/all-quotes/+page.ts
+3
-3
src/routes/(app)/[actor=did]/[rkey=tid]/all-quotes/+page.ts
···
3
3
import { PUBLIC_APPVIEW_URL } from '$env/static/public';
4
4
import type { PageLoad } from './$types';
5
5
6
+
import { getLinksMultiPath } from '$lib/queries/constellation';
6
7
import { makeAtUri } from '$lib/types/at-uri';
7
-
import { getLinks } from '$lib/queries/constellation';
8
8
9
9
export const load: PageLoad = async ({ url, params, fetch }) => {
10
10
const rpc = new XRPC({ handler: simpleFetchHandler({ service: PUBLIC_APPVIEW_URL }) });
11
11
12
12
const parentUri = makeAtUri(params.actor, 'app.bsky.feed.post', params.rkey);
13
13
14
-
const { cursor, linking_records } = await getLinks({
14
+
const { cursor, linking_records } = await getLinksMultiPath({
15
15
uri: parentUri,
16
16
collection: 'app.bsky.feed.post',
17
-
path: '.embed.record.uri',
17
+
paths: ['.embed.record.uri', '.embed.record.record.uri'],
18
18
cursor: url.searchParams.get('cursor'),
19
19
limit: 25,
20
20
});
+15
-1
src/routes/(app)/[actor=did]/[rkey=tid]/quotes/+page.svelte
+15
-1
src/routes/(app)/[actor=did]/[rkey=tid]/quotes/+page.svelte
···
1
1
<script lang="ts">
2
+
import { base } from '$app/paths';
2
3
import { page } from '$app/state';
3
4
import { PUBLIC_APP_NAME } from '$env/static/public';
4
5
import type { PageProps } from './$types';
5
6
6
7
import { paginate } from '$lib/utils/pagination';
7
8
9
+
import OverflowMenu from '$lib/components/overflow-menu.svelte';
8
10
import PageContainer from '$lib/components/page/page-container.svelte';
9
11
import PageHeader from '$lib/components/page/page-header.svelte';
10
12
import PageListing from '$lib/components/page/page-listing.svelte';
11
13
import PostFeedItem from '$lib/components/timeline/post-feed-item.svelte';
14
+
15
+
import ThreadOutlined from '$lib/components/central-icons/thread-outlined.svelte';
12
16
13
17
const { data }: PageProps = $props();
14
18
···
20
24
</svelte:head>
21
25
22
26
<PageContainer>
23
-
<PageHeader title="Quotes" />
27
+
<PageHeader title="Quotes">
28
+
<OverflowMenu
29
+
items={[
30
+
{
31
+
label: `Show all quotes`,
32
+
href: `${base}/${page.params.actor}/${page.params.rkey}/all-quotes`,
33
+
icon: ThreadOutlined,
34
+
},
35
+
]}
36
+
/>
37
+
</PageHeader>
24
38
25
39
<PageListing subject="posts" {rootUrl} {nextUrl}>
26
40
{#each data.quotes.items as post (post.uri)}