import type { FC } from '../../../lib/teact/teact'; import React, { useCallback, useEffect, useState, memo, useMemo, useRef, } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; import type { ApiMessage, ApiPoll, ApiUser, ApiPollAnswer, } from '../../../api/types'; import renderText from '../../common/helpers/renderText'; import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities'; import { formatMediaDuration } from '../../../util/dateFormat'; import type { LangFn } from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang'; import CheckboxGroup from '../../ui/CheckboxGroup'; import RadioGroup from '../../ui/RadioGroup'; import Avatar from '../../common/Avatar'; import Button from '../../ui/Button'; import Notification from '../../ui/Notification'; import PollOption from './PollOption'; import './Poll.scss'; type OwnProps = { message: ApiMessage; poll: ApiPoll; onSendVote: (options: string[]) => void; }; type StateProps = { recentVoterIds?: number[]; usersById: Record; serverTimeOffset: number; }; const SOLUTION_CONTAINER_ID = '#middle-column-portals'; const SOLUTION_DURATION = 5000; const NBSP = '\u00A0'; const Poll: FC = ({ message, poll, recentVoterIds, usersById, onSendVote, serverTimeOffset, }) => { const { loadMessage, openPollResults, requestConfetti } = getActions(); const { id: messageId, chatId } = message; const { summary, results } = poll; const [isSubmitting, setIsSubmitting] = useState(false); const [chosenOptions, setChosenOptions] = useState([]); const [isSolutionShown, setIsSolutionShown] = useState(false); const [wasSubmitted, setWasSubmitted] = useState(false); const [closePeriod, setClosePeriod] = useState( !summary.closed && summary.closeDate && summary.closeDate > 0 ? Math.min(summary.closeDate - Math.floor(Date.now() / 1000) + serverTimeOffset, summary.closePeriod!) : 0, ); // eslint-disable-next-line no-null/no-null const countdownRef = useRef(null); const { results: voteResults, totalVoters } = results; const hasVoted = voteResults && voteResults.some((r) => r.isChosen); const canVote = !summary.closed && !hasVoted; const canViewResult = !canVote && summary.isPublic && Number(results.totalVoters) > 0; const isMultiple = canVote && summary.multipleChoice; const maxVotersCount = voteResults ? Math.max(...voteResults.map((r) => r.votersCount)) : totalVoters; const correctResults = voteResults ? voteResults.reduce((answers: string[], r) => { if (r.isCorrect) { answers.push(r.option); } return answers; }, []) : []; const answers = summary.answers.map((a) => ({ label: a.text, value: a.option, hidden: Boolean(summary.quiz && summary.closePeriod && closePeriod <= 0), })); useEffect(() => { const chosen = poll.results.results?.find((result) => result.isChosen); if (isSubmitting && chosen) { if (chosen.isCorrect) { requestConfetti(); } setIsSubmitting(false); } }, [isSubmitting, poll.results.results, requestConfetti]); useEffect(() => { if (closePeriod > 0) { setTimeout(() => setClosePeriod(closePeriod - 1), 1000); } const countdownEl = countdownRef.current; if (countdownEl) { const circumference = 6 * 2 * Math.PI; const svgEl = countdownEl.lastElementChild; const timerEl = countdownEl.firstElementChild; if (closePeriod <= 5) { countdownEl.classList.add('hurry-up'); } if (!svgEl || !timerEl) { countdownEl.innerHTML = ` ${formatMediaDuration(closePeriod)} `; } else { const strokeDashOffset = ((summary.closePeriod! - closePeriod) / summary.closePeriod!) * circumference; timerEl.textContent = formatMediaDuration(closePeriod); (svgEl.firstElementChild as SVGElement).setAttribute('stroke-dashoffset', `-${strokeDashOffset}`); } } }, [closePeriod, summary.closePeriod]); useEffect(() => { if (summary.quiz && (closePeriod <= 0 || (hasVoted && !summary.closed))) { loadMessage({ chatId, messageId }); } }, [chatId, closePeriod, hasVoted, loadMessage, messageId, summary.closed, summary.quiz]); // If the client time is not synchronized, the poll must be updated after the closePeriod time has expired. useEffect(() => { let timer: number | undefined; if (summary.quiz && !summary.closed && summary.closePeriod && summary.closePeriod > 0) { timer = window.setTimeout(() => { loadMessage({ chatId, messageId }); }, summary.closePeriod * 1000); } return () => { if (timer) { window.clearTimeout(timer); } }; }, [canVote, chatId, loadMessage, messageId, summary.closePeriod, summary.closed, summary.quiz]); const recentVoters = useMemo(() => { return recentVoterIds ? recentVoterIds.reduce((result: ApiUser[], id) => { const user = usersById[id]; if (user) { result.push(user); } return result; }, []) : []; }, [usersById, recentVoterIds]); const handleRadioChange = useCallback( (option: string) => { setChosenOptions([option]); setIsSubmitting(true); setWasSubmitted(true); onSendVote([option]); }, [onSendVote], ); const handleCheckboxChange = useCallback( (options: string[]) => { setChosenOptions(options); }, [], ); const handleVoteClick = useCallback( () => { setIsSubmitting(true); setWasSubmitted(true); onSendVote(chosenOptions); }, [onSendVote, chosenOptions], ); const handleViewResultsClick = useCallback( () => { openPollResults({ chatId, messageId }); }, [chatId, messageId, openPollResults], ); const handleSolutionShow = useCallback(() => { setIsSolutionShown(true); }, []); const handleSolutionHide = useCallback(() => { setIsSolutionShown(false); setWasSubmitted(false); }, []); // Show the solution to quiz if the answer was incorrect useEffect(() => { if (wasSubmitted && hasVoted && summary.quiz && results.results && poll.results.solution) { const correctResult = results.results.find((r) => r.isChosen && r.isCorrect); if (!correctResult) { setIsSolutionShown(true); } } }, [hasVoted, wasSubmitted, results.results, summary.quiz, poll.results.solution]); const lang = useLang(); function renderResultOption(answer: ApiPollAnswer) { return ( ); } function renderRecentVoters() { return ( recentVoters.length > 0 && (
{recentVoters.map((user) => ( ))}
) ); } function renderSolution() { return ( isSolutionShown && poll.results.solution && ( ) ); } return (
{renderSolution()}
{renderText(summary.question, ['emoji', 'br'])}
{lang(getPollTypeString(summary))} {renderRecentVoters()} {closePeriod > 0 && canVote &&
} {summary.quiz && poll.results.solution && !canVote && ( )}
{canVote && (
{isMultiple ? ( ) : ( )}
)} {!canVote && (
{summary.answers.map(renderResultOption)}
)} {!canViewResult && !isMultiple && (
{getReadableVotersCount(lang, summary.quiz, results.totalVoters)}
)} {isMultiple && ( )} {canViewResult && ( )}
); }; function getPollTypeString(summary: ApiPoll['summary']) { // When we just created the poll, some properties don't exist. if (typeof summary.isPublic === 'undefined') { return NBSP; } if (summary.closed) { return 'FinalResults'; } if (summary.quiz) { return summary.isPublic ? 'QuizPoll' : 'AnonymousQuizPoll'; } return summary.isPublic ? 'PublicPoll' : 'AnonymousPoll'; } function getReadableVotersCount(lang: LangFn, isQuiz: true | undefined, count?: number) { if (!count) { return lang(isQuiz ? 'Chat.Quiz.TotalVotesEmpty' : 'Chat.Poll.TotalVotesResultEmpty'); } return lang(isQuiz ? 'Answer' : 'Vote', count, 'i'); } export default memo(withGlobal( (global, { poll }) => { const { recentVoterIds } = poll.results; const { serverTimeOffset, users: { byId: usersById } } = global; if (!recentVoterIds || recentVoterIds.length === 0) { return {}; } return { recentVoterIds, usersById, serverTimeOffset, }; }, )(Poll));