your personal website on atproto - mirror blento.app
25
fork

Configure Feed

Select the types of activity you want to include in your feed.

some small fixes

Florian 07278e41 30e874bb

+100 -93
+7 -1
lexicons/app/blento/card.json
··· 8 8 "key": "tid", 9 9 "record": { 10 10 "type": "object", 11 - "required": ["w", "h", "x", "y", "mobileW", "mobileH", "mobileX", "mobileY", "cardType", "cardData"], 11 + "required": [ 12 + "w", 13 + "h", 14 + "x", 15 + "y", 16 + "cardType" 17 + ], 12 18 "properties": { 13 19 "w": { "type": "integer" }, 14 20 "h": { "type": "integer" },
+8
src/app.css
··· 10 10 @custom-variant dark (&:where(.dark, .dark *):not(:where(.light, .light *))); 11 11 @custom-variant accent (&:where(.accent, .accent *)); 12 12 13 + /* Hide @foxui/core Avatar fallback (person icon / initials) whenever an image 14 + is rendered, so partially transparent avatars don't show the icon behind them. 15 + Targets the Avatar root by its distinctive class combo. */ 16 + .not-prose.rounded-full.isolate:has(img) > svg, 17 + .not-prose.rounded-full.isolate:has(img) > span { 18 + display: none; 19 + } 20 + 13 21 .body::-webkit-scrollbar-track { 14 22 background-color: transparent; 15 23 }
+8 -2
src/lib/atproto/auth.svelte.ts
··· 1 1 import { type AppBskyActorDefs } from '@atcute/bluesky'; 2 2 import type { ActorIdentifier, Did } from '@atcute/lexicons'; 3 3 import { page } from '$app/state'; 4 + import { saveRecentLogin } from '@foxui/social'; 4 5 5 6 let cachedProfile = $state<AppBskyActorDefs.ProfileViewDetailed | null>(null); 6 7 let cachedDid = $state<Did | null>(null); 7 8 9 + function rememberProfile(profile: AppBskyActorDefs.ProfileViewDetailed) { 10 + cachedProfile = profile; 11 + saveRecentLogin(profile); 12 + } 13 + 8 14 // Load profile client-side when authDid changes 9 15 $effect.root(() => { 10 16 $effect(() => { ··· 22 28 23 29 // If the current page already has this user's profile (e.g. viewing own page), use it 24 30 if (page.data?.profile?.did === did) { 25 - cachedProfile = page.data.profile; 31 + rememberProfile(page.data.profile); 26 32 return; 27 33 } 28 34 ··· 35 41 .get('app.bsky.actor.getProfile', { params: { actor: did } }) 36 42 .then((res) => { 37 43 if (res.ok && cachedDid === did) { 38 - cachedProfile = res.data; 44 + rememberProfile(res.data); 39 45 } 40 46 }) 41 47 .catch(() => {});
+10 -1
src/lib/website/Profile.svelte
··· 21 21 renderer.link = ({ href, title, text }) => 22 22 `<a target="_blank" href="${href}" title="${title ?? ''}">${text}</a>`; 23 23 24 - const profileUrl = $derived(`${page.url.origin}/${data.handle}`); 24 + const profileUrl = $derived.by(() => { 25 + if (page.data.customDomain) return `${page.url.origin}/`; 26 + const pubUrl = data.publication?.url; 27 + if (pubUrl && /^https?:\/\//.test(pubUrl) && !/^https?:\/\/([^/]*\.)?blento\.app/i.test(pubUrl)) { 28 + return pubUrl; 29 + } 30 + const handle = data.profile?.handle; 31 + const actor = handle && handle !== 'handle.invalid' ? handle : data.did; 32 + return `${page.url.origin}/${actor}`; 33 + }); 25 34 const profilePosition = $derived(getProfilePosition(data)); 26 35 </script> 27 36
+65 -87
src/lib/website/load.ts
··· 96 96 return { profile, publication, pronounsRecord }; 97 97 } 98 98 99 + async function tryContrail<T>(label: string, fn: () => Promise<T | null>): Promise<T | null> { 100 + try { 101 + return await fn(); 102 + } catch (e) { 103 + console.error(`Contrail ${label} failed`, e); 104 + return null; 105 + } 106 + } 107 + 99 108 /** 100 109 * Fetch only the profile bundle (bsky profile + publication + pronouns) from contrail. 101 110 * Used by single-card routes that don't need the full card list. 102 111 */ 103 - async function loadProfilesFromContrail( 104 - actor: ActorIdentifier, 105 - db: D1Database 106 - ): Promise<ContrailProfile[] | null> { 107 - try { 108 - const client = getServerClient(db); 109 - const res = await client.get('app.blento.getProfile', { params: { actor } }); 112 + function loadProfilesFromContrail(actor: ActorIdentifier, db: D1Database) { 113 + return tryContrail('getProfile', async () => { 114 + const res = await getServerClient(db).get('app.blento.getProfile', { params: { actor } }); 110 115 if (!res.ok) return null; 111 116 return (res.data.profiles ?? []) as ContrailProfile[]; 112 - } catch (e) { 113 - console.error('Contrail getProfile failed', e); 114 - return null; 115 - } 117 + }); 116 118 } 117 119 118 120 /** 119 121 * Fetch a single card from contrail by DID + rkey. 120 122 */ 121 - async function loadCardFromContrail( 122 - did: Did, 123 - rkey: string, 124 - db: D1Database 125 - ): Promise<Item | null> { 126 - try { 127 - const client = getServerClient(db); 123 + function loadCardFromContrail(did: Did, rkey: string, db: D1Database) { 124 + return tryContrail('card.getRecord', async () => { 128 125 const uri = `at://${did}/app.blento.card/${rkey}` as const; 129 - const res = await client.get('app.blento.card.getRecord', { 126 + const res = await getServerClient(db).get('app.blento.card.getRecord', { 130 127 params: { uri } 131 128 }); 132 129 if (!res.ok) return null; 133 130 return { ...(res.data.record as object) } as Item; 134 - } catch (e) { 135 - console.error('Contrail card.getRecord failed', e); 136 - return null; 137 - } 131 + }); 138 132 } 139 133 140 134 /** 141 - * Load all data for a user from Contrail in a single call (cards + profiles). 135 + * Load cards for a single page + all pages for the actor from Contrail. 136 + * Filtering cards by page at query time means we only run additional-data loaders 137 + * for card types that actually appear on the current page. 142 138 */ 143 - async function loadFromContrail( 144 - actor: ActorIdentifier, 145 - db: D1Database 146 - ): Promise<{ 147 - cards: Item[]; 148 - pages: Awaited<ReturnType<typeof listRecords>>; 149 - profiles: ContrailProfile[]; 150 - } | null> { 151 - try { 139 + function loadFromContrail(actor: ActorIdentifier, db: D1Database, page: string) { 140 + return tryContrail('cards+pages query', async () => { 152 141 const client = getServerClient(db); 153 142 const [cardRes, pageRes] = await Promise.all([ 154 143 client.get('app.blento.card.listRecords', { 155 - params: { actor, limit: 200, profiles: true } 144 + params: { actor, limit: 200, profiles: true, page } 156 145 }), 157 146 client.get('app.blento.page.listRecords', { 158 147 params: { actor, limit: 200 } ··· 178 167 pages, 179 168 profiles: (cardRes.data.profiles ?? []) as ContrailProfile[] 180 169 }; 181 - } catch (e) { 182 - console.error('Contrail query failed', e); 183 - return null; 184 - } 170 + }); 171 + } 172 + 173 + function defaultPublication( 174 + profile: WebsiteData['profile'] | undefined 175 + ): WebsiteData['publication'] { 176 + return { 177 + name: profile?.displayName || profile?.handle, 178 + description: profile?.description 179 + } as WebsiteData['publication']; 180 + } 181 + 182 + function getPronounsFromPDS(did: Did) { 183 + return getRecord({ 184 + did, 185 + collection: 'app.nearhorizon.actor.pronouns', 186 + rkey: 'self' 187 + }).catch(() => undefined) as Promise<PronounsRecord | undefined>; 188 + } 189 + 190 + function getSelfPublicationFromPDS(did: Did) { 191 + return getRecord({ 192 + did, 193 + collection: 'site.standard.publication', 194 + rkey: 'blento.self' 195 + }).catch(() => undefined); 185 196 } 186 197 187 198 export async function loadData( 188 199 handle: ActorIdentifier, 189 200 cache: CacheService | undefined, 190 - forceUpdate: boolean = false, 191 201 page: string = 'self', 192 202 env?: Record<string, string | undefined>, 193 203 platform?: App.Platform ··· 199 209 if (!did) throw error(404); 200 210 201 211 const db = platform?.env?.DB; 202 - const contrailData = db ? await loadFromContrail(handle, db) : null; 212 + const fullPage = 'blento.' + page; 213 + const contrailData = db ? await loadFromContrail(handle, db, fullPage) : null; 203 214 204 215 let cards: Item[]; 205 216 let pageRecords: Awaited<ReturnType<typeof listRecords>>; ··· 222 233 console.error('error getting records for collection app.blento.card', e); 223 234 return [] as Awaited<ReturnType<typeof listRecords>>; 224 235 }), 225 - listRecords({ did, collection: 'app.blento.page' }).catch(() => { 226 - return [] as Awaited<ReturnType<typeof listRecords>>; 227 - }), 228 - getRecord({ 229 - did, 230 - collection: 'site.standard.publication', 231 - rkey: 'blento.self' 232 - }).catch(() => undefined), 236 + listRecords({ did, collection: 'app.blento.page' }).catch( 237 + () => [] as Awaited<ReturnType<typeof listRecords>> 238 + ), 239 + getSelfPublicationFromPDS(did), 233 240 getDetailedProfile({ did }), 234 - getRecord({ 235 - did, 236 - collection: 'app.nearhorizon.actor.pronouns', 237 - rkey: 'self' 238 - }).catch(() => undefined) 241 + getPronounsFromPDS(did) 239 242 ]); 240 243 241 244 cards = cardRecords.map((v) => ({ ...v.value }) as Item); 242 245 pageRecords = pageRecs; 243 246 profile = prof; 244 247 publication = mainPub?.value as WebsiteData['publication'] | undefined; 245 - pronounsRecord = pronouns as PronounsRecord | undefined; 248 + pronounsRecord = pronouns; 246 249 } 247 250 248 251 // If no publication found from contrail profiles, check page records ··· 251 254 publication = pubFromPages?.value as WebsiteData['publication'] | undefined; 252 255 } 253 256 254 - publication ??= { 255 - name: profile?.displayName || profile?.handle, 256 - description: profile?.description 257 - } as WebsiteData['publication']; 257 + publication ??= defaultPublication(profile); 258 258 259 259 const additionalData = await loadAdditionalData(cards, { did, handle, cache, platform }, env); 260 260 ··· 315 315 const [cardRecord, prof, pronouns] = await Promise.all([ 316 316 getRecord({ did, collection: 'app.blento.card', rkey }).catch(() => undefined), 317 317 getDetailedProfile({ did }), 318 - getRecord({ 319 - did, 320 - collection: 'app.nearhorizon.actor.pronouns', 321 - rkey: 'self' 322 - }).catch(() => undefined) 318 + getPronounsFromPDS(did) 323 319 ]); 324 320 325 321 if (!cardRecord?.value) throw error(404, 'Card not found'); 326 322 cardValue = cardRecord.value as Item; 327 323 profile = prof; 328 - pronounsRecord = pronouns as PronounsRecord | undefined; 324 + pronounsRecord = pronouns; 329 325 } 330 326 331 327 if (!profile) throw error(404); ··· 357 353 handle: resolvedHandle, 358 354 did, 359 355 cards, 360 - publication: 361 - publication ?? 362 - ({ 363 - name: profile?.displayName || profile?.handle, 364 - description: profile?.description 365 - } as WebsiteData['publication']), 356 + publication: publication ?? defaultPublication(profile), 366 357 additionalData, 367 358 profile, 368 359 pronouns: formatPronouns(pronounsRecord, profile), ··· 409 400 410 401 if (!profile) { 411 402 const [pubRecord, prof, pronouns] = await Promise.all([ 412 - getRecord({ 413 - did, 414 - collection: 'site.standard.publication', 415 - rkey: 'blento.self' 416 - }).catch(() => undefined), 403 + getSelfPublicationFromPDS(did), 417 404 getDetailedProfile({ did }), 418 - getRecord({ 419 - did, 420 - collection: 'app.nearhorizon.actor.pronouns', 421 - rkey: 'self' 422 - }).catch(() => undefined) 405 + getPronounsFromPDS(did) 423 406 ]); 424 407 profile = prof; 425 408 publication = pubRecord?.value as WebsiteData['publication'] | undefined; 426 - pronounsRecord = pronouns as PronounsRecord | undefined; 409 + pronounsRecord = pronouns; 427 410 } 428 411 429 412 if (!profile) throw error(404); ··· 451 434 handle: resolvedHandle, 452 435 did, 453 436 cards, 454 - publication: 455 - publication ?? 456 - ({ 457 - name: profile?.displayName || profile?.handle, 458 - description: profile?.description 459 - } as WebsiteData['publication']), 437 + publication: publication ?? defaultPublication(profile), 460 438 additionalData, 461 439 profile, 462 440 pronouns: formatPronouns(pronounsRecord, profile),
+1 -1
src/routes/[[actor=actor]]/(pages)/+layout.server.ts
··· 15 15 throw error(404, 'Page not found'); 16 16 } 17 17 18 - return await loadData(actor, cache, false, params.page, env, platform); 18 + return await loadData(actor, cache, params.page, env, platform); 19 19 }
+1 -1
src/routes/[[actor=actor]]/.well-known/site.standard.publication/+server.ts
··· 14 14 throw error(404, 'Page not found'); 15 15 } 16 16 17 - const data = await loadData(actor, cache, false, params.page, env, platform); 17 + const data = await loadData(actor, cache, params.page, env, platform); 18 18 19 19 if (!data.publication) throw error(300); 20 20