diff --git a/src/hooks/governance/useGovernanceProposals.ts b/src/hooks/governance/useGovernanceProposals.ts index bb3c100f1d..f6721a4c90 100644 --- a/src/hooks/governance/useGovernanceProposals.ts +++ b/src/hooks/governance/useGovernanceProposals.ts @@ -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); + 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, @@ -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, @@ -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])); }, enabled: USE_GOVERNANCE_CACHE && query.trim() !== '', - queryKey: ['governance-search-cache', query], + queryKey: ['governance-search-cache', query, user], }); const { data: graphIds, isFetching: graphIdsFetching } = useQuery({ @@ -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, }); diff --git a/src/hooks/governance/useProposals.ts b/src/hooks/governance/useProposals.ts index 638aaf8e76..bc87b7d95c 100644 --- a/src/hooks/governance/useProposals.ts +++ b/src/hooks/governance/useProposals.ts @@ -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) => ({ @@ -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, index) => { diff --git a/src/locales/en/messages.po b/src/locales/en/messages.po index 2d6d4337fe..4132f24529 100644 --- a/src/locales/en/messages.po +++ b/src/locales/en/messages.po @@ -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}" diff --git a/src/modules/governance/ProposalsV3List.tsx b/src/modules/governance/ProposalsV3List.tsx index b4a64a5079..4dbda11d5c 100644 --- a/src/modules/governance/ProposalsV3List.tsx +++ b/src/modules/governance/ProposalsV3List.tsx @@ -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'; @@ -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 ( + ({ + color: theme.palette[paletteKey].main, + display: 'inline-flex', + alignItems: 'center', + gap: 0.5, + })} + > + + + + + You voted {userVote.support ? 'YAE' : 'NAY'} + + + ); +}; + const ProposalListItemRow = ({ proposal }: { proposal: ProposalListItem }) => { const trackEvent = useRootStore((store) => store.trackEvent); @@ -45,8 +68,9 @@ const ProposalListItemRow = ({ proposal }: { proposal: ProposalListItem }) => { justifyContent: 'space-between', }} > - + + {proposal.userVote && } {proposal.title} diff --git a/src/modules/governance/adapters.ts b/src/modules/governance/adapters.ts index cb3979eb9f..b5d6008f4c 100644 --- a/src/modules/governance/adapters.ts +++ b/src/modules/governance/adapters.ts @@ -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, @@ -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, @@ -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, }; } diff --git a/src/modules/governance/types.ts b/src/modules/governance/types.ts index a8589e202d..7535552c92 100644 --- a/src/modules/governance/types.ts +++ b/src/modules/governance/types.ts @@ -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. */ @@ -32,6 +40,7 @@ export type ProposalListItem = { author: string; badgeState: ProposalBadgeState; voteInfo: ProposalVoteDisplayInfo; + userVote?: UserVoteInfo | null; }; /**