+125
-7
src/entrypoints/background.ts
+125
-7
src/entrypoints/background.ts
···
11
11
deleteFronter,
12
12
getPkFronters,
13
13
FronterView,
14
+
docResolver,
14
15
} from "@/lib/utils";
15
-
import { AppBskyFeedPost } from "@atcute/bluesky";
16
+
import {
17
+
AppBskyFeedLike,
18
+
AppBskyFeedPost,
19
+
AppBskyFeedRepost,
20
+
AppBskyNotificationListNotifications,
21
+
} from "@atcute/bluesky";
16
22
import { feedViewPostSchema } from "@atcute/bluesky/types/app/feed/defs";
23
+
import { getAtprotoHandle } from "@atcute/identity";
17
24
import { is, parseResourceUri, ResourceUri } from "@atcute/lexicons";
18
-
import { AtprotoDid, parseCanonicalResourceUri } from "@atcute/lexicons/syntax";
25
+
import {
26
+
AtprotoDid,
27
+
Handle,
28
+
parseCanonicalResourceUri,
29
+
} from "@atcute/lexicons/syntax";
19
30
20
31
export default defineBackground({
21
32
persistent: true,
···
112
123
// hijack timeline fronter message because when a write is made it is either on the timeline
113
124
// or its a reply to a depth === 0 post on a threaded view, which is the same as a timeline post
114
125
browser.tabs.sendMessage(sender.tab?.id!, {
115
-
type: "TIMELINE_FRONTER",
126
+
type: "APPLY_FRONTERS",
116
127
results: Object.fromEntries(
117
128
results.flatMap((fronter) =>
118
129
fronterGetSocialAppHrefs(fronter).map((href) => [href, fronter]),
···
120
131
),
121
132
});
122
133
};
134
+
const handleNotifications = async (
135
+
items: any,
136
+
sender: globalThis.Browser.runtime.MessageSender,
137
+
) => {
138
+
const fetchReply = async (
139
+
uri: ResourceUri,
140
+
): Promise<FronterView | undefined> => {
141
+
const cachedFronter = await frontersCache.get(uri);
142
+
const fronter =
143
+
(cachedFronter ?? null) ||
144
+
(await getFronter(uri).then((fronter) => {
145
+
if (!fronter.ok) {
146
+
frontersCache.set(uri, null);
147
+
return null;
148
+
}
149
+
return fronter.value;
150
+
}));
151
+
if (!fronter) return;
152
+
const parsedUri = await cacheFronter(uri, fronter);
153
+
return {
154
+
type: "post",
155
+
rkey: parsedUri.rkey!,
156
+
...fronter,
157
+
};
158
+
};
159
+
const handleNotif = async (
160
+
item: AppBskyNotificationListNotifications.Notification,
161
+
): Promise<FronterView | undefined> => {
162
+
let postUrl: ResourceUri | null = null;
163
+
const fronterUrl: ResourceUri = item.uri;
164
+
if (
165
+
item.reason === "subscribed-post" ||
166
+
item.reason === "quote" ||
167
+
item.reason === "reply"
168
+
)
169
+
postUrl = item.uri;
170
+
if (item.reason === "repost" || item.reason === "repost-via-repost")
171
+
postUrl = (item.record as AppBskyFeedRepost.Main).subject.uri;
172
+
if (item.reason === "like" || item.reason === "like-via-repost")
173
+
postUrl = (item.record as AppBskyFeedLike.Main).subject.uri;
174
+
if (!postUrl) return;
175
+
const cachedFronter = await frontersCache.get(fronterUrl);
176
+
let fronter =
177
+
(cachedFronter ?? null) ||
178
+
(await getFronter(fronterUrl).then((fronter) => {
179
+
if (!fronter.ok) {
180
+
frontersCache.set(fronterUrl, null);
181
+
return null;
182
+
}
183
+
return fronter.value;
184
+
}));
185
+
if (!fronter) return;
186
+
if (item.reason === "reply")
187
+
fronter.replyTo = (
188
+
item.record as AppBskyFeedPost.Main
189
+
).reply?.parent.uri;
190
+
const parsedUri = await cacheFronter(fronterUrl, fronter);
191
+
const postParsedUri = expect(parseCanonicalResourceUri(postUrl));
192
+
let handle: Handle | undefined = undefined;
193
+
try {
194
+
handle =
195
+
getAtprotoHandle(
196
+
await docResolver.resolve(postParsedUri.repo as AtprotoDid),
197
+
) ?? undefined;
198
+
} catch (err) {
199
+
console.error(`failed to get handle for ${postParsedUri.repo}:`, err);
200
+
}
201
+
return {
202
+
type: "notification",
203
+
reason: item.reason,
204
+
rkey: parsedUri.rkey!,
205
+
subject: {
206
+
did: postParsedUri.repo as AtprotoDid,
207
+
rkey: postParsedUri.rkey,
208
+
handle,
209
+
},
210
+
...fronter,
211
+
};
212
+
};
213
+
const allPromises = [];
214
+
for (const item of items.notifications ?? []) {
215
+
if (!is(AppBskyNotificationListNotifications.notificationSchema, item))
216
+
continue;
217
+
console.log("Handling notification:", item);
218
+
allPromises.push(handleNotif(item));
219
+
if (item.reason === "reply" && item.record) {
220
+
const parentUri = (item.record as AppBskyFeedPost.Main).reply?.parent
221
+
.uri;
222
+
if (parentUri) allPromises.push(fetchReply(parentUri));
223
+
}
224
+
}
225
+
const results = new Map(
226
+
(await Promise.allSettled(allPromises))
227
+
.filter((result) => result.status === "fulfilled")
228
+
.flatMap((result) => result.value ?? [])
229
+
.flatMap((fronter) =>
230
+
fronterGetSocialAppHrefs(fronter).map((href) => [href, fronter]),
231
+
),
232
+
);
233
+
if (results.size === 0) return;
234
+
browser.tabs.sendMessage(sender.tab?.id!, {
235
+
type: "APPLY_FRONTERS",
236
+
results: Object.fromEntries(results),
237
+
});
238
+
};
123
239
const handleTimeline = async (
124
240
feed: any[],
125
241
sender: globalThis.Browser.runtime.MessageSender,
···
222
338
);
223
339
if (results.size === 0) return;
224
340
browser.tabs.sendMessage(sender.tab?.id!, {
225
-
type: "TIMELINE_FRONTER",
341
+
type: "APPLY_FRONTERS",
226
342
results: Object.fromEntries(results),
227
343
});
228
344
// console.log("sent timeline fronters", results);
···
235
351
) => {
236
352
// check if this request was made for fetching replies
237
353
// if anchor is not the same as current document url, that is the case
238
-
// which means the depth of the returned posts are invalid to us, in the case of THREAD_FRONTER
239
-
// if so we will use TIMELINE_FRONTER to send it back to content script
354
+
// which means the depth of the returned posts are invalid to us
240
355
let isReplyThreadFetch = false;
241
356
const parsedDocumentUri = parseSocialAppPostUrl(documentUrl);
242
357
const anchorUri = new URL(requestUrl).searchParams.get("anchor");
···
301
416
);
302
417
if (results.size === 0) return;
303
418
browser.tabs.sendMessage(sender.tab?.id!, {
304
-
type: isReplyThreadFetch ? "TIMELINE_FRONTER" : "THREAD_FRONTER",
419
+
type: "APPLY_FRONTERS",
305
420
results: Object.fromEntries(results),
306
421
});
307
422
// console.log("sent thread fronters", results);
···
344
459
break;
345
460
case "thread":
346
461
await handleThread(message, sender);
462
+
break;
463
+
case "notifications":
464
+
await handleNotifications(JSON.parse(message.data.body), sender);
347
465
break;
348
466
}
349
467
});
+42
-4
src/entrypoints/content.ts
+42
-4
src/entrypoints/content.ts
···
118
118
type: "posts",
119
119
body,
120
120
};
121
+
} else if (
122
+
response.url.includes("/xrpc/app.bsky.notification.listNotifications")
123
+
) {
124
+
detail = {
125
+
type: "notifications",
126
+
body,
127
+
};
121
128
}
122
129
if (detail) {
123
130
sendEvent(detail);
···
191
198
);
192
199
return;
193
200
}
194
-
} else {
201
+
} else if (
202
+
fronter.type === "post" ||
203
+
fronter.type === "thread_reply" ||
204
+
fronter.type === "thread_post" ||
205
+
(fronter.type === "notification" &&
206
+
(fronter.reason === "reply" || fronter.reason === "quote"))
207
+
) {
195
208
if (fronter.type === "thread_post" && fronter.depth === 0) {
196
209
if (match && match.rkey !== fronter.rkey) return;
197
210
if (el.ariaLabel !== fronter.displayName) return;
···
238
251
}
239
252
}
240
253
}
254
+
} else if (fronter.type === "notification") {
255
+
const multiOne =
256
+
el.firstElementChild?.nextElementSibling?.nextElementSibling
257
+
?.firstElementChild?.firstElementChild?.nextElementSibling
258
+
?.nextElementSibling?.firstElementChild?.firstElementChild
259
+
?.firstElementChild ?? null;
260
+
const singleOne =
261
+
el.firstElementChild?.nextElementSibling?.nextElementSibling
262
+
?.firstElementChild?.nextElementSibling?.nextElementSibling
263
+
?.firstElementChild?.firstElementChild?.firstElementChild ?? null;
264
+
displayNameElement = multiOne ?? singleOne ?? null;
265
+
if (displayNameElement?.tagName !== "A") {
266
+
console.log(
267
+
`invalid display element tag ${displayNameElement?.tagName}, expected a:`,
268
+
displayNameElement,
269
+
);
270
+
return;
271
+
}
272
+
const profileHref = displayNameElement?.getAttribute("href");
273
+
if (profileHref) {
274
+
const actorIdentifier = profileHref.split("/").slice(2)[0];
275
+
const isUser =
276
+
fronter.handle !== actorIdentifier &&
277
+
fronter.did !== actorIdentifier;
278
+
if (isUser) displayNameElement = null;
279
+
} else displayNameElement = null;
241
280
}
242
281
if (!displayNameElement) return;
243
282
return applyFronterName(displayNameElement, fronter.members);
···
276
315
applyFronters();
277
316
});
278
317
window.addEventListener("message", (event) => {
279
-
if (!["TIMELINE_FRONTER", "THREAD_FRONTER"].includes(event.data.type))
280
-
return;
281
-
console.log(`received ${event.data.type} fronters`, event.data.results);
318
+
if (event.data.type !== "APPLY_FRONTERS") return;
319
+
console.log(`received new fronters`, event.data.results);
282
320
applyFrontersToPage(new Map(Object.entries(event.data.results)), false);
283
321
});
284
322
},
+2
-2
src/entrypoints/isolated.content.ts
+2
-2
src/entrypoints/isolated.content.ts
···
41
41
data,
42
42
});
43
43
});
44
-
const messageTypes = ["TAB_FRONTER", "THREAD_FRONTER", "TIMELINE_FRONTER"];
44
+
const bgMessageTypes = ["APPLY_FRONTERS"];
45
45
browser.runtime.onMessage.addListener((message) => {
46
-
if (!messageTypes.includes(message.type)) return;
46
+
if (!bgMessageTypes.includes(message.type)) return;
47
47
window.postMessage(message);
48
48
});
49
49
const updateOnUrlChange = async () => {
+14
-2
src/lib/utils.ts
+14
-2
src/lib/utils.ts
···
27
27
} from "@atcute/identity-resolver";
28
28
import { getAtprotoHandle, getPdsEndpoint } from "@atcute/identity";
29
29
import { PersistentCache } from "./cache";
30
+
import { AppBskyNotificationListNotifications } from "@atcute/bluesky";
30
31
31
32
export type Subject = {
32
33
handle?: Handle;
···
62
63
}
63
64
| {
64
65
type: "repost";
66
+
}
67
+
| {
68
+
type: "notification";
69
+
reason: InferOutput<AppBskyNotificationListNotifications.notificationSchema>["reason"];
65
70
}
66
71
);
67
72
export type FronterType = FronterView["type"];
···
187
192
.flatMap((p) => p.value ?? []);
188
193
};
189
194
190
-
const handleResolver = new CompositeHandleResolver({
195
+
export const handleResolver = new CompositeHandleResolver({
191
196
strategy: "race",
192
197
methods: {
193
198
dns: new DohJsonHandleResolver({
···
196
201
http: new WellKnownHandleResolver(),
197
202
},
198
203
});
199
-
const docResolver = new CompositeDidDocumentResolver({
204
+
export const docResolver = new CompositeDidDocumentResolver({
200
205
methods: {
201
206
plc: new PlcDidDocumentResolver(),
202
207
web: new WebDidDocumentResolver(),
···
395
400
return [
396
401
handle ? [`${fronterGetSocialAppHref(handle, subject.rkey)}#repost`] : [],
397
402
`${fronterGetSocialAppHref(subject.did, subject.rkey)}#repost`,
403
+
].flat();
404
+
} else if (view.type === "notification" && view.subject) {
405
+
const subject = view.subject;
406
+
const handle = subject?.handle;
407
+
return [
408
+
handle ? [`${fronterGetSocialAppHref(handle, subject.rkey)}`] : [],
409
+
`${fronterGetSocialAppHref(subject.did, subject.rkey)}`,
398
410
].flat();
399
411
}
400
412
const depth = view.type === "thread_post" ? view.depth : undefined;