From 90cd9056905dff63c19082175dc4cf369a8ce49c Mon Sep 17 00:00:00 2001 From: Renaud Chaput Date: Sun, 26 May 2024 22:00:28 +0200 Subject: [PATCH] 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; };