fix: poll multiple-choice toggle + Sentry JAVASCRIPT-REACT-N

PollCreator: replace maxSelections/options.length stale-closure pattern
with isMultiple: boolean state. max_selections computed from filledOptions
at submit time. Radio inputs replaced with styled toggle buttons that
visually highlight the active selection.

PollContent: catch getPendingEvents error (Sentry JAVASCRIPT-REACT-N).
SDK throws Cannot call getPendingEvents with pendingEventOrdering ==
chronological when sending poll vote events with m.reference relation.
Silently catch so optimistic UI update stands — vote will retry on next
sync if needed.

Fixes JAVASCRIPT-REACT-N

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 00:55:50 -04:00
parent 9d4679d260
commit 986e4bb93a
2 changed files with 27 additions and 40 deletions
@@ -203,12 +203,12 @@ export function PollContent({
mx.sendEvent(roomId, 'm.poll.response' as any, { mx.sendEvent(roomId, 'm.poll.response' as any, {
'm.relates_to': { rel_type: 'm.reference', event_id: eventId }, 'm.relates_to': { rel_type: 'm.reference', event_id: eventId },
'm.selections': [answerId], 'm.selections': [answerId],
}); }).catch(() => undefined);
} else { } else {
mx.sendEvent(roomId, 'org.matrix.msc3381.poll.response' as any, { mx.sendEvent(roomId, 'org.matrix.msc3381.poll.response' as any, {
'm.relates_to': { rel_type: 'm.reference', event_id: eventId }, 'm.relates_to': { rel_type: 'm.reference', event_id: eventId },
'org.matrix.msc3381.poll.response': { answers: [answerId] }, 'org.matrix.msc3381.poll.response': { answers: [answerId] },
}); }).catch(() => undefined);
} }
}; };
+25 -38
View File
@@ -13,7 +13,7 @@ export function PollCreator({ roomId, onClose }: PollCreatorProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const [question, setQuestion] = useState(''); const [question, setQuestion] = useState('');
const [options, setOptions] = useState<string[]>(['', '']); const [options, setOptions] = useState<string[]>(['', '']);
const [maxSelections, setMaxSelections] = useState<number>(1); const [isMultiple, setIsMultiple] = useState(false);
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -54,7 +54,7 @@ export function PollCreator({ roomId, onClose }: PollCreatorProps) {
'm.poll': { 'm.poll': {
question: { 'm.text': trimmedQuestion }, question: { 'm.text': trimmedQuestion },
answers: filledOptions.map((o, i) => ({ 'm.id': `${i}`, 'm.text': o })), answers: filledOptions.map((o, i) => ({ 'm.id': `${i}`, 'm.text': o })),
max_selections: maxSelections, max_selections: isMultiple ? filledOptions.length : 1,
kind: 'm.poll.undisclosed', kind: 'm.poll.undisclosed',
}, },
body: trimmedQuestion, body: trimmedQuestion,
@@ -196,42 +196,29 @@ export function PollCreator({ roomId, onClose }: PollCreatorProps) {
<div style={{ display: 'flex', flexDirection: 'column', gap: config.space.S100 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: config.space.S100 }}>
<Text size="L400">Selection Type</Text> <Text size="L400">Selection Type</Text>
<div style={{ display: 'flex', gap: config.space.S200 }}> <div style={{ display: 'flex', gap: config.space.S200 }}>
<label {(['single', 'multiple'] as const).map((type) => {
style={{ const active = type === 'multiple' ? isMultiple : !isMultiple;
display: 'flex', return (
alignItems: 'center', <button
gap: config.space.S100, key={type}
cursor: 'pointer', type="button"
fontSize: '14px', onClick={() => setIsMultiple(type === 'multiple')}
color: 'var(--tc-surface-high)', style={{
}} padding: `${config.space.S100} ${config.space.S300}`,
> borderRadius: config.radii.R300,
<input border: `1px solid ${active ? 'var(--bg-primary-main)' : 'var(--bg-surface-border)'}`,
type="radio" background: active ? 'var(--bg-primary-main)' : 'transparent',
name="pollType" color: active ? 'var(--tc-primary-on-primary)' : 'var(--tc-surface-high)',
checked={maxSelections === 1} cursor: 'pointer',
onChange={() => setMaxSelections(1)} fontSize: '13px',
/> fontWeight: active ? 600 : 400,
Single choice transition: 'all 0.15s ease',
</label> }}
<label >
style={{ {type === 'single' ? 'Single choice' : 'Multiple choice'}
display: 'flex', </button>
alignItems: 'center', );
gap: config.space.S100, })}
cursor: 'pointer',
fontSize: '14px',
color: 'var(--tc-surface-high)',
}}
>
<input
type="radio"
name="pollType"
checked={maxSelections !== 1}
onChange={() => setMaxSelections(options.length)}
/>
Multiple choice
</label>
</div> </div>
</div> </div>