Skip to content
Open
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
34 changes: 26 additions & 8 deletions src/hooks/governance/useGovernanceProposals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,21 @@ async function fetchSubgraphVotes(proposalId: number, votingChainId: ChainId) {
*/
export const useGovernanceProposals = () => {
const { votingMachineSerivce, governanceV3Service } = useSharedDependencies();
const user = useRootStore((store) => store.account);

const cacheResult = useInfiniteQuery({
queryFn: async ({ pageParam = 0 }) => {
const proposals = await getProposalsFromCache(PAGE_SIZE, pageParam * PAGE_SIZE);
return { proposals: proposals.map(adaptCacheProposalToListItem) };
const userVotes = user
? await Promise.all(
proposals.map((p) => getUserVoteFromCache(p.id, user).catch(() => null))
)
: proposals.map(() => null);
Comment on lines +126 to +130
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

This introduces an N+1 pattern: for each proposals page (PAGE_SIZE=10) it makes one getUserVoteFromCache request per proposal, which can add significant latency and load on the cache backend. Consider batching these lookups into a single GraphQL request (e.g., a bulk endpoint, or a single query using field aliases for each proposalId) so the page fetch remains O(1) requests.

Copilot uses AI. Check for mistakes.
return {
proposals: proposals.map((p, i) => adaptCacheProposalToListItem(p, userVotes[i])),
};
},
queryKey: ['governance-proposals-cache'],
queryKey: ['governance-proposals-cache', user],
enabled: USE_GOVERNANCE_CACHE,
refetchOnMount: false,
refetchOnReconnect: false,
Expand All @@ -141,11 +149,12 @@ export const useGovernanceProposals = () => {
const enriched = await fetchProposals(
result.proposals,
votingMachineSerivce,
governanceV3Service
governanceV3Service,
user
);
return { proposals: enriched.proposals.map(adaptGraphProposalToListItem) };
},
queryKey: ['governance-proposals-graph'],
queryKey: ['governance-proposals-graph', user],
enabled: !USE_GOVERNANCE_CACHE,
refetchOnMount: false,
refetchOnReconnect: false,
Expand All @@ -164,15 +173,19 @@ export const useGovernanceProposals = () => {
*/
export const useGovernanceProposalsSearch = (query: string) => {
const { votingMachineSerivce, governanceV3Service } = useSharedDependencies();
const user = useRootStore((store) => store.account);
const formattedQuery = query.trim().split(' ').join(' & ');

const { data: cacheData, isFetching: cacheFetching } = useQuery({
queryFn: async () => {
const results = await searchProposalsFromCache(query, SEARCH_RESULTS_LIMIT);
return results.map(adaptCacheProposalToListItem);
const userVotes = user
? await Promise.all(results.map((p) => getUserVoteFromCache(p.id, user).catch(() => null)))
: results.map(() => null);
return results.map((p, i) => adaptCacheProposalToListItem(p, userVotes[i]));
Comment on lines +182 to +185
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

Same N+1 issue in search: this does one getUserVoteFromCache request per search result. If the cache path remains enabled, batching user-vote lookups into a single request would keep search responsive and reduce backend load.

Copilot uses AI. Check for mistakes.
},
enabled: USE_GOVERNANCE_CACHE && query.trim() !== '',
queryKey: ['governance-search-cache', query],
queryKey: ['governance-search-cache', query, user],
});

const { data: graphIds, isFetching: graphIdsFetching } = useQuery({
Expand All @@ -185,10 +198,15 @@ export const useGovernanceProposalsSearch = (query: string) => {
const { data: graphData, isFetching: graphProposalsFetching } = useQuery({
queryFn: async () => {
const proposals = await fetchSubgraphProposalsByIds(graphIds || []);
const enriched = await fetchProposals(proposals, votingMachineSerivce, governanceV3Service);
const enriched = await fetchProposals(
proposals,
votingMachineSerivce,
governanceV3Service,
user
);
return enriched.proposals.map(adaptGraphProposalToListItem);
},
queryKey: ['governance-search-graph-proposals', graphIds],
queryKey: ['governance-search-graph-proposals', graphIds, user],
enabled: !USE_GOVERNANCE_CACHE && graphIds !== undefined && graphIds.length > 0,
});

Expand Down
5 changes: 3 additions & 2 deletions src/hooks/governance/useProposals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,8 @@ async function fetchSubgraphProposals(pageParam: number, proposalStateFilter?: P
export async function fetchProposals(
proposals: SubgraphProposal[],
votingMachineSerivce: VotingMachineService,
governanceV3Service: GovernanceV3Service
governanceV3Service: GovernanceV3Service,
user?: string
) {
const votingMachineParams =
proposals.map((p) => ({
Expand All @@ -293,7 +294,7 @@ export async function fetchProposals(

const [proposalsMetadata, votingMachineDataes, payloadsDataes] = await Promise.all([
Promise.all(proposals.map(getSubgraphProposalMetadata)),
votingMachineSerivce.getProposalsData(votingMachineParams),
votingMachineSerivce.getProposalsData(votingMachineParams, user),
governanceV3Service.getMultiChainPayloadsData(payloadParams),
]);
const enhancedProposals = proposals.map<Proposal>((proposal, index) => {
Expand Down
1 change: 1 addition & 0 deletions src/locales/en/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -2988,6 +2988,7 @@ msgid "Delegated power"
msgstr "Delegated power"

#: src/modules/governance/proposal/VoteInfo.tsx
#: src/modules/governance/ProposalsV3List.tsx
msgid "You voted {0}"
msgstr "You voted {0}"

Expand Down
30 changes: 27 additions & 3 deletions src/modules/governance/ProposalsV3List.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Box, Paper, Skeleton, Stack, Typography } from '@mui/material';
import { CheckCircleIcon } from '@heroicons/react/solid';
import { Trans } from '@lingui/macro';
import { Box, Paper, Skeleton, Stack, SvgIcon, Typography } from '@mui/material';
import { useState } from 'react';
import InfiniteScroll from 'react-infinite-scroller';
import { NoSearchResults } from 'src/components/NoSearchResults';
Expand All @@ -12,9 +14,30 @@ import { GOVERNANCE_PAGE } from 'src/utils/events';

import { ProposalListHeader } from './ProposalListHeader';
import { StateBadge, stringToState } from './StateBadge';
import { ProposalListItem } from './types';
import { ProposalListItem, UserVoteInfo } from './types';
import { VoteBar } from './VoteBar';

const VotedIndicator = ({ userVote }: { userVote: UserVoteInfo }) => {
const paletteKey = userVote.support ? 'success' : 'error';
return (
<Box
sx={(theme) => ({
color: theme.palette[paletteKey].main,
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
})}
>
<SvgIcon sx={{ fontSize: 14 }}>
<CheckCircleIcon />
</SvgIcon>
Comment on lines +31 to +33
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

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

CheckCircleIcon from @heroicons/react renders an <svg> already; nesting it inside MUI's <SvgIcon> produces an <svg> inside another <svg>, which can cause sizing/styling issues and invalid markup. Prefer rendering the Heroicon directly (set height/width/color) or pass it via SvgIcon's component prop instead of as a child.

Copilot uses AI. Check for mistakes.
<Typography variant="subheader2" color="inherit">
<Trans>You voted {userVote.support ? 'YAE' : 'NAY'}</Trans>
</Typography>
</Box>
);
};

const ProposalListItemRow = ({ proposal }: { proposal: ProposalListItem }) => {
const trackEvent = useRootStore((store) => store.trackEvent);

Expand Down Expand Up @@ -45,8 +68,9 @@ const ProposalListItemRow = ({ proposal }: { proposal: ProposalListItem }) => {
justifyContent: 'space-between',
}}
>
<Stack direction="row" gap={3} alignItems="center">
<Stack direction="row" gap={3} alignItems="center" flexWrap="wrap">
<StateBadge state={proposal.badgeState} loading={false} />
{proposal.userVote && <VotedIndicator userVote={proposal.userVote} />}
</Stack>
<Typography variant="h3" sx={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>
{proposal.title}
Expand Down
14 changes: 13 additions & 1 deletion src/modules/governance/adapters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,12 +165,17 @@ export function buildVoteProposalFromCache(
// ============================================

export function adaptGraphProposalToListItem(p: Proposal): ProposalListItem {
const votedInfo = p.votingMachineData.votedInfo;
return {
id: p.subgraphProposal.id,
title: p.subgraphProposal.proposalMetadata.title,
shortDescription: p.subgraphProposal.proposalMetadata.shortDescription || '',
author: p.subgraphProposal.proposalMetadata.author || '',
badgeState: p.badgeState,
userVote:
votedInfo && votedInfo.votingPower !== '0'
? { support: votedInfo.support, votingPower: votedInfo.votingPower }
: null,
voteInfo: {
forVotes: p.votingInfo.forVotes,
againstVotes: p.votingInfo.againstVotes,
Expand Down Expand Up @@ -210,7 +215,10 @@ export function adaptGraphProposalToDetail(p: Proposal): ProposalDetailDisplay {
// Cache -> canonical adapters
// ============================================

export function adaptCacheProposalToListItem(p: SimplifiedProposal): ProposalListItem {
export function adaptCacheProposalToListItem(
p: SimplifiedProposal,
userVote?: ProposalVote | null
): ProposalListItem {
const voteInfo = calculateCacheVoteDisplayInfo(p.votesFor, p.votesAgainst, null, null);
return {
id: p.id,
Expand All @@ -219,6 +227,10 @@ export function adaptCacheProposalToListItem(p: SimplifiedProposal): ProposalLis
author: p.author,
badgeState: cacheStateToBadge(p.state),
voteInfo,
userVote:
userVote && userVote.votingPower !== '0'
? { support: userVote.support, votingPower: userVote.votingPower }
: null,
};
}

Expand Down
9 changes: 9 additions & 0 deletions src/modules/governance/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ export type ProposalVoteDisplayInfo = {
differentialReached: boolean;
};

/**
* The connected user's vote on a given proposal, if any.
*/
export type UserVoteInfo = {
support: boolean;
votingPower: string;
};

/**
* Data-source-agnostic list item for proposals list view.
*/
Expand All @@ -32,6 +40,7 @@ export type ProposalListItem = {
author: string;
badgeState: ProposalBadgeState;
voteInfo: ProposalVoteDisplayInfo;
userVote?: UserVoteInfo | null;
};

/**
Expand Down
Loading