diff --git a/src/lib/components/efp-stats/efp-stats.svelte b/src/lib/components/efp-stats/efp-stats.svelte
new file mode 100644
index 000000000..6c633f94b
--- /dev/null
+++ b/src/lib/components/efp-stats/efp-stats.svelte
@@ -0,0 +1,92 @@
+
+
+{#if network.enableEfp && (hasStats || showCommon)}
+
+{/if}
+
+
diff --git a/src/lib/components/efp-stats/efp-stats.unit.test.ts b/src/lib/components/efp-stats/efp-stats.unit.test.ts
new file mode 100644
index 000000000..a94ecc7f7
--- /dev/null
+++ b/src/lib/components/efp-stats/efp-stats.unit.test.ts
@@ -0,0 +1,62 @@
+import { render, screen } from '@testing-library/svelte';
+import EfpStats from './efp-stats.svelte';
+
+vi.mock('$lib/stores/wallet/network', () => ({
+ default: { enableEfp: true },
+}));
+
+vi.mock('$lib/components/identity-badge/identity-badge.svelte', async () => ({
+ default: (await import('./identity-badge-stub.svelte')).default,
+}));
+
+vi.mock('$lib/components/pile/pile.svelte', async () => ({
+ default: (await import('./pile-stub.svelte')).default,
+}));
+
+describe('efp-stats.svelte', () => {
+ it('renders follower and following counts when stats are present', () => {
+ render(EfpStats, {
+ props: {
+ address: '0x1234567890123456789012345678901234567890',
+ stats: { followers: 1200, following: 42 },
+ },
+ });
+
+ expect(screen.getByText(/1,200 followers/)).toBeInTheDocument();
+ expect(screen.getByText(/42 following/)).toBeInTheDocument();
+ expect(screen.getByRole('link')).toHaveAttribute(
+ 'href',
+ 'https://efp.app/0x1234567890123456789012345678901234567890',
+ );
+ });
+
+ it('renders nothing when stats are absent', () => {
+ const { container } = render(EfpStats, {
+ props: {
+ address: '0x1234567890123456789012345678901234567890',
+ stats: null,
+ },
+ });
+
+ expect(container.querySelector('.efp-stats')).toBeNull();
+ });
+
+ it('truncates common followers to maxCommonFollowers', () => {
+ render(EfpStats, {
+ props: {
+ address: '0x1234567890123456789012345678901234567890',
+ stats: { followers: 3, following: 1 },
+ showCommonFollowers: true,
+ maxCommonFollowers: 2,
+ commonFollowers: [
+ { address: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' },
+ { address: '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' },
+ { address: '0xcccccccccccccccccccccccccccccccccccccccc' },
+ ],
+ },
+ });
+
+ expect(screen.getByText('Followers you know')).toBeInTheDocument();
+ expect(screen.getByText('+1')).toBeInTheDocument();
+ });
+});
diff --git a/src/lib/components/efp-stats/identity-badge-stub.svelte b/src/lib/components/efp-stats/identity-badge-stub.svelte
new file mode 100644
index 000000000..11fa49a59
--- /dev/null
+++ b/src/lib/components/efp-stats/identity-badge-stub.svelte
@@ -0,0 +1,9 @@
+
+
+
diff --git a/src/lib/components/supporters-section/supporters.section.svelte b/src/lib/components/supporters-section/supporters.section.svelte
index 4bd305bc9..b93e4c92e 100644
--- a/src/lib/components/supporters-section/supporters.section.svelte
+++ b/src/lib/components/supporters-section/supporters.section.svelte
@@ -122,6 +122,9 @@
import Section from '../section/section.svelte';
import EcosystemBadge from '../ecosystem-badge/ecosystem-badge.svelte';
import unreachable from '$lib/utils/unreachable';
+ import network from '$lib/stores/wallet/network';
+ import extractSupporterAddresses from '$lib/utils/efp/extract-supporter-addresses';
+ import { getSupportersYouFollow } from '$lib/utils/efp';
let emptyStateText: string | undefined = $state();
@@ -138,6 +141,43 @@
sectionSkeleton?: SectionSkeleton | undefined;
}
+ let followedSupporters = $state(new Set
());
+ let highlightSupportersYouFollow = $state(false);
+ let loadingFollowedSupporters = $state(false);
+
+ function supporterTag(
+ address: string,
+ existingTag: string | undefined,
+ ): string | undefined {
+ if (existingTag) return existingTag;
+ if (
+ highlightSupportersYouFollow &&
+ followedSupporters.has(address.toLowerCase())
+ ) {
+ return 'Following';
+ }
+ return undefined;
+ }
+
+ async function toggleSupportersYouFollow() {
+ if (highlightSupportersYouFollow) {
+ highlightSupportersYouFollow = false;
+ followedSupporters = new Set();
+ return;
+ }
+
+ const viewer = $walletStore.address;
+ if (!viewer || supporterAddresses.length === 0) return;
+
+ loadingFollowedSupporters = true;
+ try {
+ followedSupporters = await getSupportersYouFollow(viewer, supporterAddresses);
+ highlightSupportersYouFollow = true;
+ } finally {
+ loadingFollowedSupporters = false;
+ }
+ }
+
let {
supportItems,
ownerAccountId = undefined,
@@ -149,6 +189,9 @@
infoTooltip = undefined,
sectionSkeleton = $bindable(),
}: Props = $props();
+
+ const supporterAddresses = $derived(extractSupporterAddresses(supportItems));
+
run(() => {
switch (type) {
case 'project':
@@ -185,6 +228,25 @@
}}
bind:skeletonInstance={sectionSkeleton}
>
+ {#if network.enableEfp && type === 'address' && $walletStore.address && supporterAddresses.length > 0}
+
+
+
+ {/if}
{#each supportItems as item}
{#if item.__typename === 'OneTimeDonationSupport'}
@@ -192,12 +254,14 @@
title={{
component: IdentityBadge,
props: {
- tag:
+ tag: supporterTag(
+ item.account.address,
item.account.accountId === $walletStore.dripsAccountId
? 'You'
: item.account.accountId === ownerAccountId
? 'Owner'
: undefined,
+ ),
disableTooltip: true,
address: item.account.address,
},
@@ -246,12 +310,14 @@
props: {
disableLink: true,
disableTooltip: true,
- tag:
+ tag: supporterTag(
+ item.stream.sender.account.address,
stream.sender.account.accountId === $walletStore.dripsAccountId
? 'You'
: stream.sender.account.accountId === ownerAccountId
? 'Owner'
: undefined,
+ ),
address: item.stream.sender.account.address,
},
}}
@@ -356,6 +422,23 @@