+1
env.d.ts
+1
env.d.ts
+1
package.json
+1
package.json
+11
pnpm-lock.yaml
+11
pnpm-lock.yaml
···
11
11
'@atcute/atproto':
12
12
specifier: ^3.1.1
13
13
version: 3.1.3
14
+
'@atcute/bluesky':
15
+
specifier: ^3.2.2
16
+
version: 3.2.2
14
17
'@atcute/client':
15
18
specifier: ^4.0.3
16
19
version: 4.0.3
···
66
69
67
70
'@atcute/atproto@3.1.3':
68
71
resolution: {integrity: sha512-+5u0l+8E7h6wZO7MM1HLXIPoUEbdwRtr28ZRTgsURp+Md9gkoBj9e5iMx/xM8F2Exfyb65J5RchW/WlF2mw/RQ==}
72
+
73
+
'@atcute/bluesky@3.2.2':
74
+
resolution: {integrity: sha512-L8RrMNeRLGvSHMq2KDIAGXrpuNGA87YOXpXHY1yhmovVCjQ5n55FrR6JoQaxhprdXdKKQiefxNwQQQybDrfgFQ==}
69
75
70
76
'@atcute/client@4.0.3':
71
77
resolution: {integrity: sha512-RIOZWFVLca/HiPAAUDqQPOdOreCxTbL5cb+WUf5yqQOKIu5yEAP3eksinmlLmgIrlr5qVOE7brazUUzaskFCfw==}
···
1935
1941
1936
1942
'@atcute/atproto@3.1.3':
1937
1943
dependencies:
1944
+
'@atcute/lexicons': 1.1.1
1945
+
1946
+
'@atcute/bluesky@3.2.2':
1947
+
dependencies:
1948
+
'@atcute/atproto': 3.1.3
1938
1949
'@atcute/lexicons': 1.1.1
1939
1950
1940
1951
'@atcute/client@4.0.3':
+109
-64
src/entrypoints/background.ts
+109
-64
src/entrypoints/background.ts
···
3
3
import {
4
4
type Fronter,
5
5
fronterGetSocialAppHrefs,
6
-
fronterGetSocialAppHref,
7
6
getFronter,
8
7
getSpFronters,
9
-
memberUriString,
10
8
putFronter,
11
9
frontersCache,
12
10
parseSocialAppPostUrl,
13
11
displayNameCache,
14
12
deleteFronter,
15
13
getPkFronters,
14
+
FronterType,
15
+
FronterView,
16
16
} from "@/lib/utils";
17
17
import {
18
-
parseCanonicalResourceUri,
18
+
ComAtprotoRepoApplyWrites,
19
+
ComAtprotoRepoCreateRecord,
20
+
} from "@atcute/atproto";
21
+
import { createResultSchema } from "@atcute/atproto/types/repo/applyWrites";
22
+
import { feedViewPostSchema } from "@atcute/bluesky/types/app/feed/defs";
23
+
import {
24
+
InferOutput,
25
+
is,
19
26
parseResourceUri,
20
27
ResourceUri,
28
+
safeParse,
21
29
} from "@atcute/lexicons";
30
+
import { AtprotoDid, parseCanonicalResourceUri } from "@atcute/lexicons/syntax";
22
31
23
32
export default defineBackground({
24
33
persistent: true,
···
92
101
}
93
102
// dont write if no names is specified or no sp/pk fronters are fetched
94
103
if (members.length === 0) return;
95
-
const results = [];
104
+
const results: FronterView[] = [];
96
105
for (const result of items) {
97
106
const resp = await putFronter(result.uri, members, authToken);
98
107
if (resp.ok) {
99
108
const parsedUri = await cacheFronter(result.uri, resp.value);
100
109
results.push({
110
+
type:
111
+
parsedUri.collection === "app.bsky.feed.repost"
112
+
? "repost"
113
+
: parsedUri.collection === "app.bsky.feed.like"
114
+
? "like"
115
+
: "post",
101
116
rkey: parsedUri.rkey!,
102
117
...resp.value,
103
118
});
···
112
127
type: "TIMELINE_FRONTER",
113
128
results: Object.fromEntries(
114
129
results.flatMap((fronter) =>
115
-
fronterGetSocialAppHrefs(fronter, fronter.rkey).map((href) => [
116
-
href,
117
-
fronter,
118
-
]),
130
+
fronterGetSocialAppHrefs(fronter).map((href) => [href, fronter]),
119
131
),
120
132
),
121
133
});
···
124
136
feed: any[],
125
137
sender: globalThis.Browser.runtime.MessageSender,
126
138
) => {
127
-
const handlePost = async (post: any) => {
128
-
const cachedFronter = await frontersCache.get(post.uri);
129
-
if (cachedFronter === null) return;
130
-
const promise = cachedFronter
131
-
? Promise.resolve(cachedFronter)
132
-
: getFronter(post.uri).then(async (fronter) => {
133
-
if (!fronter.ok) {
134
-
await frontersCache.set(post.uri, null);
135
-
return;
136
-
}
137
-
return fronter.value;
138
-
});
139
-
return promise.then(async (fronter) => {
140
-
if (!fronter) return;
141
-
const parsedUri = await cacheFronter(post.uri, fronter);
142
-
return {
143
-
rkey: parsedUri.rkey!,
144
-
...fronter,
139
+
const allPromises = feed.flatMap(
140
+
(item): Promise<FronterView | undefined>[] => {
141
+
if (!is(feedViewPostSchema, item)) return [];
142
+
const handleUri = async (
143
+
uri: ResourceUri,
144
+
type: "repost" | "post",
145
+
) => {
146
+
const cachedFronter = await frontersCache.get(uri);
147
+
if (cachedFronter === null) return;
148
+
const promise = cachedFronter
149
+
? Promise.resolve(cachedFronter)
150
+
: getFronter(uri).then(async (fronter) => {
151
+
if (!fronter.ok) {
152
+
await frontersCache.set(uri, null);
153
+
return;
154
+
}
155
+
return fronter.value;
156
+
});
157
+
return await promise.then(
158
+
async (fronter): Promise<FronterView | undefined> => {
159
+
if (!fronter) return;
160
+
if (type === "repost") {
161
+
const parsedPostUri = expect(
162
+
parseCanonicalResourceUri(item.post.uri),
163
+
);
164
+
fronter = {
165
+
subject: {
166
+
did: parsedPostUri.repo as AtprotoDid,
167
+
rkey: parsedPostUri.rkey,
168
+
handle:
169
+
item.post.author.handle === "handle.invalid"
170
+
? undefined
171
+
: item.post.author.handle,
172
+
},
173
+
...fronter,
174
+
};
175
+
}
176
+
const parsedUri = await cacheFronter(uri, fronter);
177
+
return {
178
+
type,
179
+
rkey: parsedUri.rkey!,
180
+
...fronter,
181
+
};
182
+
},
183
+
);
145
184
};
146
-
});
147
-
};
148
-
const allPromises = feed.flatMap((item) => {
149
-
const promises = [handlePost(item.post)];
150
-
if (item.reply?.parent) {
151
-
promises.push(handlePost(item.reply.parent));
152
-
}
153
-
if (item.reply?.root) {
154
-
promises.push(handlePost(item.reply.root));
155
-
}
156
-
return promises;
157
-
});
185
+
const promises: ReturnType<typeof handleUri>[] = [];
186
+
promises.push(handleUri(item.post.uri, "post"));
187
+
if (item.reply?.parent) {
188
+
promises.push(handleUri(item.reply.parent.uri, "post"));
189
+
}
190
+
if (item.reply?.root) {
191
+
promises.push(handleUri(item.reply.root.uri, "post"));
192
+
}
193
+
if (
194
+
item.reason &&
195
+
item.reason.$type === "app.bsky.feed.defs#reasonRepost" &&
196
+
item.reason.uri
197
+
) {
198
+
promises.push(handleUri(item.reason.uri, "repost"));
199
+
}
200
+
return promises;
201
+
},
202
+
);
158
203
const results = new Map(
159
204
(await Promise.allSettled(allPromises))
160
205
.filter((result) => result.status === "fulfilled")
161
206
.flatMap((result) => result.value ?? [])
162
207
.flatMap((fronter) =>
163
-
fronterGetSocialAppHrefs(fronter, fronter.rkey).map((href) => [
164
-
href,
165
-
fronter,
166
-
]),
208
+
fronterGetSocialAppHrefs(fronter).map((href) => [href, fronter]),
167
209
),
168
210
);
169
211
if (results.size === 0) return;
···
210
252
}
211
253
return fronter.value;
212
254
});
213
-
return promise.then(async (fronter): Promise<any> => {
214
-
if (!fronter) return;
215
-
const parsedUri = await cacheFronter(item.uri, fronter);
216
-
if (isReplyThreadFetch)
255
+
return promise.then(
256
+
async (fronter): Promise<FronterView | undefined> => {
257
+
if (!fronter) return;
258
+
const parsedUri = await cacheFronter(item.uri, fronter);
259
+
if (isReplyThreadFetch)
260
+
return {
261
+
type: "thread_reply",
262
+
rkey: parsedUri.rkey!,
263
+
...fronter,
264
+
};
265
+
if (item.depth === 0) await setTabFronter(item.uri, fronter);
266
+
const displayName = item.value.post.author.displayName;
267
+
// cache display name for later use
268
+
if (fronter.handle)
269
+
await displayNameCache.set(fronter.handle, displayName);
270
+
await displayNameCache.set(fronter.did, displayName);
217
271
return {
272
+
type: "thread_post",
218
273
rkey: parsedUri.rkey!,
274
+
displayName,
275
+
depth: item.depth,
219
276
...fronter,
220
277
};
221
-
if (item.depth === 0) await setTabFronter(item.uri, fronter);
222
-
const displayName = item.value.post.author.displayName;
223
-
// cache display name for later use
224
-
if (fronter.handle)
225
-
await displayNameCache.set(fronter.handle, displayName);
226
-
await displayNameCache.set(fronter.did, displayName);
227
-
return {
228
-
rkey: parsedUri.rkey!,
229
-
displayName,
230
-
depth: item.depth,
231
-
...fronter,
232
-
};
233
-
});
278
+
},
279
+
);
234
280
});
235
281
});
236
282
const results = new Map(
···
238
284
.filter((result) => result.status === "fulfilled")
239
285
.flatMap((result) => result.value ?? [])
240
286
.flatMap((fronter) =>
241
-
fronterGetSocialAppHrefs(fronter, fronter.rkey, fronter.depth).map(
242
-
(href) => [href, fronter],
243
-
),
287
+
fronterGetSocialAppHrefs(fronter).map((href) => [href, fronter]),
244
288
),
245
289
);
246
290
if (results.size === 0) return;
···
264
308
break;
265
309
case "write":
266
310
await handleWrite(
267
-
JSON.parse(message.data.body).results,
311
+
JSON.parse(message.data.body),
268
312
message.data.authToken,
269
313
sender,
270
314
);
271
315
break;
272
-
case "writeOne":
316
+
case "writeOne": {
273
317
await handleWrite(
274
318
[JSON.parse(message.data.body)],
275
319
message.data.authToken,
276
320
sender,
277
321
);
278
322
break;
323
+
}
279
324
case "posts":
280
325
await handleTimeline(
281
326
(JSON.parse(message.data.body) as any[]).map((post) => ({ post })),
+72
-30
src/entrypoints/content.ts
+72
-30
src/entrypoints/content.ts
···
1
-
import { decodeStorageKey } from "@/lib/cache";
2
-
import { expect } from "@/lib/result";
3
-
import {
4
-
Fronter,
5
-
fronterGetSocialAppHref,
6
-
fronterGetSocialAppHrefs,
7
-
parseSocialAppPostUrl,
8
-
} from "@/lib/utils";
9
-
import { parseResourceUri, ResourceUri } from "@atcute/lexicons";
1
+
import { FronterView, parseSocialAppPostUrl } from "@/lib/utils";
10
2
11
3
const getAuthHeader = (headers: any): string | null => {
12
4
if (headers instanceof Headers) {
···
146
138
});
147
139
respEventSetup.then((name) => (respEventName = name));
148
140
149
-
const applyFronterName = (el: Element, fronters: Fronter["members"]) => {
150
-
if (el.hasAttribute("data-fronter")) return;
141
+
const applyFronterName = (
142
+
el: Element,
143
+
fronters: FronterView["members"],
144
+
) => {
145
+
if (el.hasAttribute("data-fronter")) return false;
151
146
const s = fronters.map((f) => f.name).join(", ");
152
147
el.textContent += ` [f: ${s}]`;
153
148
el.setAttribute("data-fronter", s);
149
+
return true;
154
150
};
155
151
const applyFrontersToPage = (
156
-
fronters: Map<string, any>,
152
+
fronters: Map<string, FronterView | null>,
157
153
pageChange: boolean,
158
154
) => {
159
155
// console.log("applyFrontersToPage", fronters);
···
164
160
);
165
161
for (const el of document.querySelectorAll("[data-fronter]")) {
166
162
const previousFronter = el.getAttribute("data-fronter")!;
167
-
// remove fronter text
168
-
el.textContent = el.textContent.replace(
169
-
` [f: ${previousFronter}]`,
170
-
"",
171
-
);
163
+
if (previousFronter !== "__set__") {
164
+
// remove fronter text
165
+
el.textContent = el.textContent.replace(
166
+
` [f: ${previousFronter}]`,
167
+
"",
168
+
);
169
+
}
172
170
el.removeAttribute("data-fronter");
173
171
}
174
172
}
175
173
console.log("applyFrontersToPage", match, fronters);
176
174
if (fronters.size === 0) return;
175
+
const applyFronterToElement = (el: Element, fronter: FronterView) => {
176
+
let displayNameElement: Element | null = null;
177
+
if (fronter.type === "repost") {
178
+
displayNameElement =
179
+
el.parentElement?.parentElement?.parentElement?.parentElement
180
+
?.parentElement?.firstElementChild?.nextElementSibling
181
+
?.firstElementChild?.nextElementSibling?.firstElementChild
182
+
?.firstElementChild?.nextElementSibling?.firstElementChild
183
+
?.firstElementChild?.firstElementChild?.firstElementChild ?? null;
184
+
// sanity check
185
+
if (displayNameElement?.tagName !== "SPAN") {
186
+
console.log(
187
+
`invalid display element tag ${displayNameElement?.tagName}, expected span:`,
188
+
displayNameElement,
189
+
);
190
+
return;
191
+
}
192
+
} else {
193
+
if (fronter.type === "thread_post" && fronter.depth === 0) {
194
+
if (match && match.rkey !== fronter.rkey) return;
195
+
if (el.ariaLabel !== fronter.displayName) return;
196
+
displayNameElement =
197
+
el.firstElementChild?.firstElementChild?.firstElementChild
198
+
?.firstElementChild?.firstElementChild ?? null;
199
+
// sanity check
200
+
if (displayNameElement?.tagName !== "DIV") {
201
+
console.log(
202
+
`invalid display element tag ${displayNameElement?.tagName}, expected a:`,
203
+
displayNameElement,
204
+
);
205
+
return;
206
+
}
207
+
} else {
208
+
displayNameElement =
209
+
el.parentElement?.firstElementChild?.firstElementChild
210
+
?.firstElementChild?.firstElementChild ?? null;
211
+
// sanity check
212
+
if (displayNameElement?.tagName !== "A") {
213
+
console.log(
214
+
`invalid display element tag ${displayNameElement?.tagName}, expected a:`,
215
+
displayNameElement,
216
+
);
217
+
return;
218
+
}
219
+
}
220
+
}
221
+
if (!displayNameElement) return;
222
+
return applyFronterName(displayNameElement, fronter.members);
223
+
};
177
224
for (const el of document.getElementsByTagName("a")) {
225
+
if (el.getAttribute("data-fronter")) continue;
178
226
const path = `/${el.href.split("/").slice(3).join("/")}`;
179
-
const fronter = fronters.get(path);
180
-
if (!fronter || fronter.members?.length === 0) continue;
181
-
if (el.hasAttribute("data-fronter")) continue;
182
-
const isFocusedPost = fronter.depth === 0;
183
-
if (isFocusedPost) if (match && match.rkey !== fronter.rkey) continue;
184
-
if (isFocusedPost) if (el.ariaLabel !== fronter.displayName) continue;
185
-
const displayNameElement = isFocusedPost
186
-
? (el.firstElementChild?.firstElementChild?.firstElementChild
187
-
?.firstElementChild?.firstElementChild ?? null)
188
-
: (el.parentElement?.firstElementChild?.firstElementChild
189
-
?.firstElementChild?.firstElementChild ?? null);
190
-
if (!displayNameElement) continue;
191
-
applyFronterName(displayNameElement, fronter.members);
227
+
const elFronters = [fronters.get(path), fronters.get(`${path}#repost`)];
228
+
for (const fronter of elFronters) {
229
+
if (!fronter || fronter.members?.length === 0) continue;
230
+
if (applyFronterToElement(el, fronter)) {
231
+
el.setAttribute("data-fronter", "__set__");
232
+
}
233
+
}
192
234
}
193
235
};
194
236
let postTabObserver: MutationObserver | null = null;
+39
-18
src/entrypoints/isolated.content.ts
+39
-18
src/entrypoints/isolated.content.ts
···
3
3
import {
4
4
displayNameCache,
5
5
Fronter,
6
+
fronterGetSocialAppHref,
6
7
fronterGetSocialAppHrefs,
7
8
frontersCache,
9
+
FronterView,
8
10
parseSocialAppPostUrl,
9
11
} from "@/lib/utils";
10
12
import { parseResourceUri, ResourceUri } from "@atcute/lexicons";
···
46
48
});
47
49
const updateOnUrlChange = async () => {
48
50
const fronters = await frontersCache.getAll();
49
-
const updated = new Map<string, any>(
50
-
fronters.entries().flatMap(([storageKey, fronter]) => {
51
-
if (!fronter) return [];
52
-
const uri = decodeStorageKey(storageKey);
53
-
const rkey = expect(parseResourceUri(uri)).rkey!;
54
-
return fronterGetSocialAppHrefs(fronter, rkey).map((href) => [
55
-
href,
56
-
fronter,
57
-
]);
58
-
}),
59
-
);
51
+
const updated = new Map<string, FronterView | null>();
52
+
for (const [storageKey, fronter] of fronters.entries()) {
53
+
const uri = decodeStorageKey(storageKey);
54
+
const parsedUri = expect(parseResourceUri(uri));
55
+
if (!fronter) {
56
+
updated.set(
57
+
fronterGetSocialAppHref(parsedUri.repo, parsedUri.rkey!),
58
+
null,
59
+
);
60
+
continue;
61
+
}
62
+
const view: FronterView = {
63
+
type:
64
+
parsedUri.collection === "app.bsky.feed.repost" ? "repost" : "post",
65
+
rkey: parsedUri.rkey!,
66
+
...fronter,
67
+
};
68
+
for (const href of fronterGetSocialAppHrefs(view)) {
69
+
updated.set(href, view);
70
+
}
71
+
}
60
72
// add entry for current page
61
73
const match = parseSocialAppPostUrl(document.location.href);
62
74
if (match && !updated.has(`/profile/${match.actorIdentifier}`)) {
63
75
const maybeFronter = updated.get(
64
76
`/profile/${match.actorIdentifier}/post/${match.rkey}`,
65
77
);
66
-
if (maybeFronter)
67
-
updated.set(`/profile/${match.actorIdentifier}`, {
68
-
depth: 0,
69
-
displayName: await displayNameCache.get(match.actorIdentifier),
70
-
rkey: match.rkey,
71
-
...maybeFronter,
72
-
});
78
+
if (maybeFronter) {
79
+
const displayName = await displayNameCache.get(match.actorIdentifier);
80
+
if (displayName) {
81
+
const view: FronterView = {
82
+
...maybeFronter,
83
+
type: "thread_post",
84
+
depth: 0,
85
+
displayName,
86
+
rkey: match.rkey,
87
+
};
88
+
updated.set(`/profile/${maybeFronter.did}`, view);
89
+
if (maybeFronter.handle) {
90
+
updated.set(`/profile/${maybeFronter.handle}`, view);
91
+
}
92
+
}
93
+
}
73
94
}
74
95
window.postMessage({
75
96
type: "APPLY_CACHED_FRONTERS",
+2
-2
src/entrypoints/popup/App.svelte
+2
-2
src/entrypoints/popup/App.svelte
+40
-9
src/lib/utils.ts
+40
-9
src/lib/utils.ts
···
28
28
import { getAtprotoHandle, getPdsEndpoint } from "@atcute/identity";
29
29
import { PersistentCache } from "./cache";
30
30
31
+
export type Subject = {
32
+
handle?: Handle;
33
+
did: AtprotoDid;
34
+
rkey: RecordKey;
35
+
};
36
+
31
37
export type Fronter = {
32
38
members: {
33
39
uri?: MemberUri;
···
35
41
}[];
36
42
handle: Handle | null;
37
43
did: AtprotoDid;
44
+
subject?: Subject;
38
45
};
46
+
47
+
export type FronterView = Fronter & { rkey: RecordKey } & (
48
+
| {
49
+
type: "thread_reply";
50
+
}
51
+
| {
52
+
type: "thread_post";
53
+
displayName: string;
54
+
depth: number;
55
+
}
56
+
| {
57
+
type: "post";
58
+
}
59
+
| {
60
+
type: "like";
61
+
}
62
+
| {
63
+
type: "repost";
64
+
}
65
+
);
66
+
export type FronterType = FronterView["type"];
39
67
40
68
export const fronterSchema = v.record(
41
69
v.string(),
···
359
387
}));
360
388
};
361
389
362
-
export const fronterGetSocialAppHrefs = (
363
-
fronter: Fronter,
364
-
rkey: RecordKey,
365
-
depth?: number,
366
-
) => {
390
+
export const fronterGetSocialAppHrefs = (view: FronterView) => {
391
+
if (view.type === "repost" && view.subject) {
392
+
const subject = view.subject;
393
+
const handle = subject?.handle;
394
+
return [
395
+
handle ? [`${fronterGetSocialAppHref(handle, subject.rkey)}#repost`] : [],
396
+
`${fronterGetSocialAppHref(subject.did, subject.rkey)}#repost`,
397
+
].flat();
398
+
}
399
+
const depth = view.type === "thread_post" ? view.depth : undefined;
367
400
return [
368
-
fronter.handle
369
-
? [fronterGetSocialAppHref(fronter.handle, rkey, depth)]
370
-
: [],
371
-
fronterGetSocialAppHref(fronter.did, rkey, depth),
401
+
view.handle ? [fronterGetSocialAppHref(view.handle, view.rkey, depth)] : [],
402
+
fronterGetSocialAppHref(view.did, view.rkey, depth),
372
403
].flat();
373
404
};
374
405