diff --git a/docs/Selfhosting.md b/docs/Selfhosting.md index e3f1997..97d11a9 100644 --- a/docs/Selfhosting.md +++ b/docs/Selfhosting.md @@ -24,11 +24,11 @@ ] ``` -5. (maybe necessary? will improve performance at least) create your own kv store by running `npx wrangler kv namespace create USER_DATA_CACHE` and when asked add it to the `wrangler.jsonc` +5. optionally to improve performance: create your own kv store by running `npx wrangler kv namespace create USER_DATA_CACHE` and when asked add it to the `wrangler.jsonc` DONE :) your blento should be live after a minute or two at `your-cloudflare-worker-or-custom-domain.com` and you can edit it by signing in with your bluesky account at `your-cloudflare-worker-or-custom-domain.com/edit` 6. some cards need their own additional env keys, if you have these cards in your profile, create your keys and add them to your cloudflare worker -- github profile: GITHUB_TOKEN +- github profile: GITHUB_TOKEN (token with public_repo access) - map: PUBLIC_MAPBOX_TOKEN diff --git a/src/lib/cards/GameCards/DinoGameCard/index.ts b/src/lib/cards/GameCards/DinoGameCard/index.ts index df05d36..e037b5c 100644 --- a/src/lib/cards/GameCards/DinoGameCard/index.ts +++ b/src/lib/cards/GameCards/DinoGameCard/index.ts @@ -13,5 +13,6 @@ export const DinoGameCardDefinition = { card.mobileW = 8; card.mobileH = 6; card.cardData = {}; - } + }, + canHaveLabel: true } as CardDefinition & { type: 'dino-game' }; diff --git a/src/lib/cards/GameCards/TetrisCard/index.ts b/src/lib/cards/GameCards/TetrisCard/index.ts index 08b8272..030f890 100644 --- a/src/lib/cards/GameCards/TetrisCard/index.ts +++ b/src/lib/cards/GameCards/TetrisCard/index.ts @@ -18,5 +18,6 @@ export const TetrisCardDefinition = { card.mobileH = 12; card.cardData = {}; }, - maxH: 10 + maxH: 10, + canHaveLabel: true } as CardDefinition & { type: 'tetris' }; diff --git a/src/lib/helper.ts b/src/lib/helper.ts index ab9a622..e542f72 100644 --- a/src/lib/helper.ts +++ b/src/lib/helper.ts @@ -240,7 +240,83 @@ export function cardsEqual(a: Item, b: Item) { ); } -export function setPositionOfNewItem(newItem: Item, items: Item[]) { +export function setPositionOfNewItem( + newItem: Item, + items: Item[], + viewportCenter?: { gridY: number; isMobile: boolean } +) { + if (viewportCenter) { + const { gridY, isMobile } = viewportCenter; + + if (isMobile) { + // Place at viewport center Y + newItem.mobileY = Math.max(0, Math.round(gridY - newItem.mobileH / 2)); + newItem.mobileY = Math.floor(newItem.mobileY / 2) * 2; + + // Try to find a free X at this Y + let found = false; + for ( + newItem.mobileX = 0; + newItem.mobileX <= COLUMNS - newItem.mobileW; + newItem.mobileX += 2 + ) { + if (!items.some((item) => overlaps(newItem, item, true))) { + found = true; + break; + } + } + if (!found) { + newItem.mobileX = 0; + } + + // Desktop: derive from mobile + newItem.y = Math.max(0, Math.round(newItem.mobileY / 2)); + found = false; + for (newItem.x = 0; newItem.x <= COLUMNS - newItem.w; newItem.x += 2) { + if (!items.some((item) => overlaps(newItem, item, false))) { + found = true; + break; + } + } + if (!found) { + newItem.x = 0; + } + } else { + // Place at viewport center Y + newItem.y = Math.max(0, Math.round(gridY - newItem.h / 2)); + + // Try to find a free X at this Y + let found = false; + for (newItem.x = 0; newItem.x <= COLUMNS - newItem.w; newItem.x += 2) { + if (!items.some((item) => overlaps(newItem, item, false))) { + found = true; + break; + } + } + if (!found) { + newItem.x = 0; + } + + // Mobile: derive from desktop + newItem.mobileY = Math.max(0, Math.round(newItem.y * 2)); + found = false; + for ( + newItem.mobileX = 0; + newItem.mobileX <= COLUMNS - newItem.mobileW; + newItem.mobileX += 2 + ) { + if (!items.some((item) => overlaps(newItem, item, true))) { + found = true; + break; + } + } + if (!found) { + newItem.mobileX = 0; + } + } + return; + } + let foundPosition = false; while (!foundPosition) { for (newItem.x = 0; newItem.x <= COLUMNS - newItem.w; newItem.x++) { diff --git a/src/lib/website/EditableWebsite.svelte b/src/lib/website/EditableWebsite.svelte index 4d394c7..67d233c 100644 --- a/src/lib/website/EditableWebsite.svelte +++ b/src/lib/website/EditableWebsite.svelte @@ -129,6 +129,16 @@ let maxHeight = $derived(items.reduce((max, item) => Math.max(max, getY(item) + getH(item)), 0)); + function getViewportCenterGridY(): { gridY: number; isMobile: boolean } | undefined { + if (!container) return undefined; + const rect = container.getBoundingClientRect(); + const currentMargin = isMobile ? mobileMargin : margin; + const cellSize = (rect.width - currentMargin * 2) / COLUMNS; + const viewportCenterY = window.innerHeight / 2; + const gridY = (viewportCenterY - rect.top - currentMargin) / cellSize; + return { gridY, isMobile }; + } + function newCard(type: string = 'link', cardData?: any) { // close sidebar if open const popover = document.getElementById('mobile-menu'); @@ -157,10 +167,17 @@ if (!newItem.item) return; const item = newItem.item; - setPositionOfNewItem(item, items); + const viewportCenter = getViewportCenterGridY(); + setPositionOfNewItem(item, items, viewportCenter); items = [...items, item]; + // Push overlapping items down, then compact to fill gaps + fixCollisions(items, item, false, true); + fixCollisions(items, item, true, true); + compactItems(items, false); + compactItems(items, true); + newItem = {}; await tick(); @@ -373,6 +390,53 @@ } } + function getImageDimensions(src: string): Promise<{ width: number; height: number }> { + return new Promise((resolve) => { + const img = new Image(); + img.onload = () => resolve({ width: img.naturalWidth, height: img.naturalHeight }); + img.onerror = () => resolve({ width: 1, height: 1 }); + img.src = src; + }); + } + + function getBestGridSize( + imageWidth: number, + imageHeight: number, + candidates: [number, number][] + ): [number, number] { + const imageRatio = imageWidth / imageHeight; + let best: [number, number] = candidates[0]; + let bestDiff = Infinity; + + for (const candidate of candidates) { + const gridRatio = candidate[0] / candidate[1]; + const diff = Math.abs(Math.log(imageRatio) - Math.log(gridRatio)); + if (diff < bestDiff) { + bestDiff = diff; + best = candidate; + } + } + + return best; + } + + const desktopSizeCandidates: [number, number][] = [ + [2, 2], + [2, 4], + [4, 2], + [4, 4], + [4, 6], + [6, 4] + ]; + const mobileSizeCandidates: [number, number][] = [ + [4, 4], + [4, 6], + [4, 8], + [6, 4], + [8, 4], + [8, 6] + ]; + async function processImageFile(file: File, gridX?: number, gridY?: number) { const isGif = file.type === 'image/gif'; @@ -386,25 +450,44 @@ image: { blob: file, objectUrl } }; - // If grid position is provided + // Size card based on image aspect ratio + const { width, height } = await getImageDimensions(objectUrl); + const [dw, dh] = getBestGridSize(width, height, desktopSizeCandidates); + const [mw, mh] = getBestGridSize(width, height, mobileSizeCandidates); + item.w = dw; + item.h = dh; + item.mobileW = mw; + item.mobileH = mh; + + // If grid position is provided (image dropped on grid) if (gridX !== undefined && gridY !== undefined) { if (isMobile) { item.mobileX = gridX; item.mobileY = gridY; - // Find valid desktop position - findValidPosition(item, items, false); + // Derive desktop Y from mobile + item.x = Math.floor((COLUMNS - item.w) / 2); + item.x = Math.floor(item.x / 2) * 2; + item.y = Math.max(0, Math.round(gridY / 2)); } else { item.x = gridX; item.y = gridY; - // Find valid mobile position - findValidPosition(item, items, true); + // Derive mobile Y from desktop + item.mobileX = Math.floor((COLUMNS - item.mobileW) / 2); + item.mobileX = Math.floor(item.mobileX / 2) * 2; + item.mobileY = Math.max(0, Math.round(gridY * 2)); } items = [...items, item]; fixCollisions(items, item, isMobile); + fixCollisions(items, item, !isMobile); } else { - setPositionOfNewItem(item, items); + const viewportCenter = getViewportCenterGridY(); + setPositionOfNewItem(item, items, viewportCenter); items = [...items, item]; + fixCollisions(items, item, false, true); + fixCollisions(items, item, true, true); + compactItems(items, false); + compactItems(items, true); } await tick(); @@ -481,15 +564,12 @@ } } - for (const file of imageFiles) { - await processImageFile(file, gridX, gridY); - - // Move to next cell position - const cardW = isMobile ? 4 : 2; - gridX += cardW; - if (gridX + cardW > COLUMNS) { - gridX = 0; - gridY += isMobile ? 4 : 2; + for (let i = 0; i < imageFiles.length; i++) { + // First image gets the drop position, rest use normal placement + if (i === 0) { + await processImageFile(imageFiles[i], gridX, gridY); + } else { + await processImageFile(imageFiles[i]); } } } @@ -537,8 +617,13 @@ objectUrl }; - setPositionOfNewItem(item, items); + const viewportCenter = getViewportCenterGridY(); + setPositionOfNewItem(item, items, viewportCenter); items = [...items, item]; + fixCollisions(items, item, false, true); + fixCollisions(items, item, true, true); + compactItems(items, false); + compactItems(items, true); await tick(); @@ -758,7 +843,8 @@ bind:item={items[i]} ondelete={() => { items = items.filter((it) => it !== item); - compactItems(items, isMobile); + compactItems(items, false); + compactItems(items, true); }} onsetsize={(newW: number, newH: number) => { if (isMobile) { -- 2.51.2 From f356d8374111deeea0f2285f81255010d527c49f Mon Sep 17 00:00:00 2001 From: unbedenklich <106080544+unbedenklich@users.noreply.github.com> Date: Fri, 30 Jan 2026 15:04:16 +0100 Subject: [PATCH 2/3] guestbook card --- .claude/settings.local.json | 3 +- src/lib/atproto/index.ts | 4 +- src/lib/atproto/methods.ts | 69 ++++++++ src/lib/atproto/settings.ts | 1 + .../CreateGuestbookCardModal.svelte | 166 ++++++++++++++++++ .../cards/GuestbookCard/GuestbookCard.svelte | 126 +++++++++++++ src/lib/cards/GuestbookCard/index.ts | 64 +++++++ src/lib/cards/index.ts | 2 + .../bluesky-post/BlueskyPost.svelte | 12 +- src/lib/components/post/Post.svelte | 26 ++- 10 files changed, 466 insertions(+), 7 deletions(-) create mode 100644 src/lib/cards/GuestbookCard/CreateGuestbookCardModal.svelte create mode 100644 src/lib/cards/GuestbookCard/GuestbookCard.svelte create mode 100644 src/lib/cards/GuestbookCard/index.ts diff --git a/.claude/settings.local.json b/.claude/settings.local.json index be569ca..e55cacf 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -24,7 +24,8 @@ "Bash(pnpm dev)", "Bash(pnpm exec svelte-kit:*)", "Bash(pnpm build:*)", - "Bash(pnpm remove:*)" + "Bash(pnpm remove:*)", + "Bash(grep:*)" ] } } diff --git a/src/lib/atproto/index.ts b/src/lib/atproto/index.ts index 3fc0742..1957d6b 100644 --- a/src/lib/atproto/index.ts +++ b/src/lib/atproto/index.ts @@ -16,5 +16,7 @@ export { getBlobURL, getCDNImageBlobUrl, searchActorsTypeahead, - getAuthorFeed + getAuthorFeed, + getPostThread, + createPost } from './methods'; diff --git a/src/lib/atproto/methods.ts b/src/lib/atproto/methods.ts index 07bd491..a647929 100644 --- a/src/lib/atproto/methods.ts +++ b/src/lib/atproto/methods.ts @@ -465,3 +465,72 @@ export function getHandleOrDid(profile: AppBskyActorDefs.ProfileViewDetailed): A return profile.did; } } + +/** + * Fetches a post's thread including replies. + * @param uri - The AT URI of the post + * @param depth - How many levels of replies to fetch (default 1) + * @param client - The client to use (defaults to public Bluesky API) + * @returns The thread data or undefined on failure + */ +export async function getPostThread({ + uri, + depth = 1, + client +}: { + uri: string; + depth?: number; + client?: Client; +}) { + client ??= new Client({ + handler: simpleFetchHandler({ service: 'https://public.api.bsky.app' }) + }); + + const response = await client.get('app.bsky.feed.getPostThread', { + params: { uri: uri as ResourceUri, depth } + }); + + if (!response.ok) return; + + return response.data.thread; +} + +/** + * Creates a Bluesky post on the authenticated user's account. + * @param text - The post text + * @param facets - Optional rich text facets (links, mentions, etc.) + * @returns The response containing the post's URI and CID + * @throws If the user is not logged in + */ +export async function createPost({ + text, + facets +}: { + text: string; + facets?: Array<{ + index: { byteStart: number; byteEnd: number }; + features: Array<{ $type: string; uri?: string; did?: string; tag?: string }>; + }>; +}) { + if (!user.client || !user.did) throw new Error('No client or did'); + + const record: Record = { + $type: 'app.bsky.feed.post', + text, + createdAt: new Date().toISOString() + }; + + if (facets) { + record.facets = facets; + } + + const response = await user.client.post('com.atproto.repo.createRecord', { + input: { + collection: 'app.bsky.feed.post', + repo: user.did, + record + } + }); + + return response; +} diff --git a/src/lib/atproto/settings.ts b/src/lib/atproto/settings.ts index cb8eea3..ead0337 100644 --- a/src/lib/atproto/settings.ts +++ b/src/lib/atproto/settings.ts @@ -20,6 +20,7 @@ export const permissions = { 'app.blento.settings', 'app.blento.comment', 'app.blento.guestbook.entry', + 'app.bsky.feed.post', 'site.standard.publication', 'site.standard.document', 'xyz.statusphere.status' diff --git a/src/lib/cards/GuestbookCard/CreateGuestbookCardModal.svelte b/src/lib/cards/GuestbookCard/CreateGuestbookCardModal.svelte new file mode 100644 index 0000000..32c72c8 --- /dev/null +++ b/src/lib/cards/GuestbookCard/CreateGuestbookCardModal.svelte @@ -0,0 +1,166 @@ + + + +
{ + e.preventDefault(); + handleSubmit(); + }} + class="flex flex-col gap-2" + > + Guestbook + +
+ + +
+ + {#if mode === 'create'} +

+ This will create a post on your Bluesky account. Replies to that post will appear on your + guestbook card. +

+ + {:else} +

+ Paste a Bluesky post URL to use as your guestbook. Replies to that post will appear on your + card. +

+ + {/if} + + {#if errorMessage} + {errorMessage} + {/if} + +
+ + {#if mode === 'create'} + + {:else} + + {/if} +
+
+
diff --git a/src/lib/cards/GuestbookCard/GuestbookCard.svelte b/src/lib/cards/GuestbookCard/GuestbookCard.svelte new file mode 100644 index 0000000..4e9ff6b --- /dev/null +++ b/src/lib/cards/GuestbookCard/GuestbookCard.svelte @@ -0,0 +1,126 @@ + + +
+ {#if item.cardData.href} + + {/if} + +
+ {#if replies.length > 0} +
+ {#each replies as reply (reply.post.uri)} +
+ +
+ {/each} +
+ {:else if isLoaded} +
+ No comments yet — share your Bluesky post to get started! +
+ {:else} +
+ Loading comments... +
+ {/if} +
+
+ + diff --git a/src/lib/cards/GuestbookCard/index.ts b/src/lib/cards/GuestbookCard/index.ts new file mode 100644 index 0000000..9e4fdfe --- /dev/null +++ b/src/lib/cards/GuestbookCard/index.ts @@ -0,0 +1,64 @@ +import { getPostThread } from '$lib/atproto/methods'; +import type { CardDefinition } from '../types'; +import GuestbookCard from './GuestbookCard.svelte'; +import CreateGuestbookCardModal from './CreateGuestbookCardModal.svelte'; + +export const GuestbookCardDefinition = { + type: 'guestbook', + contentComponent: GuestbookCard, + creationModalComponent: CreateGuestbookCardModal, + sidebarButtonText: 'Guestbook', + createNew: (card) => { + card.w = 4; + card.h = 6; + card.mobileW = 8; + card.mobileH = 12; + card.cardData.label = 'Guestbook'; + }, + minW: 4, + minH: 4, + defaultColor: 'base', + canHaveLabel: true, + loadData: async (items) => { + const uris = items + .filter((item) => item.cardData?.uri) + .map((item) => item.cardData.uri as string); + + if (uris.length === 0) return {}; + + const results: Record = {}; + + await Promise.all( + uris.map(async (uri) => { + try { + const thread = await getPostThread({ uri, depth: 1 }); + if (thread && '$type' in thread && thread.$type === 'app.bsky.feed.defs#threadViewPost') { + const typedThread = thread as { replies?: unknown[] }; + results[uri] = (typedThread.replies ?? []) + .filter( + (r: unknown) => + r != null && + typeof r === 'object' && + '$type' in r && + (r as { $type: string }).$type === 'app.bsky.feed.defs#threadViewPost' + ) + .sort((a: unknown, b: unknown) => { + const timeA = new Date( + ((a as any).post?.record?.createdAt as string) ?? 0 + ).getTime(); + const timeB = new Date( + ((b as any).post?.record?.createdAt as string) ?? 0 + ).getTime(); + return timeB - timeA; + }); + } + } catch (e) { + console.error('Failed to load guestbook thread for', uri, e); + } + }) + ); + + return results; + }, + name: 'Guestbook' +} as CardDefinition & { type: 'guestbook' }; diff --git a/src/lib/cards/index.ts b/src/lib/cards/index.ts index 59e41cf..cdf8e14 100644 --- a/src/lib/cards/index.ts +++ b/src/lib/cards/index.ts @@ -32,9 +32,11 @@ import { DrawCardDefinition } from './DrawCard'; import { TimerCardDefinition } from './TimerCard'; import { SpotifyCardDefinition } from './SpotifyCard'; import { ButtonCardDefinition } from './ButtonCard'; +import { GuestbookCardDefinition } from './GuestbookCard'; // import { Model3DCardDefinition } from './Model3DCard'; export const AllCardDefinitions = [ + GuestbookCardDefinition, ButtonCardDefinition, ImageCardDefinition, VideoCardDefinition, diff --git a/src/lib/components/bluesky-post/BlueskyPost.svelte b/src/lib/components/bluesky-post/BlueskyPost.svelte index 64fb213..e3215cc 100644 --- a/src/lib/components/bluesky-post/BlueskyPost.svelte +++ b/src/lib/components/bluesky-post/BlueskyPost.svelte @@ -8,8 +8,16 @@ feedViewPost, children, showLogo = false, + showAvatar = false, + compact = false, ...restProps - }: { feedViewPost?: PostView; children?: Snippet; showLogo?: boolean } = $props(); + }: { + feedViewPost?: PostView; + children?: Snippet; + showLogo?: boolean; + showAvatar?: boolean; + compact?: boolean; + } = $props(); const postData = $derived(feedViewPost ? blueskyPostToPostData(feedViewPost) : undefined); @@ -37,6 +45,8 @@ likeHref={postData?.href} showBookmark={false} logo={showLogo ? logo : undefined} + {showAvatar} + {compact} {...restProps} > {@render children?.()} diff --git a/src/lib/components/post/Post.svelte b/src/lib/components/post/Post.svelte index 9a5fd4d..bfdb332 100644 --- a/src/lib/components/post/Post.svelte +++ b/src/lib/components/post/Post.svelte @@ -36,7 +36,10 @@ children, - logo + logo, + + showAvatar = false, + compact = false }: WithElementRef>> & { data: PostData; class?: string; @@ -61,6 +64,9 @@ customActions?: Snippet; logo?: Snippet; + + showAvatar?: boolean; + compact?: boolean; } = $props(); @@ -121,6 +127,15 @@ {/if}
+ {#if showAvatar && data.author.avatar} + + + + {/if}
@@ -161,7 +176,10 @@ {/if}
@@ -173,7 +191,7 @@
{#if data.htmlContent} @@ -185,7 +203,7 @@ - {#if showReply || showRepost || showLike || showBookmark || customActions} + {#if !compact && (showReply || showRepost || showLike || showBookmark || customActions)}
-- 2.51.2 From 9cb0273763b0802719b44026de1140496465fb28 Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Fri, 30 Jan 2026 18:49:31 +0100 Subject: [PATCH 3/3] updated blentos --- src/lib/atproto/methods.ts | 31 +++++++++++++++ src/lib/atproto/settings.ts | 2 +- .../SpecialCards/UpdatedBlentos/index.ts | 38 ++++++++++--------- 3 files changed, 52 insertions(+), 19 deletions(-) diff --git a/src/lib/atproto/methods.ts b/src/lib/atproto/methods.ts index a647929..7a646f1 100644 --- a/src/lib/atproto/methods.ts +++ b/src/lib/atproto/methods.ts @@ -101,6 +101,36 @@ export async function getDetailedProfile(data?: { did?: Did; client?: Client }) return response.data; } +export async function getBlentoOrBskyProfile(data: { did: Did; client?: Client }): Promise< + Awaited> & { + hasBlento: boolean; + } +> { + let blentoProfile; + try { + // try getting blento profile first + blentoProfile = await getRecord({ + collection: 'site.standard.publication', + did: data?.did, + rkey: 'blento.self', + client: data?.client + }); + } catch { + console.error('error getting blento profile, falling back to bsky profile'); + } + + const response = await getDetailedProfile(data); + + return { + did: data.did, + handle: response?.handle, + displayName: blentoProfile?.value?.name || response?.displayName || response?.handle, + avatar: (getCDNImageBlobUrl({ did: data?.did, blob: blentoProfile?.value?.icon }) || + response?.avatar) as `${string}:${string}`, + hasBlento: Boolean(blentoProfile.value) + }; +} + /** * Creates an AT Protocol client for a user's PDS. * @param did - The DID of the user @@ -370,6 +400,7 @@ export function getCDNImageBlobUrl({ }; }; }) { + if (!blob || !did) return; did ??= user.did; return `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${blob.ref.$link}@webp`; diff --git a/src/lib/atproto/settings.ts b/src/lib/atproto/settings.ts index ead0337..a1c5a38 100644 --- a/src/lib/atproto/settings.ts +++ b/src/lib/atproto/settings.ts @@ -20,7 +20,7 @@ export const permissions = { 'app.blento.settings', 'app.blento.comment', 'app.blento.guestbook.entry', - 'app.bsky.feed.post', + 'app.bsky.feed.post?action=create', 'site.standard.publication', 'site.standard.document', 'xyz.statusphere.status' diff --git a/src/lib/cards/SpecialCards/UpdatedBlentos/index.ts b/src/lib/cards/SpecialCards/UpdatedBlentos/index.ts index 48932f0..3d57c22 100644 --- a/src/lib/cards/SpecialCards/UpdatedBlentos/index.ts +++ b/src/lib/cards/SpecialCards/UpdatedBlentos/index.ts @@ -1,8 +1,9 @@ -import { getDetailedProfile } from '$lib/atproto'; import type { CardDefinition } from '../../types'; import UpdatedBlentosCard from './UpdatedBlentosCard.svelte'; import type { Did } from '@atcute/lexicons'; -import type { AppBskyActorDefs } from '@atcute/bluesky'; +import { getBlentoOrBskyProfile } from '$lib/atproto/methods'; + +type ProfileWithBlentoFlag = Awaited>; export const UpdatedBlentosCardDefitition = { type: 'updatedBlentos', @@ -14,36 +15,37 @@ export const UpdatedBlentosCardDefitition = { ); const recentRecords = await response.json(); const existingUsers = await cache?.get('updatedBlentos'); - const existingUsersArray: AppBskyActorDefs.ProfileViewDetailed[] = existingUsers + const existingUsersArray: ProfileWithBlentoFlag[] = existingUsers ? JSON.parse(existingUsers) : []; - const existingUsersSet = new Set(existingUsersArray.map((v) => v.did)); - - const uniqueDids = new Set(); - for (const record of recentRecords as { did: string }[]) { - if (!existingUsersSet.has(record.did as Did)) uniqueDids.add(record.did as Did); - } + const uniqueDids = new Set(recentRecords.map((v: { did: string }) => v.did as Did)); - const profiles: Promise[] = []; + const profiles: Promise[] = []; for (const did of Array.from(uniqueDids)) { - const profile = getDetailedProfile({ did }); - profiles.push(profile); - if (profiles.length > 30) break; + profiles.push(getBlentoOrBskyProfile({ did })); } for (let i = existingUsersArray.length - 1; i >= 0; i--) { // if handle is handle.invalid, remove from existing users and add to profiles to refresh - if (existingUsersArray[i].handle === 'handle.invalid') { + if ( + (existingUsersArray[i].handle === 'handle.invalid' || + (!existingUsersArray[i].avatar && !existingUsersArray[i].hasBlento)) && + !uniqueDids.has(existingUsersArray[i].did) + ) { const removed = existingUsersArray.splice(i, 1)[0]; - profiles.push(getDetailedProfile({ did: removed.did })); + profiles.push(getBlentoOrBskyProfile({ did: removed.did })); + // if in unique dids, remove from older existing users and keep the newer one + // so updated profiles go first + } else if (uniqueDids.has(existingUsersArray[i].did)) { + existingUsersArray.splice(i, 1); } } - const result = [...(await Promise.all(profiles)), ...existingUsersArray].filter( - (v) => v && v.handle !== 'handle.invalid' - ); + let result = [...(await Promise.all(profiles)), ...existingUsersArray]; + + result = result.filter((v) => v && v.handle !== 'handle.invalid'); if (cache) { await cache?.put('updatedBlentos', JSON.stringify(result));