Skip to content

Quick vote#2159

Merged
simo6529 merged 24 commits intomainfrom
quick-vote
Mar 24, 2026
Merged

Quick vote#2159
simo6529 merged 24 commits intomainfrom
quick-vote

Conversation

@simo6529
Copy link
Copy Markdown
Collaborator

@simo6529 simo6529 commented Mar 24, 2026

Summary by CodeRabbit

  • New Features
    • Added a quick-vote dialog for memes waves with support for predefined and custom vote amounts
    • Added a waves footer displaying remaining voting power and unrated items
    • Added a floating quick-vote trigger button on mobile devices
    • Enabled local storage persistence for recent vote amounts and skipped items
    • Added vote prefetching to optimize dialog load times

simo6529 added 22 commits March 12, 2026 15:59
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 24, 2026

📝 Walkthrough

Walkthrough

This pull request introduces a comprehensive "Memes Quick Vote" feature including new UI components for dialog and footer interactions, React hooks managing discovery/queue/submission state, localStorage persistence, API integration, and extensive test coverage. The feature centralizes quick-vote context logic and decouples view management from BrainMobile.

Changes

Cohort / File(s) Summary
Memes Quick Vote - Core UI Components
components/brain/left-sidebar/waves/MemesWaveFooter.tsx, components/brain/left-sidebar/waves/MemesWaveQuickVoteTrigger.tsx, components/brain/left-sidebar/waves/memes-quick-vote/MemesQuickVote*.tsx, components/brain/mobile/FloatingMemesQuickVoteTrigger.tsx
New client components rendering quick-vote dialog, footer stats, trigger buttons, and swipe-based voting interactions. Includes preview cards, vote controls, skeleton loading, and error/done states with accessibility and mobile-specific handling.
Quick Vote State Management - Discovery/Queue/Submission Hooks
hooks/useMemesQuickVoteQueue.ts, hooks/useMemesQuickVoteDiscovery.ts, hooks/useMemesQuickVoteActiveDrop.ts, hooks/useMemesQuickVoteSubmit.ts, hooks/useMemesQuickVoteSummary.ts
New hooks orchestrating quick-vote workflow: discovery pagination with deferred drop handling, queue optimistic state management, active drop selection/validation, vote submission with retry logic, and summary stats derivation. Complex state machines managing leaderboard fetching, caching, and offline-first operations.
Quick Vote Utilities & Storage
hooks/memesQuickVote.helpers.ts, hooks/memesQuickVote.query.ts, hooks/memesQuickVote.queue.helpers.ts, hooks/memesQuickVote.storageStore.ts, hooks/useMemesQuickVoteStorage.ts, hooks/useMemesQuickVoteContext.ts
Helper utilities for stats derivation, amount normalization, skipped-drop management, and API query/fetch definitions. Custom localStorage abstraction with cross-tab sync and React hooks integration. Context derivation from auth/settings for feature enablement.
Quick Vote Dialog Controller & Prefetch
hooks/useMemesQuickVoteDialogController.ts, hooks/usePrefetchMemesQuickVote.ts
Dialog lifecycle management (open/close/session ID sequencing) with prefetch reservation pattern. Prefetch hook initiates async loading of summary, discovery pages, and drop details before dialog opens.
Quick Vote Footer Stats
hooks/useMemesWaveFooterStats.ts
Derived hook exposing footer-specific stats (uncast power, unrated count, voting label, ready flag) from summary query with readiness checks.
View Management Refactoring
components/brain/BrainMobile.tsx, components/brain/mobile/brainMobileViews.ts, components/brain/mobile/useBrainMobileActiveView.ts, components/brain/mobile/BrainMobileViewContent.tsx, components/brain/mobile/BrainMobileTabs.tsx
Extracted BrainView enum and view-selection logic into dedicated modules. New useBrainMobileActiveView hook centralizes active view derivation from routes/context. New BrainMobileViewContent component renders view-specific content, decoupling from BrainMobile. Updated BrainMobile to use new hooks and integrate quick-vote dialog.
Left Sidebar & Layout Integration
components/brain/left-sidebar/web/WebLeftSidebar.tsx, components/brain/mobile/BrainMobileWaves.tsx, components/layout/AppLayout.tsx
Integrated quick-vote dialog controller into left sidebar and waves view. New WebLeftSidebarQuickVoteOwner manages shared dialog state. BrainMobileWaves now accepts quick-vote callbacks. AppLayout wires waves view with quick-vote controller via WavesQuickVoteView.
Auth Security Update
components/auth/Auth.tsx, services/auth/auth.utils.ts
Extracted address authorization logic into reusable utility function isAuthAddressAuthorized, supporting both normal and dev-auth modes with connected-accounts comparison.
API & Configuration
openapi.yaml, config/nextConfig.ts
Added unvoted_by_me query parameter to leaderboard endpoint. Added allowed dev origin to Next.js config.
Test Coverage - Core Hooks
__tests__/hooks/memesQuickVote.helpers.test.ts, __tests__/hooks/memesQuickVote.queue.helpers.test.ts, __tests__/hooks/useMemesQuickVote*.test.tsx, __tests__/hooks/usePrefetchMemesQuickVote.test.tsx
Comprehensive test suites validating helper utilities (sanitization, stats derivation, amount management), discovery/queue state machine, dialog controller session sequencing, prefetch flow, queue optimistic power tracking, and submit retry logic.
Test Coverage - Components & Integration
__tests__/components/brain/BrainMobile.test.tsx, __tests__/components/brain/left-sidebar/waves/MemesWaveFooter.test.tsx, __tests__/components/brain/left-sidebar/waves/memes-quick-vote/MemesQuickVoteDialog.test.tsx, __tests__/components/brain/mobile/BrainMobileWaves.test.tsx, __tests__/components/brain/mobile/FloatingMemesQuickVoteTrigger.test.tsx, __tests__/components/brain/mobile/BrainMobileViewContent.test.tsx, __tests__/components/layout/AppLayout.test.tsx, __tests__/components/brain/left-sidebar/web/WebLeftSidebar.test.tsx, __tests__/components/brain/left-sidebar/web/WebUnifiedWavesList.test.tsx
Component and integration tests covering dialog UI states (preview, swipe, custom amount, loading, done, error), footer stats conditional rendering, floating trigger visibility, mobile view-content rendering, dialog lifecycle and session persistence across route changes, and quick-vote dialog mount/reuse patterns.
Test Maintenance
__tests__/components/brain/left-sidebar/waves/UnifiedWavesList.test.tsx, __tests__/components/brain/my-stream/MyStreamWaveTabsLeaderboard.test.tsx, __tests__/services/auth.utils.test.ts
Updated existing tests to import BrainView from new location, added tests for new isAuthAddressAuthorized function covering dev-auth modes and connected-account matching, and normalized string quoting/formatting.
Documentation
docs/specs/2026-03-20-memes-quick-vote-refactor.md
Feature specification detailing leaderboard-based summary query, paginated discovery with local skip deferral, freshness/staleness handling, queue semantics, and phased rollout plan.

Sequence Diagram(s)

sequenceDiagram
    participant User as User/Mobile
    participant Dialog as Quick Vote Dialog
    participant Queue as useMemesQuickVoteQueue
    participant Discovery as Discovery/Leaderboard
    participant Storage as localStorage
    participant API as Backend API
    
    User->>Dialog: prefetchQuickVote()
    Dialog->>Queue: Reserve session ID
    Queue->>Discovery: Prefetch summary & discovery pages
    Discovery->>API: GET leaderboard (unvoted_by_me=true)
    API-->>Discovery: Drop IDs
    Discovery->>API: GET drop details
    API-->>Discovery: Hydrated drops
    Discovery->>Storage: Cache drops by ID
    
    User->>Dialog: openQuickVote()
    Dialog->>Queue: Use reserved session ID
    Queue->>Queue: Derive active drop from queue
    Queue-->>Dialog: activeDrop + stats
    Dialog->>User: Render preview + controls
    
    alt Swipe Vote
        User->>Dialog: Swipe left/right
        Dialog->>Dialog: Animate swipe offset
    else Click Vote
        User->>Dialog: Click vote amount
    end
    
    Dialog->>Queue: submitVote(drop, amount)
    Queue->>API: POST drop/{id}/ratings
    API-->>Queue: Success + remaining power
    Queue->>Storage: Persist recent amounts
    Queue->>Queue: Advance to next drop
    Queue->>Discovery: Refetch if needed
    Queue-->>Dialog: Update activeDrop
    Dialog->>User: Render next drop or done state
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

The diff introduces substantial new feature logic with multiple interdependent hooks managing complex state machines (discovery pagination, optimistic power tracking, session-based prefetching, submission queueing), new UI components with animation/swipe handling, storage abstraction with cross-tab sync, and extensive test coverage. The heterogeneous nature of changes (hooks, components, utilities, tests, integration points) across many files with dense logic requires careful reasoning for each subsystem and understanding of interactions between discovery, queue, submission, and dialog lifecycle.

Possibly related PRs

Suggested reviewers

  • prxt6529

Poem

🐰 A rabbit hops through waves so new,
Quick votes bloom where drops break through,
From discovery to swipe so swift,
The dialog dance—a joyful gift!
Storage whispers, hooks take flight,
Quick votes dance through day and night! 🎉

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Quick vote' is vague and generic, using non-descriptive terms that don't convey meaningful information about the substantial changeset. Use a more specific title that summarizes the main change, such as 'Add memes quick vote dialog and footer UI components' or 'Refactor quick vote feature with dialog, storage, and queue management'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch quick-vote

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (11)
__tests__/components/auth/Auth.test.tsx (1)

31-45: Consider deduplicating the isAuthAddressAuthorized mock implementation.

The same matcher logic is defined twice, which can drift over time.

♻️ Suggested cleanup
+const defaultIsAuthAddressAuthorizedMock = ({
+  address,
+  connectedAccounts,
+}: {
+  readonly address: string | null | undefined;
+  readonly connectedAccounts: readonly { readonly address: string }[];
+}) =>
+  Boolean(
+    address &&
+      connectedAccounts.some(
+        (account) => account.address.toLowerCase() === address.toLowerCase()
+      )
+  );
+
 jest.mock("@/services/auth/auth.utils", () => ({
   canStoreAnotherWalletAccount: jest.fn(() => true),
   getWalletAddress: jest.fn(() => null),
-  isAuthAddressAuthorized: jest.fn(
-    ({ address, connectedAccounts }) =>
-      Boolean(
-        address &&
-        connectedAccounts.some(
-          (account) => account.address.toLowerCase() === address.toLowerCase()
-        )
-      )
-  ),
+  isAuthAddressAuthorized: jest.fn(defaultIsAuthAddressAuthorizedMock),
   ...
 }));
 ...
-    mockIsAuthAddressAuthorized.mockImplementation(({ address, connectedAccounts }) =>
-      Boolean(
-        address &&
-        connectedAccounts.some(
-          (account) => account.address.toLowerCase() === address.toLowerCase()
-        )
-      )
-    );
+    mockIsAuthAddressAuthorized.mockImplementation(
+      defaultIsAuthAddressAuthorizedMock
+    );

Also applies to: 203-219

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@__tests__/components/auth/Auth.test.tsx` around lines 31 - 45, The
isAuthAddressAuthorized mock implementation is duplicated (appearing around the
current block and again at lines 203-219); extract the matching logic into a
single reusable helper function (e.g., isAuthAddressAuthorizedMatcher) and
replace both jest.fn(...) implementations with
jest.fn(isAuthAddressAuthorizedMatcher) so both mocks call the same function
(update references to the parameter shape used in the existing implementations
to preserve behavior).
__tests__/services/auth.utils.test.ts (1)

147-197: Add an explicit null/undefined address test for the early-return path.

Current additions are solid, but this branch is part of the contract and worth pinning in tests.

✅ Suggested test addition
+  it("returns false when address is null or undefined", () => {
+    expect(
+      isAuthAddressAuthorized({
+        address: null,
+        connectedAccounts: [{ address: "0xabc" }],
+      })
+    ).toBe(false);
+
+    expect(
+      isAuthAddressAuthorized({
+        address: undefined,
+        connectedAccounts: [{ address: "0xabc" }],
+      })
+    ).toBe(false);
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@__tests__/services/auth.utils.test.ts` around lines 147 - 197, Add a unit
test covering the early-return path when the supplied address is null or
undefined by calling isAuthAddressAuthorized with address: null (and a separate
case for undefined if desired) and a minimal connectedAccounts array, asserting
it returns false; ensure the test references isAuthAddressAuthorized to locate
the function and mirrors existing test style (use require("@/config/env") only
if environment mutation is needed) so the behavior for null/undefined addresses
is pinned.
openapi.yaml (1)

5843-5851: Clarify unauthenticated behavior for unvoted_by_me.

Line 5846 ties this filter to the authenticated user, but this operation documents only 200. Please specify whether unauthenticated requests with unvoted_by_me=true should return 401 or ignore the filter.

📌 Suggested spec update (if unauthenticated should be rejected)
         - name: unvoted_by_me
           in: query
           description: >-
             When true, only returns drops the authenticated user has not voted
-            on or currently has a 0 vote on
+            on or currently has a 0 vote on. Requires authentication.
           required: false
           schema:
             type: boolean
             default: false
       responses:
         "200":
           description: successful operation
           content:
             application/json:
               schema:
                 $ref: "#/components/schemas/ApiDropsLeaderboardPage"
+        "401":
+          description: Unauthorized
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openapi.yaml` around lines 5843 - 5851, The parameter unvoted_by_me currently
implies authentication but the operation only documents a 200 response; update
the OpenAPI spec to explicitly state expected behavior for unauthenticated
requests: either (A) add a 401 response object to the operation and update the
unvoted_by_me description to say the filter requires authentication and will
return 401 if unset, or (B) update the unvoted_by_me description to say the
filter is ignored for unauthenticated requests and keep only 200; modify the
operation responses accordingly (add a 401 response if choosing A) and mention
the authentication requirement next to the unvoted_by_me parameter so clients
know whether the filter requires auth.
components/brain/mobile/FloatingMemesQuickVoteTrigger.tsx (1)

15-19: Redundant guard conditions.

The checks for typeof uncastPower !== "number" and unratedCount <= 0 are already enforced by useMemesWaveFooterStats when determining isReady. When isReady is true, the hook guarantees uncastPower is a number and unratedCount > 0.

The redundancy is harmless (defensive coding), but simplifying would reduce noise:

Optional simplification
-  if (!isReady || typeof uncastPower !== "number" || unratedCount <= 0) {
+  if (!isReady) {
     return null;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/brain/mobile/FloatingMemesQuickVoteTrigger.tsx` around lines 15 -
19, The guard in FloatingMemesQuickVoteTrigger is overly defensive: remove the
redundant checks for typeof uncastPower !== "number" and unratedCount <= 0 and
only keep the isReady check from useMemesWaveFooterStats; update the
early-return to "if (!isReady) return null" so the hook's contract is relied
upon and the code is simplified while still returning null when the data isn't
ready.
hooks/useMemesWaveFooterStats.ts (1)

6-8: Consider exporting the MemesWaveFooterStats type.

The type is not exported, which may be helpful for consumers who want to explicitly type variables holding this hook's return value. While the return type is inferable via typeof useMemesWaveFooterStats, directly exporting the type would improve API usability.

Suggested export
-type MemesWaveFooterStats = MemesQuickVoteStats & {
+export type MemesWaveFooterStats = MemesQuickVoteStats & {
   readonly isReady: boolean;
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/useMemesWaveFooterStats.ts` around lines 6 - 8, Export the
MemesWaveFooterStats type so consumers can explicitly annotate the hook's return
value; update the declaration of MemesWaveFooterStats to be exported (export
type MemesWaveFooterStats = ...) and ensure any related exports or imports that
rely on this type (e.g., usages of useMemesWaveFooterStats) are adjusted
accordingly to reference the exported type.
hooks/useMemesQuickVoteContext.ts (1)

16-23: Consider memoizing derived values for consistency.

The contextProfile is memoized, but memesWaveId and isEnabled are recalculated on every render. While the computation is cheap, memoizing them would be more consistent with the contextProfile pattern and could prevent unnecessary downstream effects if consumers rely on referential equality.

♻️ Optional: Memoize derived values
+  const memesWaveId = useMemo(
+    () => seizeSettings.memes_wave_id ?? null,
+    [seizeSettings.memes_wave_id]
+  );
+
+  const isEnabled = useMemo(
+    () =>
+      isLoaded &&
+      typeof memesWaveId === "string" &&
+      memesWaveId.length > 0 &&
+      typeof contextProfile === "string" &&
+      contextProfile.length > 0 &&
+      activeProfileProxy === null,
+    [isLoaded, memesWaveId, contextProfile, activeProfileProxy]
+  );
-  const memesWaveId = seizeSettings.memes_wave_id ?? null;
-  const isEnabled =
-    isLoaded &&
-    typeof memesWaveId === "string" &&
-    memesWaveId.length > 0 &&
-    typeof contextProfile === "string" &&
-    contextProfile.length > 0 &&
-    activeProfileProxy === null;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/useMemesQuickVoteContext.ts` around lines 16 - 23, Derived values
memesWaveId and isEnabled are being recalculated every render while
contextProfile is memoized; wrap the computation of memesWaveId (from
seizeSettings.memes_wave_id) and the boolean isEnabled in useMemo (or the same
memo hook you used for contextProfile) so they preserve referential equality
across renders and match the existing memoization pattern (refer to
seizeSettings, memesWaveId, isEnabled, contextProfile, and activeProfileProxy to
build the memo dependencies).
__tests__/hooks/usePrefetchMemesQuickVote.test.tsx (1)

65-134: Consider adding test coverage for edge cases.

The current test verifies the happy path well. Consider adding tests for:

  • When isEnabled is false (should not prefetch)
  • API error handling during prefetch
  • Duplicate prefetch calls for the same session

These would increase confidence in the hook's robustness.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@__tests__/hooks/usePrefetchMemesQuickVote.test.tsx` around lines 65 - 134,
Add unit tests for usePrefetchMemesQuickVote covering the suggested edge cases:
create separate test cases that (1) mock useMemesQuickVoteContextMock to return
isEnabled: false and assert commonApiFetchMock is not called and no queries are
prefetched via QueryClient; (2) mock commonApiFetchMock to throw an error for
leaderboard or drop endpoints and assert the hook handles it (e.g., does not
crash and logs/returns expected state) by spying on console/process logger or
checking QueryClient state; and (3) simulate duplicate invocations by calling
the hook twice within the same test (or calling the prefetch function twice) and
assert commonApiFetchMock is called only once per unique drop id (or that
QueryClient avoids duplicate fetches) using jest mocks and
useMemesQuickVoteStorageMock to control skippedDropIds. Ensure each test uses
the existing setup patterns (QueryClient, createDrop, WAVE_ID,
commonApiFetchMock, useMemesQuickVoteContextMock, useMemesQuickVoteStorageMock)
for easy integration.
components/brain/BrainMobile.tsx (1)

181-191: Keep the memes quick-vote owner scoped to memes waves.

These guards only require “some wave” plus !isDm, so the memes-specific trigger/dialog path is still mounted on non-memes waves too. Even if the child components currently short-circuit, keeping the isMemesWave guard here avoids carrying extra quick-vote UI/state on unsupported wave types.

💡 Suggested fix
   const shouldMountFloatingQuickVoteEntry =
     isApp &&
+    isMemesWave &&
     hasWave &&
     !!wave &&
     activeView === BrainView.DEFAULT &&
     !isDropOpen &&
     !isDm;
   const shouldMountQuickVoteDialog =
-    isQuickVoteOpen ||
-    shouldMountFloatingQuickVoteEntry ||
-    activeView === BrainView.WAVES;
+    isMemesWave &&
+    (isQuickVoteOpen ||
+      shouldMountFloatingQuickVoteEntry ||
+      activeView === BrainView.WAVES);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/brain/BrainMobile.tsx` around lines 181 - 191, The quick-vote
mounting logic (shouldMountFloatingQuickVoteEntry and
shouldMountQuickVoteDialog) currently only checks for any wave and !isDm, so
memes quick-vote UI can mount on non-memes waves; update the guards to include
the isMemesWave boolean so the floating entry and dialog only mount for memes
waves (i.e., add isMemesWave to the AND list in
shouldMountFloatingQuickVoteEntry and ensure shouldMountQuickVoteDialog respects
that same memes-scoped condition alongside isQuickVoteOpen and activeView checks
involving BrainView.DEFAULT and BrainView.WAVES).
components/brain/left-sidebar/waves/memes-quick-vote/MemesQuickVoteControls.tsx (1)

189-205: Input accessibility could be improved with aria-label.

The custom amount input lacks an explicit accessible name for screen readers. While the visible label exists, adding an aria-label or ensuring the <label> properly wraps or references the input via htmlFor/id would improve accessibility.

♿ Suggested improvement for input accessibility
+                  <input
+                    id="quick-vote-custom-amount"
                     type="text"
                     inputMode="numeric"
                     pattern="[0-9]*"
+                    aria-label="Custom vote amount"
                     value={customValue}

And update the label:

-              <label className="tw-min-w-0 tw-flex-1">
+              <label htmlFor="quick-vote-custom-amount" className="tw-min-w-0 tw-flex-1">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@components/brain/left-sidebar/waves/memes-quick-vote/MemesQuickVoteControls.tsx`
around lines 189 - 205, The custom amount input in MemesQuickVoteControls lacks
an explicit accessible name; update the <input> for customValue to include an
aria-label (e.g., aria-label="Custom vote amount") or give it an id and connect
the existing visible label using htmlFor so screen readers can identify it, and
ensure handlers (onCustomChange, onCustomSubmit) and disabled/isSubmitting
behavior remain unchanged.
__tests__/components/brain/BrainMobile.test.tsx (1)

336-343: CSS class selector in test may be brittle.

The test relies on a specific CSS class combination (.tw-relative.tw-min-w-0.tw-flex-1) to verify layout positioning. Consider adding a data-testid to the wrapper element in the production code for more stable test queries.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@__tests__/components/brain/BrainMobile.test.tsx` around lines 336 - 343, The
test in __tests__/components/brain/BrainMobile.test.tsx is using a brittle CSS
selector ".tw-relative.tw-min-w-0.tw-flex-1" to locate the active pane; add a
stable data attribute (e.g., data-testid="active-pane") to the wrapper element
in the production component (the element that currently has those classes), then
update the test to query that element via getByTestId("active-pane") and assert
it contains screen.getByTestId("quick-vote-trigger") instead of using the long
class selector.
components/brain/left-sidebar/waves/memes-quick-vote/MemesQuickVoteDialog.tsx (1)

82-88: Reset the local advancing lock on rejected vote submissions.

isAdvancing is only cleared on the false branch today. If submitVote() rejects, the controls stay disabled, and the swipe path also turns that into an unhandled promise rejection. A small try/catch or finally here would make the dialog recover cleanly from transient failures.

♻️ Possible hardening
 const queueVoteAmount = async (amount: number | string) => {
-  const wasQueued = await submitVote(activeDrop, amount);
-
-  if (!wasQueued) {
-    setIsAdvancing(false);
-  }
+  try {
+    const wasQueued = await submitVote(activeDrop, amount);
+    if (!wasQueued) {
+      setIsAdvancing(false);
+    }
+  } catch {
+    setIsAdvancing(false);
+    throw;
+  }
 };

Also applies to: 122-133

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@components/brain/left-sidebar/waves/memes-quick-vote/MemesQuickVoteDialog.tsx`
around lines 82 - 88, The submitVote call in queueVoteAmount can reject and
never clears the advancing lock; wrap the await submitVote(activeDrop, amount)
in a try/catch/finally inside the queueVoteAmount function (and the other
similar block around submitVote at the second occurrence) so that
setIsAdvancing(false) is called in finally, handle/log the caught error to avoid
an unhandled rejection, and preserve the existing wasQueued logic for the
success path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@components/brain/left-sidebar/waves/memes-quick-vote/MemesQuickVotePreview.tsx`:
- Around line 216-227: The inline style always applies a transitionDuration so
the card eases toward the finger; update MemesQuickVotePreview so the style for
transitionDuration only applies for snap-back or committed exit (e.g., when
swipeExitDirection is truthy or when NOT currently dragging). Concretely,
introduce or reuse the component's dragging flag (e.g., isDragging /
isPointerDown / isSwiping) and change the style block that sets
transitionDuration: use `${SWIPE_EXIT_DURATION_MS}ms` only when
swipeExitDirection || !isDragging, otherwise leave transitionDuration undefined
(or 0) so the card follows the finger directly while dragging; keep
cardTransform, opacity, and touchAction logic unchanged.

In `@components/brain/left-sidebar/waves/MemesWaveFooter.tsx`:
- Around line 66-74: The button currently rendered in MemesWaveFooter remains
interactive even when unratedCount is 0; change the render so that when
unratedCount === 0 you render a disabled control (or non-interactive static
element) instead of an enabled button: stop attaching onClick
(handleOpenQuickVote) and pointer event handlers (handlePrefetchQuickVote), add
the disabled attribute or role="status"/aria-disabled as appropriate, and remove
or reduce hover/focus styling (the desktop-hover:hover:… and
onFocus/onMouseEnter handlers) so it looks visually inert; ensure the aria-label
still communicates the 0 state (using uncastPower and votingLabel) and that
keyboard/assistive users cannot tab to or activate the control when unratedCount
is 0.

In `@components/brain/mobile/useBrainMobileActiveView.ts`:
- Around line 149-158: The preserved-selection key ignores createParam so
entering create mode doesn't reset selection; update the context key logic used
to compute shellContextKey/currentContextKey to include createParam (and keep
viewParam) so create mode produces a different key than the normal shell view;
specifically modify shellContextKey (and therefore currentContextKey when
!waveId) to incorporate createParam alongside pathname and viewParam so that
getRouteDefaultView(createParam, ...) will take effect and stale shell
selections are not reused.

In `@config/nextConfig.ts`:
- Line 17: Replace the hard-coded allowedDevOrigins array with an
environment-driven value: add ALLOWED_DEV_ORIGINS to the env schema (e.g., in
env.schema.ts declare ALLOWED_DEV_ORIGINS as an optional string) and update the
allowedDevOrigins setting in nextConfig.ts to read from
publicEnv.ALLOWED_DEV_ORIGINS, split the comma-separated string, trim entries
and filter out empties so it produces an array; ensure the config falls back to
an empty array when the env var is unset.

In `@docs/specs/2026-03-20-memes-quick-vote-refactor.md`:
- Line 273: The document uses inconsistent pagination wording
("restart-from-page-`0`" vs "page `1`"); choose whether pages are 0-indexed or
1-indexed, standardize all occurrences to that choice, and update the lines
referencing "restart paginated discovery from page `1`", "page-`1` pass", and
"restart-from-page-`0`" so they all use the same index convention and include a
short note (one sentence) at the top of the pagination section stating the
chosen indexing convention.

In `@hooks/memesQuickVote.query.ts`:
- Around line 25-32: The cache key getMemesQuickVoteDropQueryKey currently only
varies by dropId and must be extended to include viewer identity so
viewer-specific fields (context_profile_context/eligibility/rating) don't leak
between profiles; update getMemesQuickVoteDropQueryKey to accept and include the
viewer identifiers (e.g., contextProfile and proxyId or the equivalent
context_profile_context) alongside QueryKey.DROP and dropId, and update callers
in usePrefetchMemesQuickVote.ts to pass those two values through when building
the key so the key shape matches other summary/discovery keys that include
viewer identity.

In `@hooks/useMemesQuickVoteDialogController.ts`:
- Around line 33-41: The current prefetchQuickVote marks a reserved sessionId in
prefetchedSessionIdsRef before awaiting the async prefetchMemesQuickVote, so
failures are permanently deduped; change the logic so the sessionId is only
added after a successful prefetch or, if you prefer to keep optimistic marking,
attach a .catch handler to the prefetchMemesQuickVote(sessionId) promise that
removes the sessionId from prefetchedSessionIdsRef on error (and optionally log
the error), ensuring future hovers/focuses can retry; locate this behavior in
prefetchQuickVote, reserveSessionId, prefetchedSessionIdsRef, and
prefetchMemesQuickVote to implement the fix.

In `@hooks/useMemesQuickVoteSubmit.ts`:
- Around line 113-121: The remaining power is being computed from the delta
(appliedAmount) instead of the final rating; update the logic so
nextRemainingPower is derived from the final rating: if
response.context_profile_context?.rating (nextRating) is a number compute
nextRemainingPower = Math.max(0, queuedVote.maxRating - nextRating); otherwise
compute finalRating = queuedVote.currentRating + appliedAmount and set
nextRemainingPower = Math.max(0, queuedVote.maxRating - finalRating); keep
appliedAmount logic unchanged and reference nextRating, appliedAmount,
queuedVote.currentRating, and queuedVote.maxRating to locate the code.

In `@hooks/usePrefetchMemesQuickVote.ts`:
- Around line 51-95: Wrap the discovery fetch and the subsequent drop prefetches
in a try/catch so rejections are handled instead of leaking (surround the
queryClient.fetchQuery call that uses
getMemesQuickVoteDiscoveryQueryKey/fetchMemesQuickVoteDiscoveryBatch and the
mapping that calls queryClient.prefetchQuery for
getMemesQuickVoteDropQueryKey/fetchMemesQuickVoteDrop with a try/catch), and add
a staleTime (e.g. 60_000) to the discovery fetchQuery options and to each drop
prefetchQuery options so those results do not become immediately stale (mirror
the summaryPromise staleTime). Ensure you still keep existing retry settings
(getDefaultQueryRetry / retry: false) when adding staleTime.

---

Nitpick comments:
In `@__tests__/components/auth/Auth.test.tsx`:
- Around line 31-45: The isAuthAddressAuthorized mock implementation is
duplicated (appearing around the current block and again at lines 203-219);
extract the matching logic into a single reusable helper function (e.g.,
isAuthAddressAuthorizedMatcher) and replace both jest.fn(...) implementations
with jest.fn(isAuthAddressAuthorizedMatcher) so both mocks call the same
function (update references to the parameter shape used in the existing
implementations to preserve behavior).

In `@__tests__/components/brain/BrainMobile.test.tsx`:
- Around line 336-343: The test in
__tests__/components/brain/BrainMobile.test.tsx is using a brittle CSS selector
".tw-relative.tw-min-w-0.tw-flex-1" to locate the active pane; add a stable data
attribute (e.g., data-testid="active-pane") to the wrapper element in the
production component (the element that currently has those classes), then update
the test to query that element via getByTestId("active-pane") and assert it
contains screen.getByTestId("quick-vote-trigger") instead of using the long
class selector.

In `@__tests__/hooks/usePrefetchMemesQuickVote.test.tsx`:
- Around line 65-134: Add unit tests for usePrefetchMemesQuickVote covering the
suggested edge cases: create separate test cases that (1) mock
useMemesQuickVoteContextMock to return isEnabled: false and assert
commonApiFetchMock is not called and no queries are prefetched via QueryClient;
(2) mock commonApiFetchMock to throw an error for leaderboard or drop endpoints
and assert the hook handles it (e.g., does not crash and logs/returns expected
state) by spying on console/process logger or checking QueryClient state; and
(3) simulate duplicate invocations by calling the hook twice within the same
test (or calling the prefetch function twice) and assert commonApiFetchMock is
called only once per unique drop id (or that QueryClient avoids duplicate
fetches) using jest mocks and useMemesQuickVoteStorageMock to control
skippedDropIds. Ensure each test uses the existing setup patterns (QueryClient,
createDrop, WAVE_ID, commonApiFetchMock, useMemesQuickVoteContextMock,
useMemesQuickVoteStorageMock) for easy integration.

In `@__tests__/services/auth.utils.test.ts`:
- Around line 147-197: Add a unit test covering the early-return path when the
supplied address is null or undefined by calling isAuthAddressAuthorized with
address: null (and a separate case for undefined if desired) and a minimal
connectedAccounts array, asserting it returns false; ensure the test references
isAuthAddressAuthorized to locate the function and mirrors existing test style
(use require("@/config/env") only if environment mutation is needed) so the
behavior for null/undefined addresses is pinned.

In `@components/brain/BrainMobile.tsx`:
- Around line 181-191: The quick-vote mounting logic
(shouldMountFloatingQuickVoteEntry and shouldMountQuickVoteDialog) currently
only checks for any wave and !isDm, so memes quick-vote UI can mount on
non-memes waves; update the guards to include the isMemesWave boolean so the
floating entry and dialog only mount for memes waves (i.e., add isMemesWave to
the AND list in shouldMountFloatingQuickVoteEntry and ensure
shouldMountQuickVoteDialog respects that same memes-scoped condition alongside
isQuickVoteOpen and activeView checks involving BrainView.DEFAULT and
BrainView.WAVES).

In
`@components/brain/left-sidebar/waves/memes-quick-vote/MemesQuickVoteControls.tsx`:
- Around line 189-205: The custom amount input in MemesQuickVoteControls lacks
an explicit accessible name; update the <input> for customValue to include an
aria-label (e.g., aria-label="Custom vote amount") or give it an id and connect
the existing visible label using htmlFor so screen readers can identify it, and
ensure handlers (onCustomChange, onCustomSubmit) and disabled/isSubmitting
behavior remain unchanged.

In
`@components/brain/left-sidebar/waves/memes-quick-vote/MemesQuickVoteDialog.tsx`:
- Around line 82-88: The submitVote call in queueVoteAmount can reject and never
clears the advancing lock; wrap the await submitVote(activeDrop, amount) in a
try/catch/finally inside the queueVoteAmount function (and the other similar
block around submitVote at the second occurrence) so that setIsAdvancing(false)
is called in finally, handle/log the caught error to avoid an unhandled
rejection, and preserve the existing wasQueued logic for the success path.

In `@components/brain/mobile/FloatingMemesQuickVoteTrigger.tsx`:
- Around line 15-19: The guard in FloatingMemesQuickVoteTrigger is overly
defensive: remove the redundant checks for typeof uncastPower !== "number" and
unratedCount <= 0 and only keep the isReady check from useMemesWaveFooterStats;
update the early-return to "if (!isReady) return null" so the hook's contract is
relied upon and the code is simplified while still returning null when the data
isn't ready.

In `@hooks/useMemesQuickVoteContext.ts`:
- Around line 16-23: Derived values memesWaveId and isEnabled are being
recalculated every render while contextProfile is memoized; wrap the computation
of memesWaveId (from seizeSettings.memes_wave_id) and the boolean isEnabled in
useMemo (or the same memo hook you used for contextProfile) so they preserve
referential equality across renders and match the existing memoization pattern
(refer to seizeSettings, memesWaveId, isEnabled, contextProfile, and
activeProfileProxy to build the memo dependencies).

In `@hooks/useMemesWaveFooterStats.ts`:
- Around line 6-8: Export the MemesWaveFooterStats type so consumers can
explicitly annotate the hook's return value; update the declaration of
MemesWaveFooterStats to be exported (export type MemesWaveFooterStats = ...) and
ensure any related exports or imports that rely on this type (e.g., usages of
useMemesWaveFooterStats) are adjusted accordingly to reference the exported
type.

In `@openapi.yaml`:
- Around line 5843-5851: The parameter unvoted_by_me currently implies
authentication but the operation only documents a 200 response; update the
OpenAPI spec to explicitly state expected behavior for unauthenticated requests:
either (A) add a 401 response object to the operation and update the
unvoted_by_me description to say the filter requires authentication and will
return 401 if unset, or (B) update the unvoted_by_me description to say the
filter is ignored for unauthenticated requests and keep only 200; modify the
operation responses accordingly (add a 401 response if choosing A) and mention
the authentication requirement next to the unvoted_by_me parameter so clients
know whether the filter requires auth.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0dc39fd8-1634-47db-94db-93a4615d0e66

📥 Commits

Reviewing files that changed from the base of the PR and between 9fceeea and eb17616.

📒 Files selected for processing (54)
  • __tests__/components/auth/Auth.test.tsx
  • __tests__/components/brain/BrainMobile.test.tsx
  • __tests__/components/brain/left-sidebar/waves/MemesWaveFooter.test.tsx
  • __tests__/components/brain/left-sidebar/waves/UnifiedWavesList.test.tsx
  • __tests__/components/brain/left-sidebar/waves/memes-quick-vote/MemesQuickVoteDialog.test.tsx
  • __tests__/components/brain/left-sidebar/web/WebLeftSidebar.test.tsx
  • __tests__/components/brain/left-sidebar/web/WebUnifiedWavesList.test.tsx
  • __tests__/components/brain/mobile/BrainMobileViewContent.test.tsx
  • __tests__/components/brain/mobile/BrainMobileWaves.test.tsx
  • __tests__/components/brain/mobile/FloatingMemesQuickVoteTrigger.test.tsx
  • __tests__/components/brain/my-stream/MyStreamWaveTabsLeaderboard.test.tsx
  • __tests__/components/layout/AppLayout.test.tsx
  • __tests__/hooks/memesQuickVote.helpers.test.ts
  • __tests__/hooks/memesQuickVote.queue.helpers.test.ts
  • __tests__/hooks/useMemesQuickVoteDialogController.test.tsx
  • __tests__/hooks/useMemesQuickVoteQueue.test.tsx
  • __tests__/hooks/useMemesWaveFooterStats.test.tsx
  • __tests__/hooks/usePrefetchMemesQuickVote.test.tsx
  • __tests__/services/auth.utils.test.ts
  • components/auth/Auth.tsx
  • components/brain/BrainMobile.tsx
  • components/brain/left-sidebar/waves/MemesWaveFooter.tsx
  • components/brain/left-sidebar/waves/MemesWaveQuickVoteTrigger.tsx
  • components/brain/left-sidebar/waves/memes-quick-vote/MemesQuickVoteControls.tsx
  • components/brain/left-sidebar/waves/memes-quick-vote/MemesQuickVoteDialog.tsx
  • components/brain/left-sidebar/waves/memes-quick-vote/MemesQuickVoteDialogSkeleton.tsx
  • components/brain/left-sidebar/waves/memes-quick-vote/MemesQuickVotePreview.tsx
  • components/brain/left-sidebar/web/WebLeftSidebar.tsx
  • components/brain/mobile/BrainMobileTabs.tsx
  • components/brain/mobile/BrainMobileViewContent.tsx
  • components/brain/mobile/BrainMobileWaves.tsx
  • components/brain/mobile/FloatingMemesQuickVoteTrigger.tsx
  • components/brain/mobile/brainMobileViews.ts
  • components/brain/mobile/useBrainMobileActiveView.ts
  • components/brain/my-stream/MyStreamWaveTabsLeaderboard.tsx
  • components/layout/AppLayout.tsx
  • config/nextConfig.ts
  • docs/specs/2026-03-20-memes-quick-vote-refactor.md
  • hooks/memesQuickVote.helpers.ts
  • hooks/memesQuickVote.query.ts
  • hooks/memesQuickVote.queue.helpers.ts
  • hooks/memesQuickVote.storageStore.ts
  • hooks/useMemesQuickVoteActiveDrop.ts
  • hooks/useMemesQuickVoteContext.ts
  • hooks/useMemesQuickVoteDialogController.ts
  • hooks/useMemesQuickVoteDiscovery.ts
  • hooks/useMemesQuickVoteQueue.ts
  • hooks/useMemesQuickVoteStorage.ts
  • hooks/useMemesQuickVoteSubmit.ts
  • hooks/useMemesQuickVoteSummary.ts
  • hooks/useMemesWaveFooterStats.ts
  • hooks/usePrefetchMemesQuickVote.ts
  • openapi.yaml
  • services/auth/auth.utils.ts

Comment thread components/brain/left-sidebar/waves/MemesWaveFooter.tsx
Comment thread components/brain/mobile/useBrainMobileActiveView.ts
Comment thread config/nextConfig.ts
Comment thread docs/specs/2026-03-20-memes-quick-vote-refactor.md
Comment thread hooks/memesQuickVote.query.ts
Comment thread hooks/useMemesQuickVoteDialogController.ts
Comment thread hooks/useMemesQuickVoteSubmit.ts
Comment thread hooks/usePrefetchMemesQuickVote.ts
@simo6529 simo6529 merged commit bcf872c into main Mar 24, 2026
7 checks passed
@simo6529 simo6529 deleted the quick-vote branch March 24, 2026 15:04
@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
1 Security Hotspot

See analysis details on SonarQube Cloud

@coderabbitai coderabbitai Bot mentioned this pull request Mar 25, 2026
@coderabbitai coderabbitai Bot mentioned this pull request Apr 2, 2026
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.

2 participants