view who was fronting when a record was made

feat: show fronters on reposted by

ptr.pet 7073cc29 c81d81d5

verified
+1
env.d.ts
··· 1 1 /// <reference types="@atcute/atproto" /> 2 + /// <reference types="@atcute/bluesky" />
+1
package.json
··· 25 25 }, 26 26 "dependencies": { 27 27 "@atcute/atproto": "^3.1.1", 28 + "@atcute/bluesky": "^3.2.2", 28 29 "@atcute/client": "^4.0.3", 29 30 "@atcute/identity": "^1.0.3", 30 31 "@atcute/identity-resolver": "^1.1.3",
+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
··· 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
··· 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
··· 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
··· 245 245 </div> 246 246 <div class="config-note"> 247 247 <span class="note-text"> 248 - when set, pulls fronters from PluralKit (fronter 249 - history must be public) 248 + when set, pulls fronters from PluralKit (fronters 249 + must be public) 250 250 </span> 251 251 </div> 252 252 </div>
+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