From e79f016d330085b6e2bc463e8e8c329449ae5488 Mon Sep 17 00:00:00 2001 From: Renaud Chaput Date: Tue, 2 Apr 2024 16:18:14 +0200 Subject: [PATCH 1/2] Convert `` to Typescript --- app/javascript/mastodon/api_types/polls.ts | 2 +- app/javascript/mastodon/components/poll.jsx | 248 ------------- app/javascript/mastodon/components/poll.tsx | 333 ++++++++++++++++++ .../features/ui/util/optional_motion.js | 2 +- .../features/ui/util/reduced_motion.jsx | 2 +- 5 files changed, 336 insertions(+), 251 deletions(-) delete mode 100644 app/javascript/mastodon/components/poll.jsx create mode 100644 app/javascript/mastodon/components/poll.tsx diff --git a/app/javascript/mastodon/api_types/polls.ts b/app/javascript/mastodon/api_types/polls.ts index 275ca29fd7..d7c5907736 100644 --- a/app/javascript/mastodon/api_types/polls.ts +++ b/app/javascript/mastodon/api_types/polls.ts @@ -13,7 +13,7 @@ export interface ApiPollJSON { expired: boolean; multiple: boolean; votes_count: number; - voters_count: number; + voters_count: null | number; options: ApiPollOptionJSON[]; emojis: ApiCustomEmojiJSON[]; diff --git a/app/javascript/mastodon/components/poll.jsx b/app/javascript/mastodon/components/poll.jsx deleted file mode 100644 index 1326131009..0000000000 --- a/app/javascript/mastodon/components/poll.jsx +++ /dev/null @@ -1,248 +0,0 @@ -import PropTypes from 'prop-types'; - -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; - -import classNames from 'classnames'; - -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -import escapeTextContentForBrowser from 'escape-html'; -import spring from 'react-motion/lib/spring'; - -import CheckIcon from '@/material-icons/400-24px/check.svg?react'; -import { Icon } from 'mastodon/components/icon'; -import emojify from 'mastodon/features/emoji/emoji'; -import Motion from 'mastodon/features/ui/util/optional_motion'; -import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; - -import { RelativeTimestamp } from './relative_timestamp'; - -const messages = defineMessages({ - closed: { - id: 'poll.closed', - defaultMessage: 'Closed', - }, - voted: { - id: 'poll.voted', - defaultMessage: 'You voted for this answer', - }, - votes: { - id: 'poll.votes', - defaultMessage: '{votes, plural, one {# vote} other {# votes}}', - }, -}); - -class Poll extends ImmutablePureComponent { - static propTypes = { - identity: identityContextPropShape, - poll: ImmutablePropTypes.record.isRequired, - status: ImmutablePropTypes.map.isRequired, - lang: PropTypes.string, - intl: PropTypes.object.isRequired, - disabled: PropTypes.bool, - refresh: PropTypes.func, - onVote: PropTypes.func, - onInteractionModal: PropTypes.func, - }; - - state = { - selected: {}, - expired: null, - }; - - static getDerivedStateFromProps (props, state) { - const { poll } = props; - const expires_at = poll.get('expires_at'); - const expired = poll.get('expired') || expires_at !== null && (new Date(expires_at)).getTime() < Date.now(); - return (expired === state.expired) ? null : { expired }; - } - - componentDidMount () { - this._setupTimer(); - } - - componentDidUpdate () { - this._setupTimer(); - } - - componentWillUnmount () { - clearTimeout(this._timer); - } - - _setupTimer () { - const { poll } = this.props; - clearTimeout(this._timer); - if (!this.state.expired) { - const delay = (new Date(poll.get('expires_at'))).getTime() - Date.now(); - this._timer = setTimeout(() => { - this.setState({ expired: true }); - }, delay); - } - } - - _toggleOption = value => { - if (this.props.poll.get('multiple')) { - const tmp = { ...this.state.selected }; - if (tmp[value]) { - delete tmp[value]; - } else { - tmp[value] = true; - } - this.setState({ selected: tmp }); - } else { - const tmp = {}; - tmp[value] = true; - this.setState({ selected: tmp }); - } - }; - - handleOptionChange = ({ target: { value } }) => { - this._toggleOption(value); - }; - - handleOptionKeyPress = (e) => { - if (e.key === 'Enter' || e.key === ' ') { - this._toggleOption(e.target.getAttribute('data-index')); - e.stopPropagation(); - e.preventDefault(); - } - }; - - handleVote = () => { - if (this.props.disabled) { - return; - } - - if (this.props.identity.signedIn) { - this.props.onVote(Object.keys(this.state.selected)); - } else { - this.props.onInteractionModal('vote', this.props.status); - } - }; - - handleRefresh = () => { - if (this.props.disabled) { - return; - } - - this.props.refresh(); - }; - - handleReveal = () => { - this.setState({ revealed: true }); - }; - - renderOption (option, optionIndex, showResults) { - const { poll, lang, disabled, intl } = this.props; - const pollVotesCount = poll.get('voters_count') || poll.get('votes_count'); - const percent = pollVotesCount === 0 ? 0 : (option.get('votes_count') / pollVotesCount) * 100; - const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') >= other.get('votes_count')); - const active = !!this.state.selected[`${optionIndex}`]; - const voted = option.get('voted') || (poll.get('own_votes') && poll.get('own_votes').includes(optionIndex)); - - const title = option.getIn(['translation', 'title']) || option.get('title'); - let titleHtml = option.getIn(['translation', 'titleHtml']) || option.get('titleHtml'); - - if (!titleHtml) { - const emojiMap = emojiMap(poll); - titleHtml = emojify(escapeTextContentForBrowser(title), emojiMap); - } - - return ( -
  • - - - {showResults && ( - - {({ width }) => - - } - - )} -
  • - ); - } - - render () { - const { poll, intl } = this.props; - const { revealed, expired } = this.state; - - if (!poll) { - return null; - } - - const timeRemaining = expired ? intl.formatMessage(messages.closed) : ; - const showResults = poll.get('voted') || revealed || expired; - const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item); - - let votesCount = null; - - if (poll.get('voters_count') !== null && poll.get('voters_count') !== undefined) { - votesCount = ; - } else { - votesCount = ; - } - - return ( -
    -
      - {poll.get('options').map((option, i) => this.renderOption(option, i, showResults))} -
    - -
    - {!showResults && } - {!showResults && <> · } - {showResults && !this.props.disabled && <> · } - {votesCount} - {poll.get('expires_at') && <> · {timeRemaining}} -
    -
    - ); - } - -} - -export default injectIntl(withIdentity(Poll)); diff --git a/app/javascript/mastodon/components/poll.tsx b/app/javascript/mastodon/components/poll.tsx new file mode 100644 index 0000000000..a3664a78d1 --- /dev/null +++ b/app/javascript/mastodon/components/poll.tsx @@ -0,0 +1,333 @@ +import { useCallback, useEffect, useState } from 'react'; + +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; + +import classNames from 'classnames'; + +import { spring } from 'react-motion'; + +import CheckIcon from '@/material-icons/400-24px/check.svg?react'; +import { Icon } from 'mastodon/components/icon'; +import Motion from 'mastodon/features/ui/util/optional_motion'; +import { useIdentity } from 'mastodon/identity_context'; +import type { + Poll as PollModel, + PollOption as PollOptionModel, +} from 'mastodon/models/poll'; +import type { Status } from 'mastodon/models/status'; + +import { RelativeTimestamp } from './relative_timestamp'; + +const messages = defineMessages({ + closed: { + id: 'poll.closed', + defaultMessage: 'Closed', + }, + voted: { + id: 'poll.voted', + defaultMessage: 'You voted for this answer', + }, + votes: { + id: 'poll.votes', + defaultMessage: '{votes, plural, one {# vote} other {# votes}}', + }, +}); + +export const PollOption: React.FC<{ + option: PollOptionModel; + optionIndex: number; + showResults: boolean; + percent: number; + voted: boolean; + leading: boolean; + multiple: boolean; + lang: string; + disabled: boolean; + active: boolean; + toggleOption: () => void; +}> = ({ + option, + optionIndex, + percent, + leading, + voted, + multiple, + showResults, + lang, + disabled, + active, + toggleOption, +}) => { + const intl = useIntl(); + const title = option.translation?.title ?? option.title; + const titleHtml = option.translation?.titleHtml ?? option.titleHtml; + + const handleOptionKeyPress = useCallback< + React.KeyboardEventHandler + >( + (e) => { + if (e.key === 'Enter' || e.key === ' ') { + toggleOption(); + e.stopPropagation(); + e.preventDefault(); + } + }, + [toggleOption], + ); + + return ( +
  • + + + {showResults && ( + + {({ width }) => ( + + )} + + )} +
  • + ); +}; + +export const Poll: React.FC<{ + poll: PollModel; + status: Status; + lang: string; + disabled?: boolean; + refresh?: () => void; + onVote: (votes: string[]) => void; + onInteractionModal: (interactionType: string, status: Status) => void; +}> = ({ + poll, + lang, + disabled, + refresh, + onVote, + onInteractionModal, + status, +}) => { + const intl = useIntl(); + + const expires_at = poll.expires_at; + const [expired, setExpired] = useState( + poll.expired || + (!!expires_at && new Date(expires_at).getTime() < Date.now()), + ); + + const [revealed, setRevealed] = useState(false); + + const handleReveal = useCallback(() => { + setRevealed(true); + }, []); + + const [selected, setSelected] = useState>(new Set()); + + const toggleOption = useCallback( + (option: string) => { + setSelected((prev) => { + const next = new Set(prev); + + if (poll.multiple) { + if (next.has(option)) next.delete(option); + else next.add(option); + } else { + next.add(option); + } + return next; + }); + }, + [poll.multiple], + ); + + const makeToggleOption = (option: string) => () => { + toggleOption(option); + }; + + const { signedIn } = useIdentity(); + + const handleRefresh = useCallback(() => { + if (disabled) { + return; + } + + refresh?.(); + }, [refresh, disabled]); + + const handleVote = useCallback(() => { + if (disabled) { + return; + } + + if (signedIn) { + onVote(Array.from(selected)); + } else { + onInteractionModal('vote', status); + } + + onVote(Array.from(selected)); + }, [disabled, onVote, selected, signedIn, onInteractionModal, status]); + + useEffect(() => { + if (expired || !expires_at) return () => undefined; + + const delay = new Date(expires_at).getTime() - Date.now(); + const timer = setTimeout(() => { + setExpired(true); + }, delay); + + return () => { + clearTimeout(timer); + }; + }, [expired, expires_at]); + + const timeRemaining = + expired || !expires_at ? ( + intl.formatMessage(messages.closed) + ) : ( + + ); + const showResults = poll.voted || revealed || expired; + + let votesCount = null; + + if (poll.voters_count) { + votesCount = ( + + ); + } else { + votesCount = ( + + ); + } + + return ( +
    +
      + {poll.options.map((option, i) => { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- we want `votes_count` if `voters_count` is 0 + const pollVotesCount = poll.voters_count || poll.votes_count; + const percent = + pollVotesCount === 0 + ? 0 + : (option.votes_count / pollVotesCount) * 100; + + return ( + other.title === option.title) + .every((other) => option.votes_count >= other.votes_count)} + percent={percent} + disabled={disabled || selected.size === 0} + /> + ); + })} +
    + +
    + {!showResults && ( + + )} + {!showResults && ( + <> + {' '} + ·{' '} + + )} + {showResults && !disabled && ( + <> + {' '} + ·{' '} + + )} + {votesCount} + {poll.expires_at && <> · {timeRemaining}} +
    +
    + ); +}; diff --git a/app/javascript/mastodon/features/ui/util/optional_motion.js b/app/javascript/mastodon/features/ui/util/optional_motion.js index 0b6d4d97f7..5d29eb45f6 100644 --- a/app/javascript/mastodon/features/ui/util/optional_motion.js +++ b/app/javascript/mastodon/features/ui/util/optional_motion.js @@ -1,4 +1,4 @@ -import Motion from 'react-motion/lib/Motion'; +import { Motion } from 'react-motion'; import { reduceMotion } from '../../../initial_state'; diff --git a/app/javascript/mastodon/features/ui/util/reduced_motion.jsx b/app/javascript/mastodon/features/ui/util/reduced_motion.jsx index fd044497f8..52add28945 100644 --- a/app/javascript/mastodon/features/ui/util/reduced_motion.jsx +++ b/app/javascript/mastodon/features/ui/util/reduced_motion.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { Component } from 'react'; -import Motion from 'react-motion/lib/Motion'; +import { Motion } from 'react-motion'; const stylesToKeep = ['opacity', 'backgroundOpacity']; From 90cd9056905dff63c19082175dc4cf369a8ce49c Mon Sep 17 00:00:00 2001 From: Renaud Chaput Date: Sun, 26 May 2024 22:00:28 +0200 Subject: [PATCH 2/2] Convert the polls state to plain JS --- .../mastodon/actions/importer/index.js | 2 +- app/javascript/mastodon/actions/polls.ts | 2 +- app/javascript/mastodon/components/poll.tsx | 10 +- .../mastodon/containers/media_container.jsx | 5 +- .../mastodon/containers/poll_container.js | 4 +- app/javascript/mastodon/models/poll.ts | 95 +++++++------------ app/javascript/mastodon/reducers/polls.ts | 60 +++++------- 7 files changed, 70 insertions(+), 108 deletions(-) diff --git a/app/javascript/mastodon/actions/importer/index.js b/app/javascript/mastodon/actions/importer/index.js index 047cf11910..a527043940 100644 --- a/app/javascript/mastodon/actions/importer/index.js +++ b/app/javascript/mastodon/actions/importer/index.js @@ -71,7 +71,7 @@ export function importFetchedStatuses(statuses) { } if (status.poll?.id) { - pushUnique(polls, createPollFromServerJSON(status.poll, getState().polls.get(status.poll.id))); + pushUnique(polls, createPollFromServerJSON(status.poll, getState().polls[status.poll.id])); } if (status.card) { diff --git a/app/javascript/mastodon/actions/polls.ts b/app/javascript/mastodon/actions/polls.ts index 28f729394b..65a96e8f62 100644 --- a/app/javascript/mastodon/actions/polls.ts +++ b/app/javascript/mastodon/actions/polls.ts @@ -15,7 +15,7 @@ export const importFetchedPoll = createAppAsyncThunk( dispatch( importPolls({ - polls: [createPollFromServerJSON(poll, getState().polls.get(poll.id))], + polls: [createPollFromServerJSON(poll, getState().polls[poll.id])], }), ); }, diff --git a/app/javascript/mastodon/components/poll.tsx b/app/javascript/mastodon/components/poll.tsx index a3664a78d1..e78f106d3c 100644 --- a/app/javascript/mastodon/components/poll.tsx +++ b/app/javascript/mastodon/components/poll.tsx @@ -154,7 +154,7 @@ export const Poll: React.FC<{ lang: string; disabled?: boolean; refresh?: () => void; - onVote: (votes: string[]) => void; + onVote?: (votes: string[]) => void; onInteractionModal: (interactionType: string, status: Status) => void; }> = ({ poll, @@ -218,13 +218,11 @@ export const Poll: React.FC<{ } if (signedIn) { - onVote(Array.from(selected)); + onVote?.(Array.from(selected)); } else { onInteractionModal('vote', status); } - - onVote(Array.from(selected)); - }, [disabled, onVote, selected, signedIn, onInteractionModal, status]); + }, [disabled, onVote, selected, signedIn, status, onInteractionModal]); useEffect(() => { if (expired || !expires_at) return () => undefined; @@ -290,7 +288,7 @@ export const Poll: React.FC<{ multiple={poll.multiple} voted={option.voted || poll.own_votes?.includes(i) || false} leading={poll.options - .filterNot((other) => other.title === option.title) + .filter((other) => other.title !== option.title) .every((other) => option.votes_count >= other.votes_count)} percent={percent} disabled={disabled || selected.size === 0} diff --git a/app/javascript/mastodon/containers/media_container.jsx b/app/javascript/mastodon/containers/media_container.jsx index d18602e3b5..6d6a46131a 100644 --- a/app/javascript/mastodon/containers/media_container.jsx +++ b/app/javascript/mastodon/containers/media_container.jsx @@ -7,12 +7,13 @@ import { fromJS } from 'immutable'; import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag'; import MediaGallery from 'mastodon/components/media_gallery'; import ModalRoot from 'mastodon/components/modal_root'; -import Poll from 'mastodon/components/poll'; +import { Poll } from 'mastodon/components/poll'; import Audio from 'mastodon/features/audio'; import Card from 'mastodon/features/status/components/card'; import MediaModal from 'mastodon/features/ui/components/media_modal'; import Video from 'mastodon/features/video'; import { IntlProvider } from 'mastodon/locales'; +import { createPollFromServerJSON } from 'mastodon/models/poll'; import { getScrollbarWidth } from 'mastodon/utils/scrollbar'; const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll, Hashtag, Audio }; @@ -88,7 +89,7 @@ export default class MediaContainer extends PureComponent { Object.assign(props, { ...(media ? { media: fromJS(media) } : {}), ...(card ? { card: fromJS(card) } : {}), - ...(poll ? { poll: fromJS(poll) } : {}), + ...(poll ? { poll: createPollFromServerJSON(poll) } : {}), ...(hashtag ? { hashtag: fromJS(hashtag) } : {}), ...(componentName === 'Video' ? { diff --git a/app/javascript/mastodon/containers/poll_container.js b/app/javascript/mastodon/containers/poll_container.js index 7ca840138d..4003fb192c 100644 --- a/app/javascript/mastodon/containers/poll_container.js +++ b/app/javascript/mastodon/containers/poll_container.js @@ -4,7 +4,7 @@ import { debounce } from 'lodash'; import { openModal } from 'mastodon/actions/modal'; import { fetchPoll, vote } from 'mastodon/actions/polls'; -import Poll from 'mastodon/components/poll'; +import { Poll } from 'mastodon/components/poll'; const mapDispatchToProps = (dispatch, { pollId }) => ({ refresh: debounce( @@ -32,7 +32,7 @@ const mapDispatchToProps = (dispatch, { pollId }) => ({ }); const mapStateToProps = (state, { pollId }) => ({ - poll: state.polls.get(pollId), + poll: state.polls[pollId], }); export default connect(mapStateToProps, mapDispatchToProps)(Poll); diff --git a/app/javascript/mastodon/models/poll.ts b/app/javascript/mastodon/models/poll.ts index b4ba38a9c6..6f5655680d 100644 --- a/app/javascript/mastodon/models/poll.ts +++ b/app/javascript/mastodon/models/poll.ts @@ -1,6 +1,3 @@ -import type { RecordOf } from 'immutable'; -import { Record, List } from 'immutable'; - import escapeTextContentForBrowser from 'escape-html'; import type { ApiPollJSON, ApiPollOptionJSON } from 'mastodon/api_types/polls'; @@ -9,19 +6,12 @@ import emojify from 'mastodon/features/emoji/emoji'; import { CustomEmojiFactory, makeEmojiMap } from './custom_emoji'; import type { CustomEmoji, EmojiMap } from './custom_emoji'; -interface PollOptionTranslationShape { +interface PollOptionTranslation { title: string; titleHtml: string; } -export type PollOptionTranslation = RecordOf; - -export const PollOptionTranslationFactory = Record({ - title: '', - titleHtml: '', -}); - -interface PollOptionShape extends Required { +export interface PollOption extends ApiPollOptionJSON { voted: boolean; titleHtml: string; translation: PollOptionTranslation | null; @@ -31,45 +21,30 @@ export function createPollOptionTranslationFromServerJSON( translation: { title: string }, emojiMap: EmojiMap, ) { - return PollOptionTranslationFactory({ + return { ...translation, titleHtml: emojify( escapeTextContentForBrowser(translation.title), emojiMap, ), - }); + } as PollOptionTranslation; } -export type PollOption = RecordOf; - -export const PollOptionFactory = Record({ - title: '', - votes_count: 0, - voted: false, - titleHtml: '', - translation: null, -}); - -interface PollShape +export interface Poll extends Omit { - emojis: List; - options: List; - own_votes?: List; + emojis: CustomEmoji[]; + options: PollOption[]; + own_votes?: number[]; } -export type Poll = RecordOf; -export const PollFactory = Record({ - id: '', - expires_at: '', +const pollDefaultValues = { expired: false, multiple: false, voters_count: 0, votes_count: 0, voted: false, - emojis: List(), - options: List(), - own_votes: List(), -}); + own_votes: [], +}; export function createPollFromServerJSON( serverJSON: ApiPollJSON, @@ -77,33 +52,31 @@ export function createPollFromServerJSON( ) { const emojiMap = makeEmojiMap(serverJSON.emojis); - return PollFactory({ + return { + ...pollDefaultValues, ...serverJSON, - emojis: List(serverJSON.emojis.map((emoji) => CustomEmojiFactory(emoji))), - own_votes: serverJSON.own_votes ? List(serverJSON.own_votes) : undefined, - options: List( - serverJSON.options.map((optionJSON, index) => { - const option = PollOptionFactory({ - ...optionJSON, - voted: serverJSON.own_votes?.includes(index) || false, - titleHtml: emojify( - escapeTextContentForBrowser(optionJSON.title), - emojiMap, - ), - }); + emojis: serverJSON.emojis.map((emoji) => CustomEmojiFactory(emoji)), + options: serverJSON.options.map((optionJSON, index) => { + const option = { + ...optionJSON, + voted: serverJSON.own_votes?.includes(index) || false, + titleHtml: emojify( + escapeTextContentForBrowser(optionJSON.title), + emojiMap, + ), + } as PollOption; - const prevOption = previousPoll?.options.get(index); - if (prevOption?.translation && prevOption.title === option.title) { - const { translation } = prevOption; + const prevOption = previousPoll?.options[index]; + if (prevOption?.translation && prevOption.title === option.title) { + const { translation } = prevOption; - option.set( - 'translation', - createPollOptionTranslationFromServerJSON(translation, emojiMap), - ); - } + option.translation = createPollOptionTranslationFromServerJSON( + translation, + emojiMap, + ); + } - return option; - }), - ), - }); + return option; + }), + } as Poll; } diff --git a/app/javascript/mastodon/reducers/polls.ts b/app/javascript/mastodon/reducers/polls.ts index 9b9a5d2ff8..aadf6741c1 100644 --- a/app/javascript/mastodon/reducers/polls.ts +++ b/app/javascript/mastodon/reducers/polls.ts @@ -1,5 +1,4 @@ import type { Reducer } from '@reduxjs/toolkit'; -import { Map as ImmutableMap } from 'immutable'; import { importPolls } from 'mastodon/actions/importer/polls'; import { makeEmojiMap } from 'mastodon/models/custom_emoji'; @@ -11,57 +10,48 @@ import { STATUS_TRANSLATE_UNDO, } from '../actions/statuses'; -const initialState = ImmutableMap(); +const initialState: Record = {}; type PollsState = typeof initialState; -const statusTranslateSuccess = ( - state: PollsState, - pollTranslation: Poll | undefined, -) => { - if (!pollTranslation) return state; +const statusTranslateSuccess = (state: PollsState, pollTranslation?: Poll) => { + if (!pollTranslation) return; - return state.withMutations((map) => { - const poll = state.get(pollTranslation.id); + const poll = state[pollTranslation.id]; - if (!poll) return; + if (!poll) return; - const emojiMap = makeEmojiMap(poll.emojis); + const emojiMap = makeEmojiMap(poll.emojis); - pollTranslation.options.forEach((item, index) => { - map.setIn( - [pollTranslation.id, 'options', index, 'translation'], - createPollOptionTranslationFromServerJSON(item, emojiMap), - ); - }); + pollTranslation.options.forEach((item, index) => { + const option = poll.options[index]; + if (!option) return; + + option.translation = createPollOptionTranslationFromServerJSON( + item, + emojiMap, + ); }); }; const statusTranslateUndo = (state: PollsState, id: string) => { - return state.withMutations((map) => { - const options = map.get(id)?.options; - - if (options) { - options.forEach((item, index) => - map.deleteIn([id, 'options', index, 'translation']), - ); - } + state[id]?.options.forEach((option) => { + option.translation = null; }); }; export const pollsReducer: Reducer = ( - state = initialState, + draft = initialState, action, ) => { if (importPolls.match(action)) { - return state.withMutations((polls) => { - action.payload.polls.forEach((poll) => polls.set(poll.id, poll)); + action.payload.polls.forEach((poll) => { + draft[poll.id] = poll; }); } else if (action.type === STATUS_TRANSLATE_SUCCESS) - return statusTranslateSuccess( - state, - (action.translation as { poll?: Poll }).poll, - ); - else if (action.type === STATUS_TRANSLATE_UNDO) - return statusTranslateUndo(state, action.pollId as string); - else return state; + statusTranslateSuccess(draft, (action.translation as { poll?: Poll }).poll); + else if (action.type === STATUS_TRANSLATE_UNDO) { + statusTranslateUndo(draft, action.pollId as string); + } + + return draft; };