2026-05-05 18:30:19 +02:00

950 lines
30 KiB
TypeScript

import type { TeactNode } from '../../../lib/teact/teact';
import {
memo,
useEffect,
useMemo,
useRef,
useState,
} from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type { ApiChat, ApiNewPoll } from '../../../api/types';
import type { TabState } from '../../../global/types';
import type { MessageList } from '../../../types';
import type { IconName } from '../../../types/icons';
import { MAIN_THREAD_ID } from '../../../api/types';
import { requestMeasure } from '../../../lib/fasterdom/fasterdom';
import { isChatChannel } from '../../../global/helpers';
import { getChatNotifySettings } from '../../../global/helpers/notifications';
import { getPeerTitle } from '../../../global/helpers/peers';
import {
selectChat,
selectNotifyDefaults,
selectNotifyException,
selectPeerPaidMessagesStars,
selectTabState,
} from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import { formatDateTimeToString, formatShortDuration } from '../../../util/dates/oldDateFormat';
import { DAY, HOUR } from '../../../util/dates/units';
import { generateUniqueNumberId } from '../../../util/generateUniqueId';
import { getServerTime } from '../../../util/serverTime';
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useReorderableList from '../../../hooks/useReorderableList';
import useSchedule from '../../../hooks/useSchedule';
import usePaidMessageConfirmation from '../../middle/composer/hooks/usePaidMessageConfirmation';
import CalendarModal from '../../common/CalendarModal.async';
import Icon from '../../common/icons/Icon';
import PaymentMessageConfirmDialog from '../../common/PaymentMessageConfirmDialog';
import CustomSendMenu from '../../middle/composer/CustomSendMenu.async';
import Button from '../../ui/Button';
import InputText from '../../ui/InputText';
import Menu from '../../ui/Menu';
import MenuItem from '../../ui/MenuItem';
import TextArea from '../../ui/TextArea';
import Control, {
ControlAfter,
ControlDescription,
ControlIcon,
ControlLabel,
} from '@gili/layout/Control';
import Interactive from '@gili/layout/Interactive';
import Island, {
IslandDescription,
IslandTitle,
} from '@gili/layout/Island';
import Modal, {
ModalCloseButton,
ModalHeader,
ModalHeaderAction,
ModalTitle,
} from '@gili/modal/Modal';
import Checkbox from '@gili/primitives/Checkbox';
import Radio from '@gili/primitives/Radio';
import Switch from '@gili/primitives/Switch';
import styles from './PollModal.module.scss';
const MAX_OPTION_LENGTH = 100;
const MAX_QUESTION_LENGTH = 255;
const MAX_SOLUTION_LENGTH = 200;
const CLOSE_PERIOD_OPTIONS = [
HOUR,
3 * HOUR,
8 * HOUR,
DAY,
3 * DAY,
];
const ICON_COLORS = {
anonymous: '#0a84ff',
multiple: '#ffb300',
quiz: '#34c759',
addAnswers: '#2faeff',
revote: '#6a5cff',
shuffle: '#b75bff',
duration: '#ff5e3a',
results: '#3a8cff',
} as const;
export type OwnProps = {
modal: NonNullable<TabState['pollModal']>;
isOpen: boolean;
};
type StateProps = {
chat?: ApiChat;
isChannel?: boolean;
pollMaxAnswers: number;
pollClosePeriodMax: number;
paidMessagesStars?: number;
isPaymentMessageConfirmDialogOpen: boolean;
starsBalance: number;
isStarsBalanceModalOpen: boolean;
isSilentPosting?: boolean;
};
type SettingRowProps = {
iconName?: IconName;
iconBackgroundColor?: string;
label: TeactNode;
description?: TeactNode;
checked: boolean;
disabled?: boolean;
locked?: boolean;
onChange: (checked: boolean) => void;
};
type ValueRowProps = {
iconName?: IconName;
iconBackgroundColor?: string;
label: TeactNode;
value: TeactNode;
onClick: (e: React.MouseEvent<HTMLElement>) => void;
};
type PollOption = {
id: string;
text: string;
};
const PollModal = ({
modal,
isOpen,
chat,
isChannel,
pollMaxAnswers,
pollClosePeriodMax,
paidMessagesStars,
isPaymentMessageConfirmDialogOpen,
starsBalance,
isStarsBalanceModalOpen,
isSilentPosting,
}: OwnProps & StateProps) => {
const {
closePollModal,
sendMessage,
} = getActions();
const lang = useLang();
const questionInputRef = useRef<HTMLInputElement>();
const mainButtonRef = useRef<HTMLButtonElement>();
const optionListRef = useRef<HTMLDivElement>();
const durationMenuRef = useRef<HTMLDivElement>();
const [question, setQuestion] = useState('');
const [description, setDescription] = useState('');
const [options, setOptions] = useState<PollOption[]>(() => [createPollOption()]);
const [isPublic, setIsPublic] = useState(true);
const [isMultipleAnswers, setIsMultipleAnswers] = useState(true);
const [isQuizMode, setIsQuizMode] = useState(Boolean(modal.isQuiz));
const [correctAnswerIds, setCorrectAnswerIds] = useState<string[]>([]);
const [solution, setSolution] = useState('');
const [canAddAnswers, setCanAddAnswers] = useState(true);
const [canRevote, setCanRevote] = useState(true);
const [shouldShuffleAnswers, setShouldShuffleAnswers] = useState(false);
const [closePeriod, setClosePeriod] = useState<number | undefined>();
const [closeDate, setCloseDate] = useState<number | undefined>();
const [durationAnchorAt, setDurationAnchorAt] = useState(() => getServerTime());
const [shouldHideResultsUntilClose, setShouldHideResultsUntilClose] = useState(false);
const [hasSubmitted, setHasSubmitted] = useState(false);
const [isCloseDatePickerOpen, setIsCloseDatePickerOpen] = useState(false);
const [requestCalendar, calendar] = useSchedule();
const {
isContextMenuOpen: isCustomSendMenuOpen,
handleContextMenu,
handleContextMenuClose,
handleContextMenuHide,
} = useContextMenuHandlers(mainButtonRef, !isOpen || modal.messageListType === 'scheduled');
const {
isContextMenuOpen: isDurationMenuOpen,
handleContextMenu: handleDurationMenuOpen,
handleContextMenuClose: handleDurationMenuClose,
handleContextMenuHide: handleDurationMenuHide,
} = useContextMenuHandlers(durationMenuRef, !isOpen);
const {
closeConfirmDialog,
dialogHandler,
shouldAutoApprove,
setAutoApprove,
handleWithConfirmation,
} = usePaidMessageConfirmation(
paidMessagesStars || 0,
isStarsBalanceModalOpen,
starsBalance,
true,
);
useEffect(() => {
if (!isOpen) {
return;
}
questionInputRef.current?.focus();
}, [isOpen]);
useEffect(() => {
if (isChannel) {
setIsPublic(false);
setCanAddAnswers(false);
}
}, [isChannel]);
useEffect(() => {
if (isQuizMode || !isPublic) {
setCanAddAnswers(false);
}
}, [isPublic, isQuizMode]);
useEffect(() => {
if (closePeriod || closeDate) {
return;
}
setShouldHideResultsUntilClose(false);
}, [closeDate, closePeriod]);
useEffect(() => {
if (!isMultipleAnswers && correctAnswerIds.length > 1) {
setCorrectAnswerIds(correctAnswerIds.slice(0, 1));
}
}, [correctAnswerIds, isMultipleAnswers]);
const filledOptions = useMemo(() => {
return options.map((option) => ({
id: option.id,
text: option.text.trim().substring(0, MAX_OPTION_LENGTH),
})).filter(({ text }) => Boolean(text));
}, [options]);
const correctAnswerPositions = useMemo(() => {
return correctAnswerIds.reduce<number[]>((result, id) => {
const answerIndex = filledOptions.findIndex((option) => option.id === id);
if (answerIndex >= 0) {
result.push(answerIndex);
}
return result;
}, []);
}, [correctAnswerIds, filledOptions]);
const reorderableOptionIds = useMemo(() => {
return filledOptions.map(({ id }) => id);
}, [filledOptions]);
const trimmedQuestion = useMemo(() => question.trim().substring(0, MAX_QUESTION_LENGTH), [question]);
const isInScheduledList = modal.messageListType === 'scheduled';
const canSchedule = Boolean(!paidMessagesStars && !chat?.isMonoforum);
const isCorrectAnswerInvalid = hasSubmitted && isQuizMode && !correctAnswerPositions.length;
const isAddAnswersDisabled = isQuizMode || !isPublic;
const remainingOptionsCount = Math.max(pollMaxAnswers - filledOptions.length, 0);
const isSendDisabled = !trimmedQuestion
|| filledOptions.length < 1
|| (isQuizMode && !correctAnswerPositions.length);
const hasLimitedDuration = closePeriod !== undefined || closeDate !== undefined;
const closeDateLabel = closeDate !== undefined
? formatDateTimeToString(closeDate * 1000, lang.code, true)
: closePeriod !== undefined
? formatShortDuration(lang, closePeriod)
: lang('PollSelectCloseDate');
const maxCloseDateAt = (durationAnchorAt + pollClosePeriodMax) * 1000;
const closeDatePickerSelectedAt = closeDate !== undefined
? closeDate * 1000
: (durationAnchorAt + (closePeriod || DAY)) * 1000;
const messageList: MessageList = {
chatId: modal.chatId,
threadId: modal.threadId || MAIN_THREAD_ID,
type: modal.messageListType === 'scheduled' ? 'scheduled' : 'thread',
};
const handleClose = useLastCallback(() => {
closePollModal();
});
const handleReorderOptions = useLastCallback((optionIds: string[]) => {
setOptions((currentOptions) => {
const optionsById = new Map(currentOptions.map((option) => [option.id, option]));
const nextOptions = optionIds.reduce<PollOption[]>((result, id) => {
const option = optionsById.get(id);
if (option) {
result.push(option);
}
return result;
}, []);
return normalizeOptions(nextOptions, pollMaxAnswers);
});
});
const {
draggedId: draggedOptionId,
getRowProps: getReorderableRowProps,
getDragElementProps: getReorderableDragElementProps,
getHandleProps: getReorderableHandleProps,
getPlaceholderStyle: getReorderablePlaceholderStyle,
getDragStyle: getReorderableDragStyle,
} = useReorderableList({
itemIds: reorderableOptionIds,
withAutoscroll: true,
onReorder: handleReorderOptions,
});
const updateOption = useLastCallback((id: string, value: string) => {
const nextOptions = options.map((option) => (
option.id === id ? { ...option, text: value } : option
));
setOptions(normalizeOptions(nextOptions, pollMaxAnswers));
});
const handleRemoveOption = useLastCallback((id: string) => {
const nextOptions = normalizeOptions(options.filter((option) => option.id !== id), pollMaxAnswers);
setOptions(nextOptions);
setCorrectAnswerIds(correctAnswerIds.filter((correctAnswerId) => correctAnswerId !== id));
});
const handleToggleCorrectAnswer = useLastCallback((id: string) => {
if (!isMultipleAnswers) {
setCorrectAnswerIds([id]);
return;
}
setCorrectAnswerIds((prevCorrectAnswers) => (
prevCorrectAnswers.includes(id)
? prevCorrectAnswers.filter((currentId) => currentId !== id)
: [...prevCorrectAnswers, id]
));
});
const handleOptionKeyDown = useLastCallback((event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key !== 'Enter') {
return;
}
event.preventDefault();
if (!optionListRef.current) {
return;
}
const inputs = optionListRef.current.querySelectorAll<HTMLInputElement>(`.${styles.optionTextInput} input`);
const lastInput = inputs[inputs.length - 1];
if (!lastInput) {
return;
}
requestMeasure(() => {
lastInput.focus();
});
});
const handleQuestionChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setQuestion(e.currentTarget.value);
});
const handleDescriptionChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setDescription(e.currentTarget.value);
});
const handleSolutionChange = useLastCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
setSolution(e.currentTarget.value);
});
const handleQuizModeChange = useLastCallback((checked: boolean) => {
setIsQuizMode(checked);
});
const handleMultipleAnswersChange = useLastCallback((checked: boolean) => {
setIsMultipleAnswers(checked);
});
const handleLimitedDurationChange = useLastCallback((checked: boolean) => {
if (!checked) {
setClosePeriod(undefined);
setCloseDate(undefined);
setShouldHideResultsUntilClose(false);
return;
}
const nowAt = Math.floor(Date.now() / 1000);
setDurationAnchorAt(nowAt);
setClosePeriod(DAY);
setCloseDate(undefined);
});
const handleCloseDateSave = useLastCallback((date: Date) => {
setClosePeriod(undefined);
setCloseDate(Math.round(date.getTime() / 1000));
setIsCloseDatePickerOpen(false);
});
const handleCloseCloseDatePicker = useLastCallback(() => {
setIsCloseDatePickerOpen(false);
});
const handleOpenCloseDatePicker = useLastCallback(() => {
setDurationAnchorAt(Math.floor(Date.now() / 1000));
setIsCloseDatePickerOpen(true);
});
const handleSelectClosePeriod = useLastCallback((period: number) => {
setClosePeriod(period);
setCloseDate(undefined);
});
const buildPoll = useLastCallback((): ApiNewPoll | undefined => {
const normalizedOptions = normalizeOptions(
options.map((option) => ({
...option,
text: option.text.trim().substring(0, MAX_OPTION_LENGTH),
})),
pollMaxAnswers,
);
setQuestion(trimmedQuestion);
setOptions(normalizedOptions);
setHasSubmitted(true);
if (!trimmedQuestion || filledOptions.length < 1) {
return undefined;
}
if (isQuizMode && !correctAnswerPositions.length) {
return undefined;
}
const answers = filledOptions.map(({ text }, index) => ({
text: { text },
option: String(index),
}));
const payload: ApiNewPoll = {
summary: {
id: generateUniqueNumberId().toString(),
hash: '0',
question: {
text: trimmedQuestion,
},
answers,
isPublic: !isChannel && isPublic ? true : undefined,
isMultipleChoice: isMultipleAnswers ? true : undefined,
isQuiz: isQuizMode ? true : undefined,
canAddAnswers: !isChannel && isPublic && canAddAnswers ? true : undefined,
isRevoteDisabled: !canRevote ? true : undefined,
shouldShuffleAnswers: shouldShuffleAnswers ? true : undefined,
shouldHideResultsUntilClose: shouldHideResultsUntilClose ? true : undefined,
closePeriod,
closeDate,
isCreator: true,
},
correctAnswers: isQuizMode ? correctAnswerPositions : undefined,
solution: isQuizMode ? solution.trim().substring(0, MAX_SOLUTION_LENGTH) : undefined,
};
return payload;
});
const submitPoll = useLastCallback((
poll: ApiNewPoll,
isSilent?: boolean,
scheduledAt?: number,
scheduleRepeatPeriod?: number,
) => {
sendMessage({
messageList,
text: description,
poll,
isSilent: scheduledAt ? undefined : (isSilent || isSilentPosting),
scheduledAt,
scheduleRepeatPeriod,
});
closePollModal();
});
const handleSendNow = useLastCallback((isSilent?: boolean) => {
const poll = buildPoll();
if (!poll) {
return;
}
handleWithConfirmation(submitPoll, poll, isSilent);
});
const handleSendScheduled = useLastCallback((scheduledAt: number, scheduleRepeatPeriod?: number) => {
const poll = buildPoll();
if (!poll) {
return;
}
handleWithConfirmation(submitPoll, poll, undefined, scheduledAt, scheduleRepeatPeriod);
});
const handlePrimarySend = useLastCallback(() => {
if (isInScheduledList) {
requestCalendar(handleSendScheduled);
return;
}
handleSendNow();
});
const handleSilentSend = useLastCallback(() => {
handleSendNow(true);
});
const handleScheduleSend = useLastCallback(() => {
requestCalendar(handleSendScheduled);
});
const renderHeader = useMemo(() => (
<ModalHeader>
<ModalCloseButton />
<ModalTitle>{lang('NewPoll')}</ModalTitle>
<ModalHeaderAction>
<Button
ref={mainButtonRef}
color="primary"
pill
disabled={isSendDisabled}
noForcedUpperCase
size="smaller"
onClick={handlePrimarySend}
onContextMenu={!isInScheduledList ? handleContextMenu : undefined}
>
{lang('Send')}
</Button>
{!isInScheduledList && (
<CustomSendMenu
isOpen={isCustomSendMenuOpen}
canSchedule={canSchedule}
onSendSilent={handleSilentSend}
onSendSchedule={handleScheduleSend}
onClose={handleContextMenuClose}
onCloseAnimationEnd={handleContextMenuHide}
/>
)}
</ModalHeaderAction>
</ModalHeader>
), [canSchedule, handleContextMenu, handleContextMenuClose, handleContextMenuHide,
isCustomSendMenuOpen, isInScheduledList, isSendDisabled, lang]);
return (
<>
<Modal
isOpen={isOpen}
onClose={handleClose}
header={renderHeader}
ariaLabel={lang('NewPoll')}
width="slim"
>
<IslandTitle>{lang('PollModalQuestionTitle')}</IslandTitle>
<Island>
<InputText
ref={questionInputRef}
className={styles.input}
label={lang('AskAQuestion')}
value={question}
maxLength={MAX_QUESTION_LENGTH}
error={hasSubmitted && !trimmedQuestion ? lang('PollsChooseQuestion') : undefined}
onChange={handleQuestionChange}
/>
<InputText
className={styles.input}
label={lang('DescriptionOptionalPlaceholder')}
value={description}
onChange={handleDescriptionChange}
/>
</Island>
<IslandTitle>{lang('PollModalOptionsTitle')}</IslandTitle>
<Island ref={optionListRef} className={styles.optionList} teactFastList>
{options.map((option, index) => {
const isFilledOption = Boolean(option.text.trim());
const isAddOptionRow = !isFilledOption && index === options.length - 1;
const isCorrectAnswerChecked = correctAnswerIds.includes(option.id);
const shouldShowRemoveButton = options.length > 1 && !isAddOptionRow;
const shouldShowOptionError = hasSubmitted && !filledOptions.length && index === 0;
const rowProps = isFilledOption ? getReorderableRowProps(option.id) : undefined;
const handleProps = isFilledOption ? getReorderableHandleProps(option.id) : undefined;
const dragElementProps = isFilledOption ? getReorderableDragElementProps(option.id) : undefined;
const placeholderStyle = isFilledOption ? getReorderablePlaceholderStyle(option.id) : undefined;
const dragStyle = isFilledOption ? getReorderableDragStyle(option.id) : undefined;
return (
<div
key={option.id}
ref={rowProps?.ref}
className={styles.optionRowFrame}
style={placeholderStyle}
>
<div
ref={dragElementProps?.ref}
style={dragStyle}
className={buildClassName(
styles.optionRow,
isAddOptionRow && styles.optionRowAdd,
draggedOptionId === option.id && styles.optionRowDragging,
)}
>
<div
className={buildClassName(
styles.optionLeadingIcon,
isAddOptionRow && styles.optionLeadingIconAdd,
isFilledOption && styles.optionDragHandle,
)}
role={handleProps?.role}
tabIndex={handleProps?.tabIndex}
aria-label={isFilledOption ? lang('DragToSortAria') : undefined}
onMouseDown={handleProps?.onMouseDown}
onTouchStart={handleProps?.onTouchStart}
onKeyDown={handleProps?.onKeyDown}
ref={handleProps?.ref}
>
<Icon
name={isAddOptionRow ? 'add' : 'sort'}
className={styles.optionLeadingIconGlyph}
/>
</div>
{isQuizMode && (
<div className={styles.optionSelector}>
{isMultipleAnswers ? (
<Checkbox
checked={isCorrectAnswerChecked}
disabled={isAddOptionRow}
isInvalid={isCorrectAnswerInvalid && !isCorrectAnswerChecked && !isAddOptionRow}
onChange={() => handleToggleCorrectAnswer(option.id)}
/>
) : (
<Radio
value={option.id}
checked={isCorrectAnswerChecked}
disabled={isAddOptionRow}
className={isCorrectAnswerInvalid && !isCorrectAnswerChecked && !isAddOptionRow
? styles.optionRadioInvalid
: undefined}
onChange={() => handleToggleCorrectAnswer(option.id)}
/>
)}
</div>
)}
<InputText
className={buildClassName(
styles.optionInput,
styles.optionTextInput,
isAddOptionRow && styles.optionInputAdd,
)}
placeholder={isAddOptionRow ? lang('CreatePollAddOption') : lang('OptionHint')}
value={option.text}
maxLength={MAX_OPTION_LENGTH}
error={shouldShowOptionError ? lang('PollsChooseAnswers') : undefined}
onChange={(e) => updateOption(option.id, e.currentTarget.value)}
onKeyDown={handleOptionKeyDown}
/>
{shouldShowRemoveButton && (
<Button
round
size="tiny"
color="translucent"
className={styles.optionRemove}
ariaLabel={lang('Delete')}
iconName="close"
onClick={() => handleRemoveOption(option.id)}
/>
)}
</div>
</div>
);
})}
{isCorrectAnswerInvalid && (
<IslandDescription key="correct-answer-error" className={styles.errorDescription}>
{lang('PollsChooseCorrect')}
</IslandDescription>
)}
</Island>
<IslandDescription>
{remainingOptionsCount > 0 ? (
lang('PollModalAddMoreText', { count: remainingOptionsCount }, { pluralValue: remainingOptionsCount })
) : lang('PollModalAddNoMore')}
</IslandDescription>
<IslandTitle>{lang('PollModalSettingsTitle')}</IslandTitle>
<Island>
{!isChannel && (
<SettingRow
iconName="eye"
iconBackgroundColor={ICON_COLORS.anonymous}
label={lang('PollAnswersVisible')}
description={lang('PollAnswersVisibleDescription')}
checked={isPublic}
onChange={setIsPublic}
/>
)}
<SettingRow
iconName="choice-selected"
iconBackgroundColor={ICON_COLORS.multiple}
label={lang('PollMultiple')}
description={lang('PollMultipleDescription')}
checked={isMultipleAnswers}
onChange={handleMultipleAnswersChange}
/>
{!isChannel && (
<SettingRow
iconName="add-filled"
iconBackgroundColor={ICON_COLORS.addAnswers}
label={lang('PollAllowAddingAnswers')}
description={lang('PollAllowAddingAnswersDescription')}
checked={canAddAnswers}
disabled={isAddAnswersDisabled}
locked={isAddAnswersDisabled}
onChange={setCanAddAnswers}
/>
)}
<SettingRow
iconName="reload"
iconBackgroundColor={ICON_COLORS.revote}
label={lang('PollAllowVoteChanges')}
description={lang('PollAllowVoteChangesDescription')}
checked={canRevote}
onChange={setCanRevote}
/>
<SettingRow
iconName="replace-round"
iconBackgroundColor={ICON_COLORS.shuffle}
label={lang('PollRandomOrder')}
description={lang('PollRandomOrderDescription')}
checked={shouldShuffleAnswers}
onChange={setShouldShuffleAnswers}
/>
<SettingRow
iconName="check-filled"
iconBackgroundColor={ICON_COLORS.quiz}
label={lang('PollQuiz')}
description={lang('PollQuizDescription')}
checked={isQuizMode}
onChange={handleQuizModeChange}
/>
<SettingRow
iconName="timer-filled"
iconBackgroundColor={ICON_COLORS.duration}
label={lang('PollLimitedDuration')}
description={lang('PollLimitedDurationDescription')}
checked={hasLimitedDuration}
onChange={handleLimitedDurationChange}
/>
{hasLimitedDuration ? (
<>
<div ref={durationMenuRef} className={styles.durationMenuWrapper}>
<ValueRow
label={lang('PollDuration')}
value={closeDateLabel}
onClick={handleDurationMenuOpen}
/>
<Menu
isOpen={isDurationMenuOpen}
className={buildClassName('with-menu-transitions', styles.durationMenu)}
positionX="right"
positionY="bottom"
autoClose
onClose={handleDurationMenuClose}
onCloseAnimationEnd={handleDurationMenuHide}
>
{CLOSE_PERIOD_OPTIONS.map((period) => (
<MenuItem
key={period}
disabled={period > pollClosePeriodMax}
onClick={() => handleSelectClosePeriod(period)}
>
{formatShortDuration(lang, period)}
</MenuItem>
))}
<MenuItem onClick={handleOpenCloseDatePicker}>
{lang('PollDurationOther')}
</MenuItem>
</Menu>
</div>
<SettingRow
label={lang('PollHideResultsUntilClose')}
checked={shouldHideResultsUntilClose}
onChange={setShouldHideResultsUntilClose}
/>
</>
) : undefined}
</Island>
{isQuizMode && (
<>
<IslandTitle>{lang('PollsSolutionTitle')}</IslandTitle>
<Island>
<TextArea
className={styles.textArea}
value={solution}
label={lang('PollsSolutionTitle')}
maxLength={MAX_SOLUTION_LENGTH}
noReplaceNewlines
onChange={handleSolutionChange}
/>
</Island>
<IslandDescription>{lang('CreatePollExplanationInfo')}</IslandDescription>
</>
)}
</Modal>
{calendar}
<CalendarModal
isOpen={isCloseDatePickerOpen}
selectedAt={closeDatePickerSelectedAt}
maxAt={maxCloseDateAt}
isFutureMode
withTimePicker
submitButtonLabel={lang('Save')}
onClose={handleCloseCloseDatePicker}
onSubmit={handleCloseDateSave}
/>
<PaymentMessageConfirmDialog
isOpen={isPaymentMessageConfirmDialogOpen}
onClose={closeConfirmDialog}
userName={chat ? getPeerTitle(lang, chat) : undefined}
messagePriceInStars={paidMessagesStars || 0}
messagesCount={1}
shouldAutoApprove={shouldAutoApprove}
setAutoApprove={setAutoApprove}
confirmHandler={dialogHandler}
/>
</>
);
};
const SettingRow = ({
iconName,
iconBackgroundColor,
label,
description,
checked,
disabled,
locked,
onChange,
}: SettingRowProps) => {
return (
<Interactive asLabel clickable disabled={disabled}>
<Control inputEnd>
<Switch checked={checked} disabled={disabled} locked={locked} onChange={onChange} />
<ControlIcon iconName={iconName} backgroundColor={iconBackgroundColor} />
<ControlLabel>{label}</ControlLabel>
{description !== undefined ? (
<ControlDescription>{description}</ControlDescription>
) : undefined}
</Control>
</Interactive>
);
};
const ValueRow = ({
iconName,
iconBackgroundColor,
label,
value,
onClick,
}: ValueRowProps) => {
return (
<Interactive clickable onClick={onClick}>
<Control>
<ControlIcon iconName={iconName} backgroundColor={iconBackgroundColor} />
<ControlLabel>{label}</ControlLabel>
<ControlAfter className={styles.rowValue}>
{value}
</ControlAfter>
</Control>
</Interactive>
);
};
function createPollOption(text = ''): PollOption {
return {
id: generateUniqueNumberId().toString(),
text,
};
}
function normalizeOptions(options: PollOption[], maxOptionsCount: number) {
const nextOptions = [...options];
while (
nextOptions.length > 1
&& !nextOptions[nextOptions.length - 1].text.trim()
&& !nextOptions[nextOptions.length - 2].text.trim()
) {
nextOptions.pop();
}
if (!nextOptions.length) {
nextOptions.push(createPollOption());
}
if (nextOptions.length < maxOptionsCount && nextOptions[nextOptions.length - 1].text.trim()) {
nextOptions.push(createPollOption());
}
return nextOptions;
}
export default memo(withGlobal<OwnProps>(
(global, { modal }): Complete<StateProps> => {
const tabState = selectTabState(global);
const { chatId } = modal;
const chat = selectChat(global, chatId);
return {
chat,
isChannel: chat ? isChatChannel(chat) : undefined,
pollMaxAnswers: global.appConfig.pollMaxAnswers,
pollClosePeriodMax: global.appConfig.pollClosePeriodMax,
paidMessagesStars: selectPeerPaidMessagesStars(global, chatId),
isPaymentMessageConfirmDialogOpen: tabState.isPaymentMessageConfirmDialogOpen,
starsBalance: global.stars?.balance.amount || 0,
isStarsBalanceModalOpen: Boolean(tabState.starsBalanceModal),
isSilentPosting: chat ? getChatNotifySettings(
chat,
selectNotifyDefaults(global),
selectNotifyException(global, chat.id),
)?.isSilentPosting : undefined,
};
},
)(PollModal));