import React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { addPollOption, changePollOption, changePollSettings, clearComposeSuggestions, fetchComposeSuggestions, removePoll, removePollOption, selectComposeSuggestion } from 'soapbox/actions/compose'; import AutosuggestInput from 'soapbox/components/autosuggest-input'; import { Button, Divider, HStack, Stack, Text, Toggle } from 'soapbox/components/ui'; import { useAppDispatch, useCompose, useInstance } from 'soapbox/hooks'; import DurationSelector from './duration-selector'; import type { Map as ImmutableMap } from 'immutable'; import type { AutoSuggestion } from 'soapbox/components/autosuggest-input'; const messages = defineMessages({ option_placeholder: { id: 'compose_form.poll.option_placeholder', defaultMessage: 'Answer #{number}' }, add_option: { id: 'compose_form.poll.add_option', defaultMessage: 'Add an answer' }, pollDuration: { id: 'compose_form.poll.duration', defaultMessage: 'Duration' }, removePoll: { id: 'compose_form.poll.remove', defaultMessage: 'Remove poll' }, switchToMultiple: { id: 'compose_form.poll.switch_to_multiple', defaultMessage: 'Change poll to allow multiple answers' }, switchToSingle: { id: 'compose_form.poll.switch_to_single', defaultMessage: 'Change poll to allow for a single answer' }, minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' }, hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' }, days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' }, multiSelect: { id: 'compose_form.poll.multiselect', defaultMessage: 'Multi-Select' }, multiSelectDetail: { id: 'compose_form.poll.multiselect_detail', defaultMessage: 'Allow users to select multiple answers' }, }); interface IOption { composeId: string index: number maxChars: number numOptions: number onChange(index: number, value: string): void onRemove(index: number): void onRemovePoll(): void title: string } const Option: React.FC<IOption> = ({ composeId, index, maxChars, numOptions, onChange, onRemove, onRemovePoll, title, }) => { const dispatch = useAppDispatch(); const intl = useIntl(); const suggestions = useCompose(composeId).suggestions; const handleOptionTitleChange = (event: React.ChangeEvent<HTMLInputElement>) => onChange(index, event.target.value); const handleOptionRemove = () => { if (numOptions > 2) { onRemove(index); } else { onRemovePoll(); } }; const onSuggestionsClearRequested = () => dispatch(clearComposeSuggestions(composeId)); const onSuggestionsFetchRequested = (token: string) => dispatch(fetchComposeSuggestions(composeId, token)); const onSuggestionSelected = (tokenStart: number, token: string | null, value: AutoSuggestion) => { if (token && typeof token === 'string') { dispatch(selectComposeSuggestion(composeId, tokenStart, token, value, ['poll', 'options', index])); } }; return ( <HStack alignItems='center' justifyContent='between' space={4}> <HStack alignItems='center' space={2} grow> <div className='w-6'> <Text weight='bold'>{index + 1}.</Text> </div> <AutosuggestInput className='rounded-md !bg-transparent dark:!bg-transparent' placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })} maxLength={maxChars} value={title} onChange={handleOptionTitleChange} suggestions={suggestions} onSuggestionsFetchRequested={onSuggestionsFetchRequested} onSuggestionsClearRequested={onSuggestionsClearRequested} onSuggestionSelected={onSuggestionSelected} searchTokens={[':']} autoFocus={index === 0 || index >= 2} /> </HStack> {index > 1 && ( <div> <Button theme='danger' size='sm' onClick={handleOptionRemove}> <FormattedMessage id='compose_form.poll.remove_option' defaultMessage='Delete' /> </Button> </div> )} </HStack> ); }; interface IPollForm { composeId: string } const PollForm: React.FC<IPollForm> = ({ composeId }) => { const dispatch = useAppDispatch(); const intl = useIntl(); const { configuration } = useInstance(); const compose = useCompose(composeId); const pollLimits = configuration.get('polls') as ImmutableMap<string, number>; const options = compose.poll?.options; const expiresIn = compose.poll?.expires_in; const isMultiple = compose.poll?.multiple; const maxOptions = pollLimits.get('max_options') as number; const maxOptionChars = pollLimits.get('max_characters_per_option') as number; const onRemoveOption = (index: number) => dispatch(removePollOption(composeId, index)); const onChangeOption = (index: number, title: string) => dispatch(changePollOption(composeId, index, title)); const handleAddOption = () => dispatch(addPollOption(composeId, '')); const onChangeSettings = (expiresIn: string | number | undefined, isMultiple?: boolean) => dispatch(changePollSettings(composeId, expiresIn, isMultiple)); const handleSelectDuration = (value: number) => onChangeSettings(value, isMultiple); const handleToggleMultiple = () => onChangeSettings(expiresIn, !isMultiple); const onRemovePoll = () => dispatch(removePoll(composeId)); if (!options) { return null; } return ( <Stack space={4}> <Stack space={2}> {options.map((title: string, i: number) => ( <Option composeId={composeId} title={title} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} maxChars={maxOptionChars} numOptions={options.size} onRemovePoll={onRemovePoll} /> ))} <HStack space={2}> <div className='w-6' /> {options.size < maxOptions && ( <Button theme='secondary' onClick={handleAddOption} size='sm' block > <FormattedMessage {...messages.add_option} /> </Button> )} </HStack> </Stack> <Divider /> <button type='button' onClick={handleToggleMultiple} className='text-start'> <HStack alignItems='center' justifyContent='between'> <Stack> <Text weight='medium'> {intl.formatMessage(messages.multiSelect)} </Text> <Text theme='muted' size='sm'> {intl.formatMessage(messages.multiSelectDetail)} </Text> </Stack> <Toggle checked={isMultiple} onChange={handleToggleMultiple} /> </HStack> </button> <Divider /> {/* Duration */} <Stack space={2}> <Text weight='medium'> {intl.formatMessage(messages.pollDuration)} </Text> <DurationSelector onDurationChange={handleSelectDuration} /> </Stack> {/* Remove Poll */} <div className='text-center'> <button type='button' className='text-danger-500' onClick={onRemovePoll}> {intl.formatMessage(messages.removePoll)} </button> </div> </Stack> ); }; export default PollForm;