view who was fronting when a record was made

feat: apply fronters to most notifications

ptr.pet ae26612b 8f4ed1bd

verified
Changed files
+183 -15
src
+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
··· 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
··· 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
··· 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;