From 49e5f21206fa5f83468d407c050e485f7cb68c91 Mon Sep 17 00:00:00 2001 From: DeFiVC Date: Wed, 29 Jul 2026 09:48:08 +0100 Subject: [PATCH 1/5] fix(sse-indicator): show yellow banner during reconnection phase The disconnect banner was always red even during reconnection, which confused users into thinking the connection was permanently lost. Now the banner color changes to yellow when reconnecting to provide clear visual feedback that a retry is in progress. --- frontend/src/components/dashboard/SSEStatusIndicator.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/dashboard/SSEStatusIndicator.tsx b/frontend/src/components/dashboard/SSEStatusIndicator.tsx index 41f95613..5e57cfe6 100644 --- a/frontend/src/components/dashboard/SSEStatusIndicator.tsx +++ b/frontend/src/components/dashboard/SSEStatusIndicator.tsx @@ -58,7 +58,11 @@ export function SSEStatusIndicator({ {/* Disconnect Banner */} {showDisconnectBanner && isDisconnected && (
From ae79c6125d9308b89b75d44b82af9c2e893d0f05 Mon Sep 17 00:00:00 2001 From: DeFiVC Date: Wed, 29 Jul 2026 09:48:18 +0100 Subject: [PATCH 2/5] feat(activity-history): add list virtualization for large event lists ActivityHistory.tsx rendered all events into DOM rows with no windowing, causing visible jank on scroll for accounts with many historical events. Added @tanstack/react-virtual to virtualize the list when it exceeds 50 items, while preserving the existing pagination behavior for initial fetch. --- frontend/package.json | 1 + .../components/dashboard/ActivityHistory.tsx | 175 +++++++++++++----- package-lock.json | 133 +++---------- 3 files changed, 160 insertions(+), 149 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 577e543b..44c32af1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,6 +15,7 @@ "@stellar/freighter-api": "^6.0.1", "@stellar/stellar-sdk": "^15.1.0", "@tanstack/react-query": "^5.101.0", + "@tanstack/react-virtual": "^3.14.9", "lucide-react": "^0.575.0", "next": "16.2.9", "next-themes": "^0.4.6", diff --git a/frontend/src/components/dashboard/ActivityHistory.tsx b/frontend/src/components/dashboard/ActivityHistory.tsx index 31664345..8c1d36b3 100644 --- a/frontend/src/components/dashboard/ActivityHistory.tsx +++ b/frontend/src/components/dashboard/ActivityHistory.tsx @@ -1,13 +1,16 @@ "use client"; -import React from "react"; +import React, { useRef } from "react"; import Link from "next/link"; +import { useVirtualizer } from "@tanstack/react-virtual"; import { BackendStreamEvent } from "@/lib/api-types"; import { formatAmount } from "@/lib/amount"; import TransactionTracker from "@/components/TransactionTracker"; import { Download, ExternalLink, Clock } from "lucide-react"; import { Button } from "../ui/Button"; +const VIRTUALIZATION_THRESHOLD = 50; + interface ActivityHistoryProps { events: BackendStreamEvent[]; isLoading?: boolean; @@ -17,6 +20,17 @@ export const ActivityHistory: React.FC = ({ events, isLoading, }) => { + const parentRef = useRef(null); + const shouldVirtualize = events.length > VIRTUALIZATION_THRESHOLD; + + const virtualizer = useVirtualizer({ + count: events.length, + getScrollElement: () => parentRef.current, + estimateSize: () => 120, + overscan: 5, + enabled: shouldVirtualize, + }); + const exportToCSV = () => { const headers = [ "Stream ID", @@ -139,55 +153,128 @@ export const ActivityHistory: React.FC = ({
-
- {events.map((event, index) => ( + {shouldVirtualize ? ( +
- {/* Dot */} -
- -
- {/* Content Card */} -
-
-
-

- {renderEventMessage(event)} -

- + {virtualizer.getVirtualItems().map((virtualRow) => { + const event = events[virtualRow.index]; + if (!event) return null; + return ( +
+ {/* Dot */} +
+ +
+ {/* Content Card */} +
+
+
+

+ {renderEventMessage(event)} +

+ +
+ + {event.eventType} + +
+ + {event.transactionHash && ( +
+ + + + +
+ )} +
- - {event.eventType} - + ); + })} +
+
+ ) : ( +
+ {events.map((event, index) => ( +
+ {/* Dot */} +
+
- - {event.transactionHash && ( -
- - - - + {/* Content Card */} +
+
+
+

+ {renderEventMessage(event)} +

+ +
+ + {event.eventType} +
- )} + + {event.transactionHash && ( +
+ + + + +
+ )} +
-
- ))} -
+ ))} +
+ )} {events.length === 0 && !isLoading && (
diff --git a/package-lock.json b/package-lock.json index 6f78cc24..e88d10c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -235,6 +235,7 @@ "@stellar/freighter-api": "^6.0.1", "@stellar/stellar-sdk": "^15.1.0", "@tanstack/react-query": "^5.101.0", + "@tanstack/react-virtual": "^3.14.9", "lucide-react": "^0.575.0", "next": "16.2.9", "next-themes": "^0.4.6", @@ -2398,9 +2399,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2417,9 +2415,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2436,9 +2431,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2455,9 +2447,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2896,9 +2885,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2917,9 +2903,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2938,9 +2921,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2959,9 +2939,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2980,9 +2957,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3001,9 +2975,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3207,9 +3178,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3224,9 +3192,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3241,9 +3206,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3258,9 +3220,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3275,9 +3234,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3292,9 +3248,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3309,9 +3262,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3326,9 +3276,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3343,9 +3290,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3360,9 +3304,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3377,9 +3318,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3394,9 +3332,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3411,9 +3346,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3755,9 +3687,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3779,9 +3708,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3803,9 +3729,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3976,9 +3899,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3996,9 +3916,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4015,9 +3932,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4035,9 +3949,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4217,6 +4128,33 @@ "react": "^18 || ^19" } }, + "node_modules/@tanstack/react-virtual": { + "version": "3.14.9", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.14.9.tgz", + "integrity": "sha512-qZyr0FZDP8rDC4WBhsryIZmAd9bveJvFGUJJtskWaew6/0dTRS6wZxnR6VQ5bY2KwL3LjerrHqQLk3a0GKcPXQ==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.17.7" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.17.7", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.17.7.tgz", + "integrity": "sha512-bp+v10y65sp2H7WpWfIMyxTNfl8ZVfxFTLRjPIFRryi6FV/J33z4IS53WO4pTk36KlvJ4iLiQz+oaydDC1xbcA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", @@ -9722,9 +9660,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -9747,9 +9682,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -9771,9 +9703,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -9795,9 +9724,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -9886,9 +9812,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ From 5870705cf100a9f8432fd73e00c4b0eab31a6323 Mon Sep 17 00:00:00 2001 From: DeFiVC Date: Wed, 29 Jul 2026 09:48:24 +0100 Subject: [PATCH 3/5] fix(wallet): handle wallet extension uninstalled mid-session WalletButton.tsx assumed a connected wallet stays available for the entire session. If the user disabled or uninstalled the wallet extension while connected, subsequent operations would fail with raw errors. Now the component periodically checks wallet availability and prompts reconnection with a clear message when the extension is unavailable. --- .../src/components/wallet/WalletButton.tsx | 49 ++++++++++++++++++- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/wallet/WalletButton.tsx b/frontend/src/components/wallet/WalletButton.tsx index ca78cc96..0109cab6 100644 --- a/frontend/src/components/wallet/WalletButton.tsx +++ b/frontend/src/components/wallet/WalletButton.tsx @@ -13,7 +13,7 @@ * - "Disconnect" button */ -import { useState, useRef, useEffect } from "react"; +import { useState, useRef, useEffect, useCallback } from "react"; import { useWallet } from "@/context/wallet-context"; import { shortenPublicKey, @@ -27,8 +27,29 @@ export function WalletButton() { const [modalOpen, setModalOpen] = useState(false); const [dropdownOpen, setDropdownOpen] = useState(false); const [copied, setCopied] = useState(false); + const [walletUnavailable, setWalletUnavailable] = useState(false); const dropdownRef = useRef(null); + const checkWalletAvailability = useCallback(() => { + if (typeof window === "undefined") return true; + const freighterAvailable = + "freighter" in window || "stellar" in window; + return freighterAvailable; + }, []); + + useEffect(() => { + if (status !== "connected") return; + + const interval = setInterval(() => { + if (!checkWalletAvailability()) { + setWalletUnavailable(true); + disconnect(); + } + }, 5000); + + return () => clearInterval(interval); + }, [status, checkWalletAvailability, disconnect]); + // Close dropdown when clicking outside useEffect(() => { function handleClickOutside(e: MouseEvent) { @@ -59,7 +80,11 @@ export function WalletButton() { const handleDisconnect = () => { setDropdownOpen(false); setModalOpen(false); - disconnect(); + try { + disconnect(); + } catch { + setWalletUnavailable(true); + } }; // Close modal when connection is successful @@ -78,6 +103,26 @@ export function WalletButton() { ); } + if (walletUnavailable) { + return ( +
+ + Wallet extension unavailable + + +
+ ); + } + if (status === "connected" && session) { const networkOk = isExpectedNetwork(session.network); From 4b1e318e6834964aa31f82d511cb937b52da9c01 Mon Sep 17 00:00:00 2001 From: DeFiVC Date: Wed, 29 Jul 2026 09:48:30 +0100 Subject: [PATCH 4/5] perf(stream-card): memoize IncomingStreamCard to prevent unnecessary re-renders IncomingStreamCard was re-rendering on every parent update even when its props hadn't changed. Wrapped the component in React.memo with a custom comparison function that checks stream data and withdrawing state, preventing unnecessary recalculations on the dashboard. --- .../components/streams/IncomingStreamCard.tsx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/streams/IncomingStreamCard.tsx b/frontend/src/components/streams/IncomingStreamCard.tsx index bc167934..a5ca3e7d 100644 --- a/frontend/src/components/streams/IncomingStreamCard.tsx +++ b/frontend/src/components/streams/IncomingStreamCard.tsx @@ -1,5 +1,6 @@ "use client"; +import React from "react"; import { Button } from "@/components/ui/Button"; import { useStreamingAmount } from "@/hooks/useStreamingAmount"; import type { @@ -32,7 +33,7 @@ function badgeClassName(status: IncomingStreamStatus): string { } } -export function IncomingStreamCard({ +export const IncomingStreamCard = React.memo(function IncomingStreamCard({ stream, withdrawing, onWithdraw, @@ -124,4 +125,17 @@ export function IncomingStreamCard({
); -} +}, (prevProps, nextProps) => { + return ( + prevProps.stream.deposited === nextProps.stream.deposited && + prevProps.stream.withdrawn === nextProps.stream.withdrawn && + prevProps.stream.ratePerSecond === nextProps.stream.ratePerSecond && + prevProps.stream.lastUpdateTime === nextProps.stream.lastUpdateTime && + prevProps.stream.isActive === nextProps.stream.isActive && + prevProps.stream.isPaused === nextProps.stream.isPaused && + prevProps.stream.pausedAt === nextProps.stream.pausedAt && + prevProps.stream.totalPausedDuration === nextProps.stream.totalPausedDuration && + prevProps.stream.status === nextProps.stream.status && + prevProps.withdrawing === nextProps.withdrawing + ); +}); From 95b916ca4d271de88342d13e0e727e2ecd8c4831 Mon Sep 17 00:00:00 2001 From: DeFiVC Date: Wed, 29 Jul 2026 09:48:36 +0100 Subject: [PATCH 5/5] fix: resolve pre-existing build errors in dashboard code Fixed syntax error in dashboard.ts (misplaced closing brace before catch block) and added missing useQueryClient import in dashboard-view.tsx that were preventing successful builds. --- .../components/dashboard/dashboard-view.tsx | 2 +- frontend/src/lib/dashboard.ts | 24 +++++++++---------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/frontend/src/components/dashboard/dashboard-view.tsx b/frontend/src/components/dashboard/dashboard-view.tsx index 34568f84..6a0c252c 100644 --- a/frontend/src/components/dashboard/dashboard-view.tsx +++ b/frontend/src/components/dashboard/dashboard-view.tsx @@ -18,7 +18,6 @@ import toast from "react-hot-toast"; import { getDashboardAnalytics, fetchDashboardData, - useDashboard, dashboardQueryKey, type DashboardSnapshot, type Stream, @@ -48,6 +47,7 @@ import { type StreamFormData, } from "../stream-creation/StreamCreationWizard"; import { TopUpModal } from "../stream-creation/TopUpModal"; +import { useQueryClient } from "@tanstack/react-query"; import { CancelConfirmModal } from "../stream-creation/CancelConfirmModal"; import { StreamDetailsModal } from "./StreamDetailsModal"; import { Button } from "../ui/Button"; diff --git a/frontend/src/lib/dashboard.ts b/frontend/src/lib/dashboard.ts index 39a8814f..303f1289 100644 --- a/frontend/src/lib/dashboard.ts +++ b/frontend/src/lib/dashboard.ts @@ -88,21 +88,19 @@ async function fetchStreams( for (const endpoint of endpoints) { try { const response = await fetch(`${endpoint}?${params.toString()}`, { signal }); - if (response.ok) { - const payload = (await response.json()) as - | BackendStream[] - | { data?: BackendStream[] }; - return Array.isArray(payload) ? payload : payload.data ?? []; - } - - if (response.status === 404) { - lastError = new Error(`Endpoint not found: ${endpoint}`); - continue; - } + if (response.ok) { + const payload = (await response.json()) as + | BackendStream[] + | { data?: BackendStream[] }; + return Array.isArray(payload) ? payload : payload.data ?? []; + } - lastError = new Error(`Failed to fetch streams (${response.status}) from ${endpoint}`); - } + if (response.status === 404) { + lastError = new Error(`Endpoint not found: ${endpoint}`); + continue; + } + lastError = new Error(`Failed to fetch streams (${response.status}) from ${endpoint}`); } catch (err) { if (err instanceof Error && err.name === "AbortError") { throw err;