diff --git a/components/VoteList/VoteListItem.tsx b/components/VoteList/VoteListItem.tsx index bc286494..6de8bf8d 100644 --- a/components/VoteList/VoteListItem.tsx +++ b/components/VoteList/VoteListItem.tsx @@ -1,5 +1,6 @@ import { Button, + Checkbox, Dropdown, Loader, LoadingSkeleton, @@ -47,6 +48,10 @@ export function VoteListItem(props: VoteListItemProps) { isDirty, origin, moreDetailsAction, + phase, + isSelectedForReveal, + toggleRevealSelection, + canBeRevealed, } = useVoteListItem(props); const { isOptimisticGovernorVote, explanationText } = @@ -57,6 +62,7 @@ export function VoteListItem(props: VoteListItemProps) { : ""; const voteOrigin = isOptimisticGovernorVote ? "OSnap" : origin; + const showRevealCheckbox = phase === "reveal" && canBeRevealed; return (
+ {showRevealCheckbox && ( +
e.stopPropagation()} + > + toggleRevealSelection?.()} + gap={0} + /> +
+ )}

{optimisticGovernorTitle || titleText} diff --git a/components/VoteList/VoteTableRow.tsx b/components/VoteList/VoteTableRow.tsx index 46a4c7c3..7ea403bf 100644 --- a/components/VoteList/VoteTableRow.tsx +++ b/components/VoteList/VoteTableRow.tsx @@ -1,4 +1,5 @@ import { + Checkbox, Dropdown, Loader, LoadingSkeleton, @@ -63,6 +64,10 @@ export function VoteTableRow(props: VoteListItemProps) { multipleInputProps, dropdownOptions, selectedDropdownOption, + phase, + isSelectedForReveal, + toggleRevealSelection, + canBeRevealed, } = useVoteListItem(props); const { isOptimisticGovernorVote, explanationText } = @@ -74,13 +79,34 @@ export function VoteTableRow(props: VoteListItemProps) { const voteOrigin = isOptimisticGovernorVote ? "OSnap" : origin; + const showRevealCheckbox = phase === "reveal" && canBeRevealed; + return ( - + {showRevealCheckbox && ( + e.stopPropagation()} + > + toggleRevealSelection?.()} + gap={0} + /> + + )} +
diff --git a/components/VoteList/shared.types.ts b/components/VoteList/shared.types.ts index eec07ad7..9779caa7 100644 --- a/components/VoteList/shared.types.ts +++ b/components/VoteList/shared.types.ts @@ -10,4 +10,8 @@ export interface VoteListItemProps { moreDetailsAction: () => void; setDirty?: (dirty: boolean) => void; isDirty?: boolean; + // Reveal phase selection props + isSelectedForReveal?: boolean; + toggleRevealSelection?: () => void; + canBeRevealed?: boolean; } diff --git a/components/VoteList/useVoteListItem.tsx b/components/VoteList/useVoteListItem.tsx index 37198a66..9ae5d5a3 100644 --- a/components/VoteList/useVoteListItem.tsx +++ b/components/VoteList/useVoteListItem.tsx @@ -37,6 +37,9 @@ export function useVoteListItem({ setDirty, moreDetailsAction, isDirty = false, + isSelectedForReveal, + toggleRevealSelection, + canBeRevealed, }: VoteListItemProps) { const [isCustomInput, setIsCustomInput] = useState(false); const multipleInputProps = useMultipleValuesVote({ @@ -368,5 +371,10 @@ export function useVoteListItem({ moreDetailsAction, multipleInputProps, selectedDropdownOption, + // Reveal selection props + phase, + isSelectedForReveal, + toggleRevealSelection, + canBeRevealed, }; } diff --git a/components/Votes/ActiveVotes.tsx b/components/Votes/ActiveVotes.tsx index f228bf09..506cd254 100644 --- a/components/Votes/ActiveVotes.tsx +++ b/components/Votes/ActiveVotes.tsx @@ -23,7 +23,7 @@ import { useVoteTimingContext, useWalletContext, } from "hooks"; -import { useState } from "react"; +import { useState, useEffect, useMemo } from "react"; import { VoteT } from "types"; import { ButtonInnerWrapper, @@ -64,6 +64,9 @@ export function ActiveVotes() { const { revealVotesMutation, isRevealingVotes } = useRevealVotes(address); const [selectedVotes, setSelectedVotes] = usePersistedVotes(roundId); const [dirtyInputs, setDirtyInput] = useState([]); + const [selectedRevealVotes, setSelectedRevealVotes] = useState< + Record + >({}); const { showPagination, entriesToShow, ...paginationProps } = usePagination( activeVoteList ?? [] ); @@ -72,6 +75,65 @@ export function ActiveVotes() { return dirtyInputs.some((x) => x); } + // Get all votes that are eligible for reveal + const revealableVotes = useMemo(() => { + return ( + activeVoteList?.filter( + (vote) => + vote.isCommitted && + !!vote.decryptedVote && + vote.isRevealed === false && + vote.canReveal + ) ?? [] + ); + }, [activeVoteList]); + + // Initialize selected reveal votes when entering reveal phase + useEffect(() => { + if (phase === "reveal" && revealableVotes.length > 0) { + setSelectedRevealVotes((prev) => { + const updated = { ...prev }; + // Add any new revealable votes that aren't in the selection yet + revealableVotes.forEach((vote) => { + if (updated[vote.uniqueKey] === undefined) { + updated[vote.uniqueKey] = true; // Select all by default + } + }); + // Remove votes that are no longer revealable + Object.keys(updated).forEach((key) => { + if (!revealableVotes.find((v) => v.uniqueKey === key)) { + delete updated[key]; + } + }); + return updated; + }); + } + }, [phase, revealableVotes]); + + function toggleRevealVoteSelection(uniqueKey: string) { + setSelectedRevealVotes((prev) => ({ + ...prev, + [uniqueKey]: !(prev[uniqueKey] ?? true), // Toggle from default true + })); + } + + function isVoteSelectedForReveal(uniqueKey: string): boolean { + return selectedRevealVotes[uniqueKey] ?? true; // Default to checked + } + + function canVoteBeRevealed(vote: VoteT): boolean { + return ( + vote.isCommitted === true && + !!vote.decryptedVote && + vote.isRevealed === false && + vote.canReveal === true + ); + } + + const selectedRevealCount = revealableVotes.filter( + (vote) => selectedRevealVotes[vote.uniqueKey] ?? true + ).length; + const actionStatus = calculateActionStatus(); type ActionStatus = { tooltip?: string; @@ -115,7 +177,7 @@ export function ActiveVotes() { ? isDirty() : true : false; - const hasVotesToReveal = getVotesToReveal().length > 0; + const hasVotesToReveal = selectedRevealCount > 0; // the current account is editing a previously committed value from another account, either delegate or delegator const isEditingUnknownVote = Boolean( activeVoteList?.filter((vote) => { @@ -217,7 +279,7 @@ export function ActiveVotes() { } if (isReveal) { actionConfig.hidden = false; - actionConfig.label = "Reveal all votes"; + actionConfig.label = `Reveal ${selectedRevealCount}/${revealableVotes.length} votes`; if (!hasSigningKey) { actionConfig.label = "Sign"; actionConfig.onClick = () => sign(); @@ -286,14 +348,8 @@ export function ActiveVotes() { } function getVotesToReveal() { - return ( - activeVoteList?.filter( - (vote) => - vote.isCommitted && - !!vote.decryptedVote && - vote.isRevealed === false && - vote.canReveal - ) ?? [] + return revealableVotes.filter( + (vote) => selectedRevealVotes[vote.uniqueKey] ?? true ); } @@ -320,6 +376,10 @@ export function ActiveVotes() { inputs[index] = dirty; return [...inputs]; }), + // Reveal phase selection props + isSelectedForReveal: isVoteSelectedForReveal(vote.uniqueKey), + toggleRevealSelection: () => toggleRevealVoteSelection(vote.uniqueKey), + canBeRevealed: canVoteBeRevealed(vote), })); return (