+23
-44
src/components/ProfileView.svelte
+23
-44
src/components/ProfileView.svelte
···
1
1
<script lang="ts">
2
-
import { AtpClient, resolveDidDoc, resolveHandle } from '$lib/at/client.svelte';
3
-
import {
4
-
isHandle,
5
-
type ActorIdentifier,
6
-
type AtprotoDid,
7
-
type Did,
8
-
type Handle
9
-
} from '@atcute/lexicons/syntax';
2
+
import { AtpClient, resolveDidDoc } from '$lib/at/client.svelte';
3
+
import { isDid, isHandle, type ActorIdentifier, type Did } from '@atcute/lexicons/syntax';
10
4
import TimelineView from './TimelineView.svelte';
11
5
import ProfileInfo from './ProfileInfo.svelte';
12
6
import type { State as PostComposerState } from './PostComposer.svelte';
···
14
8
import { accounts, generateColorForDid } from '$lib/accounts';
15
9
import { img } from '$lib/cdn';
16
10
import { isBlob } from '@atcute/lexicons/interfaces';
17
-
import type { AppBskyActorProfile } from '@atcute/bluesky';
18
11
import {
19
12
handles,
20
13
profiles,
···
34
27
35
28
let { client, actor, onBack, postComposerState = $bindable() }: Props = $props();
36
29
37
-
let profile = $state<AppBskyActorProfile.Main | null>(profiles.get(actor as Did) ?? null);
30
+
const profile = $derived(profiles.get(actor as Did));
38
31
const displayName = $derived(profile?.displayName ?? '');
32
+
const handle = $derived(isHandle(actor) ? actor : handles.get(actor as Did));
39
33
let loading = $state(true);
40
34
let error = $state<string | null>(null);
41
-
let did = $state<AtprotoDid | null>(null);
42
-
let handle = $state<Handle | null>(handles.get(actor as Did) ?? null);
35
+
let did = $state(isDid(actor) ? actor : null);
43
36
44
37
let userBlocked = $state(false);
45
38
let blockedByTarget = $state(false);
···
47
40
const loadProfile = async (identifier: ActorIdentifier) => {
48
41
loading = true;
49
42
error = null;
50
-
profile = null;
51
-
handle = isHandle(identifier) ? identifier : null;
52
43
53
-
const resDid = await resolveHandle(identifier);
54
-
if (resDid.ok) did = resDid.value;
55
-
else {
56
-
error = resDid.error;
57
-
loading = false;
44
+
const docRes = await resolveDidDoc(identifier);
45
+
if (docRes.ok) {
46
+
did = docRes.value.did;
47
+
handles.set(did, docRes.value.handle);
48
+
} else {
49
+
error = docRes.error;
58
50
return;
59
51
}
60
52
61
-
if (!handle) handle = handles.get(did) ?? null;
62
-
63
-
if (!handle) {
64
-
const resHandle = await resolveDidDoc(did);
65
-
if (resHandle.ok) {
66
-
handle = resHandle.value.handle;
67
-
handles.set(did, resHandle.value.handle);
68
-
}
69
-
}
70
-
71
53
// check block relationship
72
54
if (client.user?.did) {
73
55
let blockRel = getBlockRelationship(client.user.did, did);
74
56
blockRel = blockFlags.get(client.user.did)?.has(did)
75
57
? blockRel
76
-
: {
77
-
userBlocked: await fetchBlocked(client, did, client.user.did),
78
-
blockedByTarget: await fetchBlocked(client, client.user.did, did)
79
-
};
58
+
: await (async () => {
59
+
const [userBlocked, blockedByTarget] = await Promise.all([
60
+
await fetchBlocked(client, did, client.user!.did),
61
+
await fetchBlocked(client, client.user!.did, did)
62
+
]);
63
+
return { userBlocked, blockedByTarget };
64
+
})();
80
65
userBlocked = blockRel.userBlocked;
81
66
blockedByTarget = blockRel.blockedByTarget;
82
67
}
···
87
72
return;
88
73
}
89
74
90
-
const res = await client.getProfile(did);
91
-
if (res.ok) {
92
-
profile = res.value;
93
-
profiles.set(did, res.value);
94
-
} else error = res.error;
75
+
const res = await client.getProfile(did, true);
76
+
if (res.ok) profiles.set(did, res.value);
77
+
else error = res.error;
95
78
96
79
loading = false;
97
80
};
···
122
105
<Icon icon="heroicons:arrow-left-20-solid" width={24} />
123
106
</button>
124
107
<h2 class="text-xl font-bold">
125
-
{displayName.length > 0
126
-
? displayName
127
-
: loading
128
-
? 'loading...'
129
-
: (handle ?? actor ?? 'profile')}
108
+
{displayName.length > 0 ? displayName : loading ? 'loading...' : (handle ?? 'handle.invalid')}
130
109
</h2>
131
110
<div class="grow"></div>
132
111
{#if did && client.user && client.user.did !== did}
···
163
142
{#if did}
164
143
<div class="px-4 pb-4">
165
144
<div class="relative z-10 {bannerUrl ? '-mt-12' : 'mt-4'} mb-4">
166
-
<ProfileInfo {client} {did} bind:profile />
145
+
<ProfileInfo {client} {did} {profile} />
167
146
</div>
168
147
169
148
<TimelineView
+5
-5
src/components/TimelineView.svelte
+5
-5
src/components/TimelineView.svelte
···
15
15
} from '$lib/state.svelte';
16
16
import Icon from '@iconify/svelte';
17
17
import { buildThreads, filterThreads, type ThreadPost } from '$lib/thread';
18
-
import type { AtprotoDid } from '@atcute/lexicons/syntax';
18
+
import type { Did } from '@atcute/lexicons/syntax';
19
19
import NotLoggedIn from './NotLoggedIn.svelte';
20
20
21
21
interface Props {
22
22
client?: AtpClient | null;
23
-
targetDid?: AtprotoDid;
23
+
targetDid?: Did;
24
24
postComposerState: PostComposerState;
25
25
class?: string;
26
26
// whether to show replies that are not the user's own posts
···
63
63
loaderState.status = 'LOADING';
64
64
65
65
try {
66
-
await fetchTimeline(client, did as AtprotoDid, 7, showReplies);
66
+
await fetchTimeline(client, did, 7, showReplies);
67
67
// only fetch interactions if logged in (because if not who is the interactor)
68
68
if (client.user) {
69
69
if (!fetchingInteractions) {
···
83
83
}
84
84
85
85
loading = false;
86
-
const cursor = postCursors.get(did as AtprotoDid);
86
+
const cursor = postCursors.get(did);
87
87
if (cursor && cursor.end) loaderState.complete();
88
88
};
89
89
···
92
92
// if we saw all posts dont try to load more.
93
93
// this only really happens if the user has no posts at all
94
94
// but we do have to handle it to not cause an infinite loop
95
-
const cursor = did ? postCursors.get(did as AtprotoDid) : undefined;
95
+
const cursor = did ? postCursors.get(did) : undefined;
96
96
if (!cursor?.end) loadMore();
97
97
}
98
98
});
+29
-13
src/lib/at/client.svelte.ts
+29
-13
src/lib/at/client.svelte.ts
···
30
30
import { MiniDocQuery, type MiniDoc } from './slingshot';
31
31
import { BacklinksQuery, type Backlinks, type BacklinksSource } from './constellation';
32
32
import type { Records, XRPCProcedures } from '@atcute/lexicons/ambient';
33
-
import { cache as rawCache } from '$lib/cache';
33
+
import { cache as rawCache, ttl } from '$lib/cache';
34
34
import { AppBskyActorProfile } from '@atcute/bluesky';
35
35
import { WebSocket } from '@soffinal/websocket';
36
36
import type { Notification } from './stardust';
···
77
77
78
78
const cache = cacheWithRecords;
79
79
80
+
export const invalidateRecordCache = async (uri: ResourceUri) => {
81
+
console.log(`invalidating cached for ${uri}`);
82
+
await cache.invalidate('fetchRecord', `fetchRecord~${uri}`);
83
+
};
84
+
export const setRecordCache = (uri: ResourceUri, record: unknown) =>
85
+
cache.set('fetchRecord', `fetchRecord~${uri}`, record, ttl);
86
+
80
87
export const xhrPost = (
81
88
url: string,
82
89
body: Blob | File,
···
88
95
const xhr = new XMLHttpRequest();
89
96
xhr.open('POST', url);
90
97
91
-
if (onProgress && xhr.upload) {
98
+
if (onProgress && xhr.upload)
92
99
xhr.upload.onprogress = (event: ProgressEvent) => {
93
100
if (event.lengthComputable) onProgress(event.loaded, event.total);
94
101
};
95
-
}
96
102
97
103
Object.keys(headers).forEach((key) => xhr.setRequestHeader(key, headers[key]));
98
104
···
145
151
TKey extends RecordKeySchema,
146
152
Schema extends RecordSchema<TObject, TKey>,
147
153
Output extends InferInput<Schema>
148
-
>(schema: Schema, uri: ResourceUri): Promise<Result<RecordOutput<Output>, string>> {
154
+
>(
155
+
schema: Schema,
156
+
uri: ResourceUri,
157
+
noCache?: boolean
158
+
): Promise<Result<RecordOutput<Output>, string>> {
149
159
const parsedUri = expect(parseResourceUri(uri));
150
160
if (parsedUri.collection !== schema.object.shape.$type.expected)
151
161
return err(
152
162
`collections don't match: ${parsedUri.collection} != ${schema.object.shape.$type.expected}`
153
163
);
154
-
return await this.getRecord(schema, parsedUri.repo!, parsedUri.rkey!);
164
+
return await this.getRecord(schema, parsedUri.repo!, parsedUri.rkey!, noCache);
155
165
}
156
166
157
167
async getRecord<
···
163
173
>(
164
174
schema: Schema,
165
175
repo: ActorIdentifier,
166
-
rkey: RecordKey
176
+
rkey: RecordKey,
177
+
noCache?: boolean
167
178
): Promise<Result<RecordOutput<Output>, string>> {
168
179
const collection = schema.object.shape.$type.expected;
169
180
170
181
try {
171
-
const rawValue = await cache.fetchRecord(
172
-
toResourceUri({ repo, collection, rkey, fragment: undefined })
173
-
);
182
+
const uri = toResourceUri({ repo, collection, rkey, fragment: undefined });
183
+
if (noCache) await invalidateRecordCache(uri);
184
+
const rawValue = await cache.fetchRecord(uri);
174
185
175
186
const parsed = safeParse(schema, rawValue.value);
176
187
if (!parsed.ok) return err(parsed.message);
···
185
196
}
186
197
}
187
198
188
-
async getProfile(repo?: ActorIdentifier): Promise<Result<AppBskyActorProfile.Main, string>> {
199
+
async getProfile(
200
+
repo?: ActorIdentifier,
201
+
noCache?: boolean
202
+
): Promise<Result<AppBskyActorProfile.Main, string>> {
189
203
repo = repo ?? this.user?.did;
190
204
if (!repo) return err('not authenticated');
191
-
return map(await this.getRecord(AppBskyActorProfile.mainSchema, repo, 'self'), (d) => d.record);
205
+
return map(
206
+
await this.getRecord(AppBskyActorProfile.mainSchema, repo, 'self', noCache),
207
+
(d) => d.record
208
+
);
192
209
}
193
210
194
211
async listRecords<Collection extends keyof Records>(
···
218
235
});
219
236
if (!res.ok) return err(`${res.data.error}: ${res.data.message ?? 'no details'}`);
220
237
221
-
for (const record of res.data.records)
222
-
await cache.set('fetchRecord', `fetchRecord~${record.uri}`, record, 60 * 60 * 24);
238
+
for (const record of res.data.records) setRecordCache(record.uri, record);
223
239
224
240
return ok(res.data);
225
241
}
+3
-1
src/lib/cache.ts
+3
-1
src/lib/cache.ts
···
210
210
}
211
211
}
212
212
213
+
export const ttl = 60 * 60 * 3; // 3 hours
214
+
213
215
export const cache = createCache({
214
216
storage: {
215
217
type: 'custom',
···
217
219
storage: new IDBStorage()
218
220
}
219
221
},
220
-
ttl: 60 * 60 * 24, // 24 hours
222
+
ttl,
221
223
onError: (err) => console.error(err)
222
224
});
+3
-1
src/lib/state.svelte.ts
+3
-1
src/lib/state.svelte.ts
···
1
1
import { writable } from 'svelte/store';
2
2
import {
3
3
AtpClient,
4
+
setRecordCache,
4
5
type NotificationsStream,
5
6
type NotificationsStreamEvent
6
7
} from './at/client.svelte';
···
476
477
477
478
export const fetchTimeline = async (
478
479
client: AtpClient,
479
-
subject: AtprotoDid,
480
+
subject: Did,
480
481
limit: number = 6,
481
482
withBacklinks: boolean = true
482
483
) => {
···
545
546
cid: commit.cid
546
547
}
547
548
];
549
+
await setRecordCache(uri, commit.record);
548
550
const client = clients.get(did) ?? viewClient;
549
551
const hydrated = await hydratePosts(client, did, posts, hydrateCacheFn);
550
552
if (!hydrated.ok) {