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:
@@ -70,6 +70,51 @@ test('tallyResponses: re-voting after an empty clear counts the newest non-empty
|
||||
assert.equal(total, 1);
|
||||
});
|
||||
|
||||
test('tallyResponses: voters list the senders per answer, consistent with counts', () => {
|
||||
const responses: PollResponse[] = [
|
||||
{ sender: '@a:s', ts: 1, answerIds: ['1'] },
|
||||
{ sender: '@b:s', ts: 1, answerIds: ['1'] },
|
||||
{ sender: '@c:s', ts: 1, answerIds: ['2'] },
|
||||
];
|
||||
const { counts, voters } = tallyResponses(responses, me);
|
||||
assert.deepEqual([...(voters.get('1') ?? [])].sort(), ['@a:s', '@b:s']);
|
||||
assert.deepEqual(voters.get('2'), ['@c:s']);
|
||||
// voters must always agree with counts
|
||||
for (const [id, list] of voters) {
|
||||
assert.equal(list.length, counts.get(id));
|
||||
}
|
||||
});
|
||||
|
||||
test('tallyResponses: a re-vote moves the sender to the new answer in voters', () => {
|
||||
const responses: PollResponse[] = [
|
||||
{ sender: '@a:s', ts: 1, answerIds: ['1'] },
|
||||
{ sender: '@a:s', ts: 5, answerIds: ['2'] }, // changed vote
|
||||
];
|
||||
const { voters } = tallyResponses(responses, me);
|
||||
assert.equal(voters.get('1'), undefined); // no longer under the old answer
|
||||
assert.deepEqual(voters.get('2'), ['@a:s']);
|
||||
});
|
||||
|
||||
test('tallyResponses: a cleared latest response drops the sender from every voter list', () => {
|
||||
const responses: PollResponse[] = [
|
||||
{ sender: '@a:s', ts: 1, answerIds: ['1', '2'] },
|
||||
{ sender: '@a:s', ts: 2, answerIds: [] }, // cleared
|
||||
];
|
||||
const { voters } = tallyResponses(responses, me);
|
||||
assert.equal(voters.get('1'), undefined);
|
||||
assert.equal(voters.get('2'), undefined);
|
||||
});
|
||||
|
||||
test('tallyResponses: multi-select lists a sender under each chosen answer', () => {
|
||||
const responses: PollResponse[] = [
|
||||
{ sender: '@a:s', ts: 1, answerIds: ['1', '3'] },
|
||||
{ sender: '@b:s', ts: 1, answerIds: ['3'] },
|
||||
];
|
||||
const { voters } = tallyResponses(responses, me);
|
||||
assert.deepEqual(voters.get('1'), ['@a:s']);
|
||||
assert.deepEqual([...(voters.get('3') ?? [])].sort(), ['@a:s', '@b:s']);
|
||||
});
|
||||
|
||||
test('resultsVisible: disclosed always, undisclosed only when ended', () => {
|
||||
assert.equal(resultsVisible(false, false), true); // disclosed, open
|
||||
assert.equal(resultsVisible(false, true), true); // disclosed, ended
|
||||
|
||||
@@ -102,6 +102,9 @@ export type PollResponse = {
|
||||
export type PollTally = {
|
||||
/** answerId → number of voters who selected it. */
|
||||
counts: Map<string, number>;
|
||||
/** answerId → the sender ids who selected it (latest response per sender). Always
|
||||
* consistent with `counts` — `voters.get(id).length === counts.get(id)`. */
|
||||
voters: Map<string, string[]>;
|
||||
/** answer ids the current user selected (from their latest response). */
|
||||
myVotes: Set<string>;
|
||||
/** number of distinct voters counted. */
|
||||
@@ -126,6 +129,7 @@ export function tallyResponses(responses: PollResponse[], myUserId: string): Pol
|
||||
}
|
||||
|
||||
const counts = new Map<string, number>();
|
||||
const voters = new Map<string, string[]>();
|
||||
const myVotes = new Set<string>();
|
||||
let total = 0;
|
||||
for (const [sender, r] of latestBySender) {
|
||||
@@ -133,11 +137,14 @@ export function tallyResponses(responses: PollResponse[], myUserId: string): Pol
|
||||
total += 1;
|
||||
for (const id of r.answerIds) {
|
||||
counts.set(id, (counts.get(id) ?? 0) + 1);
|
||||
const list = voters.get(id);
|
||||
if (list) list.push(sender);
|
||||
else voters.set(id, [sender]);
|
||||
if (sender === myUserId) myVotes.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
return { counts, myVotes, total };
|
||||
return { counts, voters, myVotes, total };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user