2026-05-22 12:08:50 -04:00
|
|
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
2026-05-15 15:21:20 -04:00
|
|
|
import React, { useCallback, useEffect, useState } from 'react';
|
2026-06-25 04:02:13 -04:00
|
|
|
import { Box, color, config, Text, toRem } from 'folds';
|
2026-05-15 15:21:20 -04:00
|
|
|
import { RelationsEvent } from 'matrix-js-sdk/lib/models/relations';
|
2026-05-20 21:39:35 -04:00
|
|
|
import { RoomEvent } from 'matrix-js-sdk';
|
2026-05-15 13:37:03 -04:00
|
|
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
2026-05-15 00:47:21 -04:00
|
|
|
|
|
|
|
|
type PollTextValue = Array<{ body: string }> | string;
|
|
|
|
|
|
|
|
|
|
function extractText(val: PollTextValue | undefined): string {
|
|
|
|
|
if (!val) return '';
|
|
|
|
|
if (typeof val === 'string') return val;
|
|
|
|
|
return val[0]?.body ?? '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type PollAnswer = {
|
|
|
|
|
'm.id'?: string;
|
|
|
|
|
id?: string;
|
|
|
|
|
'm.text'?: PollTextValue;
|
|
|
|
|
'org.matrix.msc3381.poll.answer'?: { body: string };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type PollData = {
|
|
|
|
|
question?: { body?: string; 'm.text'?: PollTextValue };
|
|
|
|
|
answers?: PollAnswer[];
|
2026-06-03 11:23:44 -04:00
|
|
|
max_selections?: number;
|
2026-05-15 00:47:21 -04:00
|
|
|
};
|
|
|
|
|
|
2026-05-15 15:21:20 -04:00
|
|
|
type VoteState = {
|
|
|
|
|
counts: Map<string, number>;
|
2026-06-03 11:23:44 -04:00
|
|
|
myVotes: Set<string>;
|
2026-05-15 15:21:20 -04:00
|
|
|
total: number;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function computeVotes(
|
|
|
|
|
mx: ReturnType<typeof useMatrixClient>,
|
|
|
|
|
roomId: string,
|
|
|
|
|
eventId: string,
|
2026-05-22 17:17:26 -04:00
|
|
|
_isStable: boolean,
|
2026-05-15 15:21:20 -04:00
|
|
|
): VoteState {
|
2026-06-03 11:23:44 -04:00
|
|
|
const empty: VoteState = { counts: new Map(), myVotes: new Set(), total: 0 };
|
2026-05-15 15:21:20 -04:00
|
|
|
const room = mx.getRoom(roomId);
|
|
|
|
|
if (!room) return empty;
|
|
|
|
|
|
|
|
|
|
const timelineSet = room.getUnfilteredTimelineSet();
|
|
|
|
|
const stableRels = timelineSet.relations.getChildEventsForEvent(
|
|
|
|
|
eventId,
|
|
|
|
|
'm.reference',
|
2026-05-21 23:30:50 -04:00
|
|
|
'm.poll.response',
|
2026-05-15 15:21:20 -04:00
|
|
|
);
|
|
|
|
|
const unstableRels = timelineSet.relations.getChildEventsForEvent(
|
|
|
|
|
eventId,
|
|
|
|
|
'org.matrix.msc3381.poll.response' as any,
|
2026-05-21 23:30:50 -04:00
|
|
|
'org.matrix.msc3381.poll.response',
|
2026-05-15 15:21:20 -04:00
|
|
|
);
|
|
|
|
|
|
2026-06-03 11:23:44 -04:00
|
|
|
// Per-sender keep only the latest response (which may include multiple selections)
|
|
|
|
|
const latestBySender = new Map<string, { ts: number; answerIds: string[] }>();
|
2026-05-15 15:21:20 -04:00
|
|
|
const myUserId = mx.getSafeUserId();
|
|
|
|
|
|
|
|
|
|
const processRelations = (rels: typeof stableRels, stable: boolean) => {
|
|
|
|
|
const events = rels?.getRelations() ?? [];
|
|
|
|
|
for (const ev of events) {
|
|
|
|
|
if (ev.isRedacted()) continue;
|
|
|
|
|
const sender = ev.getSender();
|
|
|
|
|
if (!sender) continue;
|
|
|
|
|
const content = ev.getContent();
|
2026-06-03 11:23:44 -04:00
|
|
|
let answerIds: string[] = [];
|
2026-05-15 15:21:20 -04:00
|
|
|
if (stable) {
|
2026-06-03 11:23:44 -04:00
|
|
|
answerIds = (content['m.selections'] as string[] | undefined) ?? [];
|
2026-05-15 15:21:20 -04:00
|
|
|
} else {
|
2026-06-03 11:23:44 -04:00
|
|
|
answerIds =
|
2026-06-03 11:40:28 -04:00
|
|
|
((content['org.matrix.msc3381.poll.response'] as any)?.answers as string[] | undefined) ??
|
|
|
|
|
[];
|
2026-05-15 15:21:20 -04:00
|
|
|
}
|
2026-06-03 11:23:44 -04:00
|
|
|
if (answerIds.length === 0) continue;
|
2026-05-15 15:21:20 -04:00
|
|
|
const ts = ev.getTs();
|
|
|
|
|
const existing = latestBySender.get(sender);
|
|
|
|
|
if (!existing || ts > existing.ts) {
|
2026-06-03 11:23:44 -04:00
|
|
|
latestBySender.set(sender, { ts, answerIds });
|
2026-05-15 15:21:20 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
processRelations(stableRels, true);
|
|
|
|
|
processRelations(unstableRels, false);
|
|
|
|
|
|
|
|
|
|
const counts = new Map<string, number>();
|
2026-06-03 11:23:44 -04:00
|
|
|
const myVotes = new Set<string>();
|
|
|
|
|
for (const [sender, { answerIds }] of latestBySender) {
|
|
|
|
|
for (const id of answerIds) {
|
|
|
|
|
counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
|
|
|
if (sender === myUserId) myVotes.add(id);
|
|
|
|
|
}
|
2026-05-15 15:21:20 -04:00
|
|
|
}
|
|
|
|
|
|
2026-06-03 11:23:44 -04:00
|
|
|
return { counts, myVotes, total: latestBySender.size };
|
2026-05-15 15:21:20 -04:00
|
|
|
}
|
|
|
|
|
|
2026-05-15 13:37:03 -04:00
|
|
|
export function PollContent({
|
|
|
|
|
content,
|
|
|
|
|
roomId,
|
|
|
|
|
eventId,
|
|
|
|
|
}: {
|
|
|
|
|
content: Record<string, unknown>;
|
|
|
|
|
roomId?: string;
|
|
|
|
|
eventId?: string;
|
|
|
|
|
}) {
|
|
|
|
|
const mx = useMatrixClient();
|
2026-05-22 17:17:26 -04:00
|
|
|
const _isStable = !!content['m.poll'];
|
2026-05-15 13:37:03 -04:00
|
|
|
|
2026-05-21 20:49:33 -04:00
|
|
|
const poll = (content['m.poll'] ?? content['org.matrix.msc3381.poll.start']) as
|
|
|
|
|
| PollData
|
|
|
|
|
| undefined;
|
2026-05-15 00:47:21 -04:00
|
|
|
|
2026-05-15 15:21:20 -04:00
|
|
|
const [votes, setVotes] = useState<VoteState>(() => {
|
2026-06-03 11:23:44 -04:00
|
|
|
if (!roomId || !eventId) return { counts: new Map(), myVotes: new Set(), total: 0 };
|
2026-05-22 17:17:26 -04:00
|
|
|
return computeVotes(mx, roomId, eventId, _isStable);
|
2026-05-15 15:21:20 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Refresh votes whenever Relations events fire
|
|
|
|
|
const refresh = useCallback(() => {
|
|
|
|
|
if (!roomId || !eventId) return;
|
2026-05-22 17:17:26 -04:00
|
|
|
setVotes(computeVotes(mx, roomId, eventId, _isStable));
|
|
|
|
|
}, [mx, roomId, eventId, _isStable]);
|
2026-05-15 15:21:20 -04:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!roomId || !eventId) return;
|
|
|
|
|
const room = mx.getRoom(roomId);
|
|
|
|
|
if (!room) return;
|
|
|
|
|
const timelineSet = room.getUnfilteredTimelineSet();
|
|
|
|
|
|
|
|
|
|
const stableRels = timelineSet.relations.getChildEventsForEvent(
|
|
|
|
|
eventId,
|
|
|
|
|
'm.reference',
|
2026-05-21 23:30:50 -04:00
|
|
|
'm.poll.response',
|
2026-05-15 15:21:20 -04:00
|
|
|
);
|
|
|
|
|
const unstableRels = timelineSet.relations.getChildEventsForEvent(
|
|
|
|
|
eventId,
|
|
|
|
|
'org.matrix.msc3381.poll.response' as any,
|
2026-05-21 23:30:50 -04:00
|
|
|
'org.matrix.msc3381.poll.response',
|
2026-05-15 15:21:20 -04:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
stableRels?.on(RelationsEvent.Add, refresh);
|
|
|
|
|
stableRels?.on(RelationsEvent.Remove, refresh);
|
|
|
|
|
stableRels?.on(RelationsEvent.Redaction, refresh);
|
|
|
|
|
unstableRels?.on(RelationsEvent.Add, refresh);
|
|
|
|
|
unstableRels?.on(RelationsEvent.Remove, refresh);
|
|
|
|
|
unstableRels?.on(RelationsEvent.Redaction, refresh);
|
2026-05-20 21:39:35 -04:00
|
|
|
// Also listen at room level: if no votes exist yet, the Relations object is null
|
|
|
|
|
// and the listeners above are no-ops. The room timeline event catches the first vote.
|
|
|
|
|
const onTimeline = (ev: any) => {
|
|
|
|
|
const type = ev.getType?.();
|
|
|
|
|
const relatesTo = ev.getContent?.()?.['m.relates_to'];
|
|
|
|
|
if (
|
|
|
|
|
(type === 'm.poll.response' || type === 'org.matrix.msc3381.poll.response') &&
|
|
|
|
|
relatesTo?.event_id === eventId
|
|
|
|
|
) {
|
|
|
|
|
refresh();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
const room2 = mx.getRoom(roomId);
|
|
|
|
|
room2?.on(RoomEvent.Timeline, onTimeline);
|
|
|
|
|
|
2026-05-15 15:21:20 -04:00
|
|
|
return () => {
|
|
|
|
|
stableRels?.off(RelationsEvent.Add, refresh);
|
|
|
|
|
stableRels?.off(RelationsEvent.Remove, refresh);
|
|
|
|
|
stableRels?.off(RelationsEvent.Redaction, refresh);
|
|
|
|
|
unstableRels?.off(RelationsEvent.Add, refresh);
|
|
|
|
|
unstableRels?.off(RelationsEvent.Remove, refresh);
|
|
|
|
|
unstableRels?.off(RelationsEvent.Redaction, refresh);
|
2026-05-20 21:39:35 -04:00
|
|
|
room2?.off(RoomEvent.Timeline, onTimeline);
|
2026-05-15 15:21:20 -04:00
|
|
|
};
|
|
|
|
|
}, [mx, roomId, eventId, refresh]);
|
|
|
|
|
|
2026-05-15 00:47:21 -04:00
|
|
|
if (!poll) {
|
|
|
|
|
return (
|
2026-06-25 04:02:13 -04:00
|
|
|
<Text priority="300">
|
2026-05-15 00:47:21 -04:00
|
|
|
<i>Poll (unreadable format)</i>
|
|
|
|
|
</Text>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const questionText =
|
|
|
|
|
extractText((poll.question as any)?.['m.text']) ||
|
|
|
|
|
(poll.question as any)?.body ||
|
|
|
|
|
'Untitled poll';
|
|
|
|
|
|
2026-05-15 13:37:03 -04:00
|
|
|
const canVote = !!roomId && !!eventId;
|
2026-06-03 11:23:44 -04:00
|
|
|
const maxSelections = (poll as any).max_selections ?? 1;
|
|
|
|
|
const isMultiple = maxSelections > 1;
|
|
|
|
|
const { counts, myVotes, total } = votes;
|
2026-05-15 13:37:03 -04:00
|
|
|
|
|
|
|
|
const handleVote = (answerId: string) => {
|
|
|
|
|
if (!roomId || !eventId) return;
|
2026-06-03 11:23:44 -04:00
|
|
|
|
|
|
|
|
const newVotes = new Set(myVotes);
|
|
|
|
|
if (newVotes.has(answerId)) {
|
|
|
|
|
newVotes.delete(answerId);
|
|
|
|
|
} else {
|
|
|
|
|
if (!isMultiple) newVotes.clear();
|
|
|
|
|
newVotes.add(answerId);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 15:21:20 -04:00
|
|
|
// Optimistic local update
|
|
|
|
|
setVotes((prev) => {
|
|
|
|
|
const next = new Map(prev.counts);
|
2026-06-03 11:23:44 -04:00
|
|
|
// Remove all old vote counts for this user
|
|
|
|
|
for (const id of prev.myVotes) {
|
|
|
|
|
const c = next.get(id) ?? 1;
|
|
|
|
|
if (c <= 1) next.delete(id);
|
|
|
|
|
else next.set(id, c - 1);
|
2026-05-15 15:21:20 -04:00
|
|
|
}
|
2026-06-03 11:23:44 -04:00
|
|
|
// Add new vote counts
|
|
|
|
|
for (const id of newVotes) {
|
|
|
|
|
next.set(id, (next.get(id) ?? 0) + 1);
|
|
|
|
|
}
|
|
|
|
|
const hadVotes = prev.myVotes.size > 0;
|
|
|
|
|
const hasVotes = newVotes.size > 0;
|
|
|
|
|
const newTotal = prev.total + (hasVotes && !hadVotes ? 1 : !hasVotes && hadVotes ? -1 : 0);
|
|
|
|
|
return { counts: next, myVotes: newVotes, total: newTotal };
|
2026-05-15 15:21:20 -04:00
|
|
|
});
|
2026-06-03 11:23:44 -04:00
|
|
|
|
|
|
|
|
const selectionsArr = Array.from(newVotes);
|
2026-05-22 17:17:26 -04:00
|
|
|
if (_isStable) {
|
2026-05-15 13:37:03 -04:00
|
|
|
mx.sendEvent(roomId, 'm.poll.response' as any, {
|
|
|
|
|
'm.relates_to': { rel_type: 'm.reference', event_id: eventId },
|
2026-06-03 11:23:44 -04:00
|
|
|
'm.selections': selectionsArr,
|
2026-06-03 00:55:50 -04:00
|
|
|
}).catch(() => undefined);
|
2026-05-15 13:37:03 -04:00
|
|
|
} else {
|
|
|
|
|
mx.sendEvent(roomId, 'org.matrix.msc3381.poll.response' as any, {
|
|
|
|
|
'm.relates_to': { rel_type: 'm.reference', event_id: eventId },
|
2026-06-03 11:23:44 -04:00
|
|
|
'org.matrix.msc3381.poll.response': { answers: selectionsArr },
|
2026-06-03 00:55:50 -04:00
|
|
|
}).catch(() => undefined);
|
2026-05-15 13:37:03 -04:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-15 15:21:20 -04:00
|
|
|
const answers = poll.answers ?? [];
|
|
|
|
|
|
2026-05-15 00:47:21 -04:00
|
|
|
return (
|
2026-05-15 13:37:03 -04:00
|
|
|
<Box
|
2026-05-23 23:01:13 -04:00
|
|
|
data-poll-content
|
2026-05-15 13:37:03 -04:00
|
|
|
direction="Column"
|
|
|
|
|
gap="200"
|
|
|
|
|
style={{ maxWidth: '340px', paddingTop: '2px', paddingBottom: '4px' }}
|
|
|
|
|
>
|
2026-06-25 04:02:13 -04:00
|
|
|
<Text
|
|
|
|
|
as="div"
|
|
|
|
|
size="T200"
|
|
|
|
|
priority="300"
|
2026-05-23 23:01:13 -04:00
|
|
|
data-poll-content-label
|
2026-05-15 00:47:21 -04:00
|
|
|
style={{
|
|
|
|
|
fontWeight: 700,
|
|
|
|
|
letterSpacing: '0.12em',
|
|
|
|
|
textTransform: 'uppercase',
|
2026-06-25 04:02:13 -04:00
|
|
|
marginBottom: config.space.S100,
|
2026-05-15 00:47:21 -04:00
|
|
|
}}
|
|
|
|
|
>
|
2026-06-03 11:23:44 -04:00
|
|
|
{`◉ Poll · ${isMultiple ? 'Multiple choice' : 'Single choice'}`}
|
2026-06-25 04:02:13 -04:00
|
|
|
</Text>
|
2026-05-15 00:47:21 -04:00
|
|
|
<Text size="T400" style={{ fontWeight: 600 }}>
|
|
|
|
|
{questionText}
|
|
|
|
|
</Text>
|
|
|
|
|
<Box direction="Column" gap="100" style={{ marginTop: '2px' }}>
|
2026-05-15 15:21:20 -04:00
|
|
|
{answers.map((answer, i) => {
|
2026-05-15 00:47:21 -04:00
|
|
|
const text =
|
|
|
|
|
extractText((answer as any)['m.text']) ||
|
|
|
|
|
(answer as any)['org.matrix.msc3381.poll.answer']?.body ||
|
|
|
|
|
`Option ${i + 1}`;
|
|
|
|
|
const id = answer['m.id'] ?? answer.id ?? String(i);
|
2026-06-03 11:23:44 -04:00
|
|
|
const selected = myVotes.has(id);
|
2026-05-15 15:21:20 -04:00
|
|
|
const voteCount = counts.get(id) ?? 0;
|
|
|
|
|
const pct = total > 0 ? Math.round((voteCount / total) * 100) : 0;
|
2026-05-15 00:47:21 -04:00
|
|
|
return (
|
2026-05-15 13:37:03 -04:00
|
|
|
<button
|
2026-05-15 00:47:21 -04:00
|
|
|
key={id}
|
2026-05-15 13:37:03 -04:00
|
|
|
type="button"
|
2026-05-23 23:01:13 -04:00
|
|
|
data-poll-answer
|
|
|
|
|
data-selected={selected}
|
2026-05-15 13:37:03 -04:00
|
|
|
onClick={canVote ? () => handleVote(id) : undefined}
|
2026-05-15 00:47:21 -04:00
|
|
|
style={{
|
2026-06-25 04:02:13 -04:00
|
|
|
padding: `${config.space.S200} ${config.space.S300}`,
|
|
|
|
|
borderRadius: config.radii.R300,
|
|
|
|
|
background: selected ? color.Primary.Container : color.SurfaceVariant.Container,
|
|
|
|
|
border: `${config.borderWidth.B300} solid ${
|
|
|
|
|
selected ? color.Primary.Main : color.SurfaceVariant.ContainerLine
|
|
|
|
|
}`,
|
2026-05-15 00:47:21 -04:00
|
|
|
lineHeight: 1.4,
|
2026-05-15 13:37:03 -04:00
|
|
|
textAlign: 'left',
|
|
|
|
|
cursor: canVote ? 'pointer' : 'default',
|
2026-06-03 11:23:44 -04:00
|
|
|
color: 'inherit',
|
2026-05-15 13:37:03 -04:00
|
|
|
display: 'flex',
|
2026-05-15 15:21:20 -04:00
|
|
|
flexDirection: 'column',
|
2026-06-25 04:02:13 -04:00
|
|
|
gap: config.space.S100,
|
2026-05-15 13:37:03 -04:00
|
|
|
width: '100%',
|
2026-05-15 15:21:20 -04:00
|
|
|
position: 'relative',
|
|
|
|
|
overflow: 'hidden',
|
2026-06-03 11:23:44 -04:00
|
|
|
transition: 'border-color 0.15s, background 0.15s',
|
2026-05-15 00:47:21 -04:00
|
|
|
}}
|
|
|
|
|
>
|
2026-05-15 15:21:20 -04:00
|
|
|
{total > 0 && (
|
|
|
|
|
<span
|
|
|
|
|
aria-hidden
|
|
|
|
|
style={{
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
inset: 0,
|
2026-06-03 11:23:44 -04:00
|
|
|
right: 'auto',
|
2026-05-15 15:21:20 -04:00
|
|
|
width: `${pct}%`,
|
2026-06-25 04:02:13 -04:00
|
|
|
background: selected
|
|
|
|
|
? color.Primary.ContainerActive
|
|
|
|
|
: color.SurfaceVariant.ContainerActive,
|
2026-05-15 15:21:20 -04:00
|
|
|
pointerEvents: 'none',
|
2026-06-03 11:23:44 -04:00
|
|
|
transition: 'width 0.3s ease',
|
2026-05-15 15:21:20 -04:00
|
|
|
}}
|
|
|
|
|
/>
|
2026-05-15 13:37:03 -04:00
|
|
|
)}
|
2026-05-21 20:49:33 -04:00
|
|
|
<span
|
2026-06-25 04:02:13 -04:00
|
|
|
style={{
|
|
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
gap: config.space.S200,
|
|
|
|
|
position: 'relative',
|
|
|
|
|
}}
|
2026-05-21 20:49:33 -04:00
|
|
|
>
|
2026-06-25 04:02:13 -04:00
|
|
|
<span
|
|
|
|
|
style={{
|
|
|
|
|
flexShrink: 0,
|
|
|
|
|
width: toRem(14),
|
|
|
|
|
height: toRem(14),
|
|
|
|
|
border: `${config.borderWidth.B300} solid ${
|
|
|
|
|
selected ? color.Primary.Main : color.Primary.ContainerLine
|
|
|
|
|
}`,
|
|
|
|
|
borderRadius: isMultiple ? config.radii.R300 : config.radii.Pill,
|
|
|
|
|
background: selected ? color.Primary.Main : 'transparent',
|
|
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
color: color.Primary.OnMain,
|
|
|
|
|
transition: 'all 0.15s',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{selected && isMultiple ? (
|
|
|
|
|
<Text as="span" size="T200" style={{ lineHeight: 1 }}>
|
|
|
|
|
✓
|
|
|
|
|
</Text>
|
|
|
|
|
) : null}
|
|
|
|
|
</span>
|
|
|
|
|
<Text as="span" size="T300" style={{ flexGrow: 1 }}>
|
|
|
|
|
{text}
|
|
|
|
|
</Text>
|
2026-05-15 15:21:20 -04:00
|
|
|
{total > 0 && (
|
2026-06-25 04:02:13 -04:00
|
|
|
<Text as="span" size="T200" priority="300" style={{ flexShrink: 0 }}>
|
|
|
|
|
{pct}%
|
|
|
|
|
</Text>
|
2026-05-15 15:21:20 -04:00
|
|
|
)}
|
|
|
|
|
</span>
|
2026-05-15 13:37:03 -04:00
|
|
|
</button>
|
2026-05-15 00:47:21 -04:00
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</Box>
|
2026-06-25 04:02:13 -04:00
|
|
|
<Text size="T200" priority="300" style={{ marginTop: '2px' }}>
|
2026-05-15 15:21:20 -04:00
|
|
|
<i>
|
2026-06-03 11:23:44 -04:00
|
|
|
{total > 0 ? `${total} vote${total === 1 ? '' : 's'} · ` : ''}
|
2026-05-15 15:21:20 -04:00
|
|
|
{canVote
|
2026-06-03 11:23:44 -04:00
|
|
|
? isMultiple
|
|
|
|
|
? 'Select all that apply'
|
|
|
|
|
: myVotes.size > 0
|
|
|
|
|
? 'Click to change'
|
|
|
|
|
: 'Click to vote'
|
|
|
|
|
: 'Voting not available'}
|
2026-05-15 15:21:20 -04:00
|
|
|
</i>
|
|
|
|
|
</Text>
|
2026-05-15 00:47:21 -04:00
|
|
|
</Box>
|
|
|
|
|
);
|
|
|
|
|
}
|