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 /// <reference types="@atcute/atproto" />
··· 1 /// <reference types="@atcute/atproto" /> 2 + /// <reference types="@atcute/bluesky" />
+1
package.json
··· 25 }, 26 "dependencies": { 27 "@atcute/atproto": "^3.1.1", 28 "@atcute/client": "^4.0.3", 29 "@atcute/identity": "^1.0.3", 30 "@atcute/identity-resolver": "^1.1.3",
··· 25 }, 26 "dependencies": { 27 "@atcute/atproto": "^3.1.1", 28 + "@atcute/bluesky": "^3.2.2", 29 "@atcute/client": "^4.0.3", 30 "@atcute/identity": "^1.0.3", 31 "@atcute/identity-resolver": "^1.1.3",
+11
pnpm-lock.yaml
··· 11 '@atcute/atproto': 12 specifier: ^3.1.1 13 version: 3.1.3 14 '@atcute/client': 15 specifier: ^4.0.3 16 version: 4.0.3 ··· 66 67 '@atcute/atproto@3.1.3': 68 resolution: {integrity: sha512-+5u0l+8E7h6wZO7MM1HLXIPoUEbdwRtr28ZRTgsURp+Md9gkoBj9e5iMx/xM8F2Exfyb65J5RchW/WlF2mw/RQ==} 69 70 '@atcute/client@4.0.3': 71 resolution: {integrity: sha512-RIOZWFVLca/HiPAAUDqQPOdOreCxTbL5cb+WUf5yqQOKIu5yEAP3eksinmlLmgIrlr5qVOE7brazUUzaskFCfw==} ··· 1935 1936 '@atcute/atproto@3.1.3': 1937 dependencies: 1938 '@atcute/lexicons': 1.1.1 1939 1940 '@atcute/client@4.0.3':
··· 11 '@atcute/atproto': 12 specifier: ^3.1.1 13 version: 3.1.3 14 + '@atcute/bluesky': 15 + specifier: ^3.2.2 16 + version: 3.2.2 17 '@atcute/client': 18 specifier: ^4.0.3 19 version: 4.0.3 ··· 69 70 '@atcute/atproto@3.1.3': 71 resolution: {integrity: sha512-+5u0l+8E7h6wZO7MM1HLXIPoUEbdwRtr28ZRTgsURp+Md9gkoBj9e5iMx/xM8F2Exfyb65J5RchW/WlF2mw/RQ==} 72 + 73 + '@atcute/bluesky@3.2.2': 74 + resolution: {integrity: sha512-L8RrMNeRLGvSHMq2KDIAGXrpuNGA87YOXpXHY1yhmovVCjQ5n55FrR6JoQaxhprdXdKKQiefxNwQQQybDrfgFQ==} 75 76 '@atcute/client@4.0.3': 77 resolution: {integrity: sha512-RIOZWFVLca/HiPAAUDqQPOdOreCxTbL5cb+WUf5yqQOKIu5yEAP3eksinmlLmgIrlr5qVOE7brazUUzaskFCfw==} ··· 1941 1942 '@atcute/atproto@3.1.3': 1943 dependencies: 1944 + '@atcute/lexicons': 1.1.1 1945 + 1946 + '@atcute/bluesky@3.2.2': 1947 + dependencies: 1948 + '@atcute/atproto': 3.1.3 1949 '@atcute/lexicons': 1.1.1 1950 1951 '@atcute/client@4.0.3':
+109 -64
src/entrypoints/background.ts
··· 3 import { 4 type Fronter, 5 fronterGetSocialAppHrefs, 6 - fronterGetSocialAppHref, 7 getFronter, 8 getSpFronters, 9 - memberUriString, 10 putFronter, 11 frontersCache, 12 parseSocialAppPostUrl, 13 displayNameCache, 14 deleteFronter, 15 getPkFronters, 16 } from "@/lib/utils"; 17 import { 18 - parseCanonicalResourceUri, 19 parseResourceUri, 20 ResourceUri, 21 } from "@atcute/lexicons"; 22 23 export default defineBackground({ 24 persistent: true, ··· 92 } 93 // dont write if no names is specified or no sp/pk fronters are fetched 94 if (members.length === 0) return; 95 - const results = []; 96 for (const result of items) { 97 const resp = await putFronter(result.uri, members, authToken); 98 if (resp.ok) { 99 const parsedUri = await cacheFronter(result.uri, resp.value); 100 results.push({ 101 rkey: parsedUri.rkey!, 102 ...resp.value, 103 }); ··· 112 type: "TIMELINE_FRONTER", 113 results: Object.fromEntries( 114 results.flatMap((fronter) => 115 - fronterGetSocialAppHrefs(fronter, fronter.rkey).map((href) => [ 116 - href, 117 - fronter, 118 - ]), 119 ), 120 ), 121 }); ··· 124 feed: any[], 125 sender: globalThis.Browser.runtime.MessageSender, 126 ) => { 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, 145 }; 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 - }); 158 const results = new Map( 159 (await Promise.allSettled(allPromises)) 160 .filter((result) => result.status === "fulfilled") 161 .flatMap((result) => result.value ?? []) 162 .flatMap((fronter) => 163 - fronterGetSocialAppHrefs(fronter, fronter.rkey).map((href) => [ 164 - href, 165 - fronter, 166 - ]), 167 ), 168 ); 169 if (results.size === 0) return; ··· 210 } 211 return fronter.value; 212 }); 213 - return promise.then(async (fronter): Promise<any> => { 214 - if (!fronter) return; 215 - const parsedUri = await cacheFronter(item.uri, fronter); 216 - if (isReplyThreadFetch) 217 return { 218 rkey: parsedUri.rkey!, 219 ...fronter, 220 }; 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 - }); 234 }); 235 }); 236 const results = new Map( ··· 238 .filter((result) => result.status === "fulfilled") 239 .flatMap((result) => result.value ?? []) 240 .flatMap((fronter) => 241 - fronterGetSocialAppHrefs(fronter, fronter.rkey, fronter.depth).map( 242 - (href) => [href, fronter], 243 - ), 244 ), 245 ); 246 if (results.size === 0) return; ··· 264 break; 265 case "write": 266 await handleWrite( 267 - JSON.parse(message.data.body).results, 268 message.data.authToken, 269 sender, 270 ); 271 break; 272 - case "writeOne": 273 await handleWrite( 274 [JSON.parse(message.data.body)], 275 message.data.authToken, 276 sender, 277 ); 278 break; 279 case "posts": 280 await handleTimeline( 281 (JSON.parse(message.data.body) as any[]).map((post) => ({ post })),
··· 3 import { 4 type Fronter, 5 fronterGetSocialAppHrefs, 6 getFronter, 7 getSpFronters, 8 putFronter, 9 frontersCache, 10 parseSocialAppPostUrl, 11 displayNameCache, 12 deleteFronter, 13 getPkFronters, 14 + FronterType, 15 + FronterView, 16 } from "@/lib/utils"; 17 import { 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, 26 parseResourceUri, 27 ResourceUri, 28 + safeParse, 29 } from "@atcute/lexicons"; 30 + import { AtprotoDid, parseCanonicalResourceUri } from "@atcute/lexicons/syntax"; 31 32 export default defineBackground({ 33 persistent: true, ··· 101 } 102 // dont write if no names is specified or no sp/pk fronters are fetched 103 if (members.length === 0) return; 104 + const results: FronterView[] = []; 105 for (const result of items) { 106 const resp = await putFronter(result.uri, members, authToken); 107 if (resp.ok) { 108 const parsedUri = await cacheFronter(result.uri, resp.value); 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", 116 rkey: parsedUri.rkey!, 117 ...resp.value, 118 }); ··· 127 type: "TIMELINE_FRONTER", 128 results: Object.fromEntries( 129 results.flatMap((fronter) => 130 + fronterGetSocialAppHrefs(fronter).map((href) => [href, fronter]), 131 ), 132 ), 133 }); ··· 136 feed: any[], 137 sender: globalThis.Browser.runtime.MessageSender, 138 ) => { 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 + ); 184 }; 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 + ); 203 const results = new Map( 204 (await Promise.allSettled(allPromises)) 205 .filter((result) => result.status === "fulfilled") 206 .flatMap((result) => result.value ?? []) 207 .flatMap((fronter) => 208 + fronterGetSocialAppHrefs(fronter).map((href) => [href, fronter]), 209 ), 210 ); 211 if (results.size === 0) return; ··· 252 } 253 return fronter.value; 254 }); 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); 271 return { 272 + type: "thread_post", 273 rkey: parsedUri.rkey!, 274 + displayName, 275 + depth: item.depth, 276 ...fronter, 277 }; 278 + }, 279 + ); 280 }); 281 }); 282 const results = new Map( ··· 284 .filter((result) => result.status === "fulfilled") 285 .flatMap((result) => result.value ?? []) 286 .flatMap((fronter) => 287 + fronterGetSocialAppHrefs(fronter).map((href) => [href, fronter]), 288 ), 289 ); 290 if (results.size === 0) return; ··· 308 break; 309 case "write": 310 await handleWrite( 311 + JSON.parse(message.data.body), 312 message.data.authToken, 313 sender, 314 ); 315 break; 316 + case "writeOne": { 317 await handleWrite( 318 [JSON.parse(message.data.body)], 319 message.data.authToken, 320 sender, 321 ); 322 break; 323 + } 324 case "posts": 325 await handleTimeline( 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"; 10 11 const getAuthHeader = (headers: any): string | null => { 12 if (headers instanceof Headers) { ··· 146 }); 147 respEventSetup.then((name) => (respEventName = name)); 148 149 - const applyFronterName = (el: Element, fronters: Fronter["members"]) => { 150 - if (el.hasAttribute("data-fronter")) return; 151 const s = fronters.map((f) => f.name).join(", "); 152 el.textContent += ` [f: ${s}]`; 153 el.setAttribute("data-fronter", s); 154 }; 155 const applyFrontersToPage = ( 156 - fronters: Map<string, any>, 157 pageChange: boolean, 158 ) => { 159 // console.log("applyFrontersToPage", fronters); ··· 164 ); 165 for (const el of document.querySelectorAll("[data-fronter]")) { 166 const previousFronter = el.getAttribute("data-fronter")!; 167 - // remove fronter text 168 - el.textContent = el.textContent.replace( 169 - ` [f: ${previousFronter}]`, 170 - "", 171 - ); 172 el.removeAttribute("data-fronter"); 173 } 174 } 175 console.log("applyFrontersToPage", match, fronters); 176 if (fronters.size === 0) return; 177 for (const el of document.getElementsByTagName("a")) { 178 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); 192 } 193 }; 194 let postTabObserver: MutationObserver | null = null;
··· 1 + import { FronterView, parseSocialAppPostUrl } from "@/lib/utils"; 2 3 const getAuthHeader = (headers: any): string | null => { 4 if (headers instanceof Headers) { ··· 138 }); 139 respEventSetup.then((name) => (respEventName = name)); 140 141 + const applyFronterName = ( 142 + el: Element, 143 + fronters: FronterView["members"], 144 + ) => { 145 + if (el.hasAttribute("data-fronter")) return false; 146 const s = fronters.map((f) => f.name).join(", "); 147 el.textContent += ` [f: ${s}]`; 148 el.setAttribute("data-fronter", s); 149 + return true; 150 }; 151 const applyFrontersToPage = ( 152 + fronters: Map<string, FronterView | null>, 153 pageChange: boolean, 154 ) => { 155 // console.log("applyFrontersToPage", fronters); ··· 160 ); 161 for (const el of document.querySelectorAll("[data-fronter]")) { 162 const previousFronter = el.getAttribute("data-fronter")!; 163 + if (previousFronter !== "__set__") { 164 + // remove fronter text 165 + el.textContent = el.textContent.replace( 166 + ` [f: ${previousFronter}]`, 167 + "", 168 + ); 169 + } 170 el.removeAttribute("data-fronter"); 171 } 172 } 173 console.log("applyFrontersToPage", match, fronters); 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 + }; 224 for (const el of document.getElementsByTagName("a")) { 225 + if (el.getAttribute("data-fronter")) continue; 226 const path = `/${el.href.split("/").slice(3).join("/")}`; 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 + } 234 } 235 }; 236 let postTabObserver: MutationObserver | null = null;
+39 -18
src/entrypoints/isolated.content.ts
··· 3 import { 4 displayNameCache, 5 Fronter, 6 fronterGetSocialAppHrefs, 7 frontersCache, 8 parseSocialAppPostUrl, 9 } from "@/lib/utils"; 10 import { parseResourceUri, ResourceUri } from "@atcute/lexicons"; ··· 46 }); 47 const updateOnUrlChange = async () => { 48 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 - ); 60 // add entry for current page 61 const match = parseSocialAppPostUrl(document.location.href); 62 if (match && !updated.has(`/profile/${match.actorIdentifier}`)) { 63 const maybeFronter = updated.get( 64 `/profile/${match.actorIdentifier}/post/${match.rkey}`, 65 ); 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 - }); 73 } 74 window.postMessage({ 75 type: "APPLY_CACHED_FRONTERS",
··· 3 import { 4 displayNameCache, 5 Fronter, 6 + fronterGetSocialAppHref, 7 fronterGetSocialAppHrefs, 8 frontersCache, 9 + FronterView, 10 parseSocialAppPostUrl, 11 } from "@/lib/utils"; 12 import { parseResourceUri, ResourceUri } from "@atcute/lexicons"; ··· 48 }); 49 const updateOnUrlChange = async () => { 50 const fronters = await frontersCache.getAll(); 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 + } 72 // add entry for current page 73 const match = parseSocialAppPostUrl(document.location.href); 74 if (match && !updated.has(`/profile/${match.actorIdentifier}`)) { 75 const maybeFronter = updated.get( 76 `/profile/${match.actorIdentifier}/post/${match.rkey}`, 77 ); 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 + } 94 } 95 window.postMessage({ 96 type: "APPLY_CACHED_FRONTERS",
+2 -2
src/entrypoints/popup/App.svelte
··· 245 </div> 246 <div class="config-note"> 247 <span class="note-text"> 248 - when set, pulls fronters from PluralKit (fronter 249 - history must be public) 250 </span> 251 </div> 252 </div>
··· 245 </div> 246 <div class="config-note"> 247 <span class="note-text"> 248 + when set, pulls fronters from PluralKit (fronters 249 + must be public) 250 </span> 251 </div> 252 </div>
+40 -9
src/lib/utils.ts
··· 28 import { getAtprotoHandle, getPdsEndpoint } from "@atcute/identity"; 29 import { PersistentCache } from "./cache"; 30 31 export type Fronter = { 32 members: { 33 uri?: MemberUri; ··· 35 }[]; 36 handle: Handle | null; 37 did: AtprotoDid; 38 }; 39 40 export const fronterSchema = v.record( 41 v.string(), ··· 359 })); 360 }; 361 362 - export const fronterGetSocialAppHrefs = ( 363 - fronter: Fronter, 364 - rkey: RecordKey, 365 - depth?: number, 366 - ) => { 367 return [ 368 - fronter.handle 369 - ? [fronterGetSocialAppHref(fronter.handle, rkey, depth)] 370 - : [], 371 - fronterGetSocialAppHref(fronter.did, rkey, depth), 372 ].flat(); 373 }; 374
··· 28 import { getAtprotoHandle, getPdsEndpoint } from "@atcute/identity"; 29 import { PersistentCache } from "./cache"; 30 31 + export type Subject = { 32 + handle?: Handle; 33 + did: AtprotoDid; 34 + rkey: RecordKey; 35 + }; 36 + 37 export type Fronter = { 38 members: { 39 uri?: MemberUri; ··· 41 }[]; 42 handle: Handle | null; 43 did: AtprotoDid; 44 + subject?: Subject; 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"]; 67 68 export const fronterSchema = v.record( 69 v.string(), ··· 387 })); 388 }; 389 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; 400 return [ 401 + view.handle ? [fronterGetSocialAppHref(view.handle, view.rkey, depth)] : [], 402 + fronterGetSocialAppHref(view.did, view.rkey, depth), 403 ].flat(); 404 }; 405