Skip to content

Commit

Permalink
Convert polls to Typescript / Immutable Records (mastodon#29789)
Browse files Browse the repository at this point in the history
  • Loading branch information
renchap authored Dec 10, 2024
1 parent e4e35ab commit ded799f
Show file tree
Hide file tree
Showing 15 changed files with 272 additions and 186 deletions.
20 changes: 6 additions & 14 deletions app/javascript/mastodon/actions/importer/index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { createPollFromServerJSON } from 'mastodon/models/poll';

import { importAccounts } from '../accounts_typed';

import { normalizeStatus, normalizePoll } from './normalizer';
import { normalizeStatus } from './normalizer';
import { importPolls } from './polls';

export const STATUS_IMPORT = 'STATUS_IMPORT';
export const STATUSES_IMPORT = 'STATUSES_IMPORT';
export const POLLS_IMPORT = 'POLLS_IMPORT';
export const FILTERS_IMPORT = 'FILTERS_IMPORT';

function pushUnique(array, object) {
Expand All @@ -25,10 +27,6 @@ export function importFilters(filters) {
return { type: FILTERS_IMPORT, filters };
}

export function importPolls(polls) {
return { type: POLLS_IMPORT, polls };
}

export function importFetchedAccount(account) {
return importFetchedAccounts([account]);
}
Expand Down Expand Up @@ -73,7 +71,7 @@ export function importFetchedStatuses(statuses) {
}

if (status.poll?.id) {
pushUnique(polls, normalizePoll(status.poll, getState().getIn(['polls', status.poll.id])));
pushUnique(polls, createPollFromServerJSON(status.poll, getState().polls.get(status.poll.id)));
}

if (status.card) {
Expand All @@ -83,15 +81,9 @@ export function importFetchedStatuses(statuses) {

statuses.forEach(processStatus);

dispatch(importPolls(polls));
dispatch(importPolls({ polls }));
dispatch(importFetchedAccounts(accounts));
dispatch(importStatuses(normalStatuses));
dispatch(importFilters(filters));
};
}

export function importFetchedPoll(poll) {
return (dispatch, getState) => {
dispatch(importPolls([normalizePoll(poll, getState().getIn(['polls', poll.id]))]));
};
}
39 changes: 2 additions & 37 deletions app/javascript/mastodon/actions/importer/normalizer.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import escapeTextContentForBrowser from 'escape-html';

import { makeEmojiMap } from 'mastodon/models/custom_emoji';

import emojify from '../../features/emoji/emoji';
import { expandSpoilers } from '../../initial_state';

const domParser = new DOMParser();

const makeEmojiMap = emojis => emojis.reduce((obj, emoji) => {
obj[`:${emoji.shortcode}:`] = emoji;
return obj;
}, {});

export function searchTextFromRawStatus (status) {
const spoilerText = status.spoiler_text || '';
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
Expand Down Expand Up @@ -112,38 +109,6 @@ export function normalizeStatusTranslation(translation, status) {
return normalTranslation;
}

export function normalizePoll(poll, normalOldPoll) {
const normalPoll = { ...poll };
const emojiMap = makeEmojiMap(poll.emojis);

normalPoll.options = poll.options.map((option, index) => {
const normalOption = {
...option,
voted: poll.own_votes && poll.own_votes.includes(index),
titleHtml: emojify(escapeTextContentForBrowser(option.title), emojiMap),
};

if (normalOldPoll && normalOldPoll.getIn(['options', index, 'title']) === option.title) {
normalOption.translation = normalOldPoll.getIn(['options', index, 'translation']);
}

return normalOption;
});

return normalPoll;
}

export function normalizePollOptionTranslation(translation, poll) {
const emojiMap = makeEmojiMap(poll.get('emojis').toJS());

const normalTranslation = {
...translation,
titleHtml: emojify(escapeTextContentForBrowser(translation.title), emojiMap),
};

return normalTranslation;
}

export function normalizeAnnouncement(announcement) {
const normalAnnouncement = { ...announcement };
const emojiMap = makeEmojiMap(normalAnnouncement.emojis);
Expand Down
7 changes: 7 additions & 0 deletions app/javascript/mastodon/actions/importer/polls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createAction } from '@reduxjs/toolkit';

import type { Poll } from 'mastodon/models/poll';

export const importPolls = createAction<{ polls: Poll[] }>(
'poll/importMultiple',
);
61 changes: 0 additions & 61 deletions app/javascript/mastodon/actions/polls.js

This file was deleted.

40 changes: 40 additions & 0 deletions app/javascript/mastodon/actions/polls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { apiGetPoll, apiPollVote } from 'mastodon/api/polls';
import type { ApiPollJSON } from 'mastodon/api_types/polls';
import { createPollFromServerJSON } from 'mastodon/models/poll';
import {
createAppAsyncThunk,
createDataLoadingThunk,
} from 'mastodon/store/typed_functions';

import { importPolls } from './importer/polls';

export const importFetchedPoll = createAppAsyncThunk(
'poll/importFetched',
(args: { poll: ApiPollJSON }, { dispatch, getState }) => {
const { poll } = args;

dispatch(
importPolls({
polls: [createPollFromServerJSON(poll, getState().polls.get(poll.id))],
}),
);
},
);

export const vote = createDataLoadingThunk(
'poll/vote',
({ pollId, choices }: { pollId: string; choices: string[] }) =>
apiPollVote(pollId, choices),
async (poll, { dispatch, discardLoadData }) => {
await dispatch(importFetchedPoll({ poll }));
return discardLoadData;
},
);

export const fetchPoll = createDataLoadingThunk(
'poll/fetch',
({ pollId }: { pollId: string }) => apiGetPoll(pollId),
async (poll, { dispatch }) => {
await dispatch(importFetchedPoll({ poll }));
},
);
10 changes: 10 additions & 0 deletions app/javascript/mastodon/api/polls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { apiRequestGet, apiRequestPost } from 'mastodon/api';
import type { ApiPollJSON } from 'mastodon/api_types/polls';

export const apiGetPoll = (pollId: string) =>
apiRequestGet<ApiPollJSON>(`/v1/polls/${pollId}`);

export const apiPollVote = (pollId: string, choices: string[]) =>
apiRequestPost<ApiPollJSON>(`/v1/polls/${pollId}/votes`, {
data: { choices },
});
4 changes: 2 additions & 2 deletions app/javascript/mastodon/api_types/polls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ export interface ApiPollJSON {
options: ApiPollOptionJSON[];
emojis: ApiCustomEmojiJSON[];

voted: boolean;
own_votes: number[];
voted?: boolean;
own_votes?: number[];
}
9 changes: 2 additions & 7 deletions app/javascript/mastodon/components/poll.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,10 @@ const messages = defineMessages({
},
});

const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => {
obj[`:${emoji.get('shortcode')}:`] = emoji.toJS();
return obj;
}, {});

class Poll extends ImmutablePureComponent {
static propTypes = {
identity: identityContextPropShape,
poll: ImmutablePropTypes.map.isRequired,
poll: ImmutablePropTypes.record.isRequired,
status: ImmutablePropTypes.map.isRequired,
lang: PropTypes.string,
intl: PropTypes.object.isRequired,
Expand Down Expand Up @@ -150,7 +145,7 @@ class Poll extends ImmutablePureComponent {
let titleHtml = option.getIn(['translation', 'titleHtml']) || option.get('titleHtml');

if (!titleHtml) {
const emojiMap = makeEmojiMap(poll);
const emojiMap = emojiMap(poll);
titleHtml = emojify(escapeTextContentForBrowser(title), emojiMap);
}

Expand Down
6 changes: 3 additions & 3 deletions app/javascript/mastodon/containers/poll_container.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ import Poll from 'mastodon/components/poll';
const mapDispatchToProps = (dispatch, { pollId }) => ({
refresh: debounce(
() => {
dispatch(fetchPoll(pollId));
dispatch(fetchPoll({ pollId }));
},
1000,
{ leading: true },
),

onVote (choices) {
dispatch(vote(pollId, choices));
dispatch(vote({ pollId, choices }));
},

onInteractionModal (type, status) {
Expand All @@ -32,7 +32,7 @@ const mapDispatchToProps = (dispatch, { pollId }) => ({
});

const mapStateToProps = (state, { pollId }) => ({
poll: state.getIn(['polls', pollId]),
poll: state.polls.get(pollId),
});

export default connect(mapStateToProps, mapDispatchToProps)(Poll);
14 changes: 2 additions & 12 deletions app/javascript/mastodon/models/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@ import type {
ApiAccountRoleJSON,
ApiAccountJSON,
} from 'mastodon/api_types/accounts';
import type { ApiCustomEmojiJSON } from 'mastodon/api_types/custom_emoji';
import emojify from 'mastodon/features/emoji/emoji';
import { unescapeHTML } from 'mastodon/utils/html';

import { CustomEmojiFactory } from './custom_emoji';
import type { CustomEmoji } from './custom_emoji';
import { CustomEmojiFactory, makeEmojiMap } from './custom_emoji';
import type { CustomEmoji, EmojiMap } from './custom_emoji';

// AccountField
interface AccountFieldShape extends Required<ApiAccountFieldJSON> {
Expand Down Expand Up @@ -102,15 +101,6 @@ export const accountDefaultValues: AccountShape = {

const AccountFactory = ImmutableRecord<AccountShape>(accountDefaultValues);

type EmojiMap = Record<string, ApiCustomEmojiJSON>;

function makeEmojiMap(emojis: ApiCustomEmojiJSON[]) {
return emojis.reduce<EmojiMap>((obj, emoji) => {
obj[`:${emoji.shortcode}:`] = emoji;
return obj;
}, {});
}

function createAccountField(
jsonField: ApiAccountFieldJSON,
emojiMap: EmojiMap,
Expand Down
23 changes: 20 additions & 3 deletions app/javascript/mastodon/models/custom_emoji.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,32 @@
import type { RecordOf } from 'immutable';
import { Record } from 'immutable';
import type { RecordOf, List as ImmutableList } from 'immutable';
import { Record as ImmutableRecord, isList } from 'immutable';

import type { ApiCustomEmojiJSON } from 'mastodon/api_types/custom_emoji';

type CustomEmojiShape = Required<ApiCustomEmojiJSON>; // no changes from server shape
export type CustomEmoji = RecordOf<CustomEmojiShape>;

export const CustomEmojiFactory = Record<CustomEmojiShape>({
export const CustomEmojiFactory = ImmutableRecord<CustomEmojiShape>({
shortcode: '',
static_url: '',
url: '',
category: '',
visible_in_picker: false,
});

export type EmojiMap = Record<string, ApiCustomEmojiJSON>;

export function makeEmojiMap(
emojis: ApiCustomEmojiJSON[] | ImmutableList<CustomEmoji>,
) {
if (isList(emojis)) {
return emojis.reduce<EmojiMap>((obj, emoji) => {
obj[`:${emoji.shortcode}:`] = emoji.toJS();
return obj;
}, {});
} else
return emojis.reduce<EmojiMap>((obj, emoji) => {
obj[`:${emoji.shortcode}:`] = emoji;
return obj;
}, {});
}
Loading

0 comments on commit ded799f

Please sign in to comment.