Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
${EDIT_STREAM_FLOW_STREAM}
${STREAM_STATE_BADGE_STREAM_FRAGMENT}
fragment StreamPageStream on Stream {
id
...StreamStateBadgeStream
...EditStreamFlowStream
...DeleteStreamConfirmStep
Expand Down Expand Up @@ -196,7 +197,11 @@
);
</script>

<HeadMeta title={stream.name ?? 'Stream'} />
<HeadMeta
title={stream.name ?? 'Stream'}
image="/api/share-images/stream/{stream.id}.png"
twitterImage="/api/share-images/stream/{stream.id}.png"
/>

<div class="wrapper">
<div class="header">
Expand Down
134 changes: 130 additions & 4 deletions src/routes/api/share-images/(pages)/[type]/[id]/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { error } from '@sveltejs/kit';
import { ShareImageType } from './types.js';
import { ShareImageType, type ShareImageVisual } from './types.js';
import { gql } from 'graphql-request';
import query from '$lib/graphql/dripsQL.js';
import network from '$lib/stores/wallet/network.js';
Expand All @@ -11,12 +11,21 @@ import type {
ProjectQueryVariables,
OrcidQuery,
OrcidQueryVariables,
StreamQuery,
StreamQueryVariables,
} 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 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';
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';
import { MULTIPLIERS } from '$lib/stores/amt-delta-unit/amt-delta-unit.store.js';

function isShareImageType(value: string): value is ShareImageType {
return Object.values(ShareImageType).includes(value as ShareImageType);
Expand Down Expand Up @@ -101,7 +110,7 @@ async function loadProjectData(f: typeof fetch, projectUrl: string) {
stats: claimed
? [
{
icon: 'DripList',
visuals: [{ type: 'drip-list-icon', data: undefined } as const],
label: `${chainData.splits.dependencies.length} dependencie${chainData.splits.dependencies.length === 1 ? '' : 's'}`,
},
]
Expand Down Expand Up @@ -140,7 +149,7 @@ async function loadDripListData(f: typeof fetch, id: string) {
avatarSrc: null,
stats: [
{
icon: 'DripList',
visuals: [{ type: 'drip-list-icon', data: undefined } as const],
label: `${dripList.splits.length} recipient${dripList.splits.length === 1 ? '' : 's'}`,
},
],
Expand All @@ -161,7 +170,7 @@ async function loadEcosystemData(f: typeof fetch, id: string) {
avatarSrc: null,
stats: [
{
icon: 'DripList',
visuals: [{ type: 'drip-list-icon', data: undefined } as const],
label: `${ecosystem.graph.nodes.length - 1} recipient${ecosystem.graph.nodes.length - 2 === 1 ? '' : 's'}`,
},
],
Expand Down Expand Up @@ -230,13 +239,130 @@ async function loadRpgfRoundData(f: typeof fetch, id: string) {
};
}

async function loadStreamData(f: typeof fetch, id: string) {
const { senderAccountId, tokenAddress, dripId } = decodeStreamId(id);

const streamQuery = gql`
${DRIP_LIST_BADGE_FRAGMENT}
${ECOSYSTEM_BADGE_FRAGMENT}
query Stream($senderAccountId: ID!, $chains: [SupportedChain!]) {
streams(chains: $chains, where: { senderId: $senderAccountId }) {
id
name
sender {
account {
accountId
driver
address
}
chainData {
chain
}
}
receiver {
__typename
... on User {
account {
accountId
driver
address
}
}
...DripListBadge
...EcosystemBadge
}
config {
amountPerSecond {
amount
tokenAddress
}
}
}
}
`;

const res = await query<StreamQuery, StreamQueryVariables>(
streamQuery,
{ senderAccountId, chains: [network.gqlName] },
f,
);

const expectedStreamId = makeStreamId(senderAccountId, tokenAddress, dripId);

const stream = res.streams.find((s) => 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) * BigInt(MULTIPLIERS['30-days']),
tokenAddress: stream.config.amountPerSecond.tokenAddress,
},
decimals,
undefined,
false,
);

// Construct visuals
const senderAddress = (stream.sender.account as AddressDriverAccount).address;
const senderVisual: ShareImageVisual = { type: 'identity', data: senderAddress };

const tokenIcon: ShareImageVisual = { type: 'coin-flying', data: undefined };

let receiverVisual: ShareImageVisual;

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: [
{
visuals: [senderVisual, tokenIcon, receiverVisual],
label: `${formattedAmount} ${symbol} / month`,
},
],
};
}

const LOAD_FNS = {
[ShareImageType.WAVE_PROGRAM]: loadWaveProgramData,
[ShareImageType.PROJECT]: loadProjectData,
[ShareImageType.DRIP_LIST]: loadDripListData,
[ShareImageType.ECOSYSTEM]: loadEcosystemData,
[ShareImageType.ORCID]: loadOrcidData,
[ShareImageType.RPGF_ROUND]: loadRpgfRoundData,
[ShareImageType.STREAM]: loadStreamData,
} as const;

export const load = async ({ params }) => {
Expand Down
25 changes: 14 additions & 11 deletions src/routes/api/share-images/(pages)/[type]/[id]/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
<script lang="ts">
import DripList from '$lib/components/icons/DripList.svelte';
import getContrastColor from '$lib/utils/get-contrast-text-color';
import type { Component } from 'svelte';
import ShareImageVisualRenderer from './ShareImageVisualRenderer.svelte';
import backgroundImage from './background-image';
import getContrastColor from '$lib/utils/get-contrast-text-color';

const { data } = $props();
const { bgColor, type, headline, avatarSrc, stats } = $derived(data);

const ICON_MAP: Record<string, Component<{ style?: string }>> = {
DripList: DripList,
};

const contrastColor = $derived(getContrastColor(bgColor));
const renderedBgImage = $derived(backgroundImage(bgColor, contrastColor));
</script>
Expand Down Expand Up @@ -39,11 +34,10 @@
{#if (stats?.length ?? 0) > 0}
<div class="stats">
{#each stats as stat (stat.label)}
{@const Icon = ICON_MAP[stat.icon]}
<div class="stat">
{#if Icon}
<Icon style="fill: {contrastColor}; height: 32px; width: 32px;" />
{/if}
{#each stat.visuals as visual (visual)}
<ShareImageVisualRenderer {visual} color={contrastColor} />
{/each}
<span class="label">{stat.label}</span>
</div>
{/each}
Expand Down Expand Up @@ -137,4 +131,13 @@
align-items: center;
gap: 8px;
}

.stat-icon {
width: 32px;
height: 32px;
border-radius: 50%;
border: 1px solid black;
object-fit: cover;
background-color: white;
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<script lang="ts">
import type { ShareImageVisual } from './types';
import IdentityBadge from '$lib/components/identity-badge/identity-badge.svelte';
import EcosystemBadge from '$lib/components/ecosystem-badge/ecosystem-badge.svelte';
import DripListIcon from '$lib/components/icons/DripList.svelte';
import CoinFlying from '$lib/components/icons/CoinFlying.svelte';

interface Props {
visual: ShareImageVisual;
color: string;
}

let { visual, color }: Props = $props();
</script>

<div class="badge-wrapper">
{#if visual.type === 'coin-flying'}
<CoinFlying style="fill: {color}; height: 32px; width: 32px;" />
{:else if visual.type === 'drip-list-icon'}
<DripListIcon style="fill: {color}; height: 32px; width: 32px;" />
{:else if visual.type === 'identity'}
<IdentityBadge
address={visual.data}
showIdentity={false}
showAvatar={true}
size="medium"
disableLink={true}
disableTooltip={true}
/>
{:else if visual.type === 'drip-list'}
<div style="display: flex; align-items: center; gap: 8px;">
<div
style="
height: 32px;
width: 32px;
background-color: transparent;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid {color};
"
>
<DripListIcon style="fill: {color}; height: 80%; width: 80%;" />
</div>
<span style="font-size: 24px; color: {color}; white-space: nowrap;">
{visual.data.name}
</span>
</div>
{:else if visual.type === 'ecosystem'}
<EcosystemBadge
ecosystem={visual.data}
showName={false}
showAvatar={true}
avatarSize="small"
disabled={true}
/>
{/if}
</div>

<style>
.badge-wrapper {
/* Ensure badges don't have unexpected margins */
display: flex;
align-items: center;
}
</style>
11 changes: 11 additions & 0 deletions src/routes/api/share-images/(pages)/[type]/[id]/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,15 @@ export enum ShareImageType {
ECOSYSTEM = 'ecosystem',
ORCID = 'orcid',
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 ShareImageVisual =
| { type: 'coin-flying'; data: undefined }
| { type: 'drip-list-icon'; data: undefined }
| { type: 'identity'; data: string }
| { type: 'drip-list'; data: DripListBadgeFragment }
| { type: 'ecosystem'; data: EcosystemBadgeFragment };