Skip to content

feat: enhance polling functionality with voting transparency on the governance page#683

Open
ibsule wants to merge 17 commits into
livepeer:mainfrom
ibsule:governance-vote-transparency
Open

feat: enhance polling functionality with voting transparency on the governance page#683
ibsule wants to merge 17 commits into
livepeer:mainfrom
ibsule:governance-vote-transparency

Conversation

@ibsule

@ibsule ibsule commented May 27, 2026

Copy link
Copy Markdown

Description

This PR introduces a new poll vote transparency experience on the governance voting page through the following:

  • Adds a full PollVote UI flow (desktop/mobile vote tables, vote detail/popover views) so users can inspect who voted and how much stake was used.
  • Expands polling data/query coverage to include richer vote-event fields and wiring needed to render detailed vote data.
  • Updates the voting page/widget UX to make navigating between overview and vote details easier.

Why

Governance users need transparent, in-context access to vote details (who voted, vote stake, and related metadata) to better understand outcomes and build trust in the polling process without leaving the poll flow.

Type of Change

  • feat: New feature
  • fix: Bug fix
  • docs: Documentation update
  • style: Code style/formatting changes (no logic changes)
  • refactor: Code refactoring (no behavior change)
  • perf: Performance improvement
  • test: Adding or updating tests
  • build: Build system or dependency changes
  • ci: CI/CD changes
  • chore: Other changes

Related Issue(s)

Related: #310
Closes: #482

Changes Made

  • Added a new PollVote feature set for deeper vote visibility on governance pages.
  • Introduced new components: PollVoteDetail, DesktopVoteTable, MobileVoteView, MobileVoteCards, PollVotePopover, and components/PollVote/index.tsx.
  • Updated data layer and queries (apollo/subgraph.ts, lib/api/polls.ts, queries/poll.graphql, new queries/voteEvents.graphql) to fetch richer vote details.
  • Enhanced pages/voting/[poll].tsx and components/PollVotingWidget/index.tsx to support improved vote UX/navigation.

Testing

  • Tested locally
  • Added/updated tests
  • All tests passing

Impact / Risk

Risk level: Low

Impacted areas: UI

Rollback plan: PR revert

Screenshots / Recordings (if applicable)

Before

image

After

image image

The ENS names are not displayed here due to my RPC urls setup, but in production (with the production RPC urls) they display just fine.

Summary by CodeRabbit

  • New Features
    • Added a richer, responsive poll vote UI (desktop table + mobile cards) with improved voter identity, support badges, stake/percentage formatting, transaction details, and timestamps.
    • Introduced vote detail views and a “Voting History” popover showing per-voter vote totals and a timeline of past votes.
    • Updated the voting page with a “Votes” tab and a “View votes” action that preserves the current page state.
  • Updates
    • Enhanced vote/support labeling with an “Unknown” fallback and improved poll title loading.
  • Documentation
    • Refreshed generated typings and query/hook support for the new vote-events data.

ibsule added 3 commits May 27, 2026 12:01
…overnance page

- Updated GraphQL queries to include additional fields for votes, such as choiceID, voter, voteStake, and nonVoteStake.
- Introduced a new `parsePollText` function to handle proposal parsing from IPFS.
- Enhanced the PollVotingWidget component to display a link to view votes and improved the layout for better user experience.
- Added a new PollVotingTable component for displaying detailed vote information.
- Updated the voting page to support tab navigation between overview and votes, improving accessibility to voting data.

These changes aim to provide a more comprehensive voting experience and better data handling for polls.
…te components

- Removed duplicate imports and organized import statements for clarity in DesktopVoteTable, index, MobileVoteCards, and MobileVoteView components.
- Ensured consistent formatting and spacing in various files to enhance readability.
- Updated GraphQL queries to maintain structure and improve maintainability.
- Minor adjustments to the PollVotingWidget and PollVotePopover components for better integration.
- Updated the PollVote components to include vote stake information, allowing for better transparency in voting data.
- Modified the `onSelect` function to pass `voteStake` along with the voter's address and ENS name.
- Introduced a `formatVoteStake` function to format and display vote stakes consistently across DesktopVoteTable, MobileVoteCards, and MobileVoteView components.
- Adjusted GraphQL queries and state management to accommodate the new vote stake data, improving overall functionality and user experience.
@ibsule ibsule requested a review from ECWireless as a code owner May 27, 2026 20:29
@vercel

vercel Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

@ibsule is attempting to deploy a commit to the Livepeer Foundation Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds vote-event querying and responsive vote UI for governance polls: GraphQL updates and generated Apollo hooks, desktop/mobile components to show votes and per-voter history, ENS resolution and vote decoration, and tab-driven page navigation (overview vs. votes).

Changes

Governance Voting Transparency

Layer / File(s) Summary
GraphQL schema and generated Apollo types
queries/poll.graphql, queries/vote.graphql, queries/voteEvents.graphql, apollo/subgraph.ts
Poll query now selects vote fields (choiceID, voter, voteStake, nonVoteStake); vote query extended to include nested poll.votes with voteStake; new voteEvents query added with filtering and descending-timestamp ordering. Apollo types, documents, and hooks generated for all updated and new operations.
Vote support mapping and poll text parsing
lib/api/types/votes.ts, lib/api/polls.ts
POLL_VOTES extended with unknown entry; VOTING_SUPPORT_MAP maps PollChoice.Yes/No to vote badge styles and includes "Unknown" fallback; parsePollText helper added to fetch and parse IPFS poll proposals asynchronously.
Desktop vote table component
components/PollVote/DesktopVoteTable.tsx
React-table-based vote table with memoized columns for voter identity, support badge, weighted stake, timestamp, and transaction display, plus a history action cell triggering voter selection callback. Responsive container hidden until desktop breakpoint.
Mobile vote cards and detail view
components/PollVote/MobileVoteCards.tsx, components/PollVote/MobileVoteView.tsx, components/PollVote/PollVoteDetail.tsx
Mobile-optimized paginated vote list rendering individual vote rows with voter, support, stake, and history button. Dual-layout detail component showing vote card (mobile) or timeline entry (desktop) with async-loaded poll title, badge, stake percentage, and transaction display.
Vote history popover and modal
components/PollVote/PollVotePopover.tsx
Modal popover querying vote events filtered by voter, computing voting statistics (total, for, against), and rendering a timeline of vote details with Livepeer explorer link and summary header showing vote counts and badge stats.
Vote events pagination and aggregation hook
hooks/useGetAllPollVotesEvents.ts
useGetAllVoteEvents hook iteratively fetches vote events using timestamp-based cursor pagination, aggregates results across pages with independent loading/error state, and prevents state updates after unmount.
Vote orchestration and responsive rendering
components/PollVote/index.tsx
useVotes hook queries poll votes and vote events, deduplicates voters, resolves voter ENS names with per-effect in-memory cache, and decorates votes with latest matching event transaction hash and timestamp. PollVote index component provides responsive table/card rendering, pagination for mobile, voter selection, and popover integration.
Page navigation and widget integration
pages/voting/[poll].tsx, components/PollVotingWidget/index.tsx
Voting page adds view-based tab navigation (overview vs. votes) driven by URL query parameter, computes votesTabHref to preserve route while toggling view mode. PollVotingWidget gains optional votesTabHref prop and renders "View votes" CTA as either a Next.js Link (when prop provided) or anchor to #votes-section fallback.

Sequence Diagram

sequenceDiagram
  participant User
  participant Page as voting/[poll].tsx
  participant Widget as PollVotingWidget
  participant Table as PollVote
  participant Apollo as Apollo Hooks
  participant Popover as PollVotePopover
  User->>Page: Load governance poll page
  Page->>Page: Derive view mode from query, compute votesTabHref
  Page->>Widget: Render with votesTabHref prop
  Widget->>Widget: Render "View votes" link
  User->>Widget: Click "View votes"
  Widget->>Page: Navigate to view=votes
  Page->>Table: Render vote table (view=votes)
  Table->>Apollo: useVotesQuery (poll votes + events)
  Apollo-->>Table: Decorated votes with ENS, tx hash, timestamp
  Table->>Page: Render desktop table or mobile cards
  User->>Table: Click voter row/card
  Table->>Popover: Display voter's vote history
  Popover->>Apollo: useVoteEventsQuery (voter filter)
  Apollo-->>Popover: Sorted vote events
  Popover->>Page: Render timeline of PollVoteDetail entries
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main enhancement: adding voting transparency to the governance page through poll functionality improvements.
Description check ✅ Passed The PR description comprehensively covers objectives, changes, testing, and impact with screenshots. It follows the template structure with proper sections for feature type, related issues, changes made, and risk assessment.
Linked Issues check ✅ Passed The PR successfully addresses issue #482 by bringing voting transparency to the Governance page with detailed voter information, stake amounts, and voting history, matching the existing implementation pattern from #457.
Out of Scope Changes check ✅ Passed All changes are directly scoped to voting transparency features on the governance page. No unrelated modifications or scope creep detected across GraphQL queries, components, hooks, and styling updates.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@components/PollVote/index.tsx`:
- Around line 114-118: The Index component currently ignores the error returned
by useVotes, causing request failures to be rendered as an empty “No votes
found” state; update Index to consume the error value returned from useVotes
(the error/pollError or pollVoteEventsError aggregated into error) and render an
explicit error state (message and optional retry UI) when error is truthy before
the empty-state logic, ensuring you still respect votesLoading/loading to show
spinners while loading and only show the empty-state when there is no error and
no votes.

In `@components/PollVote/MobileVoteView.tsx`:
- Around line 75-77: In MobileVoteView.tsx there are external anchor elements
that set target="_blank" (the one rendering
href={`https://explorer.livepeer.org/accounts/${vote.voter}/delegating`} and the
other at the later occurrence) but do not include rel protection; update both
anchor elements (in the MobileVoteView component) to add rel="noopener
noreferrer" alongside target="_blank" to prevent reverse-tabnabbing and other
security issues.

In `@components/PollVote/PollVoteDetail.tsx`:
- Around line 80-82: Add rel="noopener noreferrer" to any anchor that opens in a
new tab to prevent reverse-tabnabbing; specifically update the anchor rendering
href={`/voting/${vote.poll.id}`} (inside the PollVoteDetail component) and the
other anchor instance around lines ~185-187 to include rel="noopener noreferrer"
alongside target="_blank" so both links use target="_blank" rel="noopener
noreferrer".

In `@components/PollVote/PollVotePopover.tsx`:
- Around line 41-43: PollVotePopover is currently counting votes by checking
choiceID === "0" and "1" which undercounts when events use enum keys like
PollChoice.Yes/PollChoice.No; update the counting to normalize each
voteEvent.choiceID via the existing POLL_VOTES/VOTING_SUPPORT_MAP semantics (or
a small helper normalizeChoiceID) and then compute totals by testing the
normalized support value (e.g., SUPPORT_FOR vs SUPPORT_AGAINST) rather than raw
"0"/"1" strings so voteEvents.map/ filter logic uses the canonical support
mapping for for/against/abstain.
- Around line 60-62: In PollVotePopover (the JSX anchor that renders
href={`https://explorer.livepeer.org/accounts/${voter}/delegating`} and uses
target="_blank"), add the rel="noopener noreferrer" attribute to the anchor
element to prevent reverse-tabnabbing; update the anchor in the PollVotePopover
component where the external account link is created to include rel="noopener
noreferrer".

In `@lib/api/polls.ts`:
- Around line 64-69: parsePollText currently calls catIpfsJson(proposal) and
parsePollIpfs without handling IPFS/network errors, so wrap the fetch and parse
in a try/catch inside parsePollText (the function returning Promise<Fm | null)
and return null on any failure; ensure you call catIpfsJson<IpfsPoll>(proposal)
and then parsePollIpfs(ipfsObject) inside the try, and in the catch optionally
log the error (e.g., via console.error or existing logger) before returning null
to avoid unhandled promise rejections.

In `@pages/voting/`[poll].tsx:
- Around line 314-316: The current implementation hides tab panes with CSS
(using view) but keeps both component trees mounted so voteContent() still
executes; change to conditional JSX rendering so only the active pane mounts by
using a state/derived variable (e.g., safeView or view) to choose between
rendering the Overview component and the Votes component (replace the Box
wrappers that use display toggles with conditional expressions like safeView ===
"overview" ? <Overview .../> : <Votes .../>), and ensure voteContent() is only
called/used inside the Votes branch so expensive work runs only when Votes is
active.
- Line 55: The query-derived view value is not validated: normalize it by
creating a safeView (e.g., const safeView = view === "votes" ? "votes" :
"overview") and use safeView everywhere the component reads tab state or
conditionally renders (replace uses of view in tab state initialization and the
conditional rendering blocks around the overview/votes content, e.g., where view
is referenced at the tab state and the render checks currently at lines ~315 and
~469); this ensures any unsupported query value falls back to "overview" and
prevents empty main content.

In `@queries/voteEvents.graphql`:
- Around line 1-6: The voteEvents query call in
components/PollVote/PollVotePopover.tsx is missing a bounded `first` variable
and can return unbounded results; update the useVoteEventsQuery invocation in
PollVotePopover (the hook call) to pass a safe limit (e.g., `first: 200`)
consistent with components/PollVote/index.tsx, or alternatively set a default in
the GraphQL definition by changing `$first: Int` to `$first: Int = 200` in
queries/voteEvents.graphql so the query is always paginated.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 346f92d3-4693-46b7-a689-465fd40a8b1e

📥 Commits

Reviewing files that changed from the base of the PR and between 9959381 and 9e80ed9.

📒 Files selected for processing (13)
  • apollo/subgraph.ts
  • components/PollVote/DesktopVoteTable.tsx
  • components/PollVote/MobileVoteCards.tsx
  • components/PollVote/MobileVoteView.tsx
  • components/PollVote/PollVoteDetail.tsx
  • components/PollVote/PollVotePopover.tsx
  • components/PollVote/index.tsx
  • components/PollVotingWidget/index.tsx
  • lib/api/polls.ts
  • lib/api/types/votes.ts
  • pages/voting/[poll].tsx
  • queries/poll.graphql
  • queries/voteEvents.graphql

Comment thread components/PollVote/index.tsx
Comment thread components/PollVote/MobileVoteView.tsx
Comment thread components/PollVote/PollVoteDetail.tsx
Comment thread components/PollVote/PollVotePopover.tsx Outdated
Comment thread components/PollVote/PollVotePopover.tsx
Comment thread lib/api/polls.ts
Comment thread pages/voting/[poll].tsx
Comment thread pages/voting/[poll].tsx
Comment thread queries/voteEvents.graphql
- Renamed the "Stake Used" header to "Weight" for clarity.
- Updated the accessor ID from "stakeUsed" to "weight"

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 8

♻️ Duplicate comments (9)
lib/api/polls.ts (1)

64-69: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle IPFS/parsing errors in parsePollText.

  • Problem: catIpfsJson/parsePollIpfs errors are not caught.
  • Why it matters: A transient IPFS failure can bubble up and break vote-detail rendering.
  • Suggested fix: Wrap the body in try/catch and return null on failure (optionally log once).
Proposed patch
 export const parsePollText = async (proposal: string): Promise<Fm | null> => {
-  const ipfsObject = await catIpfsJson<IpfsPoll>(proposal);
-  const attributes = parsePollIpfs(ipfsObject);
-
-  return attributes;
+  try {
+    const ipfsObject = await catIpfsJson<IpfsPoll>(proposal);
+    return parsePollIpfs(ipfsObject);
+  } catch {
+    return null;
+  }
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/api/polls.ts` around lines 64 - 69, parsePollText currently awaits
catIpfsJson and parsePollIpfs without handling failures, so any IPFS or parsing
error can propagate; wrap the body of parsePollText in a try/catch that catches
any thrown error from catIpfsJson or parsePollIpfs and returns null on failure
(optionally call a logger once with context like "parsePollText failed" and the
error), keeping the signature Promise<Fm | null> unchanged and ensuring any
caught error does not rethrow.
queries/voteEvents.graphql (1)

1-6: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add a safe default for $first to prevent unbounded vote-event fetches.

  • Problem: $first is optional and this query forwards it directly; an existing consumer (components/PollVote/PollVotePopover.tsx) omits first.
  • Why it matters: Large voter histories can produce oversized responses and hurt latency/UI responsiveness.
  • Suggested fix: Set a default in the query signature ($first: Int = 200) and keep explicit first at callsites where needed.
#!/bin/bash
# Verify all voteEvents hook callsites provide a bounded `first`.
rg -n -C3 --type=ts --type=tsx 'useVoteEventsQuery\s*\('
rg -n -C3 --type=ts --type=tsx 'variables\s*:\s*\{[^}]*first'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@queries/voteEvents.graphql` around lines 1 - 6, The voteEvents GraphQL query
(query voteEvents) accepts an optional $first and can be called without a bound
(e.g., from components/PollVote/PollVotePopover.tsx via useVoteEventsQuery),
which risks huge responses; set a safe default by changing the query signature
to provide a default for $first (e.g., = 200) and leave explicit first values at
callsites that need different limits; after updating, verify consumers
(useVoteEventsQuery callsites) still behave correctly and add explicit first
where necessary.
components/PollVote/index.tsx (1)

133-177: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Problem: useVotes returns an error value (Line 117), but the Index component ignores it (Line 133) and renders "No votes found" (Line 172-177) even when a request fails.

Why it matters: Users see misleading empty-state messages when GraphQL queries fail, masking real operational problems and preventing retry attempts.

Suggested fix: Consume the error from useVotes and render an explicit error state with a message (and optionally retry UI) before the empty-state logic.

🛠️ Proposed fix
-  const { votes, loading } = useVotes(pollId);
+  const { votes, loading, error } = useVotes(pollId);

   // ... existing loading spinner code ...

+  if (error) {
+    return (
+      <Flex
+        css={{
+          flexDirection: "column",
+          alignItems: "center",
+          gap: "$3",
+          marginTop: "$4",
+        }}
+      >
+        <Text css={{ color: "$tomato11", textAlign: "center" }}>
+          Failed to load votes. Please try again.
+        </Text>
+      </Flex>
+    );
+  }
+
   if (!votes.length)
     return (
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/PollVote/index.tsx` around lines 133 - 177, The Index component
currently destructures only { votes, loading } from useVotes but ignores the
returned error; update the destructuring to include error (e.g., const { votes,
loading, error } = useVotes(pollId)) and add an early render branch that checks
error and displays an explicit error state (a user-friendly message and optional
retry control) before the existing empty-state check that renders "No votes
found"; ensure this error branch runs when error is truthy and loading is false
so failed GraphQL requests show the error UI instead of the empty votes message.
pages/voting/[poll].tsx (2)

314-471: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Problem: Both "Overview" and "Votes" content sections are rendered and mounted (Line 314-471), with only CSS display toggling visibility. This means voteContent() (Line 470) executes even when the Overview tab is active.

Why it matters: The votes table performs data fetching and rendering work unnecessarily when users stay on Overview, wasting resources and potentially slowing initial page load.

Suggested fix: Render conditionally in JSX so only the active tab's content mounts.

⚡ Proposed conditional rendering
-            <Box css={{ marginTop: "$4" }}>
-              <Box css={{ display: view === "overview" ? "block" : "none" }}>
+            <Box css={{ marginTop: "$4" }}>
+              {view === "overview" ? (
                 <Box
                   css={{
                     display: "grid",
                     // ... overview content ...
                   }}
                 >
                   <Stat ... />
                   <Stat ... />
                 </Box>
                 <Card ...>
                   <MarkdownRenderer>...</MarkdownRenderer>
                 </Card>
-              </Box>
-            </Box>
-
-            <Box css={{ display: view === "votes" ? "block" : "none" }}>
-              {voteContent()}
-            </Box>
+              ) : (
+                voteContent()
+              )}
+            </Box>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pages/voting/`[poll].tsx around lines 314 - 471, The Overview and Votes
blocks are always mounted and only hidden via CSS (so voteContent() runs even
when view !== "votes"); update the JSX to conditionally render those sections
instead of toggling CSS display—i.e., replace the Box that wraps the overview
content (the block using view === "overview" ? "block" : "none" which contains
the two Stat components and the Card/MarkdownRenderer/pollData.attributes) with
a conditional expression that only renders when view === "overview", and
likewise render voteContent() only when view === "votes" (so voteContent() is
called only when the votes view is active).

55-55: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Problem: The view query parameter is not validated (Line 55). Any unsupported value (e.g., ?view=invalid) will result in both content sections being hidden (Line 315, 469), rendering an empty main area.

Why it matters: Users following stale or malformed URLs see a blank page instead of defaulting to the Overview tab.

Suggested fix: Normalize view to only "overview" or "votes" before using it.

✅ Proposed normalization
   const { query } = router;
-  const view = query?.view?.toString().toLowerCase() || "overview";
+  const rawView = query?.view?.toString().toLowerCase() || "overview";
+  const view = rawView === "votes" ? "votes" : "overview";

Then use view everywhere (tab state, conditional rendering).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pages/voting/`[poll].tsx at line 55, The query-derived view value is not
validated and can be any string; change the normalization at the declaration
(the const view = ... line) to only allow "overview" or "votes" and default to
"overview" for any other value (e.g., convert toLowerCase(), then check against
the allowed set and fallback). Use that normalized view variable everywhere the
component reads tab state and in the conditional renders (the places currently
hiding content when view is invalid) so stale/malformed ?view params show the
Overview tab instead of a blank page.
components/PollVote/PollVotePopover.tsx (2)

60-61: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Problem: External link with target="_blank" is missing rel="noopener noreferrer" (Line 60-61).

Why it matters: Creates a reverse-tabnabbing vulnerability where the Livepeer explorer page could manipulate the opener window.

Suggested fix: Add rel="noopener noreferrer" to the <Link> component.

🔒 Proposed fix
         <Link
           href={`https://explorer.livepeer.org/accounts/${voter}/delegating`}
           target="_blank"
+          rel="noopener noreferrer"
           css={{
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/PollVote/PollVotePopover.tsx` around lines 60 - 61, In
PollVotePopover.tsx update the external Link to the Livepeer explorer (the Link
element whose href is
`https://explorer.livepeer.org/accounts/${voter}/delegating` and has
`target="_blank"`) to include the security attributes `rel="noopener
noreferrer"`; locate the Link in the PollVotePopover component and add the rel
attribute so the anchor prevents reverse-tabnabbing when opening the external
page.

41-42: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Problem: Vote statistics hardcode choiceID === "0" and "1" (Line 41-42), but vote events may use PollChoice.Yes/PollChoice.No enum keys, causing incorrect totals.

Why it matters: The "For" and "Against" counts will undercount or show zero when events arrive with enum keys instead of string literals, breaking the voting history display.

Suggested fix: Normalize choiceID via VOTING_SUPPORT_MAP or POLL_VOTES before counting, so both "0"/"1" and Yes/No are recognized as equivalent.

🔧 Proposed fix
   const stats = React.useMemo(() => {
     if (!voteEvents.length) return null;
+    
+    // Normalize choice to support both "0"/"1" and Yes/No
+    const isFor = (choiceID: string) => choiceID === "0" || choiceID === "Yes";
+    const isAgainst = (choiceID: string) => choiceID === "1" || choiceID === "No";

     return {
       total: voteEvents.length,
-      for: voteEvents.filter((v) => v.choiceID === "0").length,
-      against: voteEvents.filter((v) => v.choiceID === "1").length,
+      for: voteEvents.filter((v) => isFor(v.choiceID)).length,
+      against: voteEvents.filter((v) => isAgainst(v.choiceID)).length,
     };
   }, [voteEvents]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/PollVote/PollVotePopover.tsx` around lines 41 - 42, The current
tally uses literal checks choiceID === "0" / "1" which misses enum keys; update
the counting in PollVotePopover to normalize each voteEvent.choiceID via the
existing VOTING_SUPPORT_MAP (or POLL_VOTES) mapping before comparing, e.g., map
choiceID to its normalized support value (For/Against) then count by comparing
against the normalized keys; locate the code around voteEvents.filter(...) and
replace the direct string checks with a small normalization step that converts
enums like PollChoice.Yes/PollChoice.No and "0"/"1" into the same canonical
identifiers before computing the for/against totals.
components/PollVote/MobileVoteView.tsx (1)

75-76: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Problem: External links with target="_blank" are missing rel="noopener noreferrer" (Line 75-76 for Livepeer explorer, Line 145-146 for Arbiscan).

Why it matters: This creates a reverse-tabnabbing security vulnerability where the opened page can access window.opener and potentially redirect the original page to a phishing site.

Suggested fix: Add rel="noopener noreferrer" to both <Link> components.

🔒 Proposed security fix
           <Link
             href={`https://explorer.livepeer.org/accounts/${vote.voter}/delegating`}
             target="_blank"
+            rel="noopener noreferrer"
             css={{
           <Link
             href={`https://arbiscan.io/tx/${vote.transactionHash}`#eventlog``}
             target="_blank"
+            rel="noopener noreferrer"
             css={{

Also applies to: 145-146

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/PollVote/MobileVoteView.tsx` around lines 75 - 76, The external
anchor/Link elements in MobileVoteView.tsx that open a new tab (the one with
href={`https://explorer.livepeer.org/accounts/${vote.voter}/delegating`} and the
Arbiscan link) use target="_blank" but are missing rel="noopener noreferrer";
update both Link/anchor elements to include rel="noopener noreferrer" alongside
target="_blank" to prevent reverse-tabnabbing and ensure window.opener is not
exposed.
components/PollVote/PollVoteDetail.tsx (1)

80-81: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Problem: Links with target="_blank" are missing rel="noopener noreferrer" (Line 80-81 in mobile layout, Line 185-186 in desktop layout).

Why it matters: Even for internal links, omitting rel with target="_blank" creates a security gap and can impact performance by allowing the opened tab to access window.opener.

Suggested fix: Add rel="noopener noreferrer" to both <Link> components.

🔒 Proposed fix
           <Link
             href={`/voting/${vote.poll.id}`}
             target="_blank"
+            rel="noopener noreferrer"
             css={{

(Apply the same change to the second occurrence at Line 185-186)

Also applies to: 185-186

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/PollVote/PollVoteDetail.tsx` around lines 80 - 81, In
PollVoteDetail.tsx update both Link elements that open the poll in a new tab
(the mobile Link with href={`/voting/${vote.poll.id}`} and the desktop Link at
the second occurrence) to include rel="noopener noreferrer" alongside
target="_blank" to prevent window.opener access; locate the two Link components
in the PollVoteDetail component and add the rel attribute to each.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@components/PollVote/DesktopVoteTable.tsx`:
- Around line 51-60: Guard against a missing or null mapping returned by
VOTING_SUPPORT_MAP for row.original.choiceID before accessing support.style:
update the DesktopVoteTable rendering logic to check if support is truthy (from
VOTING_SUPPORT_MAP[row.original.choiceID]) and fall back to a default support
object or render a safe "Unknown" Badge when support is undefined/null, then use
that safe object when reading support.style for the Badge.
- Around line 77-79: The "Weight" column in DesktopVoteTable uses accessor
"voteStake" / id "weight" but lacks an explicit numeric sortType, so
react-table's default alphanumeric sort can misorder stake values; update the
column definition in DesktopVoteTable.tsx to add a sortType that compares
voteStake numerically (e.g., convert rowA.original.voteStake and
rowB.original.voteStake to numbers with parseFloat/Number and return their
difference, with a NaN-safe fallback) so sorting is based on numeric stake
rather than string ordering.

In `@components/PollVote/index.tsx`:
- Around line 121-208: Add a JSDoc comment block immediately above the Index
component declaration describing the component's purpose (renders desktop or
mobile vote views for a poll), list the props with types (PollVotingTableProps
and the pollId prop), and document the return value (React.ReactElement). Ensure
the JSDoc references the component name "Index" and mentions key behaviors such
as pagination, vote formatting via formatVoteStake, and that it opens
PollVotePopover for selected voters.
- Around line 30-119: The useVotes hook lacks JSDoc; add a JSDoc comment
immediately above the useVotes declaration that briefly describes the hook's
purpose (fetches and decorates votes and vote events for a given poll),
documents the parameter (pollId: string) and the return shape (votes:
PollVoteType[], loading: boolean, error: any), and mentions any important
behavior (it resolves ENS names via getEnsForVotes and derives
transactionHash/timestamp from vote events). Reference the hook name useVotes
and core behavior implemented by the decorateVotes async helper so reviewers can
locate the implementation.

In `@components/PollVote/MobileVoteView.tsx`:
- Around line 30-34: Add a JSDoc block above the exported MobileVoteView
function describing its purpose (renders the mobile view for a single poll vote)
and include `@param` entries for the MobileVoteViewProps fields (vote,
formatVoteStake, onSelect) and an `@returns` describing the React element;
reference the MobileVoteView function and the MobileVoteViewProps type so the
doc ties to the component signature. Ensure the description is a one- or
two-sentence summary and each `@param` briefly explains expected types/behavior
(e.g., vote object, formatVoteStake callback, onSelect handler).

In `@components/PollVote/PollVoteDetail.tsx`:
- Around line 21-23: Add a JSDoc block above the exported React component Index
describing the component's purpose and its props (PollVoteDetailProps) at
minimum; mention what the component renders (vote details), any important
behavior (uses POLL_VOTES via choiceID and local title state), and annotate the
function signature so the documentation satisfies the project's contributor
guidelines.

In `@components/PollVote/PollVotePopover.tsx`:
- Around line 19-26: Add JSDoc above the exported React component Index
describing its purpose and props; document that Index is a
React.FC<PollVotePopoverProps> that displays vote events for a given voter and
accepts props voter, ensName, and onClose, and mention side effects like
useVoteEventsQuery usage and loading state (isLoading). Ensure the JSDoc
includes a short description, `@param` entries for voter, ensName, and onClose,
and an `@returns` description for the rendered JSX so the component complies with
contributor documentation standards.

In `@components/PollVotingWidget/index.tsx`:
- Line 2: Remove the no-op empty import "import {} from
\"`@components/PollVote`\";" in components/PollVotingWidget/index.tsx; locate the
import statement (the empty destructuring import) and delete it, and if PollVote
functionality is actually required by this module, replace the empty import with
the correct named or default import (e.g., import PollVote or import {
SomeExport } from "`@components/PollVote`") after verifying usages of PollVote in
this file.

---

Duplicate comments:
In `@components/PollVote/index.tsx`:
- Around line 133-177: The Index component currently destructures only { votes,
loading } from useVotes but ignores the returned error; update the destructuring
to include error (e.g., const { votes, loading, error } = useVotes(pollId)) and
add an early render branch that checks error and displays an explicit error
state (a user-friendly message and optional retry control) before the existing
empty-state check that renders "No votes found"; ensure this error branch runs
when error is truthy and loading is false so failed GraphQL requests show the
error UI instead of the empty votes message.

In `@components/PollVote/MobileVoteView.tsx`:
- Around line 75-76: The external anchor/Link elements in MobileVoteView.tsx
that open a new tab (the one with
href={`https://explorer.livepeer.org/accounts/${vote.voter}/delegating`} and the
Arbiscan link) use target="_blank" but are missing rel="noopener noreferrer";
update both Link/anchor elements to include rel="noopener noreferrer" alongside
target="_blank" to prevent reverse-tabnabbing and ensure window.opener is not
exposed.

In `@components/PollVote/PollVoteDetail.tsx`:
- Around line 80-81: In PollVoteDetail.tsx update both Link elements that open
the poll in a new tab (the mobile Link with href={`/voting/${vote.poll.id}`} and
the desktop Link at the second occurrence) to include rel="noopener noreferrer"
alongside target="_blank" to prevent window.opener access; locate the two Link
components in the PollVoteDetail component and add the rel attribute to each.

In `@components/PollVote/PollVotePopover.tsx`:
- Around line 60-61: In PollVotePopover.tsx update the external Link to the
Livepeer explorer (the Link element whose href is
`https://explorer.livepeer.org/accounts/${voter}/delegating` and has
`target="_blank"`) to include the security attributes `rel="noopener
noreferrer"`; locate the Link in the PollVotePopover component and add the rel
attribute so the anchor prevents reverse-tabnabbing when opening the external
page.
- Around line 41-42: The current tally uses literal checks choiceID === "0" /
"1" which misses enum keys; update the counting in PollVotePopover to normalize
each voteEvent.choiceID via the existing VOTING_SUPPORT_MAP (or POLL_VOTES)
mapping before comparing, e.g., map choiceID to its normalized support value
(For/Against) then count by comparing against the normalized keys; locate the
code around voteEvents.filter(...) and replace the direct string checks with a
small normalization step that converts enums like PollChoice.Yes/PollChoice.No
and "0"/"1" into the same canonical identifiers before computing the for/against
totals.

In `@lib/api/polls.ts`:
- Around line 64-69: parsePollText currently awaits catIpfsJson and
parsePollIpfs without handling failures, so any IPFS or parsing error can
propagate; wrap the body of parsePollText in a try/catch that catches any thrown
error from catIpfsJson or parsePollIpfs and returns null on failure (optionally
call a logger once with context like "parsePollText failed" and the error),
keeping the signature Promise<Fm | null> unchanged and ensuring any caught error
does not rethrow.

In `@pages/voting/`[poll].tsx:
- Around line 314-471: The Overview and Votes blocks are always mounted and only
hidden via CSS (so voteContent() runs even when view !== "votes"); update the
JSX to conditionally render those sections instead of toggling CSS display—i.e.,
replace the Box that wraps the overview content (the block using view ===
"overview" ? "block" : "none" which contains the two Stat components and the
Card/MarkdownRenderer/pollData.attributes) with a conditional expression that
only renders when view === "overview", and likewise render voteContent() only
when view === "votes" (so voteContent() is called only when the votes view is
active).
- Line 55: The query-derived view value is not validated and can be any string;
change the normalization at the declaration (the const view = ... line) to only
allow "overview" or "votes" and default to "overview" for any other value (e.g.,
convert toLowerCase(), then check against the allowed set and fallback). Use
that normalized view variable everywhere the component reads tab state and in
the conditional renders (the places currently hiding content when view is
invalid) so stale/malformed ?view params show the Overview tab instead of a
blank page.

In `@queries/voteEvents.graphql`:
- Around line 1-6: The voteEvents GraphQL query (query voteEvents) accepts an
optional $first and can be called without a bound (e.g., from
components/PollVote/PollVotePopover.tsx via useVoteEventsQuery), which risks
huge responses; set a safe default by changing the query signature to provide a
default for $first (e.g., = 200) and leave explicit first values at callsites
that need different limits; after updating, verify consumers (useVoteEventsQuery
callsites) still behave correctly and add explicit first where necessary.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8ab75bc9-eb5f-459e-8095-1b6a104c7660

📥 Commits

Reviewing files that changed from the base of the PR and between 9959381 and 488711c.

📒 Files selected for processing (13)
  • apollo/subgraph.ts
  • components/PollVote/DesktopVoteTable.tsx
  • components/PollVote/MobileVoteCards.tsx
  • components/PollVote/MobileVoteView.tsx
  • components/PollVote/PollVoteDetail.tsx
  • components/PollVote/PollVotePopover.tsx
  • components/PollVote/index.tsx
  • components/PollVotingWidget/index.tsx
  • lib/api/polls.ts
  • lib/api/types/votes.ts
  • pages/voting/[poll].tsx
  • queries/poll.graphql
  • queries/voteEvents.graphql

Comment thread components/PollVote/DesktopVoteTable.tsx Outdated
Comment thread components/PollVote/DesktopVoteTable.tsx
Comment thread components/PollVote/index.tsx
Comment thread components/PollVote/index.tsx
Comment thread components/PollVote/MobileVoteView.tsx
Comment thread components/PollVote/PollVoteDetail.tsx
Comment thread components/PollVote/PollVotePopover.tsx
Comment thread components/PollVotingWidget/index.tsx Outdated
ibsule added 3 commits June 10, 2026 11:48
…tting

- Updated `PollVoteDetail` to fetch and display total vote stakes
- Modified GraphQL queries to include poll details and associated votes
- Adjusted the rendering of vote stakes in the UI
…te display

- Updated `DesktopVoteTable` to handle unknown voting support by displaying a default badge.
- Introduced sorting functionality for vote stakes in `DesktopVoteTable`.
- Enhanced `useVotes` hook to manage loading and error states.
- Added error message display in the voting UI when fetching votes fails.
- Improved mobile and desktop vote views with additional details and consistent link handling.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
components/PollVote/PollVotePopover.tsx (1)

23-30: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle vote-events query errors explicitly.

  • Problem: useVoteEventsQuery errors are not handled; failed requests currently fall through to the “No votes found” state.
  • Why it matters: Users get incorrect transparency data during network/API failures, and incidents are harder to detect.
  • Suggested fix: Read error from the hook and render an explicit error state before the empty-state branch.
Proposed patch
-  const { data: votesEventsData, loading: isLoading } = useVoteEventsQuery({
+  const {
+    data: votesEventsData,
+    loading: isLoading,
+    error,
+  } = useVoteEventsQuery({
     variables: {
       first: 200,
       where: {
         voter: voter,
       },
     },
   });
...
-      ) : voteEvents.length > 0 ? (
+      ) : error ? (
+        <Text
+          css={{ color: "$neutral11", textAlign: "center", marginTop: "$4" }}
+        >
+          Unable to load voting history. Please try again.
+        </Text>
+      ) : voteEvents.length > 0 ? (

Also applies to: 178-223

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/PollVote/PollVotePopover.tsx` around lines 23 - 30, The
useVoteEventsQuery hook call in the PollVotePopover component is not capturing
or handling query errors, causing failed requests to fall through and display as
"No votes found" instead of showing an actual error state. Extract the error
property from the useVoteEventsQuery destructuring (alongside data and loading),
and add an explicit error state render that checks for this error condition
before rendering the empty-state/no votes found branch to ensure users see
proper error messaging during network or API failures.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@components/PollVote/PollVoteDetail.tsx`:
- Around line 25-44: The PollVoteDetail component is issuing a useVoteQuery
request for each individual vote item rendered, causing N+1 query problems when
there are many votes in the history. Move the data fetching from useVoteQuery
(currently on line 27) up to the parent PollVotePopover component, and batch the
queries by fetching the needed vote records once. Create a lookup map or
precompute the stake weight text for each vote at the parent level, then pass
the precomputed weight text as a prop to PollVoteDetail instead of having each
instance calculate it independently using getStake. This eliminates redundant
queries and repeated payload transfers from the subgraph.

In `@queries/vote.graphql`:
- Around line 6-12: In the vote query, replace the poll.votes field (which
fetches the full array with voteStake and id) with poll.tally and select the yes
and no aggregate fields instead. This avoids fetching the entire votes array and
uses the schema's pre-computed aggregates, which ensures correct vote
calculations regardless of pagination or result truncation.

---

Outside diff comments:
In `@components/PollVote/PollVotePopover.tsx`:
- Around line 23-30: The useVoteEventsQuery hook call in the PollVotePopover
component is not capturing or handling query errors, causing failed requests to
fall through and display as "No votes found" instead of showing an actual error
state. Extract the error property from the useVoteEventsQuery destructuring
(alongside data and loading), and add an explicit error state render that checks
for this error condition before rendering the empty-state/no votes found branch
to ensure users see proper error messaging during network or API failures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d2de6b77-f66d-4975-9c73-c596ec145a5f

📥 Commits

Reviewing files that changed from the base of the PR and between 488711c and 28a60eb.

📒 Files selected for processing (12)
  • apollo/subgraph.ts
  • components/PollVote/DesktopVoteTable.tsx
  • components/PollVote/MobileVoteCards.tsx
  • components/PollVote/MobileVoteView.tsx
  • components/PollVote/PollVoteDetail.tsx
  • components/PollVote/PollVotePopover.tsx
  • components/PollVote/index.tsx
  • components/PollVotingWidget/index.tsx
  • lib/api/polls.ts
  • lib/api/types/votes.ts
  • pages/voting/[poll].tsx
  • queries/vote.graphql
💤 Files with no reviewable changes (1)
  • components/PollVotingWidget/index.tsx

Comment thread components/PollVote/PollVoteDetail.tsx
Comment thread queries/vote.graphql
@JJassonn69

Copy link
Copy Markdown
Contributor

@ibsule Everything looks great, just a couple of small nitpicks before we can merge.

  • The same votes are ordered differently by breakpoint: desktop defaults to weight descending, while mobile sorts by latest timestamp. Can you make both platforms consistent by ordering by weight?

  • The desktop table falls back to VOTING_SUPPORT_MAP["Unknown"], but the mobile/detail code dereferences support.style from VOTING_SUPPORT_MAP[vote.choiceID] / POLL_VOTES[vote.choiceID] without a fallback. Unknown votes should be rare, but it would be better to make the fallback consistent so future changes are not one-sided.

  • The votes UI uses subgraph collection fields as if they are complete, but neither poll.votes nor voteEvents(first: 200) gives that contract. This can make vote rows, transaction/timestamp data, counts, and percentage denominators silently partial for larger polls or voter histories. Please either paginate/fetch the full required raw vote/event set, or intentionally present this as a bounded slice and avoid full-total language/math based on partial arrays.

  • components/PollVote/PollVotePopover.tsx should handle useVoteEventsQuery errors explicitly. Right now a failed voting-history request falls through to No votes found for this voter., which is misleading. Please follow the pattern used by the main vote table: read error from useVoteEventsQuery and render an explicit error state before the empty-state branch.

ibsule added 4 commits June 23, 2026 01:12
- Changed the sorting mechanism for paginated votes to prioritize vote stakes instead of timestamps
… optimizations

- Removed unnecessary pagination parameters from GraphQL queries in PollVote components for fetching full votes/events

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
queries/voteEvents.graphql (1)

1-2: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

$first is declared but never applied, so voteEvents is currently unbounded.

Problem: On Line 1, $first is part of the operation signature, but on Line 2 it is not passed to voteEvents(...).
Why it matters: Any caller expecting bounded results cannot enforce limits, which can cause large responses and slower UI paths as vote-event history grows.
Suggested fix: Apply first: $first in the query and set a safe default ($first: Int = 200) or require callers to pass it explicitly.

Proposed patch
-query voteEvents($first: Int, $where: VoteEvent_filter) {
-  voteEvents(orderBy: timestamp, orderDirection: desc, where: $where) {
+query voteEvents($first: Int = 200, $where: VoteEvent_filter) {
+  voteEvents(
+    first: $first
+    orderBy: timestamp
+    orderDirection: desc
+    where: $where
+  ) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@queries/voteEvents.graphql` around lines 1 - 2, The $first variable is
declared in the query operation signature but is not being used in the
voteEvents field call, causing queries to be unbounded. Add the first parameter
to the voteEvents call by including first: $first in the arguments passed to
voteEvents alongside orderBy, orderDirection, and where. Additionally, consider
setting a default value for the $first variable (such as 200) in the operation
signature to provide a safe default limit when callers do not explicitly specify
one.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@components/PollVote/index.tsx`:
- Around line 158-160: The sorted array initialization uses parseInt to compare
voteStake values, which truncates decimal portions and causes precision loss
when ranking votes. Replace parseInt with parseFloat or Number in the sort
comparator function for both a.voteStake and b.voteStake to preserve the full
numeric precision of stake values and ensure correct vote ordering regardless of
fractional stake amounts.

---

Duplicate comments:
In `@queries/voteEvents.graphql`:
- Around line 1-2: The $first variable is declared in the query operation
signature but is not being used in the voteEvents field call, causing queries to
be unbounded. Add the first parameter to the voteEvents call by including first:
$first in the arguments passed to voteEvents alongside orderBy, orderDirection,
and where. Additionally, consider setting a default value for the $first
variable (such as 200) in the operation signature to provide a safe default
limit when callers do not explicitly specify one.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c12e9f54-916c-4442-9b6c-23286b3cf9e0

📥 Commits

Reviewing files that changed from the base of the PR and between 28a60eb and e8aa64f.

📒 Files selected for processing (5)
  • apollo/subgraph.ts
  • components/PollVote/MobileVoteView.tsx
  • components/PollVote/PollVotePopover.tsx
  • components/PollVote/index.tsx
  • queries/voteEvents.graphql

Comment thread components/PollVote/index.tsx
- Changed the sorting mechanism for paginated votes to use Number for vote stakes.
- Removed unnecessary pagination parameter from GraphQL query for vote events.
@JJassonn69

JJassonn69 commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Looks solid and all the issues have been resolved. We will have to deploy first in the livepeer vercel account to make sure that the issues we saw regarding the incorrect font sizes and older governance pools showing incorrect total non voters and voters is a deployment issue and not logic since you dont seem to have made any changes related to those.

ECWireless and others added 2 commits June 23, 2026 07:50
- Enhanced GraphQL queries for vote events by adding a pagination parameter.
- Refactored the PollVote component to utilize a custom hook for fetching vote events.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apollo/subgraph.ts (1)

11170-11187: 🎯 Functional Correctness | 🟠 Major

Regenerate apollo/subgraph.ts from source GraphQL definitions.

Problem: The voteEvents operation declares an unused $first variable. The source GraphQL file (queries/voteEvents.graphql) does not include this parameter, but the generated TypeScript file is out of sync.

Why it matters: GraphQL validation can reject the document due to the unused variable, causing the vote-history query to fail at runtime.

Suggested fix: Run pnpm codegen to regenerate the file from the correct source. Do not manually edit auto-generated files.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apollo/subgraph.ts` around lines 11170 - 11187, The VoteEventsDocument query
in apollo/subgraph.ts has an unused `$first: Int` parameter declared that is not
referenced anywhere in the query body, causing GraphQL validation to fail. This
file is auto-generated and is out of sync with the source GraphQL definitions.
Regenerate the apollo/subgraph.ts file by running the pnpm codegen command to
pull the correct schema from the source GraphQL file queries/voteEvents.graphql,
and do not manually edit this auto-generated file.
♻️ Duplicate comments (1)
queries/voteEvents.graphql (1)

1-2: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Add a bounded default page size to voteEvents.

Problem: Line 1/Line 2 define and execute voteEvents without a first bound.
Why it matters: Large voter histories can return very large payloads, causing slow UI responses/timeouts in both vote list and voter-history flows.
Suggested fix: Add a default limit at the query level and wire it into the field call so all consumers are protected.

Proposed patch
-query voteEvents($where: VoteEvent_filter) {
-  voteEvents(orderBy: timestamp, orderDirection: desc, where: $where) {
+query voteEvents($where: VoteEvent_filter, $first: Int = 200) {
+  voteEvents(
+    first: $first
+    orderBy: timestamp
+    orderDirection: desc
+    where: $where
+  ) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@queries/voteEvents.graphql` around lines 1 - 2, The voteEvents query
definition lacks a bounded page size limit on the voteEvents field call, which
allows unbounded result sets that can cause performance issues. Add a `first`
parameter to the voteEvents field invocation in the query with a reasonable
default limit (for example, 1000 or similar based on your system requirements).
Ensure the `first` parameter is either hardcoded as a default or parameterized
as a variable in the query definition so that all consumers of this query are
protected from returning excessively large result sets.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@apollo/subgraph.ts`:
- Around line 11170-11187: The VoteEventsDocument query in apollo/subgraph.ts
has an unused `$first: Int` parameter declared that is not referenced anywhere
in the query body, causing GraphQL validation to fail. This file is
auto-generated and is out of sync with the source GraphQL definitions.
Regenerate the apollo/subgraph.ts file by running the pnpm codegen command to
pull the correct schema from the source GraphQL file queries/voteEvents.graphql,
and do not manually edit this auto-generated file.

---

Duplicate comments:
In `@queries/voteEvents.graphql`:
- Around line 1-2: The voteEvents query definition lacks a bounded page size
limit on the voteEvents field call, which allows unbounded result sets that can
cause performance issues. Add a `first` parameter to the voteEvents field
invocation in the query with a reasonable default limit (for example, 1000 or
similar based on your system requirements). Ensure the `first` parameter is
either hardcoded as a default or parameterized as a variable in the query
definition so that all consumers of this query are protected from returning
excessively large result sets.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c0dc9ab7-fd8b-45a5-9a8f-25ac7fa51f3f

📥 Commits

Reviewing files that changed from the base of the PR and between e8aa64f and 698aac5.

📒 Files selected for processing (3)
  • apollo/subgraph.ts
  • components/PollVote/index.tsx
  • queries/voteEvents.graphql

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
components/PollVote/index.tsx (1)

55-110: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

useVotes has an async race and unmount-update hazard.

  • Problem: decorateVotes performs async work without cancellation/version guards, while the effect can re-run every poll interval.
  • Why it matters: Older runs can overwrite newer state, and unmounted components can still receive setVotes/setVotesLoading calls.
  • Suggested fix: Add effect cleanup with a cancelled flag (or run-id token) and gate all state writes to only the latest active run.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/PollVote/index.tsx` around lines 55 - 110, The decorateVotes async
function lacks protection against race conditions where older runs can overwrite
newer state or update unmounted components. Add a cleanup function to the
useEffect that sets a cancelled flag to true, then create a cancelled tracking
variable (such as a useRef or local variable in the effect scope) initialized to
false at the start of decorateVotes. Before each state update (setVotes and
setVotesLoading), check if cancelled is true and skip the update if so. Return
the cleanup function from the useEffect to set the cancelled flag before the
effect re-runs or component unmounts, ensuring only the latest active run can
update component state.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apollo/subgraph.ts`:
- Around line 11171-11177: The voteEvents query uses only timestamp for
ordering, which is non-deterministic when multiple events share the same
timestamp and can cause rows to be dropped or duplicated at page boundaries
during pagination. Add a secondary tie-breaker field to the orderBy clause in
the voteEvents query (such as a unique identifier like id) to establish a
stable, deterministic sort order that prevents pagination issues and ensures
vote history remains complete and correct.

In `@hooks/useGetAllPollVotesEvents.ts`:
- Around line 24-26: The useEffect hook in useGetAllPollVotesEvents returns
early when voteEventsData is missing, preventing setLoadingAll(false) from being
called when an error occurs without data. Add an error handling path that checks
if an error exists and voteEventsData is not available, then call
setLoadingAll(false) (and optionally reset allVoteEvents) to prevent the loading
state from remaining true indefinitely. This same fix should be applied to the
similar useEffect blocks referenced at lines 59-60 and 84-85 to ensure
consistent error handling across all data fetching effects in this hook.
- Around line 37-43: The pagination logic in the while loop uses timestamp_lt as
the cursor, but without a deterministic tie-breaker or stable ordering by a
unique identifier like id, events with the same timestamp can be skipped between
pages. Modify the fetchMore variables object to include both timestamp_lt and an
id-based ordering constraint as a tie-breaker, or alternatively implement a
deduplication strategy using event id across paginated results to ensure no vote
events are lost when boundary timestamps are shared. Ensure the upstream query
is ordered deterministically by both timestamp and id to prevent gaps in
pagination.

---

Outside diff comments:
In `@components/PollVote/index.tsx`:
- Around line 55-110: The decorateVotes async function lacks protection against
race conditions where older runs can overwrite newer state or update unmounted
components. Add a cleanup function to the useEffect that sets a cancelled flag
to true, then create a cancelled tracking variable (such as a useRef or local
variable in the effect scope) initialized to false at the start of
decorateVotes. Before each state update (setVotes and setVotesLoading), check if
cancelled is true and skip the update if so. Return the cleanup function from
the useEffect to set the cancelled flag before the effect re-runs or component
unmounts, ensuring only the latest active run can update component state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 89bc37cb-01f7-43d1-a744-f64b5b87259a

📥 Commits

Reviewing files that changed from the base of the PR and between 698aac5 and 26f545f.

📒 Files selected for processing (5)
  • apollo/subgraph.ts
  • components/PollVote/PollVotePopover.tsx
  • components/PollVote/index.tsx
  • hooks/useGetAllPollVotesEvents.ts
  • queries/voteEvents.graphql

Comment thread apollo/subgraph.ts
Comment thread hooks/useGetAllPollVotesEvents.ts
Comment thread hooks/useGetAllPollVotesEvents.ts
- Added logic to stop loading and clear vote events when an error occurs.
- Updated dependencies in the effect hook to include error state.
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.

Extend Voting Transparency to Governance page

3 participants