import React, { FormEventHandler, useState } from 'react'; import FocusTrap from 'focus-trap-react'; import { Box, Button, Dialog, Header, Icon, IconButton, Icons, Input, Overlay, OverlayBackdrop, OverlayCenter, Spinner, Text, color, config, } from 'folds'; import { Room } from 'matrix-js-sdk'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { stopPropagation } from '../../utils/keyboard'; import { useModalStyle } from '../../hooks/useModalStyle'; interface PollCreatorProps { roomId: string; room: Room; onClose: () => void; } export function PollCreator({ roomId, onClose }: PollCreatorProps) { const mx = useMatrixClient(); const modalStyle = useModalStyle(440); const [question, setQuestion] = useState(''); const [options, setOptions] = useState(['', '']); const [isMultiple, setIsMultiple] = useState(false); // For multiple-choice polls: the most options a voter may pick. Defaults high // so an untouched multiple poll means "select all that apply" (the previous // behavior); the effective value is clamped to the current option count. const [maxSelections, setMaxSelections] = useState(10); // Results visibility: disclosed (live results, default) vs undisclosed (hidden // until the poll is ended). const [disclosed, setDisclosed] = useState(true); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const handleOptionChange = (index: number, value: string) => { setOptions((prev) => { const next = [...prev]; next[index] = value; return next; }); }; const handleAddOption = () => { if (options.length >= 10) return; setOptions((prev) => [...prev, '']); }; const handleRemoveOption = (index: number) => { if (options.length <= 2) return; setOptions((prev) => prev.filter((_, i) => i !== index)); }; const handleSubmit: FormEventHandler = async (e) => { e.preventDefault(); if (submitting) return; const trimmedQuestion = question.trim(); if (!trimmedQuestion) { setError('Please enter a question.'); return; } const filledOptions = options.map((o) => o.trim()).filter((o) => o.length > 0); if (filledOptions.length < 2) { setError('Please provide at least 2 answer options.'); return; } setError(null); setSubmitting(true); try { // Text fallback for clients that don't understand polls: the question + a // numbered list of the options. const fallbackBody = [trimmedQuestion, ...filledOptions.map((o, i) => `${i + 1}. ${o}`)].join( '\n', ); await mx.sendEvent(roomId, 'm.poll.start' as any, { 'm.poll': { question: { 'm.text': trimmedQuestion }, answers: filledOptions.map((o, i) => ({ 'm.id': `${i}`, 'm.text': o })), max_selections: isMultiple ? Math.min(Math.max(2, maxSelections), filledOptions.length) : 1, kind: disclosed ? 'm.poll.disclosed' : 'm.poll.undisclosed', }, body: fallbackBody, msgtype: 'm.text', }); onClose(); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to send poll.'); setSubmitting(false); } }; return ( }> {/* Header */}
Create Poll
{/* Body */} {/* Question */} Question ) => setQuestion(e.target.value)} autoFocus /> {/* Options */} Options {options.map((opt, index) => ( ) => handleOptionChange(index, e.target.value) } aria-label={`Option ${index + 1}`} /> handleRemoveOption(index)} disabled={options.length <= 2} aria-label={`Remove option ${index + 1}`} > ))} {options.length < 10 && ( )} {/* Selection type */} Selection Type {(['single', 'multiple'] as const).map((type) => { const active = type === 'multiple' ? isMultiple : !isMultiple; return ( ); })} {isMultiple && ( Voters can pick up to ) => setMaxSelections( Math.min(options.length, Math.max(2, parseInt(e.target.value, 10) || 2)), ) } style={{ width: '4rem' }} /> of {options.length} options )} {/* Results visibility */} Results {(['live', 'hidden'] as const).map((mode) => { const active = mode === 'live' ? disclosed : !disclosed; return ( ); })} {/* Error */} {error && ( {error} )} {/* Footer actions */}
); }