view who was fronting when a record was made

feat: add pk fronters, improve settings

ptr.pet 2da508cc b1eaddbe

verified
Changed files
+509 -83
src
+369
src/components/FronterList.svelte
··· 1 + <script lang="ts"> 2 + import { fetchMember, type MemberUri } from "@/lib/utils"; 3 + 4 + interface Props { 5 + fronters: string[]; 6 + onUpdate: (fronters: string[]) => void; 7 + label?: string; 8 + placeholder?: string; 9 + note?: string; 10 + fetchNames?: boolean; // If true, treat as PK member IDs and fetch names 11 + } 12 + 13 + let { 14 + fronters = $bindable([]), 15 + onUpdate, 16 + label = "FRONTERS", 17 + placeholder = "enter_identifier", 18 + note = "list of identifiers", 19 + fetchNames = false, 20 + }: Props = $props(); 21 + 22 + let inputValue = $state(""); 23 + let inputElement: HTMLInputElement; 24 + let memberNames = $state<Map<string, string | null>>(new Map()); 25 + let memberErrors = $state<Map<string, string>>(new Map()); 26 + 27 + const fetchMemberName = async (memberId: string) => { 28 + try { 29 + const memberUri: MemberUri = { type: "pk", memberId }; 30 + const name = await fetchMember(memberUri); 31 + if (name) { 32 + memberNames.set(memberId, name); 33 + memberErrors.delete(memberId); 34 + } else { 35 + memberNames.set(memberId, null); 36 + memberErrors.set(memberId, "Member not found"); 37 + } 38 + } catch (error) { 39 + memberNames.set(memberId, null); 40 + memberErrors.set(memberId, `Error: ${error}`); 41 + } 42 + // Trigger reactivity 43 + memberNames = new Map(memberNames); 44 + memberErrors = new Map(memberErrors); 45 + }; 46 + 47 + const addFronter = (name: string) => { 48 + const trimmedName = name.trim(); 49 + if (!trimmedName || fronters.includes(trimmedName)) return; 50 + 51 + const updatedFronters = [...fronters, trimmedName]; 52 + fronters = updatedFronters; 53 + onUpdate(updatedFronters); 54 + inputValue = ""; 55 + 56 + // Fetch the member name if this is a PK fronter 57 + if (fetchNames) { 58 + fetchMemberName(trimmedName); 59 + } 60 + }; 61 + 62 + const removeFronter = (index: number) => { 63 + const identifier = fronters[index]; 64 + const updatedFronters = fronters.filter((_, i) => i !== index); 65 + fronters = updatedFronters; 66 + onUpdate(updatedFronters); 67 + 68 + // Clean up the member name cache if this is a PK fronter 69 + if (fetchNames) { 70 + memberNames.delete(identifier); 71 + memberErrors.delete(identifier); 72 + memberNames = new Map(memberNames); 73 + memberErrors = new Map(memberErrors); 74 + } 75 + 76 + inputElement?.focus(); 77 + }; 78 + 79 + const handleKeyPress = (event: KeyboardEvent) => { 80 + if (event.key === "Enter" || event.key === "," || event.key === " ") { 81 + event.preventDefault(); 82 + addFronter(inputValue); 83 + } else if ( 84 + event.key === "Backspace" && 85 + inputValue === "" && 86 + fronters.length > 0 87 + ) { 88 + // Remove last tag when backspacing on empty input 89 + removeFronter(fronters.length - 1); 90 + } 91 + }; 92 + 93 + const handleInput = (event: Event) => { 94 + const target = event.target as HTMLInputElement; 95 + const value = target.value; 96 + 97 + // Check for comma or space at the end 98 + if (value.endsWith(",") || value.endsWith(" ")) { 99 + addFronter(value.slice(0, -1)); 100 + } else { 101 + inputValue = value; 102 + } 103 + }; 104 + 105 + const focusInput = () => { 106 + inputElement?.focus(); 107 + }; 108 + 109 + // Load existing member names on mount (only for PK fronters) 110 + $effect(() => { 111 + if (fetchNames) { 112 + fronters.forEach((identifier) => { 113 + if ( 114 + !memberNames.has(identifier) && 115 + !memberErrors.has(identifier) 116 + ) { 117 + fetchMemberName(identifier); 118 + } 119 + }); 120 + } 121 + }); 122 + 123 + // Helper function to get display text for a fronter 124 + const getDisplayText = (identifier: string) => { 125 + if (!fetchNames) return identifier; 126 + return memberNames.get(identifier) || identifier; 127 + }; 128 + 129 + // Helper function to check if we should show error/loading state 130 + const getStatusInfo = (identifier: string) => { 131 + if (!fetchNames) return null; 132 + 133 + if (memberErrors.has(identifier)) { 134 + return { type: "error", text: memberErrors.get(identifier) }; 135 + } 136 + if (memberNames.get(identifier) === undefined) { 137 + return { type: "loading", text: "loading..." }; 138 + } 139 + return null; 140 + }; 141 + </script> 142 + 143 + <div class="config-card"> 144 + <div class="config-row"> 145 + <span class="config-label">{label}</span> 146 + <div 147 + class="tag-input-container" 148 + onclick={focusInput} 149 + onkeydown={(e) => e.key === "Enter" && focusInput()} 150 + role="textbox" 151 + tabindex="0" 152 + > 153 + <div class="tag-input-wrapper"> 154 + {#each fronters as identifier, index} 155 + <div class="fronter-tag"> 156 + <div class="tag-content"> 157 + <span class="tag-text"> 158 + {getDisplayText(identifier)} 159 + </span> 160 + {#if getStatusInfo(identifier)} 161 + {@const status = getStatusInfo(identifier)} 162 + {#if status} 163 + <span class="tag-{status.type}" 164 + >{status.text}</span 165 + > 166 + {/if} 167 + {/if} 168 + </div> 169 + <button 170 + onclick={() => removeFronter(index)} 171 + class="tag-remove" 172 + title="Remove fronter" 173 + > 174 + × 175 + </button> 176 + </div> 177 + {/each} 178 + <input 179 + bind:this={inputElement} 180 + type="text" 181 + placeholder={fronters.length === 0 ? placeholder : ""} 182 + value={inputValue} 183 + oninput={handleInput} 184 + onkeydown={handleKeyPress} 185 + class="tag-input" 186 + /> 187 + </div> 188 + </div> 189 + </div> 190 + 191 + <div class="config-note"> 192 + <span class="note-text">{note}</span> 193 + </div> 194 + </div> 195 + 196 + <style> 197 + .config-card { 198 + background: #0d0d0d; 199 + border: 1px solid #2a2a2a; 200 + border-left: 3px solid #444444; 201 + padding: 10px; 202 + display: flex; 203 + flex-direction: column; 204 + gap: 6px; 205 + transition: border-left-color 0.2s ease; 206 + } 207 + 208 + .config-card:hover { 209 + border-left-color: #555555; 210 + } 211 + 212 + .config-row { 213 + display: flex; 214 + align-items: center; 215 + gap: 12px; 216 + margin-bottom: 0; 217 + } 218 + 219 + .config-label { 220 + font-size: 12px; 221 + color: #cccccc; 222 + letter-spacing: 1px; 223 + font-weight: 700; 224 + white-space: nowrap; 225 + min-width: 90px; 226 + } 227 + 228 + .tag-input-container { 229 + flex: 1; 230 + background: #181818; 231 + border: 1px solid #333333; 232 + transition: border-color 0.2s ease; 233 + cursor: text; 234 + min-height: 42px; 235 + display: flex; 236 + align-items: center; 237 + } 238 + 239 + .tag-input-container:focus-within { 240 + border-color: #666666; 241 + } 242 + 243 + .tag-input-container:focus-within:has(.fronter-tag) { 244 + border-bottom-color: #00ff41; 245 + } 246 + 247 + .tag-input-wrapper { 248 + display: flex; 249 + flex-wrap: wrap; 250 + align-items: center; 251 + gap: 6px; 252 + padding: 8px 12px; 253 + width: 100%; 254 + min-height: 26px; 255 + } 256 + 257 + .fronter-tag { 258 + display: flex; 259 + align-items: center; 260 + background: #2a2a2a; 261 + border: 1px solid #444444; 262 + border-radius: 3px; 263 + padding: 4px 6px; 264 + gap: 6px; 265 + font-size: 11px; 266 + color: #ffffff; 267 + font-weight: 600; 268 + line-height: 1; 269 + transition: all 0.15s ease; 270 + animation: tagAppear 0.2s ease-out; 271 + } 272 + 273 + .fronter-tag:hover { 274 + background: #333333; 275 + border-color: #555555; 276 + } 277 + 278 + .tag-content { 279 + display: flex; 280 + flex-direction: column; 281 + gap: 2px; 282 + } 283 + 284 + .tag-text { 285 + white-space: nowrap; 286 + letter-spacing: 0.5px; 287 + } 288 + 289 + .tag-error { 290 + font-size: 9px; 291 + color: #ff6666; 292 + font-weight: 500; 293 + letter-spacing: 0.3px; 294 + } 295 + 296 + .tag-loading { 297 + font-size: 9px; 298 + color: #888888; 299 + font-weight: 500; 300 + letter-spacing: 0.3px; 301 + font-style: italic; 302 + } 303 + 304 + .tag-remove { 305 + background: none; 306 + border: none; 307 + color: #888888; 308 + font-size: 14px; 309 + font-weight: 700; 310 + cursor: pointer; 311 + padding: 0; 312 + line-height: 1; 313 + transition: color 0.15s ease; 314 + display: flex; 315 + align-items: center; 316 + justify-content: center; 317 + width: 14px; 318 + height: 14px; 319 + margin-left: 2px; 320 + } 321 + 322 + .tag-remove:hover { 323 + color: #ff4444; 324 + } 325 + 326 + .tag-input { 327 + background: transparent; 328 + border: none; 329 + outline: none; 330 + color: #ffffff; 331 + font-family: inherit; 332 + font-size: 12px; 333 + font-weight: 500; 334 + flex: 1; 335 + min-width: 120px; 336 + height: 26px; 337 + } 338 + 339 + .tag-input::placeholder { 340 + color: #777777; 341 + font-size: 12px; 342 + } 343 + 344 + .config-note { 345 + padding: 0; 346 + background: transparent; 347 + border: none; 348 + margin: 0; 349 + } 350 + 351 + .note-text { 352 + font-size: 11px; 353 + color: #bbbbbb; 354 + line-height: 1.3; 355 + font-weight: 500; 356 + letter-spacing: 0.5px; 357 + } 358 + 359 + @keyframes tagAppear { 360 + 0% { 361 + opacity: 0; 362 + transform: scale(0.8); 363 + } 364 + 100% { 365 + opacity: 1; 366 + transform: scale(1); 367 + } 368 + } 369 + </style>
+12 -11
src/entrypoints/background.ts
··· 55 55 authToken: string | null, 56 56 sender: globalThis.Browser.runtime.MessageSender, 57 57 ) => { 58 - const fronterName = await storage.getItem<string>("sync:fronter"); 59 - const spFronters = (await getSpFronters()).map((m) => memberUriString(m)); 60 58 if (!authToken) return; 61 - const fronter = { 62 - names: fronterName?.split(",").map((name) => name.trim()) ?? [], 63 - members: spFronters, 64 - }; 59 + const frontersArray = await storage.getItem<string[]>("sync:fronters"); 60 + let members: Parameters<typeof putFronter>["1"] = frontersArray ?? []; 61 + if (members.length === 0) { 62 + const pkFronters = await storage.getItem<string[]>("sync:pk-fronter"); 63 + if (pkFronters) { 64 + members = pkFronters.map((id) => ({ type: "pk", memberId: id })); 65 + } else { 66 + members = await getSpFronters(); 67 + } 68 + } 65 69 // dont write if no names is specified or no sp/pk fronters are fetched 66 - if (fronter.names.length === 0 && fronter.members.length === 0) return; 70 + if (members.length === 0) return; 67 71 const results = []; 68 72 for (const result of items) { 69 - const resp = await putFronter( 70 - { subject: result.uri, ...fronter }, 71 - authToken, 72 - ); 73 + const resp = await putFronter(result.uri, members, authToken); 73 74 if (resp.ok) { 74 75 const parsedUri = cacheFronter(result.uri, resp.value); 75 76 results.push({
+4 -4
src/entrypoints/content.ts
··· 111 111 }); 112 112 respEventSetup.then((name) => (respEventName = name)); 113 113 114 - const applyFronterName = (el: Element, fronterNames: string[]) => { 114 + const applyFronterName = (el: Element, fronters: Fronter["members"]) => { 115 115 if (el.getAttribute("data-fronter")) return; 116 - const s = fronterNames.join(", "); 116 + const s = fronters.map((f) => f.name).join(", "); 117 117 el.textContent += ` [f: ${s}]`; 118 118 el.setAttribute("data-fronter", s); 119 119 }; ··· 123 123 for (const el of document.getElementsByTagName("a")) { 124 124 const path = `/${el.href.split("/").slice(3).join("/")}`; 125 125 const fronter = fronters.get(path); 126 - if (!fronter) continue; 126 + if (!fronter || fronter.members.length === 0) continue; 127 127 const isFocusedPost = fronter.depth === 0; 128 128 if (isFocusedPost && match && match.rkey !== fronter.rkey) continue; 129 129 if (isFocusedPost && el.ariaLabel !== fronter.displayName) continue; ··· 134 134 ?.firstElementChild?.firstElementChild ?? null); 135 135 if (!displayNameElement) continue; 136 136 // console.log(path, fronter, displayNameElement); 137 - applyFronterName(displayNameElement, fronter.names); 137 + applyFronterName(displayNameElement, fronter.members); 138 138 } 139 139 }; 140 140 let postTabObserver: MutationObserver | null = null;
+52 -30
src/entrypoints/popup/App.svelte
··· 1 1 <script lang="ts"> 2 2 import { expect } from "@/lib/result"; 3 - import { getFronter } from "@/lib/utils"; 3 + import { getFronter, getMemberPublicUri } from "@/lib/utils"; 4 4 import { isResourceUri } from "@atcute/lexicons"; 5 5 import type { ResourceUri } from "@atcute/lexicons/syntax"; 6 + import FronterList from "@/components/FronterList.svelte"; 6 7 7 8 let recordAtUri = $state(""); 8 9 let queryResult = $state(""); 9 10 let isQuerying = $state(false); 10 - let fronterName = $state(""); 11 + let fronters = $state<string[]>([]); 12 + let pkFronters = $state<string[]>([]); 11 13 let spToken = $state(""); 12 14 let isFromCurrentTab = $state(false); 13 15 14 - const makeOutput = (fronter: any) => { 15 - return `HANDLE: ${fronter.handle ?? "handle.invalid"}<br>FRONTER(S): ${fronter.names.join(", ")}`; 16 + const makeOutput = (record: any) => { 17 + const fronters = record.members 18 + .map((f: any) => { 19 + if (!f.uri) return f.name; 20 + const publicUri = getMemberPublicUri(f.uri); 21 + if (!publicUri) return f.name; 22 + return `<a href="${publicUri}">${f.name}</a>`; 23 + }) 24 + .join(", "); 25 + return [ 26 + `HANDLE: ${record.handle ?? `handle.invalid (${record.did})`}`, 27 + `FRONTER(S): ${fronters}`, 28 + ].join("<br>"); 16 29 }; 17 30 18 31 const queryRecord = async (recordUri: ResourceUri) => { ··· 32 45 } 33 46 }; 34 47 35 - const updateFronter = (event: any) => { 36 - fronterName = (event.target as HTMLInputElement).value; 37 - storage.setItem("sync:fronter", fronterName); 48 + const updateFronters = (newFronters: string[]) => { 49 + fronters = newFronters; 50 + storage.setItem("sync:fronters", newFronters); 51 + }; 52 + 53 + const updatePkFronters = (newPkFronters: string[]) => { 54 + pkFronters = newPkFronters; 55 + storage.setItem("sync:pk-fronter", newPkFronters); 38 56 }; 39 57 40 58 const updateSpToken = (event: any) => { ··· 55 73 }; 56 74 57 75 onMount(async () => { 58 - const fronter = await storage.getItem<string>("sync:fronter"); 59 - if (fronter) { 60 - fronterName = fronter; 76 + const frontersArray = await storage.getItem<string[]>("sync:fronters"); 77 + if (frontersArray && Array.isArray(frontersArray)) { 78 + fronters = frontersArray; 79 + } 80 + 81 + const pkFrontersArray = 82 + await storage.getItem<string[]>("sync:pk-fronter"); 83 + if (pkFrontersArray && Array.isArray(pkFrontersArray)) { 84 + pkFronters = pkFrontersArray; 61 85 } 62 86 63 87 const token = await storage.getItem<string>("sync:sp_token"); ··· 172 196 </div> 173 197 <div class="config-card"> 174 198 <div class="config-row"> 175 - <span class="config-label">SP_TOKEN</span> 199 + <span class="config-label">SP TOKEN</span> 176 200 <input 177 201 type="password" 178 202 placeholder="enter_simply_plural_token" ··· 184 208 </div> 185 209 <div class="config-note"> 186 210 <span class="note-text"> 187 - token requires only read permissions 188 - </span> 189 - </div> 190 - </div> 191 - <div class="config-card"> 192 - <div class="config-row"> 193 - <span class="config-label">FRONTER_NAME</span> 194 - <input 195 - type="text" 196 - placeholder="enter_identifier" 197 - oninput={updateFronter} 198 - bind:value={fronterName} 199 - class="config-input" 200 - class:has-value={fronterName} 201 - /> 202 - </div> 203 - <div class="config-note"> 204 - <span class="note-text"> 205 - overrides Simply Plural fronters when set 211 + when set, pulls fronters from Simply Plural (token 212 + only requires read permissions) 206 213 </span> 207 214 </div> 208 215 </div> 216 + <FronterList 217 + bind:fronters={pkFronters} 218 + onUpdate={updatePkFronters} 219 + label="PK FRONTERS" 220 + placeholder="enter_member_ids" 221 + note="PluralKit member IDs, overrides SP fronters" 222 + fetchNames={true} 223 + /> 224 + <FronterList 225 + bind:fronters 226 + onUpdate={updateFronters} 227 + label="FRONTERS" 228 + placeholder="enter_fronter_names" 229 + note="just names, overrides SP & PK fronters" 230 + /> 209 231 </section> 210 232 </div> 211 233
+72 -38
src/lib/utils.ts
··· 27 27 import { getAtprotoHandle, getPdsEndpoint } from "@atcute/identity"; 28 28 29 29 export type Fronter = { 30 - memberUris: MemberUri[]; 31 - names: string[]; 30 + members: { 31 + uri?: MemberUri; 32 + name: string; 33 + }[]; 32 34 handle: Handle | null; 33 35 did: AtprotoDid; 34 36 }; 35 37 36 - const fronterSchema = v.record( 38 + export const fronterSchema = v.record( 37 39 v.string(), 38 40 v.object({ 39 41 $type: v.literal("systems.gaze.atfronter.fronter"), 40 42 subject: v.resourceUriString(), 41 - names: v.array(v.string()), 42 - members: v.array(v.genericUriString()), // identifier(s) for pk or sp or etc. (maybe member record in the future?) 43 + members: v.array( 44 + v.object({ 45 + name: v.string(), 46 + uri: v.optional(v.genericUriString()), // identifier(s) for pk or sp or etc. (maybe member record in the future?) 47 + }), 48 + ), 43 49 }), 44 50 ); 51 + export type FronterSchema = InferOutput<typeof fronterSchema>; 45 52 46 - type MemberUri = 53 + export type MemberUri = 47 54 | { type: "at"; recordUri: ResourceUri } 48 - | { type: "pk"; systemId: string; memberId: string } 55 + | { type: "pk"; memberId: string } 49 56 | { type: "sp"; systemId: string; memberId: string }; 50 57 51 58 export const parseMemberId = (memberId: GenericUri): MemberUri => { ··· 53 60 switch (uri.protocol) { 54 61 case "pk:": { 55 62 const split = uri.pathname.split("/").slice(1); 56 - return { type: "pk", systemId: split[0], memberId: split[1] }; 63 + return { type: "pk", memberId: split[0] }; 57 64 } 58 65 case "sp:": { 59 66 const split = uri.pathname.split("/").slice(1); ··· 70 77 export const memberUriString = (memberUri: MemberUri): GenericUri => { 71 78 switch (memberUri.type) { 72 79 case "pk": { 73 - return `pk://api.pluralkit.com/${memberUri.systemId}/${memberUri.memberId}`; 80 + return `pk://api.pluralkit.me/${memberUri.memberId}`; 74 81 } 75 82 case "sp": { 76 83 return `sp://api.apparyllis.com/${memberUri.systemId}/${memberUri.memberId}`; ··· 80 87 } 81 88 } 82 89 }; 90 + export const getMemberPublicUri = (memberUri: MemberUri) => { 91 + switch (memberUri.type) { 92 + case "pk": { 93 + return `https://dash.pluralkit.me/profile/m/${memberUri.memberId}`; 94 + } 95 + case "sp": { 96 + return null; 97 + } 98 + case "at": { 99 + return `https://pdsls.dev/${memberUri.recordUri}`; 100 + } 101 + } 102 + }; 83 103 84 104 let memberCache = new Map<string, any>(); 85 105 export const fetchMember = async ( 86 106 memberUri: MemberUri, 87 107 ): Promise<string | undefined> => { 108 + const s = memberUriString(memberUri); 109 + const cached = memberCache.get(s); 88 110 switch (memberUri.type) { 89 111 case "sp": { 90 - const s = memberUriString(memberUri); 91 - const cached = memberCache.get(s); 92 112 if (cached) return cached.content.name; 93 113 const token = await storage.getItem<string>("sync:sp_token"); 94 114 if (!token) return; ··· 105 125 memberCache.set(s, member); 106 126 return member.content.name; 107 127 } 128 + case "pk": { 129 + if (cached) return cached.name; 130 + const resp = await fetch( 131 + `https://api.pluralkit.me/v2/members/${memberUri.memberId}`, 132 + ); 133 + if (!resp.ok) return; 134 + const member = await resp.json(); 135 + memberCache.set(s, member); 136 + return member.name; 137 + } 108 138 } 109 139 }; 110 140 111 - export const getFronterNames = async ( 112 - name: string[], 113 - memberUris: MemberUri[], 114 - ) => { 115 - let fronterNames = name; 116 - if (memberUris.length > 0) { 117 - fronterNames = ( 118 - await Promise.allSettled(memberUris.map((m) => fetchMember(m))) 119 - ) 120 - .filter((p) => p.status === "fulfilled") 121 - .flatMap((p) => p.value ?? []); 122 - } 123 - return fronterNames; 141 + export const getFronterNames = async (members: (string | MemberUri)[]) => { 142 + const promises = await Promise.allSettled( 143 + members.map(async (m): Promise<Fronter["members"][0] | null> => { 144 + if (typeof m === "string") 145 + return Promise.resolve({ uri: undefined, name: m }); 146 + const name = await fetchMember(m); 147 + return name ? { uri: m, name } : null; 148 + }), 149 + ); 150 + return promises 151 + .filter((p) => p.status === "fulfilled") 152 + .flatMap((p) => p.value ?? []); 124 153 }; 125 154 126 155 const handleResolver = new CompositeHandleResolver({ ··· 187 216 const maybeTyped = safeParse(fronterSchema, maybeRecord.data.value); 188 217 if (!maybeTyped.ok) return err(maybeTyped.message); 189 218 190 - let memberUris, fronterNames; 219 + let members: Fronter["members"]; 191 220 try { 192 - memberUris = maybeTyped.value.members.map((m) => parseMemberId(m)); 193 - // fronterNames = await getFronterNames(maybeTyped.value.names, memberUris); 194 - fronterNames = maybeTyped.value.names; 221 + members = maybeTyped.value.members.map((m) => ({ 222 + name: m.name, 223 + uri: m.uri ? parseMemberId(m.uri) : undefined, 224 + })); 195 225 } catch (error) { 196 226 return err(`error fetching fronter names: ${error}`); 197 227 } 198 228 199 229 return ok({ 200 - memberUris, 201 - names: fronterNames, 230 + members, 202 231 handle, 203 232 did, 204 233 }); 205 234 }; 206 235 207 236 export const putFronter = async ( 208 - record: Omit<InferOutput<typeof fronterSchema>, "$type">, 237 + subject: FronterSchema["subject"], 238 + members: (string | MemberUri)[], 209 239 authToken: string, 210 240 ): Promise<Result<Fronter, string>> => { 211 - const parsedRecordUri = parseResourceUri(record.subject); 241 + const parsedRecordUri = parseResourceUri(subject); 212 242 if (!parsedRecordUri.ok) return err(parsedRecordUri.error); 213 243 const { repo, collection, rkey } = parsedRecordUri.value; 214 244 ··· 218 248 // make client 219 249 const atpClient = await getAtpClient(did); 220 250 221 - let memberUris, fronterNames; 251 + let filteredMembers: Fronter["members"]; 222 252 try { 223 - memberUris = record.members.map((m) => parseMemberId(m)); 224 - fronterNames = await getFronterNames(record.names, memberUris); 253 + filteredMembers = await getFronterNames(members); 225 254 } catch (error) { 226 255 return err(`error fetching fronter names: ${error}`); 227 256 } ··· 232 261 repo: did, 233 262 collection: fronterSchema.object.shape.$type.expected, 234 263 rkey: `${collection}_${rkey}`, 235 - record: { ...record, names: fronterNames }, 264 + record: { 265 + subject, 266 + members: filteredMembers.map((member) => ({ 267 + name: member.name, 268 + uri: member.uri ? memberUriString(member.uri) : undefined, 269 + })), 270 + }, 236 271 validate: false, 237 272 }, 238 273 headers: { authorization: `Bearer ${authToken}` }, ··· 243 278 return ok({ 244 279 did, 245 280 handle, 246 - names: fronterNames, 247 - memberUris, 281 + members: filteredMembers, 248 282 }); 249 283 }; 250 284