Skip to content

feat: stream list virtualization (#156) and tab-visibility polling pause (#157) - #643

Merged
Austinaminu2 merged 7 commits into
FlowwStar:mainfrom
darcszn:feat/issues-156-157-virtualization-visibility
Aug 27, 2026
Merged

feat: stream list virtualization (#156) and tab-visibility polling pause (#157)#643
Austinaminu2 merged 7 commits into
FlowwStar:mainfrom
darcszn:feat/issues-156-157-virtualization-visibility

Conversation

@darcszn

@darcszn darcszn commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #156
Closes #157

──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Issue #156 — Stream List Virtualization

Problem

The dashboard and streams page rendered all stream cards in a flat list by calling .map() directly on arrays that can hold up to 1,000 items. With 500 streams, 500+ DOM nodes were created on mount, causing slow initial renders, high memory usage, and scroll jank on lower-end devices.

Solution

Introduced @tanstack/react-virtual (v3.14.10) for windowed rendering — only cards visible in the viewport (plus an overscan buffer) are in the DOM at any time.

Files changed

components/streams/virtual-stream-list.tsx (new)
A component that:

  • Renders a plain for fewer than 50 streams — no virtualization overhead for small lists.
  • Switches to powered by @tanstack/react-virtual for 50+ streams.
  • Uses measureElement + ResizeObserver for dynamic row heights — stream cards vary in height and this handles them correctly without any fixed-height assumptions.
  • Overscan of 4 items above and below the viewport, satisfying the 3–5 item spec.
  • Accepts selectable, selectedIds, and onToggleSelect props so the existing bulk-select feature on the streams page continues to work seamlessly through virtualization.
  • Includes a fallback while data is loading.

app/app/dashboard.tsx (updated)
Replaced the three all.map / received.map / sent.map blocks inside the All / Receiving / Sending tabs with .

app/app/streams/page.tsx (updated)
Replaced filtered.map(...) with . Bulk-select state is wired directly through the new props.

package.json (updated)
Added "@tanstack/react-virtual": "^3.14.10" to dependencies.

Acceptance criteria

  • ✅ Stream lists virtualized — only visible cards are in the DOM (50+ threshold)
  • ✅ Smooth scrolling — translateY absolute positioning, no layout thrash
  • ✅ Overscan buffer of 4 items above and below viewport
  • ✅ Dynamic row heights via measureElement + ResizeObserver
  • ✅ Fallback to flat list for < 50 streams

──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Issue #157 — Pause Polling When Tab Is Hidden

Problem

useStreams polled the Soroban RPC on a 30-second interval regardless of whether the browser tab was visible or hidden. Users who open multiple FlowStar tabs or leave the app in the background waste bandwidth, battery, and RPC rate-limit quota.

Solution

Used the Page Visibility API (document.visibilitychange) to pause the polling interval when the tab is hidden and immediately resume + refetch when it becomes visible again.

Files changed

hooks/use-page-visibility.ts (new)
A focused usePageVisibility({ onVisible, onHidden }) hook that:

  • Registers a single visibilitychange listener on mount and removes it on unmount.
  • Stores callbacks in refs so the DOM listener is registered exactly once — no teardown/re-register cycle when the caller re-renders with new function references.
  • SSR-safe: the listener is only attached inside useEffect (browser-only).
  • Works correctly with multiple tabs open — each tab's document fires its own independent visibilitychange event.

hooks/use-streams.ts (updated)

  • Polling logic refactored into startPolling() and stopPolling() helpers to allow the visibility handlers to control the interval without re-running the main useEffect.

  • usePageVisibility integrated:

    • onHidden → records the time the tab was hidden and calls stopPolling().
    • onVisible → calls fetch() immediately (no stale data on re-focus), restarts the interval via startPolling(), and sets isRefreshingAfterHidden = true if the tab was hidden for ≥ 3 seconds.
  • isRefreshingAfterHidden: boolean added to the CategorizedStreams return type.

  • The main polling useEffect checks document.hidden before starting — if the page loads in a hidden tab (e.g. Ctrl+click), polling stays paused until the tab is first focused.

  • useAutoWithdraw manages its own separate interval and is intentionally not touched — auto-withdraw scheduling is unaffected.

app/app/dashboard.tsx and app/app/streams/page.tsx (updated)
Both pages destructure isRefreshingAfterHidden from useStreams() and render a brief spinning Refreshing… indicator in the page header when it is true. The indicator uses aria-live="polite" for accessibility.

Acceptance criteria

  • ✅ Polling pauses when the browser tab is hidden or minimized
  • ✅ Polling resumes immediately when the tab becomes visible
  • ✅ Fresh data fetch triggers on tab re-focus
  • ✅ Works with multiple FlowStar tabs open (each tab is independent)
  • ✅ No impact on auto-withdraw scheduling
  • ✅ "Refreshing…" indicator shown when resuming after hidden ≥ 3 seconds

──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Changed files

┌────────────────────────────────────────────┬──────────┐
│ File │ Status │
├────────────────────────────────────────────┼──────────┤
│ hooks/use-page-visibility.ts │ New │
├────────────────────────────────────────────┼──────────┤
│ hooks/use-streams.ts │ Modified │
├────────────────────────────────────────────┼──────────┤
│ components/streams/virtual-stream-list.tsx │ New │
├────────────────────────────────────────────┼──────────┤
│ app/app/dashboard.tsx │ Modified │
├────────────────────────────────────────────┼──────────┤
│ app/app/streams/page.tsx │ Modified │
├────────────────────────────────────────────┼──────────┤
│ package.json │ Modified │
└────────────────────────────────────────────┴──────────┘

@drips-wave

drips-wave Bot commented Aug 27, 2026

Copy link
Copy Markdown

@darcszn Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@Austinaminu2
Austinaminu2 merged commit 84a4e83 into FlowwStar:main Aug 27, 2026
0 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf: Pause background polling when browser tab is hidden perf: Add virtual scrolling for stream lists with 100+ entries

2 participants