From 458662ecf37a51fb92397d7245146cae1256b371 Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Tue, 23 Dec 2025 14:45:10 +0100 Subject: [PATCH 01/20] Initial pass support stream share images --- .../(pages)/[type]/[id]/+page.server.ts | 105 ++++++++++++++++++ .../(pages)/[type]/[id]/+page.svelte | 41 ++++++- .../share-images/(pages)/[type]/[id]/types.ts | 1 + 3 files changed, 146 insertions(+), 1 deletion(-) diff --git a/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts b/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts index fa868c5bc..00ca5baf7 100644 --- a/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts +++ b/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts @@ -17,6 +17,9 @@ import { fetchEcosystem } from '../../../../../(pages)/app/(app)/ecosystems/[eco import getOrcidDisplayName from '$lib/utils/orcids/display-name.js'; import { getRound } from '$lib/utils/rpgf/rpgf.js'; import { getWaveProgram } from '$lib/utils/wave/wavePrograms.js'; +import makeStreamId, { decodeStreamId } from '$lib/utils/streams/make-stream-id.js'; +import formatTokenAmount from '$lib/utils/format-token-amount.js'; +import { DRIPS_DEFAULT_TOKEN_LIST } from '$lib/stores/tokens/token-list.js'; function isShareImageType(value: string): value is ShareImageType { return Object.values(ShareImageType).includes(value as ShareImageType); @@ -230,6 +233,107 @@ async function loadRpgfRoundData(f: typeof fetch, id: string) { }; } +async function loadStreamData(f: typeof fetch, id: string) { + try { + const { senderAccountId, tokenAddress, dripId } = decodeStreamId(id); + + const streamQuery = gql` + query StreamShareImage($senderAccountId: ID!, $chains: [SupportedChain!]) { + streams(chains: $chains, where: { senderId: $senderAccountId }) { + id + name + sender { + account { + accountId + driver + } + chainData { + chain + } + } + receiver { + __typename + ... on User { + account { + accountId + driver + } + } + ... on DripList { + name + } + ... on EcosystemMainAccount { + name + owner { + address + } + } + } + config { + amountPerSecond { + amount + tokenAddress + } + } + } + } + `; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const res = await query( + streamQuery, + { senderAccountId, chains: [network.gqlName] }, + f, + ); + + const expectedStreamId = makeStreamId(senderAccountId, tokenAddress, dripId); + + const stream = res.streams.find( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (s: any) => s.id.toLowerCase() === expectedStreamId.toLowerCase(), + ); + + if (!stream) return null; + + const token = DRIPS_DEFAULT_TOKEN_LIST.find( + (t) => + t.address.toLowerCase() === tokenAddress.toLowerCase() && t.chainId === network.chainId, + ); + + const decimals = token?.decimals ?? 18; + const symbol = token?.symbol ?? 'Tokens'; + + const formattedAmount = formatTokenAmount( + { + amount: BigInt(stream.config.amountPerSecond.amount), + tokenAddress: stream.config.amountPerSecond.tokenAddress, + }, + decimals, + undefined, + false, + ); + + // Placeholder icons for now - we will need to improve this or pass generic icons + const senderIcon = `/api/twemoji-avatar.png?emoji=👤&bgColor=FFFFFF`; + const tokenIcon = token?.logoURI || `/api/twemoji-avatar.png?emoji=💰&bgColor=FFFFFF`; + const receiverIcon = `/api/twemoji-avatar.png?emoji=📥&bgColor=FFFFFF`; + + return { + bgColor: '#5555FF', // Default stream color + type: 'Stream', + headline: + stream.name || + (stream.receiver.__typename === 'DripList' ? 'Continuous donation' : 'Unnamed stream'), + avatarSrc: null, + streamIcons: [senderIcon, tokenIcon, receiverIcon], + streamAmount: `${formattedAmount} ${symbol} / month`, + stats: [], + }; + } catch (_) { + return null; + } +} + const LOAD_FNS = { [ShareImageType.WAVE_PROGRAM]: loadWaveProgramData, [ShareImageType.PROJECT]: loadProjectData, @@ -237,6 +341,7 @@ const LOAD_FNS = { [ShareImageType.ECOSYSTEM]: loadEcosystemData, [ShareImageType.ORCID]: loadOrcidData, [ShareImageType.RPGF_ROUND]: loadRpgfRoundData, + [ShareImageType.STREAM]: loadStreamData, } as const; export const load = async ({ params }) => { diff --git a/src/routes/api/share-images/(pages)/[type]/[id]/+page.svelte b/src/routes/api/share-images/(pages)/[type]/[id]/+page.svelte index 0446f6c6c..6c907675c 100644 --- a/src/routes/api/share-images/(pages)/[type]/[id]/+page.svelte +++ b/src/routes/api/share-images/(pages)/[type]/[id]/+page.svelte @@ -5,7 +5,7 @@ import backgroundImage from './background-image'; const { data } = $props(); - const { bgColor, type, headline, avatarSrc, stats } = $derived(data); + const { bgColor, type, headline, avatarSrc, stats, streamIcons, streamAmount } = $derived(data); const ICON_MAP: Record> = { DripList: DripList, @@ -49,6 +49,20 @@ {/each} {/if} + + {#if streamIcons && streamIcons.length > 0} + + {/if} @@ -137,4 +151,29 @@ align-items: center; gap: 8px; } + + .stream-footer { + display: flex; + align-items: center; + gap: 16px; + margin-top: 24px; + } + + .stream-icons { + display: flex; + gap: 8px; + } + + .stream-icon { + width: 64px; + height: 64px; + border-radius: 50%; + border: 2px solid black; + object-fit: cover; + background-color: white; /* Fallback */ + } + + .stream-amount { + font-size: 32px; + } diff --git a/src/routes/api/share-images/(pages)/[type]/[id]/types.ts b/src/routes/api/share-images/(pages)/[type]/[id]/types.ts index e766d9044..8dcb49c0d 100644 --- a/src/routes/api/share-images/(pages)/[type]/[id]/types.ts +++ b/src/routes/api/share-images/(pages)/[type]/[id]/types.ts @@ -15,4 +15,5 @@ export enum ShareImageType { ECOSYSTEM = 'ecosystem', ORCID = 'orcid', RPGF_ROUND = 'rpgf-round', + STREAM = 'stream', } From 1823e02bd6fca78b751aad293fece8009343d871 Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Tue, 23 Dec 2025 16:58:55 +0100 Subject: [PATCH 02/20] Fix stream type and name for shared image --- .../api/share-images/(pages)/[type]/[id]/+page.server.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts b/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts index 00ca5baf7..89db36c9f 100644 --- a/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts +++ b/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts @@ -320,10 +320,13 @@ async function loadStreamData(f: typeof fetch, id: string) { return { bgColor: '#5555FF', // Default stream color - type: 'Stream', + type: 'Continuous Donation', headline: stream.name || - (stream.receiver.__typename === 'DripList' ? 'Continuous donation' : 'Unnamed stream'), + (stream.receiver.__typename === 'DripList' || + stream.receiver.__typename === 'EcosystemMainAccount' + ? stream.receiver.name + : 'Unnamed stream'), avatarSrc: null, streamIcons: [senderIcon, tokenIcon, receiverIcon], streamAmount: `${formattedAmount} ${symbol} / month`, From b59aaecc96ee9956b86a7158d15ec68e8081f7b9 Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Tue, 23 Dec 2025 17:37:19 +0100 Subject: [PATCH 03/20] Refactor to use exsiting stats structure --- .../(pages)/[type]/[id]/+page.server.ts | 15 +++-- .../(pages)/[type]/[id]/+page.svelte | 55 +++++-------------- 2 files changed, 24 insertions(+), 46 deletions(-) diff --git a/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts b/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts index 89db36c9f..dd71e69fe 100644 --- a/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts +++ b/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts @@ -104,7 +104,7 @@ async function loadProjectData(f: typeof fetch, projectUrl: string) { stats: claimed ? [ { - icon: 'DripList', + icons: ['DripList'], label: `${chainData.splits.dependencies.length} dependencie${chainData.splits.dependencies.length === 1 ? '' : 's'}`, }, ] @@ -143,7 +143,7 @@ async function loadDripListData(f: typeof fetch, id: string) { avatarSrc: null, stats: [ { - icon: 'DripList', + icons: ['DripList'], label: `${dripList.splits.length} recipient${dripList.splits.length === 1 ? '' : 's'}`, }, ], @@ -164,7 +164,7 @@ async function loadEcosystemData(f: typeof fetch, id: string) { avatarSrc: null, stats: [ { - icon: 'DripList', + icons: ['DripList'], label: `${ecosystem.graph.nodes.length - 1} recipient${ecosystem.graph.nodes.length - 2 === 1 ? '' : 's'}`, }, ], @@ -328,9 +328,12 @@ async function loadStreamData(f: typeof fetch, id: string) { ? stream.receiver.name : 'Unnamed stream'), avatarSrc: null, - streamIcons: [senderIcon, tokenIcon, receiverIcon], - streamAmount: `${formattedAmount} ${symbol} / month`, - stats: [], + stats: [ + { + icons: [senderIcon, tokenIcon, receiverIcon], + label: `${formattedAmount} ${symbol} / month`, + }, + ], }; } catch (_) { return null; diff --git a/src/routes/api/share-images/(pages)/[type]/[id]/+page.svelte b/src/routes/api/share-images/(pages)/[type]/[id]/+page.svelte index 6c907675c..b5e09b0f7 100644 --- a/src/routes/api/share-images/(pages)/[type]/[id]/+page.svelte +++ b/src/routes/api/share-images/(pages)/[type]/[id]/+page.svelte @@ -5,7 +5,7 @@ import backgroundImage from './background-image'; const { data } = $props(); - const { bgColor, type, headline, avatarSrc, stats, streamIcons, streamAmount } = $derived(data); + const { bgColor, type, headline, avatarSrc, stats } = $derived(data); const ICON_MAP: Record> = { DripList: DripList, @@ -39,30 +39,21 @@ {#if (stats?.length ?? 0) > 0}
{#each stats as stat (stat.label)} - {@const Icon = ICON_MAP[stat.icon]}
- {#if Icon} - - {/if} + {#each stat.icons as visual (visual)} + {#if ICON_MAP[visual]} + {@const Icon = ICON_MAP[visual]} + + {:else} + + + {/if} + {/each} {stat.label}
{/each}
{/if} - - {#if streamIcons && streamIcons.length > 0} - - {/if} @@ -152,28 +143,12 @@ gap: 8px; } - .stream-footer { - display: flex; - align-items: center; - gap: 16px; - margin-top: 24px; - } - - .stream-icons { - display: flex; - gap: 8px; - } - - .stream-icon { - width: 64px; - height: 64px; + .stat-icon { + width: 32px; + height: 32px; border-radius: 50%; - border: 2px solid black; + border: 1px solid black; object-fit: cover; - background-color: white; /* Fallback */ - } - - .stream-amount { - font-size: 32px; + background-color: white; } From ffd36fefe10dd03d9b022a313833bc2bba9ff8ba Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Tue, 23 Dec 2025 17:39:01 +0100 Subject: [PATCH 04/20] The middle of the stream "icon" should be coin flying --- src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts | 2 +- src/routes/api/share-images/(pages)/[type]/[id]/+page.svelte | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts b/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts index dd71e69fe..7af374559 100644 --- a/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts +++ b/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts @@ -315,7 +315,7 @@ async function loadStreamData(f: typeof fetch, id: string) { // Placeholder icons for now - we will need to improve this or pass generic icons const senderIcon = `/api/twemoji-avatar.png?emoji=👤&bgColor=FFFFFF`; - const tokenIcon = token?.logoURI || `/api/twemoji-avatar.png?emoji=💰&bgColor=FFFFFF`; + const tokenIcon = 'CoinFlying'; const receiverIcon = `/api/twemoji-avatar.png?emoji=📥&bgColor=FFFFFF`; return { diff --git a/src/routes/api/share-images/(pages)/[type]/[id]/+page.svelte b/src/routes/api/share-images/(pages)/[type]/[id]/+page.svelte index b5e09b0f7..c8aa4c343 100644 --- a/src/routes/api/share-images/(pages)/[type]/[id]/+page.svelte +++ b/src/routes/api/share-images/(pages)/[type]/[id]/+page.svelte @@ -1,5 +1,6 @@ @@ -43,12 +85,20 @@ {#each stats as stat (stat.label)}
{#each stat.icons as visual (visual)} - {#if ICON_MAP[visual]} - {@const Icon = ICON_MAP[visual]} - - {:else} - - + {#if typeof visual === 'string'} + {#if ICON_MAP[visual]} + {@const Icon = ICON_MAP[visual]} + + {:else} + + + {/if} + {:else if BADGE_CONFIG[visual.type]} + {@const config = BADGE_CONFIG[visual.type]} + {@const Badge = config.component} +
+ +
{/if} {/each} {stat.label} @@ -153,4 +203,10 @@ object-fit: cover; background-color: white; } + + .badge-wrapper { + /* Ensure badges don't have unexpected margins */ + display: flex; + align-items: center; + } diff --git a/src/routes/api/share-images/(pages)/[type]/[id]/types.ts b/src/routes/api/share-images/(pages)/[type]/[id]/types.ts index 8dcb49c0d..b3d06d5b2 100644 --- a/src/routes/api/share-images/(pages)/[type]/[id]/types.ts +++ b/src/routes/api/share-images/(pages)/[type]/[id]/types.ts @@ -17,3 +17,12 @@ export enum ShareImageType { RPGF_ROUND = 'rpgf-round', STREAM = 'stream', } + +import type { DripListBadgeFragment } from '$lib/components/drip-list-badge/__generated__/gql.generated'; +import type { EcosystemBadgeFragment } from '$lib/components/ecosystem-badge/__generated__/gql.generated'; + +export type VisualBadge = + | string + | { type: 'identity'; data: string } + | { type: 'drip-list'; data: DripListBadgeFragment } + | { type: 'ecosystem'; data: EcosystemBadgeFragment }; From 0a09fed02e04843e9aa2d5ac57d119675846116c Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Tue, 23 Dec 2025 18:12:00 +0100 Subject: [PATCH 06/20] Get address directly from user gql objs --- .../share-images/(pages)/[type]/[id]/+page.server.ts | 8 +++++--- .../api/share-images/(pages)/[type]/[id]/+page.svelte | 11 +++++++---- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts b/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts index 16b0c8d71..40790ad33 100644 --- a/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts +++ b/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts @@ -12,9 +12,9 @@ import type { OrcidQuery, OrcidQueryVariables, } from './__generated__/gql.generated.js'; +import type { AddressDriverAccount } from '$lib/graphql/__generated__/base-types'; import { DRIP_LIST_BADGE_FRAGMENT } from '$lib/components/drip-list-badge/drip-list-badge.svelte'; import { ECOSYSTEM_BADGE_FRAGMENT } from '$lib/components/ecosystem-badge/ecosystem-badge.svelte'; -import extractAddressFromAccountId from '$lib/utils/sdk/utils/extract-address-from-accountId.js'; import filterCurrentChainData from '$lib/utils/filter-current-chain-data.js'; import { fetchEcosystem } from '../../../../../(pages)/app/(app)/ecosystems/[ecosystemId]/fetch-ecosystem.js'; import getOrcidDisplayName from '$lib/utils/orcids/display-name.js'; @@ -251,6 +251,7 @@ async function loadStreamData(f: typeof fetch, id: string) { account { accountId driver + address } chainData { chain @@ -262,6 +263,7 @@ async function loadStreamData(f: typeof fetch, id: string) { account { accountId driver + address } } ...DripListBadge @@ -320,7 +322,7 @@ async function loadStreamData(f: typeof fetch, id: string) { // If sender is AddressDriver (standard user), convert ID to address if (senderDriverId === network.contracts.ADDRESS_DRIVER) { - const senderAddress = extractAddressFromAccountId(stream.sender.account.accountId); + const senderAddress = (stream.sender.account as AddressDriverAccount).address; senderVisual = { type: 'identity', data: senderAddress }; } else { // TODO: Handle other drivers (e.g. NFT driver) if needed. @@ -346,7 +348,7 @@ async function loadStreamData(f: typeof fetch, id: string) { const receiverDriverId = stream.receiver.account.driver; if (receiverDriverId === network.contracts.ADDRESS_DRIVER) { - const receiverAddress = extractAddressFromAccountId(stream.receiver.account.accountId); + const receiverAddress = (stream.receiver.account as AddressDriverAccount).address; receiverVisual = { type: 'identity', data: receiverAddress }; } else { // Fallback diff --git a/src/routes/api/share-images/(pages)/[type]/[id]/+page.svelte b/src/routes/api/share-images/(pages)/[type]/[id]/+page.svelte index 48a4f8424..5375f1c14 100644 --- a/src/routes/api/share-images/(pages)/[type]/[id]/+page.svelte +++ b/src/routes/api/share-images/(pages)/[type]/[id]/+page.svelte @@ -18,10 +18,13 @@ const BADGE_CONFIG: Record< string, - { component: Component; getProps: (data: unknown) => Record } + { + component: Component>; + getProps: (data: unknown) => Record; + } > = { identity: { - component: IdentityBadge, + component: IdentityBadge as unknown as Component>, getProps: (data) => ({ address: data as string, showIdentity: false, @@ -32,7 +35,7 @@ }), }, 'drip-list': { - component: DripListBadge, + component: DripListBadge as unknown as Component>, getProps: (data) => ({ dripList: data as import('$lib/components/drip-list-badge/__generated__/gql.generated').DripListBadgeFragment, @@ -43,7 +46,7 @@ }), }, ecosystem: { - component: EcosystemBadge, + component: EcosystemBadge as unknown as Component>, getProps: (data) => ({ ecosystem: data as import('$lib/components/ecosystem-badge/__generated__/gql.generated').EcosystemBadgeFragment, From 13076981831843881e6710a0149e27e8feab6143 Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Wed, 24 Dec 2025 15:56:04 +0100 Subject: [PATCH 07/20] Properly use switch statement for stream share image --- .../(pages)/[type]/[id]/+page.server.ts | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts b/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts index 40790ad33..360e8833b 100644 --- a/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts +++ b/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts @@ -341,27 +341,25 @@ async function loadStreamData(f: typeof fetch, id: string) { case 'EcosystemMainAccount': receiverVisual = { type: 'ecosystem', data: stream.receiver }; break; - case 'User': - default: - // Handle User receiver - if (stream.receiver.__typename === 'User') { - const receiverDriverId = stream.receiver.account.driver; - - if (receiverDriverId === network.contracts.ADDRESS_DRIVER) { - const receiverAddress = (stream.receiver.account as AddressDriverAccount).address; - receiverVisual = { type: 'identity', data: receiverAddress }; - } else { - // Fallback - receiverVisual = { - type: 'identity', - data: '0x0000000000000000000000000000000000000000', - }; - } + case 'User': { + const receiverDriverId = stream.receiver.account.driver; + + if (receiverDriverId === network.contracts.ADDRESS_DRIVER) { + const receiverAddress = (stream.receiver.account as AddressDriverAccount).address; + receiverVisual = { type: 'identity', data: receiverAddress }; } else { - // Fallback for unknown - receiverVisual = { type: 'identity', data: '0x0000000000000000000000000000000000000000' }; + // Fallback + receiverVisual = { + type: 'identity', + data: '0x0000000000000000000000000000000000000000', + }; } break; + } + default: + // Fallback for unknown + receiverVisual = { type: 'identity', data: '0x0000000000000000000000000000000000000000' }; + break; } return { From dcb64d60a35c2b246e213139674039d2b624a0e5 Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Wed, 24 Dec 2025 16:00:34 +0100 Subject: [PATCH 08/20] Simplify handling of sender address for stream share image --- .../(pages)/[type]/[id]/+page.server.ts | 31 +++---------------- 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts b/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts index 360e8833b..ab7fc507c 100644 --- a/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts +++ b/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts @@ -314,21 +314,8 @@ async function loadStreamData(f: typeof fetch, id: string) { ); // Construct visuals - const senderDriverId = stream.sender.account.driver; - let senderVisual: VisualBadge = { - type: 'identity', - data: '0x0000000000000000000000000000000000000000', - }; - - // If sender is AddressDriver (standard user), convert ID to address - if (senderDriverId === network.contracts.ADDRESS_DRIVER) { - const senderAddress = (stream.sender.account as AddressDriverAccount).address; - senderVisual = { type: 'identity', data: senderAddress }; - } else { - // TODO: Handle other drivers (e.g. NFT driver) if needed. - // For now fallback to a placeholder or maybe try to treat as address if possible. - // But usually stream sender is a user. - } + const senderAddress = (stream.sender.account as AddressDriverAccount).address; + const senderVisual: VisualBadge = { type: 'identity', data: senderAddress }; const tokenIcon = 'CoinFlying'; @@ -342,18 +329,8 @@ async function loadStreamData(f: typeof fetch, id: string) { receiverVisual = { type: 'ecosystem', data: stream.receiver }; break; case 'User': { - const receiverDriverId = stream.receiver.account.driver; - - if (receiverDriverId === network.contracts.ADDRESS_DRIVER) { - const receiverAddress = (stream.receiver.account as AddressDriverAccount).address; - receiverVisual = { type: 'identity', data: receiverAddress }; - } else { - // Fallback - receiverVisual = { - type: 'identity', - data: '0x0000000000000000000000000000000000000000', - }; - } + const receiverAddress = (stream.receiver.account as AddressDriverAccount).address; + receiverVisual = { type: 'identity', data: receiverAddress }; break; } default: From 2b203ed437af2c19d707b50a733b64ef5005c82c Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Wed, 24 Dec 2025 16:02:56 +0100 Subject: [PATCH 09/20] Remove surrounding try catch block from stream share image load --- .../(pages)/[type]/[id]/+page.server.ts | 199 +++++++++--------- 1 file changed, 95 insertions(+), 104 deletions(-) diff --git a/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts b/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts index ab7fc507c..1bd677ef0 100644 --- a/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts +++ b/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts @@ -237,128 +237,119 @@ async function loadRpgfRoundData(f: typeof fetch, id: string) { } async function loadStreamData(f: typeof fetch, id: string) { - try { - const { senderAccountId, tokenAddress, dripId } = decodeStreamId(id); - - const streamQuery = gql` - query StreamShareImage($senderAccountId: ID!, $chains: [SupportedChain!]) { - ${DRIP_LIST_BADGE_FRAGMENT} - ${ECOSYSTEM_BADGE_FRAGMENT} - streams(chains: $chains, where: { senderId: $senderAccountId }) { - id - name - sender { + const { senderAccountId, tokenAddress, dripId } = decodeStreamId(id); + + const streamQuery = gql` + query StreamShareImage($senderAccountId: ID!, $chains: [SupportedChain!]) { + ${DRIP_LIST_BADGE_FRAGMENT} + ${ECOSYSTEM_BADGE_FRAGMENT} + streams(chains: $chains, where: { senderId: $senderAccountId }) { + id + name + sender { + account { + accountId + driver + address + } + chainData { + chain + } + } + receiver { + __typename + ... on User { account { accountId driver address } - chainData { - chain - } - } - receiver { - __typename - ... on User { - account { - accountId - driver - address - } - } - ...DripListBadge - ...EcosystemBadge } - config { - amountPerSecond { - amount - tokenAddress - } + ...DripListBadge + ...EcosystemBadge + } + config { + amountPerSecond { + amount + tokenAddress } } } - `; + } + `; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const res = await query( - streamQuery, - { senderAccountId, chains: [network.gqlName] }, - f, - ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const res = await query(streamQuery, { senderAccountId, chains: [network.gqlName] }, f); + + const expectedStreamId = makeStreamId(senderAccountId, tokenAddress, dripId); - const expectedStreamId = makeStreamId(senderAccountId, tokenAddress, dripId); + const stream = res.streams.find( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (s: any) => s.id.toLowerCase() === expectedStreamId.toLowerCase(), + ); - const stream = res.streams.find( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (s: any) => s.id.toLowerCase() === expectedStreamId.toLowerCase(), - ); + if (!stream) return null; - if (!stream) return null; + const token = DRIPS_DEFAULT_TOKEN_LIST.find( + (t) => t.address.toLowerCase() === tokenAddress.toLowerCase() && t.chainId === network.chainId, + ); - const token = DRIPS_DEFAULT_TOKEN_LIST.find( - (t) => - t.address.toLowerCase() === tokenAddress.toLowerCase() && t.chainId === network.chainId, - ); + const decimals = token?.decimals ?? 18; + const symbol = token?.symbol ?? 'Tokens'; + + const formattedAmount = formatTokenAmount( + { + amount: BigInt(stream.config.amountPerSecond.amount), + tokenAddress: stream.config.amountPerSecond.tokenAddress, + }, + decimals, + undefined, + false, + ); - const decimals = token?.decimals ?? 18; - const symbol = token?.symbol ?? 'Tokens'; + // Construct visuals + const senderAddress = (stream.sender.account as AddressDriverAccount).address; + const senderVisual: VisualBadge = { type: 'identity', data: senderAddress }; + + const tokenIcon = 'CoinFlying'; + + let receiverVisual: VisualBadge; + + switch (stream.receiver.__typename) { + case 'DripList': + receiverVisual = { type: 'drip-list', data: stream.receiver }; + break; + case 'EcosystemMainAccount': + receiverVisual = { type: 'ecosystem', data: stream.receiver }; + break; + case 'User': { + const receiverAddress = (stream.receiver.account as AddressDriverAccount).address; + receiverVisual = { type: 'identity', data: receiverAddress }; + break; + } + default: + // Fallback for unknown + receiverVisual = { type: 'identity', data: '0x0000000000000000000000000000000000000000' }; + break; + } - const formattedAmount = formatTokenAmount( + return { + bgColor: '#5555FF', // Default stream color + type: 'Continuous Donation', + headline: + stream.name || + (stream.receiver.__typename === 'DripList' || + stream.receiver.__typename === 'EcosystemMainAccount' + ? stream.receiver.name + : 'Unnamed stream'), + avatarSrc: null, + stats: [ { - amount: BigInt(stream.config.amountPerSecond.amount), - tokenAddress: stream.config.amountPerSecond.tokenAddress, + icons: [senderVisual, tokenIcon, receiverVisual], + label: `${formattedAmount} ${symbol} / month`, }, - decimals, - undefined, - false, - ); - - // Construct visuals - const senderAddress = (stream.sender.account as AddressDriverAccount).address; - const senderVisual: VisualBadge = { type: 'identity', data: senderAddress }; - - const tokenIcon = 'CoinFlying'; - - let receiverVisual: VisualBadge; - - switch (stream.receiver.__typename) { - case 'DripList': - receiverVisual = { type: 'drip-list', data: stream.receiver }; - break; - case 'EcosystemMainAccount': - receiverVisual = { type: 'ecosystem', data: stream.receiver }; - break; - case 'User': { - const receiverAddress = (stream.receiver.account as AddressDriverAccount).address; - receiverVisual = { type: 'identity', data: receiverAddress }; - break; - } - default: - // Fallback for unknown - receiverVisual = { type: 'identity', data: '0x0000000000000000000000000000000000000000' }; - break; - } - - return { - bgColor: '#5555FF', // Default stream color - type: 'Continuous Donation', - headline: - stream.name || - (stream.receiver.__typename === 'DripList' || - stream.receiver.__typename === 'EcosystemMainAccount' - ? stream.receiver.name - : 'Unnamed stream'), - avatarSrc: null, - stats: [ - { - icons: [senderVisual, tokenIcon, receiverVisual], - label: `${formattedAmount} ${symbol} / month`, - }, - ], - }; - } catch (_) { - return null; - } + ], + }; } const LOAD_FNS = { From 5b0ecf7b250fc1051f87f468f234ef588651dbce Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Wed, 24 Dec 2025 16:41:27 +0100 Subject: [PATCH 10/20] Generalize icons to visuals --- .../(pages)/[type]/[id]/+page.server.ts | 10 ++++----- .../(pages)/[type]/[id]/+page.svelte | 21 ++++++++++++------- .../share-images/(pages)/[type]/[id]/types.ts | 2 +- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts b/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts index 1bd677ef0..eef15d1ba 100644 --- a/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts +++ b/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts @@ -107,7 +107,7 @@ async function loadProjectData(f: typeof fetch, projectUrl: string) { stats: claimed ? [ { - icons: ['DripList'], + visuals: [{ type: 'icon', data: 'DripList' }], label: `${chainData.splits.dependencies.length} dependencie${chainData.splits.dependencies.length === 1 ? '' : 's'}`, }, ] @@ -146,7 +146,7 @@ async function loadDripListData(f: typeof fetch, id: string) { avatarSrc: null, stats: [ { - icons: ['DripList'], + visuals: [{ type: 'icon', data: 'DripList' }], label: `${dripList.splits.length} recipient${dripList.splits.length === 1 ? '' : 's'}`, }, ], @@ -167,7 +167,7 @@ async function loadEcosystemData(f: typeof fetch, id: string) { avatarSrc: null, stats: [ { - icons: ['DripList'], + visuals: [{ type: 'icon', data: 'DripList' }], label: `${ecosystem.graph.nodes.length - 1} recipient${ecosystem.graph.nodes.length - 2 === 1 ? '' : 's'}`, }, ], @@ -311,7 +311,7 @@ async function loadStreamData(f: typeof fetch, id: string) { const senderAddress = (stream.sender.account as AddressDriverAccount).address; const senderVisual: VisualBadge = { type: 'identity', data: senderAddress }; - const tokenIcon = 'CoinFlying'; + const tokenIcon: VisualBadge = { type: 'icon', data: 'CoinFlying' }; let receiverVisual: VisualBadge; @@ -345,7 +345,7 @@ async function loadStreamData(f: typeof fetch, id: string) { avatarSrc: null, stats: [ { - icons: [senderVisual, tokenIcon, receiverVisual], + visuals: [senderVisual, tokenIcon, receiverVisual], label: `${formattedAmount} ${symbol} / month`, }, ], diff --git a/src/routes/api/share-images/(pages)/[type]/[id]/+page.svelte b/src/routes/api/share-images/(pages)/[type]/[id]/+page.svelte index 5375f1c14..419a924cf 100644 --- a/src/routes/api/share-images/(pages)/[type]/[id]/+page.svelte +++ b/src/routes/api/share-images/(pages)/[type]/[id]/+page.svelte @@ -19,10 +19,18 @@ const BADGE_CONFIG: Record< string, { - component: Component>; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + component: Component; getProps: (data: unknown) => Record; } > = { + icon: { + component: null as unknown as Component>, // Special case handled manually in the template for now, or we can make a wrapper. + // To strictly follow the pattern, we'd need an IconWrapper component. + // But for now, let's keep the manual check but triggered by `type === 'icon'`. + getProps: (_) => ({}), + }, + identity: { component: IdentityBadge as unknown as Component>, getProps: (data) => ({ @@ -87,14 +95,11 @@
{#each stats as stat (stat.label)}
- {#each stat.icons as visual (visual)} - {#if typeof visual === 'string'} - {#if ICON_MAP[visual]} - {@const Icon = ICON_MAP[visual]} + {#each stat.visuals as visual (visual)} + {#if visual.type === 'icon'} + {#if ICON_MAP[visual.data]} + {@const Icon = ICON_MAP[visual.data]} - {:else} - - {/if} {:else if BADGE_CONFIG[visual.type]} {@const config = BADGE_CONFIG[visual.type]} diff --git a/src/routes/api/share-images/(pages)/[type]/[id]/types.ts b/src/routes/api/share-images/(pages)/[type]/[id]/types.ts index b3d06d5b2..7ca886643 100644 --- a/src/routes/api/share-images/(pages)/[type]/[id]/types.ts +++ b/src/routes/api/share-images/(pages)/[type]/[id]/types.ts @@ -22,7 +22,7 @@ import type { DripListBadgeFragment } from '$lib/components/drip-list-badge/__ge import type { EcosystemBadgeFragment } from '$lib/components/ecosystem-badge/__generated__/gql.generated'; export type VisualBadge = - | string + | { type: 'icon'; data: string } | { type: 'identity'; data: string } | { type: 'drip-list'; data: DripListBadgeFragment } | { type: 'ecosystem'; data: EcosystemBadgeFragment }; From 2203c04da67cdbddb4c719e7bfa5bbdde9f438dc Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Wed, 24 Dec 2025 16:55:49 +0100 Subject: [PATCH 11/20] Treat icons like badges --- .../(pages)/[type]/[id]/+page.server.ts | 8 ++--- .../(pages)/[type]/[id]/+page.svelte | 34 ++++++++----------- .../share-images/(pages)/[type]/[id]/types.ts | 3 +- 3 files changed, 21 insertions(+), 24 deletions(-) diff --git a/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts b/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts index eef15d1ba..d283f6b02 100644 --- a/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts +++ b/src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts @@ -107,7 +107,7 @@ async function loadProjectData(f: typeof fetch, projectUrl: string) { stats: claimed ? [ { - visuals: [{ type: 'icon', data: 'DripList' }], + visuals: [{ type: 'drip-list-icon', data: undefined }], label: `${chainData.splits.dependencies.length} dependencie${chainData.splits.dependencies.length === 1 ? '' : 's'}`, }, ] @@ -146,7 +146,7 @@ async function loadDripListData(f: typeof fetch, id: string) { avatarSrc: null, stats: [ { - visuals: [{ type: 'icon', data: 'DripList' }], + visuals: [{ type: 'drip-list-icon', data: undefined }], label: `${dripList.splits.length} recipient${dripList.splits.length === 1 ? '' : 's'}`, }, ], @@ -167,7 +167,7 @@ async function loadEcosystemData(f: typeof fetch, id: string) { avatarSrc: null, stats: [ { - visuals: [{ type: 'icon', data: 'DripList' }], + visuals: [{ type: 'drip-list-icon', data: undefined }], label: `${ecosystem.graph.nodes.length - 1} recipient${ecosystem.graph.nodes.length - 2 === 1 ? '' : 's'}`, }, ], @@ -311,7 +311,7 @@ async function loadStreamData(f: typeof fetch, id: string) { const senderAddress = (stream.sender.account as AddressDriverAccount).address; const senderVisual: VisualBadge = { type: 'identity', data: senderAddress }; - const tokenIcon: VisualBadge = { type: 'icon', data: 'CoinFlying' }; + const tokenIcon: VisualBadge = { type: 'coin-flying', data: undefined }; let receiverVisual: VisualBadge; diff --git a/src/routes/api/share-images/(pages)/[type]/[id]/+page.svelte b/src/routes/api/share-images/(pages)/[type]/[id]/+page.svelte index 419a924cf..75cdd51b0 100644 --- a/src/routes/api/share-images/(pages)/[type]/[id]/+page.svelte +++ b/src/routes/api/share-images/(pages)/[type]/[id]/+page.svelte @@ -1,21 +1,16 @@ @@ -97,13 +36,7 @@ {#each stats as stat (stat.label)}
{#each stat.visuals as visual (visual)} - {#if BADGE_CONFIG[visual.type]} - {@const config = BADGE_CONFIG[visual.type]} - {@const Badge = config.component} -
- -
- {/if} + {/each} {stat.label}
@@ -207,10 +140,4 @@ object-fit: cover; background-color: white; } - - .badge-wrapper { - /* Ensure badges don't have unexpected margins */ - display: flex; - align-items: center; - } diff --git a/src/routes/api/share-images/(pages)/[type]/[id]/VisualRenderer.svelte b/src/routes/api/share-images/(pages)/[type]/[id]/VisualRenderer.svelte new file mode 100644 index 000000000..c7d077492 --- /dev/null +++ b/src/routes/api/share-images/(pages)/[type]/[id]/VisualRenderer.svelte @@ -0,0 +1,88 @@ + + +{#if BADGE_CONFIG[visual.type]} + {@const config = BADGE_CONFIG[visual.type]} + {@const Badge = config.component} +
+ +
+{/if} + + From cf5f8eb0151e1a91ce73c61a5299491e54a7d2a8 Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Wed, 24 Dec 2025 17:33:35 +0100 Subject: [PATCH 13/20] Simplify rendering in visual renderer --- .../(pages)/[type]/[id]/VisualRenderer.svelte | 96 +++++++------------ 1 file changed, 32 insertions(+), 64 deletions(-) diff --git a/src/routes/api/share-images/(pages)/[type]/[id]/VisualRenderer.svelte b/src/routes/api/share-images/(pages)/[type]/[id]/VisualRenderer.svelte index c7d077492..39b8a402a 100644 --- a/src/routes/api/share-images/(pages)/[type]/[id]/VisualRenderer.svelte +++ b/src/routes/api/share-images/(pages)/[type]/[id]/VisualRenderer.svelte @@ -1,5 +1,4 @@ -{#if BADGE_CONFIG[visual.type]} - {@const config = BADGE_CONFIG[visual.type]} - {@const Badge = config.component} -
- -
-{/if} +
+ {#if visual.type === 'coin-flying'} + + {:else if visual.type === 'drip-list-icon'} + + {:else if visual.type === 'identity'} + + {:else if visual.type === 'drip-list'} + + {:else if visual.type === 'ecosystem'} + + {/if} +