Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,25 @@ import {
TreasuryVoteSupport,
useTreasuryVoteEventsQuery,
} from "apollo";
import { useEnsData } from "hooks";
import React from "react";

import TreasuryVoteDetail from "./TreasuryVoteDetail";
import TreasuryVoteHistoryModal from "./TreasuryVoteHistoryModal";

interface TreasuryVotePopoverProps {
voter: string;
ensName?: string;
onClose: () => void;
formatWeight: (weight: string) => string;
}

const Index: React.FC<TreasuryVotePopoverProps> = ({
voter,
ensName,
onClose,
formatWeight,
}) => {
const ensIdentity = useEnsData(voter);
const ensName = ensIdentity?.name || ensIdentity?.idShort || "";
const { data: votesData, loading: isLoading } = useTreasuryVoteEventsQuery({
variables: {
where: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,14 @@ import { Column } from "react-table";
import { VoteReasonPopover } from "./VoteReasonPopover";

export type Vote = TreasuryVote & {
ensName?: string;
transactionHash?: string;
timestamp?: number;
};

export interface TreasuryVoteTableProps {
votes: Vote[];
formatWeight: (weight: string) => string;
onSelect: (voter: { address: string; ensName?: string }) => void;
onSelect: (voter: { address: string }) => void;
pageSize?: number;
totalPages?: number;
currentPage?: number;
Expand All @@ -37,7 +36,7 @@ export const DesktopVoteTable: React.FC<TreasuryVoteTableProps> = ({
() => [
{
Header: "Voter",
accessor: "ensName",
Copy link
Contributor

Choose a reason for hiding this comment

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

The "Voter" column accessor changed from a string field to an object, breaking sort functionality in react-table

Fix on Vercel

accessor: "voter",
id: "voter",
Cell: ({ row }) => (
<Box css={{ minWidth: 120 }}>
Expand Down Expand Up @@ -206,7 +205,6 @@ export const DesktopVoteTable: React.FC<TreasuryVoteTableProps> = ({
e.stopPropagation();
onSelect({
address: row.original.voter.id,
ensName: row.original.ensName,
});
}}
css={{
Expand Down
19 changes: 11 additions & 8 deletions components/Treasury/TreasuryVoteTable/Views/VoteItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@ import {
CounterClockwiseClockIcon,
} from "@radix-ui/react-icons";
import { TreasuryVoteSupport } from "apollo/subgraph";
import { useEnsData } from "hooks";
import { useState } from "react";

import { Vote } from "./DesktopVoteTable";
import { VoteReasonPopover } from "./VoteReasonPopover";

interface VoteViewProps {
vote: Vote;
onSelect: (voter: { address: string; ensName?: string }) => void;
onSelect: (voter: { address: string }) => void;
formatWeight: (weight: string) => string;
isMobile?: boolean;
}
Expand Down Expand Up @@ -53,6 +54,8 @@ export function VoteView({

function MobileVoteView({ vote, onSelect, formatWeight }: VoteViewProps) {
const [isExpanded, setIsExpanded] = useState(false);
const ensIdentity = useEnsData(vote.voter.id);
const displayName = ensIdentity?.name || ensIdentity?.idShort || "";
const support =
VOTING_SUPPORT_MAP[vote.support] ||
VOTING_SUPPORT_MAP[TreasuryVoteSupport.Abstain];
Expand Down Expand Up @@ -113,12 +116,12 @@ function MobileVoteView({ vote, onSelect, formatWeight }: VoteViewProps) {
},
}}
>
{vote.ensName}
{displayName}
</Link>
</Heading>
<Box
as="button"
aria-label={`See ${vote.ensName || vote.voter.id}'s voting history`}
aria-label={`See ${displayName || vote.voter.id}'s voting history`}
css={{
display: "flex",
alignItems: "center",
Expand All @@ -141,9 +144,7 @@ function MobileVoteView({ vote, onSelect, formatWeight }: VoteViewProps) {
color: "$primary11",
},
}}
onClick={() =>
onSelect({ address: vote.voter.id, ensName: vote.ensName })
}
onClick={() => onSelect({ address: vote.voter.id })}
>
<Text size="1" css={{ fontWeight: 600, color: "inherit" }}>
History
Expand Down Expand Up @@ -277,6 +278,8 @@ function MobileVoteView({ vote, onSelect, formatWeight }: VoteViewProps) {
}

function DesktopVoteView({ vote, onSelect, formatWeight }: VoteViewProps) {
const ensIdentity = useEnsData(vote.voter.id);
const displayName = ensIdentity?.name || ensIdentity?.idShort || "";
const support =
VOTING_SUPPORT_MAP[vote.support] ||
VOTING_SUPPORT_MAP[TreasuryVoteSupport.Abstain];
Expand Down Expand Up @@ -330,7 +333,7 @@ function DesktopVoteView({ vote, onSelect, formatWeight }: VoteViewProps) {
}}
size="2"
>
{vote.ensName}
{displayName}
</Text>
</Link>
</Box>
Expand Down Expand Up @@ -492,7 +495,7 @@ function DesktopVoteView({ vote, onSelect, formatWeight }: VoteViewProps) {
as="button"
onClick={(e) => {
e.stopPropagation();
onSelect({ address: vote.voter.id, ensName: vote.ensName });
onSelect({ address: vote.voter.id });
}}
css={{
display: "inline-flex",
Expand Down
89 changes: 25 additions & 64 deletions components/Treasury/TreasuryVoteTable/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import Spinner from "@components/Spinner";
import { getEnsForVotes } from "@lib/api/ens";
import { formatAddress, lptFormatter } from "@lib/utils";
import { lptFormatter } from "@lib/utils";
import { Flex, Text } from "@livepeer/design-system";
import { useTreasuryVoteEventsQuery, useTreasuryVotesQuery } from "apollo";
import React, { useEffect, useMemo, useState } from "react";
import React, { useMemo, useState } from "react";
import { useWindowSize } from "react-use";

import TreasuryVotePopover from "./TreasuryVotePopover";
Expand Down Expand Up @@ -40,69 +39,33 @@ const useVotes = (proposalId: string) => {
},
});

const [votes, setVotes] = useState<Vote[]>([]);
const [votesLoading, setVotesLoading] = useState(false);
useEffect(() => {
if (
!treasuryVotesData?.treasuryVotes ||
!treasuryVoteEventsData?.treasuryVoteEvents
) {
setVotes([]);
const votes = useMemo(() => {
const votesList = treasuryVotesData?.treasuryVotes;
const eventsList = treasuryVoteEventsData?.treasuryVoteEvents;

if (!votesList || !eventsList) {
return [];
}
const decorateVotes = async () => {
setVotesLoading(true);
const uniqueVoters = Array.from(
new Set(treasuryVotesData?.treasuryVotes?.map((v) => v.voter.id) ?? [])
);
const localEnsCache: { [address: string]: string } = {};

await Promise.all(
uniqueVoters.map(async (address) => {
try {
if (localEnsCache[address]) {
return;
}
const ensAddress = await getEnsForVotes(address);

if (ensAddress && ensAddress.name) {
localEnsCache[address] = ensAddress.name;
} else {
localEnsCache[address] = formatAddress(address);
}
} catch (e) {
console.warn(`Failed to fetch ENS for ${address}`, e);
}
})
);
const votes =
treasuryVotesData?.treasuryVotes?.map((vote) => {
const events = (treasuryVoteEventsData?.treasuryVoteEvents ?? [])
.filter((event) => event.voter.id === vote.voter.id)
.sort((a, b) => b.timestamp - a.timestamp);

const latestEvent = events[0];
const ensName = localEnsCache[vote.voter.id] ?? "";

return {
...vote,
reason: latestEvent?.reason || vote.reason || "",
ensName,
transactionHash: latestEvent?.transaction.id ?? "",
timestamp: latestEvent?.timestamp,
};
}) ?? [];
setVotes(votes as Vote[]);
setVotesLoading(false);
};
decorateVotes();
}, [
treasuryVotesData?.treasuryVotes,
treasuryVoteEventsData?.treasuryVoteEvents,
]);

return votesList.map((vote) => {
const events = eventsList
.filter((event) => event.voter.id === vote.voter.id)
.sort((a, b) => b.timestamp - a.timestamp);

const latestEvent = events[0];
Comment on lines +50 to +55
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
return votesList.map((vote) => {
const events = eventsList
.filter((event) => event.voter.id === vote.voter.id)
.sort((a, b) => b.timestamp - a.timestamp);
const latestEvent = events[0];
// Build a map of voter ID to latest event in a single pass
// First sort events by timestamp descending, then build map (first event per voter is latest)
const sortedEvents = [...eventsList].sort(
(a, b) => b.timestamp - a.timestamp
);
const latestEventByVoter = new Map();
for (const event of sortedEvents) {
if (!latestEventByVoter.has(event.voter.id)) {
latestEventByVoter.set(event.voter.id, event);
}
}
return votesList.map((vote) => {
const latestEvent = latestEventByVoter.get(vote.voter.id);

The useVotes hook has O(votes × events log events) performance issue from filtering and sorting events for each vote independently

Fix on Vercel


return {
...vote,
reason: latestEvent?.reason || vote.reason || "",
transactionHash: latestEvent?.transaction.id ?? "",
timestamp: latestEvent?.timestamp,
};
}) as Vote[];
Comment on lines +50 to +63
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

useVotes currently does an eventsList.filter(...).sort(...) for every vote, and only uses events[0]. This is O(votes × events log events) and will get expensive as vote/event counts grow. Consider building a single Map<voterId, latestEvent> in one pass over eventsList (or sorting eventsList once) and then decorating votes via a lookup, avoiding per-vote sorts/filters.

Copilot uses AI. Check for mistakes.
}, [treasuryVotesData, treasuryVoteEventsData]);
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

The useMemo dependency array is [treasuryVotesData, treasuryVoteEventsData], but Apollo often returns new wrapper objects even when the underlying lists are unchanged. That can cause this memo to recompute on most renders. Depending on treasuryVotesData?.treasuryVotes and treasuryVoteEventsData?.treasuryVoteEvents (or votesList/eventsList references) would make the memoization effective.

Suggested change
}, [treasuryVotesData, treasuryVoteEventsData]);
}, [treasuryVotesData?.treasuryVotes, treasuryVoteEventsData?.treasuryVoteEvents]);

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
}, [treasuryVotesData, treasuryVoteEventsData]);
}, [treasuryVotesData?.treasuryVotes, treasuryVoteEventsData?.treasuryVoteEvents]);

useMemo dependency array uses wrapper objects instead of actual data arrays, causing unnecessary recomputation on every render

Fix on Vercel


return {
votes,
loading: loading || votesLoading || treasuryVoteEventsLoading,
loading: loading || treasuryVoteEventsLoading,
error: error || treasuryVoteEventsError,
};
};
Expand All @@ -113,7 +76,6 @@ const Index: React.FC<TreasuryVoteTableProps> = ({ proposalId }) => {

const [selectedVoter, setSelectedVoter] = useState<{
address: string;
ensName?: string;
} | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const pageSize = 10;
Expand Down Expand Up @@ -191,7 +153,6 @@ const Index: React.FC<TreasuryVoteTableProps> = ({ proposalId }) => {
{selectedVoter && (
<TreasuryVotePopover
voter={selectedVoter.address}
ensName={selectedVoter.ensName}
onClose={() => setSelectedVoter(null)}
formatWeight={formatWeight}
/>
Expand Down