From ef2c0d4ce5d6783c2df50261f94f262168d8c893 Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Fri, 9 Jan 2026 03:03:33 +0100 Subject: [PATCH] add at proto collection card --- src/lib/EditableWebsite.svelte | 4 +- src/lib/Website.svelte | 11 ++- .../ATProtoCollectionsCard.svelte | 48 +++++++++++ .../SidebarItemATProtoCollectionsCard.svelte | 29 +++++++ src/lib/cards/ATProtoCollectionsCard/index.ts | 25 ++++++ .../BlueskyPostCard/BlueskyPostCard.svelte | 9 +- src/lib/cards/BlueskyPostCard/index.ts | 12 ++- .../LivestreamCard/LivestreamCard.svelte | 86 ++++++++----------- src/lib/cards/LivestreamCard/index.ts | 51 +++++++++++ .../UpdatedBlentos/UpdatedBlentosCard.svelte | 5 +- .../SpecialCards/UpdatedBlentos/index.ts | 8 +- src/lib/cards/index.ts | 4 +- src/lib/cards/types.ts | 3 + .../bluesky-post/BlueskyPost.svelte | 2 +- src/lib/helper.ts | 3 +- src/lib/oauth/atproto.ts | 12 +++ src/lib/website/context.ts | 1 + src/lib/website/load.ts | 81 ++++++----------- 18 files changed, 277 insertions(+), 117 deletions(-) create mode 100644 src/lib/cards/ATProtoCollectionsCard/ATProtoCollectionsCard.svelte create mode 100644 src/lib/cards/ATProtoCollectionsCard/SidebarItemATProtoCollectionsCard.svelte create mode 100644 src/lib/cards/ATProtoCollectionsCard/index.ts diff --git a/src/lib/EditableWebsite.svelte b/src/lib/EditableWebsite.svelte index 166f3ea..49415d3 100644 --- a/src/lib/EditableWebsite.svelte +++ b/src/lib/EditableWebsite.svelte @@ -23,7 +23,7 @@ import { tick, type Component } from 'svelte'; import type { CreationModalComponentProps } from './cards/types'; import { dev } from '$app/environment'; - import { setDidContext } from './website/context'; + import { setDidContext, setHandleContext } from './website/context'; import BaseEditingCard from './cards/BaseCard/BaseEditingCard.svelte'; let { @@ -67,6 +67,8 @@ // svelte-ignore state_referenced_locally setDidContext(did); + // svelte-ignore state_referenced_locally + setHandleContext(handle); const getX = (item: Item) => (isMobile ? (item.mobileX ?? item.x) : item.x); const getY = (item: Item) => (isMobile ? (item.mobileY ?? item.y) : item.y); diff --git a/src/lib/Website.svelte b/src/lib/Website.svelte index 227421d..a8578da 100644 --- a/src/lib/Website.svelte +++ b/src/lib/Website.svelte @@ -4,8 +4,10 @@ import { setIsMobile, sortItems } from './helper'; import type { Item } from './types'; import { innerWidth } from 'svelte/reactivity/window'; - import { setDidContext } from './website/context'; + import { setDidContext, setHandleContext } from './website/context'; import BaseCard from './cards/BaseCard/BaseCard.svelte'; + import { onMount } from 'svelte'; + import { describeRepo } from './oauth/atproto'; let { handle, did, items, data }: { handle: string; did: string; items: Item[]; data: any } = $props(); @@ -16,6 +18,9 @@ // svelte-ignore state_referenced_locally setDidContext(did); + // svelte-ignore state_referenced_locally + setHandleContext(handle); + let maxHeight = $derived( items.reduce( @@ -25,6 +30,10 @@ ); let container: HTMLDivElement | undefined = $state(); + + onMount(() => { + describeRepo({did}); + });
diff --git a/src/lib/cards/ATProtoCollectionsCard/ATProtoCollectionsCard.svelte b/src/lib/cards/ATProtoCollectionsCard/ATProtoCollectionsCard.svelte new file mode 100644 index 0000000..4f0342a --- /dev/null +++ b/src/lib/cards/ATProtoCollectionsCard/ATProtoCollectionsCard.svelte @@ -0,0 +1,48 @@ + + +
+
+ My AT Protocol Collections + + {#if collections} + {collections.length} + {/if} +
+
+ {#each collections ?? [] as collection} + + {/each} +
+
diff --git a/src/lib/cards/ATProtoCollectionsCard/SidebarItemATProtoCollectionsCard.svelte b/src/lib/cards/ATProtoCollectionsCard/SidebarItemATProtoCollectionsCard.svelte new file mode 100644 index 0000000..83f8b35 --- /dev/null +++ b/src/lib/cards/ATProtoCollectionsCard/SidebarItemATProtoCollectionsCard.svelte @@ -0,0 +1,29 @@ + + + diff --git a/src/lib/cards/ATProtoCollectionsCard/index.ts b/src/lib/cards/ATProtoCollectionsCard/index.ts new file mode 100644 index 0000000..e830988 --- /dev/null +++ b/src/lib/cards/ATProtoCollectionsCard/index.ts @@ -0,0 +1,25 @@ +import { describeRepo } from '$lib/oauth/atproto'; +import type { CardDefinition } from '../types'; +import ATProtoCollectionsCard from './ATProtoCollectionsCard.svelte'; +import SidebarItemATProtoCollectionsCard from './SidebarItemATProtoCollectionsCard.svelte'; + +export const ATProtoCollectionsCardDefinition = { + type: 'atprotocollections', + contentComponent: ATProtoCollectionsCard, + loadData: async (items, { did }) => { + const data = (await describeRepo({ did })).data; + console.log(data); + const collections = new Set(); + for (const collection of data.collections) { + const split = collection.split('.'); + if (split.length > 1) collections.add(split[1] + '.' + split[0]); + } + + return Array.from(collections); + }, + createNew: (item) => { + item.w = 2; + item.mobileW = 4; + }, + sidebarComponent: SidebarItemATProtoCollectionsCard +} as CardDefinition & { type: 'atprotocollections' }; diff --git a/src/lib/cards/BlueskyPostCard/BlueskyPostCard.svelte b/src/lib/cards/BlueskyPostCard/BlueskyPostCard.svelte index 379194f..1b697f6 100644 --- a/src/lib/cards/BlueskyPostCard/BlueskyPostCard.svelte +++ b/src/lib/cards/BlueskyPostCard/BlueskyPostCard.svelte @@ -1,15 +1,20 @@
{#if feed?.[0].post} - +
{:else} Your latest bluesky post will appear here. diff --git a/src/lib/cards/BlueskyPostCard/index.ts b/src/lib/cards/BlueskyPostCard/index.ts index ee343cb..50673b0 100644 --- a/src/lib/cards/BlueskyPostCard/index.ts +++ b/src/lib/cards/BlueskyPostCard/index.ts @@ -1,3 +1,4 @@ +import { AtpBaseClient } from '@atproto/api'; import type { CardDefinition } from '../types'; import BlueskyPostCard from './BlueskyPostCard.svelte'; import SidebarItemBlueskyPostCard from './SidebarItemBlueskyPostCard.svelte'; @@ -12,5 +13,14 @@ export const BlueskyPostCardDefinition = { card.h = 2; card.mobileH = 4; }, - sidebarComponent: SidebarItemBlueskyPostCard + sidebarComponent: SidebarItemBlueskyPostCard, + loadData: async (items, { did }) => { + const agent = new AtpBaseClient({ service: 'https://api.bsky.app' }); + const authorFeed = await agent.app.bsky.feed.getAuthorFeed({ + actor: did, + filter: 'posts_no_replies', + limit: 2 + }); + return JSON.parse(JSON.stringify(authorFeed.data)); + } } as CardDefinition & { type: 'latestPost' }; diff --git a/src/lib/cards/LivestreamCard/LivestreamCard.svelte b/src/lib/cards/LivestreamCard/LivestreamCard.svelte index 80541aa..4da1482 100644 --- a/src/lib/cards/LivestreamCard/LivestreamCard.svelte +++ b/src/lib/cards/LivestreamCard/LivestreamCard.svelte @@ -1,72 +1,58 @@ diff --git a/src/lib/cards/LivestreamCard/index.ts b/src/lib/cards/LivestreamCard/index.ts index 4d1b5f9..a6ede57 100644 --- a/src/lib/cards/LivestreamCard/index.ts +++ b/src/lib/cards/LivestreamCard/index.ts @@ -1,4 +1,6 @@ import { client } from '$lib/oauth'; +import { listRecords } from '$lib/oauth/atproto'; +import { getImageBlobUrl } from '$lib/website/utils'; import EmbedCard from '../EmbedCard/EmbedCard.svelte'; import type { CardDefinition } from '../types'; import LivestreamCard from './LivestreamCard.svelte'; @@ -14,6 +16,55 @@ export const LivestreamCardDefitition = { card.h = 1; card.mobileH = 2; card.mobileW = 4; + }, + loadData: async (items, { did }) => { + const records = await listRecords({ did, collection: 'place.stream.livestream', limit: 3 }); + console.log(records); + + let latestLivestream: + | { + createdAt: string; + title: string; + thumb?: string; + href: string; + online?: boolean; + } + | undefined; + const values = Object.values(records); + if (values?.length > 0) { + const latest = JSON.parse(JSON.stringify(values[0])); + console.log(latest); + + latestLivestream = { + createdAt: latest.value.createdAt, + title: latest.value.title as string, + thumb: getImageBlobUrl({ link: latest.value.thumb?.ref.$link, did }), + href: latest.value.canonicalUrl || latest.value.url, + online: undefined + }; + } + + if (latestLivestream) { + try { + const segmentsResponse = await fetch( + 'https://stream.place/xrpc/place.stream.live.getSegments?userDID=' + + encodeURIComponent(did) + ); + const segments = await segmentsResponse.json(); + + const lastSegment = segments.segments[0]; + const startTime = new Date(lastSegment.record.startTime).getTime(); + + const FIVE_MINUTES = 5 * 60 * 1000; + const now = Date.now(); + + latestLivestream.online = now - startTime <= FIVE_MINUTES; + } catch (error) { + console.error(error); + } + } + + return latestLivestream; } } as CardDefinition & { type: 'latestLivestream' }; diff --git a/src/lib/cards/SpecialCards/UpdatedBlentos/UpdatedBlentosCard.svelte b/src/lib/cards/SpecialCards/UpdatedBlentos/UpdatedBlentosCard.svelte index 5ebe9ca..1397af1 100644 --- a/src/lib/cards/SpecialCards/UpdatedBlentos/UpdatedBlentosCard.svelte +++ b/src/lib/cards/SpecialCards/UpdatedBlentos/UpdatedBlentosCard.svelte @@ -7,7 +7,10 @@ let { item }: ContentComponentProps = $props(); - const recentRecords = getAdditionalUserData().recentRecords; + + const data = getAdditionalUserData(); + // svelte-ignore state_referenced_locally + const recentRecords = (data[item.cardType] as any); let profiles: ProfileViewDetailed[] = $state([]); diff --git a/src/lib/cards/SpecialCards/UpdatedBlentos/index.ts b/src/lib/cards/SpecialCards/UpdatedBlentos/index.ts index 3079b39..d5cde6c 100644 --- a/src/lib/cards/SpecialCards/UpdatedBlentos/index.ts +++ b/src/lib/cards/SpecialCards/UpdatedBlentos/index.ts @@ -3,5 +3,11 @@ import UpdatedBlentosCard from './UpdatedBlentosCard.svelte'; export const UpdatedBlentosCardDefitition = { type: 'updatedBlentos', - contentComponent: UpdatedBlentosCard + contentComponent: UpdatedBlentosCard, + loadData: async () => { + const response = await fetch( + 'https://ufos-api.microcosm.blue/records?collection=app.blento.card' + ); + return await response.json(); + } } as CardDefinition & { type: 'updatedBlentos' }; diff --git a/src/lib/cards/index.ts b/src/lib/cards/index.ts index 742e4f5..1c21d9a 100644 --- a/src/lib/cards/index.ts +++ b/src/lib/cards/index.ts @@ -1,3 +1,4 @@ +import { ATProtoCollectionsCardDefinition } from './ATProtoCollectionsCard'; import { BlueskyPostCardDefinition } from './BlueskyPostCard'; import { EmbedCardDefinition } from './EmbedCard'; import { ImageCardDefinition } from './ImageCard'; @@ -19,7 +20,8 @@ export const AllCardDefinitions = [ LivestreamCardDefitition, LivestreamEmbedCardDefitition, EmbedCardDefinition, - MapCardDefinition + MapCardDefinition, + ATProtoCollectionsCardDefinition ] as const; export const CardDefinitionsByType = AllCardDefinitions.reduce( diff --git a/src/lib/cards/types.ts b/src/lib/cards/types.ts index 0a672eb..31c3ea0 100644 --- a/src/lib/cards/types.ts +++ b/src/lib/cards/types.ts @@ -36,4 +36,7 @@ export type CardDefinition = { sidebarComponent?: Component; sidebarButtonText?: string; + + loadData?: (items: Item[], { did, handle }: { did: string; handle: string }) => Promise; + dataKey?: string; }; diff --git a/src/lib/components/bluesky-post/BlueskyPost.svelte b/src/lib/components/bluesky-post/BlueskyPost.svelte index 27a062f..849dc2c 100644 --- a/src/lib/components/bluesky-post/BlueskyPost.svelte +++ b/src/lib/components/bluesky-post/BlueskyPost.svelte @@ -30,7 +30,7 @@ {/snippet} {#if postData} - + {@render children?.()} {/if} diff --git a/src/lib/helper.ts b/src/lib/helper.ts index 4f006dc..beebc34 100644 --- a/src/lib/helper.ts +++ b/src/lib/helper.ts @@ -148,4 +148,5 @@ export const [getIsMobile, setIsMobile] = createContext<() => boolean>(); export const [getCanEdit, setCanEdit] = createContext<() => boolean>(); -export const [getAdditionalUserData, setAdditionalUserData] = createContext(); \ No newline at end of file +export const [getAdditionalUserData, setAdditionalUserData] = + createContext>(); diff --git a/src/lib/oauth/atproto.ts b/src/lib/oauth/atproto.ts index 71fc48c..d57faad 100644 --- a/src/lib/oauth/atproto.ts +++ b/src/lib/oauth/atproto.ts @@ -194,4 +194,16 @@ export async function uploadImage({ await putRecord({ collection, record: { [key]: blobInfo }, rkey }); return blobInfo; +} + +export async function describeRepo({ did }: { did: string }) { + const pds = await getPDS(did); + + const agent = new AtpBaseClient({ service: pds }); + + const repo = await agent.com.atproto.repo.describeRepo({ + repo: did + }); + + return repo; } \ No newline at end of file diff --git a/src/lib/website/context.ts b/src/lib/website/context.ts index 63b3092..a0be5ff 100644 --- a/src/lib/website/context.ts +++ b/src/lib/website/context.ts @@ -11,6 +11,7 @@ export const [getUpdateRecordFunctionsContext, setUpdateRecordFunctionsContext] createContext(); export const [getDidContext, setDidContext] = createContext(); +export const [getHandleContext, setHandleContext] = createContext(); export const [getDataContext, setDataContext] = createContext(); diff --git a/src/lib/website/load.ts b/src/lib/website/load.ts index ad4b102..8dbfebc 100644 --- a/src/lib/website/load.ts +++ b/src/lib/website/load.ts @@ -9,6 +9,8 @@ import type { Record as ListRecord } from '@atproto/api/dist/client/types/com/at import { data } from './data'; import { AtpBaseClient } from '@atproto/api'; import { env } from '$env/dynamic/private'; +import { CardDefinitionsByType } from '$lib/cards'; +import type { Item } from '$lib/types'; export async function loadData(handle: string) { const did = await resolveHandle({ handle }); @@ -62,77 +64,42 @@ export async function loadData(handle: string) { } const cardTypes = new Set( - Object.values(downloadedData['app.blento.card']).map((v) => v.value.cardType) + Object.values(downloadedData['app.blento.card']).map((v) => v.value.cardType) as string[] ); - let recentRecords; - if (cardTypes.has('updatedBlentos')) { - try { - // https://ufos-api.microcosm.blue/records?collection=app.blento.card - const response = await fetch( - 'https://ufos-api.microcosm.blue/records?collection=app.blento.card' + const cardTypesArray = Array.from(cardTypes); + + const additionDataPromises: Record> = {}; + + const handleAndDid = { did, handle }; + + for (const cardType of cardTypesArray) { + const cardDef = CardDefinitionsByType[cardType]; + + if (cardDef.loadData) { + additionDataPromises[cardType] = cardDef.loadData( + Object.values(downloadedData['app.blento.card']) + .filter((v) => cardType == v.value.cardType) + .map((v) => v.value) as Item[], + handleAndDid ); - recentRecords = await response.json(); - } catch (error) { - console.error('failed to fetch recent records', error); } } - let recentPosts; + await Promise.all(Object.values(additionDataPromises)); - if (cardTypes.has('latestPost')) { + const additionalData: Record = {}; + for (const [key, value] of Object.entries(additionDataPromises)) { try { - const agent = new AtpBaseClient({ service: 'https://api.bsky.app' }); - const authorFeed = await agent.app.bsky.feed.getAuthorFeed({ - actor: did, - filter: 'posts_no_replies', - limit: 2 - }); - console.log(authorFeed.data); - recentPosts = JSON.parse(JSON.stringify(authorFeed.data)); + additionalData[key] = await value; } catch (error) { - console.error('failed to fetch recent posts', error); + console.log('error loading', key, error); } } - let metrics; - // try { - // const endAt = Date.now(); - - // const startAt = new Date(); - // startAt.setFullYear(startAt.getFullYear() - 1); - - // const params = new URLSearchParams({ - // startAt: startAt.getTime().toString(), - // endAt: endAt.toString(), - // unit: 'year', - // timezone: 'America/Los_Angeles', - // path: '/' + handle - // }); - - // const url = `https://umami-wispy-dream-8048.fly.dev/api/websites/${env.ANALYTICS_WEBSITE_ID}/stats?${params}`; - - // console.log(url); - // const metricsResponse = await fetch(url, { - // method: 'GET', - // headers: { - // Authorization: 'Bearer ' + env.ANALYTICS_TOKEN - // } - // }); - - // metrics = await metricsResponse.json(); - // console.log(metrics); - // } catch (error) { - // console.error(error); - // } - return { did, data: JSON.parse(JSON.stringify(downloadedData)) as DownloadedData, - additionalData: { - recentRecords, - recentPosts, - metrics - } + additionalData }; } -- 2.51.2