Convert the polls state to plain JS

This commit is contained in:
Renaud Chaput 2024-05-26 22:00:28 +02:00
parent e79f016d33
commit 90cd905690
No known key found for this signature in database
GPG key ID: BCFC859D49B46990
7 changed files with 70 additions and 108 deletions

View file

@ -71,7 +71,7 @@ export function importFetchedStatuses(statuses) {
} }
if (status.poll?.id) { 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) { if (status.card) {

View file

@ -15,7 +15,7 @@ export const importFetchedPoll = createAppAsyncThunk(
dispatch( dispatch(
importPolls({ importPolls({
polls: [createPollFromServerJSON(poll, getState().polls.get(poll.id))], polls: [createPollFromServerJSON(poll, getState().polls[poll.id])],
}), }),
); );
}, },

View file

@ -154,7 +154,7 @@ export const Poll: React.FC<{
lang: string; lang: string;
disabled?: boolean; disabled?: boolean;
refresh?: () => void; refresh?: () => void;
onVote: (votes: string[]) => void; onVote?: (votes: string[]) => void;
onInteractionModal: (interactionType: string, status: Status) => void; onInteractionModal: (interactionType: string, status: Status) => void;
}> = ({ }> = ({
poll, poll,
@ -218,13 +218,11 @@ export const Poll: React.FC<{
} }
if (signedIn) { if (signedIn) {
onVote(Array.from(selected)); onVote?.(Array.from(selected));
} else { } else {
onInteractionModal('vote', status); onInteractionModal('vote', status);
} }
}, [disabled, onVote, selected, signedIn, status, onInteractionModal]);
onVote(Array.from(selected));
}, [disabled, onVote, selected, signedIn, onInteractionModal, status]);
useEffect(() => { useEffect(() => {
if (expired || !expires_at) return () => undefined; if (expired || !expires_at) return () => undefined;
@ -290,7 +288,7 @@ export const Poll: React.FC<{
multiple={poll.multiple} multiple={poll.multiple}
voted={option.voted || poll.own_votes?.includes(i) || false} voted={option.voted || poll.own_votes?.includes(i) || false}
leading={poll.options leading={poll.options
.filterNot((other) => other.title === option.title) .filter((other) => other.title !== option.title)
.every((other) => option.votes_count >= other.votes_count)} .every((other) => option.votes_count >= other.votes_count)}
percent={percent} percent={percent}
disabled={disabled || selected.size === 0} disabled={disabled || selected.size === 0}

View file

@ -7,12 +7,13 @@ import { fromJS } from 'immutable';
import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag'; import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag';
import MediaGallery from 'mastodon/components/media_gallery'; import MediaGallery from 'mastodon/components/media_gallery';
import ModalRoot from 'mastodon/components/modal_root'; 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 Audio from 'mastodon/features/audio';
import Card from 'mastodon/features/status/components/card'; import Card from 'mastodon/features/status/components/card';
import MediaModal from 'mastodon/features/ui/components/media_modal'; import MediaModal from 'mastodon/features/ui/components/media_modal';
import Video from 'mastodon/features/video'; import Video from 'mastodon/features/video';
import { IntlProvider } from 'mastodon/locales'; import { IntlProvider } from 'mastodon/locales';
import { createPollFromServerJSON } from 'mastodon/models/poll';
import { getScrollbarWidth } from 'mastodon/utils/scrollbar'; import { getScrollbarWidth } from 'mastodon/utils/scrollbar';
const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll, Hashtag, Audio }; const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll, Hashtag, Audio };
@ -88,7 +89,7 @@ export default class MediaContainer extends PureComponent {
Object.assign(props, { Object.assign(props, {
...(media ? { media: fromJS(media) } : {}), ...(media ? { media: fromJS(media) } : {}),
...(card ? { card: fromJS(card) } : {}), ...(card ? { card: fromJS(card) } : {}),
...(poll ? { poll: fromJS(poll) } : {}), ...(poll ? { poll: createPollFromServerJSON(poll) } : {}),
...(hashtag ? { hashtag: fromJS(hashtag) } : {}), ...(hashtag ? { hashtag: fromJS(hashtag) } : {}),
...(componentName === 'Video' ? { ...(componentName === 'Video' ? {

View file

@ -4,7 +4,7 @@ import { debounce } from 'lodash';
import { openModal } from 'mastodon/actions/modal'; import { openModal } from 'mastodon/actions/modal';
import { fetchPoll, vote } from 'mastodon/actions/polls'; import { fetchPoll, vote } from 'mastodon/actions/polls';
import Poll from 'mastodon/components/poll'; import { Poll } from 'mastodon/components/poll';
const mapDispatchToProps = (dispatch, { pollId }) => ({ const mapDispatchToProps = (dispatch, { pollId }) => ({
refresh: debounce( refresh: debounce(
@ -32,7 +32,7 @@ const mapDispatchToProps = (dispatch, { pollId }) => ({
}); });
const mapStateToProps = (state, { pollId }) => ({ const mapStateToProps = (state, { pollId }) => ({
poll: state.polls.get(pollId), poll: state.polls[pollId],
}); });
export default connect(mapStateToProps, mapDispatchToProps)(Poll); export default connect(mapStateToProps, mapDispatchToProps)(Poll);

View file

@ -1,6 +1,3 @@
import type { RecordOf } from 'immutable';
import { Record, List } from 'immutable';
import escapeTextContentForBrowser from 'escape-html'; import escapeTextContentForBrowser from 'escape-html';
import type { ApiPollJSON, ApiPollOptionJSON } from 'mastodon/api_types/polls'; 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 { CustomEmojiFactory, makeEmojiMap } from './custom_emoji';
import type { CustomEmoji, EmojiMap } from './custom_emoji'; import type { CustomEmoji, EmojiMap } from './custom_emoji';
interface PollOptionTranslationShape { interface PollOptionTranslation {
title: string; title: string;
titleHtml: string; titleHtml: string;
} }
export type PollOptionTranslation = RecordOf<PollOptionTranslationShape>; export interface PollOption extends ApiPollOptionJSON {
export const PollOptionTranslationFactory = Record<PollOptionTranslationShape>({
title: '',
titleHtml: '',
});
interface PollOptionShape extends Required<ApiPollOptionJSON> {
voted: boolean; voted: boolean;
titleHtml: string; titleHtml: string;
translation: PollOptionTranslation | null; translation: PollOptionTranslation | null;
@ -31,45 +21,30 @@ export function createPollOptionTranslationFromServerJSON(
translation: { title: string }, translation: { title: string },
emojiMap: EmojiMap, emojiMap: EmojiMap,
) { ) {
return PollOptionTranslationFactory({ return {
...translation, ...translation,
titleHtml: emojify( titleHtml: emojify(
escapeTextContentForBrowser(translation.title), escapeTextContentForBrowser(translation.title),
emojiMap, emojiMap,
), ),
}); } as PollOptionTranslation;
} }
export type PollOption = RecordOf<PollOptionShape>; export interface Poll
export const PollOptionFactory = Record<PollOptionShape>({
title: '',
votes_count: 0,
voted: false,
titleHtml: '',
translation: null,
});
interface PollShape
extends Omit<ApiPollJSON, 'emojis' | 'options' | 'own_votes'> { extends Omit<ApiPollJSON, 'emojis' | 'options' | 'own_votes'> {
emojis: List<CustomEmoji>; emojis: CustomEmoji[];
options: List<PollOption>; options: PollOption[];
own_votes?: List<number>; own_votes?: number[];
} }
export type Poll = RecordOf<PollShape>;
export const PollFactory = Record<PollShape>({ const pollDefaultValues = {
id: '',
expires_at: '',
expired: false, expired: false,
multiple: false, multiple: false,
voters_count: 0, voters_count: 0,
votes_count: 0, votes_count: 0,
voted: false, voted: false,
emojis: List<CustomEmoji>(), own_votes: [],
options: List<PollOption>(), };
own_votes: List(),
});
export function createPollFromServerJSON( export function createPollFromServerJSON(
serverJSON: ApiPollJSON, serverJSON: ApiPollJSON,
@ -77,33 +52,31 @@ export function createPollFromServerJSON(
) { ) {
const emojiMap = makeEmojiMap(serverJSON.emojis); const emojiMap = makeEmojiMap(serverJSON.emojis);
return PollFactory({ return {
...pollDefaultValues,
...serverJSON, ...serverJSON,
emojis: List(serverJSON.emojis.map((emoji) => CustomEmojiFactory(emoji))), emojis: serverJSON.emojis.map((emoji) => CustomEmojiFactory(emoji)),
own_votes: serverJSON.own_votes ? List(serverJSON.own_votes) : undefined, options: serverJSON.options.map((optionJSON, index) => {
options: List( const option = {
serverJSON.options.map((optionJSON, index) => { ...optionJSON,
const option = PollOptionFactory({ voted: serverJSON.own_votes?.includes(index) || false,
...optionJSON, titleHtml: emojify(
voted: serverJSON.own_votes?.includes(index) || false, escapeTextContentForBrowser(optionJSON.title),
titleHtml: emojify( emojiMap,
escapeTextContentForBrowser(optionJSON.title), ),
emojiMap, } as PollOption;
),
});
const prevOption = previousPoll?.options.get(index); const prevOption = previousPoll?.options[index];
if (prevOption?.translation && prevOption.title === option.title) { if (prevOption?.translation && prevOption.title === option.title) {
const { translation } = prevOption; const { translation } = prevOption;
option.set( option.translation = createPollOptionTranslationFromServerJSON(
'translation', translation,
createPollOptionTranslationFromServerJSON(translation, emojiMap), emojiMap,
); );
} }
return option; return option;
}), }),
), } as Poll;
});
} }

View file

@ -1,5 +1,4 @@
import type { Reducer } from '@reduxjs/toolkit'; import type { Reducer } from '@reduxjs/toolkit';
import { Map as ImmutableMap } from 'immutable';
import { importPolls } from 'mastodon/actions/importer/polls'; import { importPolls } from 'mastodon/actions/importer/polls';
import { makeEmojiMap } from 'mastodon/models/custom_emoji'; import { makeEmojiMap } from 'mastodon/models/custom_emoji';
@ -11,57 +10,48 @@ import {
STATUS_TRANSLATE_UNDO, STATUS_TRANSLATE_UNDO,
} from '../actions/statuses'; } from '../actions/statuses';
const initialState = ImmutableMap<string, Poll>(); const initialState: Record<string, Poll> = {};
type PollsState = typeof initialState; type PollsState = typeof initialState;
const statusTranslateSuccess = ( const statusTranslateSuccess = (state: PollsState, pollTranslation?: Poll) => {
state: PollsState, if (!pollTranslation) return;
pollTranslation: Poll | undefined,
) => {
if (!pollTranslation) return state;
return state.withMutations((map) => { const poll = state[pollTranslation.id];
const poll = state.get(pollTranslation.id);
if (!poll) return; if (!poll) return;
const emojiMap = makeEmojiMap(poll.emojis); const emojiMap = makeEmojiMap(poll.emojis);
pollTranslation.options.forEach((item, index) => { pollTranslation.options.forEach((item, index) => {
map.setIn( const option = poll.options[index];
[pollTranslation.id, 'options', index, 'translation'], if (!option) return;
createPollOptionTranslationFromServerJSON(item, emojiMap),
); option.translation = createPollOptionTranslationFromServerJSON(
}); item,
emojiMap,
);
}); });
}; };
const statusTranslateUndo = (state: PollsState, id: string) => { const statusTranslateUndo = (state: PollsState, id: string) => {
return state.withMutations((map) => { state[id]?.options.forEach((option) => {
const options = map.get(id)?.options; option.translation = null;
if (options) {
options.forEach((item, index) =>
map.deleteIn([id, 'options', index, 'translation']),
);
}
}); });
}; };
export const pollsReducer: Reducer<PollsState> = ( export const pollsReducer: Reducer<PollsState> = (
state = initialState, draft = initialState,
action, action,
) => { ) => {
if (importPolls.match(action)) { if (importPolls.match(action)) {
return state.withMutations((polls) => { action.payload.polls.forEach((poll) => {
action.payload.polls.forEach((poll) => polls.set(poll.id, poll)); draft[poll.id] = poll;
}); });
} else if (action.type === STATUS_TRANSLATE_SUCCESS) } else if (action.type === STATUS_TRANSLATE_SUCCESS)
return statusTranslateSuccess( statusTranslateSuccess(draft, (action.translation as { poll?: Poll }).poll);
state, else if (action.type === STATUS_TRANSLATE_UNDO) {
(action.translation as { poll?: Poll }).poll, statusTranslateUndo(draft, action.pollId as string);
); }
else if (action.type === STATUS_TRANSLATE_UNDO)
return statusTranslateUndo(state, action.pollId as string); return draft;
else return state;
}; };