feat(polls): "See who voted" — per-answer voter list
The poll card showed vote counts but never who voted, even though computePollState already parses a sender for every response. Surface it: - tallyResponses now also returns voters: Map<answerId, senderId[]>, built in the same latest-response-per-sender loop as the counts, so voters can never disagree with the numbers (voters.get(id).length === counts.get(id)). +5 unit tests. - PollContent adds a "Show who voted" toggle, shown only when results are visible (disclosed live, or undisclosed after end — so a secret ballot stays secret). When on, each answer lists its voters' display names (getMemberName), rendered as a sibling of the answer button so the radiogroup keyboard model is untouched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import { Box, Chip, color, config, Icon, Icons, Text, toRem } from 'folds';
|
||||
import { RelationsEvent } from 'matrix-js-sdk/lib/models/relations';
|
||||
import { MatrixEvent, Room, RoomEvent, PollEvent } from 'matrix-js-sdk';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { getMemberName } from '../../../utils/room';
|
||||
import {
|
||||
ParsedPoll,
|
||||
PollResponse,
|
||||
@@ -16,7 +17,12 @@ import {
|
||||
winningAnswerIds,
|
||||
} from '../../../utils/poll';
|
||||
|
||||
const EMPTY_TALLY: PollTally = { counts: new Map(), myVotes: new Set(), total: 0 };
|
||||
const EMPTY_TALLY: PollTally = {
|
||||
counts: new Map(),
|
||||
voters: new Map(),
|
||||
myVotes: new Set(),
|
||||
total: 0,
|
||||
};
|
||||
|
||||
type PollState = { tally: PollTally; isEnded: boolean };
|
||||
|
||||
@@ -106,6 +112,7 @@ export function PollContent({
|
||||
const [pending, setPending] = useState<Set<string> | null>(null);
|
||||
const [confirmEnd, setConfirmEnd] = useState(false);
|
||||
const [ending, setEnding] = useState(false);
|
||||
const [showVoters, setShowVoters] = useState(false);
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
if (!eventId || !parsed) return;
|
||||
@@ -194,8 +201,11 @@ export function PollContent({
|
||||
const canEnd = !!eventId && !isEnded && (senderId === mx.getUserId() || canRedact);
|
||||
|
||||
const myVotes = pending ?? tally.myVotes;
|
||||
const { counts, total } = tally;
|
||||
const { counts, total, voters } = tally;
|
||||
const winners = isEnded && showResults ? new Set(winningAnswerIds(counts)) : new Set<string>();
|
||||
// Voter identities are shown under the same rule as the counts: disclosed polls
|
||||
// reveal them live, undisclosed polls only once ended.
|
||||
const canShowVoters = showResults && total > 0;
|
||||
|
||||
const handleVote = (answerId: string) => {
|
||||
if (!roomId || !eventId || !canVote) return;
|
||||
@@ -320,8 +330,8 @@ export function PollContent({
|
||||
? 0
|
||||
: -1;
|
||||
return (
|
||||
<React.Fragment key={id}>
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
data-poll-answer
|
||||
data-selected={selected}
|
||||
@@ -416,9 +426,32 @@ export function PollContent({
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
{showVoters && canShowVoters && (voters.get(id)?.length ?? 0) > 0 && (
|
||||
<Text
|
||||
size="T200"
|
||||
priority="300"
|
||||
style={{ padding: `0 ${config.space.S300} ${config.space.S100}` }}
|
||||
>
|
||||
{(voters.get(id) ?? []).map((s) => getMemberName(room, s)).join(', ')}
|
||||
</Text>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
{canShowVoters && (
|
||||
<Box>
|
||||
<Chip
|
||||
variant={showVoters ? 'Primary' : 'SurfaceVariant'}
|
||||
radii="Pill"
|
||||
aria-pressed={showVoters}
|
||||
onClick={() => setShowVoters((v) => !v)}
|
||||
before={<Icon size="50" src={Icons.User} />}
|
||||
>
|
||||
<Text size="T200">{showVoters ? 'Hide voters' : 'Show who voted'}</Text>
|
||||
</Chip>
|
||||
</Box>
|
||||
)}
|
||||
<Box alignItems="Center" justifyContent="SpaceBetween" gap="200">
|
||||
<Text size="T200" priority="300" style={{ minWidth: 0 }}>
|
||||
<i>{footerNote}</i>
|
||||
|
||||
Reference in New Issue
Block a user