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 @@
+
+
+
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