import type { ChangeEvent, RefObject } from 'react'; import type { FC } from '../../../lib/teact/teact'; import React, { memo, useCallback, useEffect, useLayoutEffect, useRef, useState, } from '../../../lib/teact/teact'; import type { ApiNewPoll } from '../../../api/types'; import captureEscKeyListener from '../../../util/captureEscKeyListener'; import parseMessageInput from '../../../util/parseMessageInput'; import useLang from '../../../hooks/useLang'; import Button from '../../ui/Button'; import Modal from '../../ui/Modal'; import InputText from '../../ui/InputText'; import Checkbox from '../../ui/Checkbox'; import RadioGroup from '../../ui/RadioGroup'; import './PollModal.scss'; export type OwnProps = { isOpen: boolean; shouldBeAnonymous?: boolean; isQuiz?: boolean; onSend: (pollSummary: ApiNewPoll) => void; onClear: () => void; }; const MAX_LIST_HEIGHT = 320; const MAX_OPTIONS_COUNT = 10; const MAX_OPTION_LENGTH = 100; const MAX_QUESTION_LENGTH = 255; const MAX_SOLUTION_LENGTH = 200; const PollModal: FC = ({ isOpen, isQuiz, shouldBeAnonymous, onSend, onClear, }) => { // eslint-disable-next-line no-null/no-null const questionInputRef = useRef(null); // eslint-disable-next-line no-null/no-null const optionsListRef = useRef(null); // eslint-disable-next-line no-null/no-null const solutionRef = useRef(null); const [question, setQuestion] = useState(''); const [options, setOptions] = useState(['']); const [isAnonymous, setIsAnonymous] = useState(true); const [isMultipleAnswers, setIsMultipleAnswers] = useState(false); const [isQuizMode, setIsQuizMode] = useState(isQuiz || false); const [solution, setSolution] = useState(''); const [correctOption, setCorrectOption] = useState(); const [hasErrors, setHasErrors] = useState(false); const lang = useLang(); const focusInput = useCallback((ref: RefObject) => { if (isOpen && ref.current) { ref.current.focus(); } }, [isOpen]); useEffect(() => (isOpen ? captureEscKeyListener(onClear) : undefined), [isOpen, onClear]); useEffect(() => { if (!isOpen) { setQuestion(''); setOptions(['']); setIsAnonymous(true); setIsMultipleAnswers(false); setIsQuizMode(isQuiz || false); setSolution(''); setCorrectOption(undefined); setHasErrors(false); } }, [isQuiz, isOpen]); useEffect(() => focusInput(questionInputRef), [focusInput, isOpen]); useLayoutEffect(() => { const solutionEl = solutionRef.current; if (solutionEl && solution !== solutionEl.innerHTML) { solutionEl.innerHTML = solution; } }, [solution]); const addNewOption = useCallback((newOptions: string[] = []) => { setOptions([...newOptions, '']); requestAnimationFrame(() => { const list = optionsListRef.current; if (!list) { return; } list.classList.toggle('overflown', list.scrollHeight > MAX_LIST_HEIGHT); list.scrollTo({ top: list.scrollHeight, behavior: 'smooth' }); }); }, []); const handleCreate = useCallback(() => { setHasErrors(false); if (!isOpen) { return; } const questionTrimmed = question.trim().substring(0, MAX_QUESTION_LENGTH); const optionsTrimmed = options.map((o) => o.trim().substring(0, MAX_OPTION_LENGTH)).filter((o) => o.length); if (!questionTrimmed || optionsTrimmed.length < 2) { setQuestion(questionTrimmed); if (optionsTrimmed.length) { if (optionsTrimmed.length < 2) { addNewOption(optionsTrimmed); } else { setOptions(optionsTrimmed); } } else { addNewOption(); } setHasErrors(true); return; } if (isQuizMode && (correctOption === undefined || !optionsTrimmed[correctOption])) { setHasErrors(true); return; } const answers = optionsTrimmed .map((text, index) => ({ text: text.trim(), option: String(index), ...(index === correctOption && { correct: true }), })); const payload: ApiNewPoll = { summary: { question: questionTrimmed, answers, ...(!isAnonymous && { isPublic: true }), ...(isMultipleAnswers && { multipleChoice: true }), ...(isQuizMode && { quiz: true }), }, }; if (isQuizMode) { const { text, entities } = (solution && parseMessageInput(solution.substring(0, MAX_SOLUTION_LENGTH))) || {}; payload.quiz = { correctAnswers: [String(correctOption)], ...(text && { solution: text }), ...(entities && { solutionEntities: entities }), }; } onSend(payload); }, [ isOpen, question, options, isQuizMode, correctOption, isAnonymous, isMultipleAnswers, onSend, addNewOption, solution, ]); const updateOption = useCallback((index: number, text: string) => { const newOptions = [...options]; newOptions[index] = text; if (newOptions[newOptions.length - 1].trim().length && newOptions.length < MAX_OPTIONS_COUNT) { addNewOption(newOptions); } else { setOptions(newOptions); } }, [options, addNewOption]); const removeOption = useCallback((index: number) => { const newOptions = [...options]; newOptions.splice(index, 1); setOptions(newOptions); if (correctOption !== undefined) { if (correctOption === index) { setCorrectOption(undefined); } else if (index < correctOption) { setCorrectOption(correctOption - 1); } } requestAnimationFrame(() => { if (!optionsListRef.current) { return; } optionsListRef.current.classList.toggle('overflown', optionsListRef.current.scrollHeight > MAX_LIST_HEIGHT); }); }, [correctOption, options]); const handleCorrectOptionChange = useCallback((newValue: string) => { setCorrectOption(Number(newValue)); }, [setCorrectOption]); const handleIsAnonymousChange = useCallback((e: ChangeEvent) => { setIsAnonymous(e.target.checked); }, []); const handleMultipleAnswersChange = useCallback((e: ChangeEvent) => { setIsMultipleAnswers(e.target.checked); }, []); const handleQuizModeChange = useCallback((e: ChangeEvent) => { setIsQuizMode(e.target.checked); }, []); const handleKeyPress = useCallback((e: React.KeyboardEvent) => { if (e.keyCode === 13) { handleCreate(); } }, [handleCreate]); const handleQuestionChange = useCallback((e: ChangeEvent) => { setQuestion(e.target.value); }, []); const getQuestionError = useCallback(() => { if (hasErrors && !question.trim().length) { return lang('lng_polls_choose_question'); } return undefined; }, [hasErrors, lang, question]); const getOptionsError = useCallback((index: number) => { const optionsTrimmed = options.map((o) => o.trim()).filter((o) => o.length); if (hasErrors && optionsTrimmed.length < 2 && !options[index].trim().length) { return lang('lng_polls_choose_answers'); } return undefined; }, [hasErrors, lang, options]); function renderHeader() { return (
{lang('NewPoll')}
); } function renderOptions() { return options.map((option, index) => (
updateOption(index, e.currentTarget.value)} onKeyPress={handleKeyPress} /> {index !== options.length - 1 && ( )}
)); } function renderRadioOptions() { return renderOptions() .map((label, index) => ({ value: String(index), label, hidden: index === options.length - 1 })); } function renderQuizNoOptionError() { const optionsTrimmed = options.map((o) => o.trim()).filter((o) => o.length); return isQuizMode && (correctOption === undefined || !optionsTrimmed[correctOption]) && (

{lang('lng_polls_choose_correct')}

); } return (

{lang('PollOptions')}

{hasErrors && renderQuizNoOptionError()} {isQuizMode ? ( ) : ( renderOptions() )}
{!shouldBeAnonymous && ( )} {isQuizMode && ( <>

{lang('lng_polls_solution_title')}

setSolution(e.currentTarget.innerHTML)} />
{lang('CreatePoll.ExplanationInfo')}
)}
); }; export default memo(PollModal);