diff --git a/app/soapbox/normalizers/account.ts b/app/soapbox/normalizers/account.ts index 1c536ee4a..41dd1dd0f 100644 --- a/app/soapbox/normalizers/account.ts +++ b/app/soapbox/normalizers/account.ts @@ -13,7 +13,6 @@ import { import emojify from 'soapbox/features/emoji/emoji'; import { normalizeEmoji } from 'soapbox/normalizers/emoji'; -import { IAccount } from 'soapbox/types'; import { acctFull } from 'soapbox/utils/accounts'; import { unescapeHTML } from 'soapbox/utils/html'; import { mergeDefined, makeEmojiMap } from 'soapbox/utils/normalizers'; @@ -200,7 +199,7 @@ const normalizeFqn = (account: ImmutableMap) => { return account.set('fqn', acctFull(account)); }; -export const normalizeAccount = (account: Record): IAccount => { +export const normalizeAccount = (account: Record) => { return AccountRecord( ImmutableMap(fromJS(account)).withMutations(account => { normalizePleromaLegacyFields(account); diff --git a/app/soapbox/normalizers/status.ts b/app/soapbox/normalizers/status.ts index 8db44ba39..21cdb42bd 100644 --- a/app/soapbox/normalizers/status.ts +++ b/app/soapbox/normalizers/status.ts @@ -15,7 +15,6 @@ import { normalizeCard } from 'soapbox/normalizers/card'; import { normalizeEmoji } from 'soapbox/normalizers/emoji'; import { normalizeMention } from 'soapbox/normalizers/mention'; import { normalizePoll } from 'soapbox/normalizers/poll'; -import { IStatus } from 'soapbox/types'; // https://docs.joinmastodon.org/entities/status/ export const StatusRecord = ImmutableRecord({ @@ -136,7 +135,7 @@ const fixQuote = (status: ImmutableMap) => { }); }; -export const normalizeStatus = (status: Record): IStatus => { +export const normalizeStatus = (status: Record) => { return StatusRecord( ImmutableMap(fromJS(status)).withMutations(status => { normalizeAttachments(status); diff --git a/app/soapbox/reducers/accounts.js b/app/soapbox/reducers/accounts.ts similarity index 73% rename from app/soapbox/reducers/accounts.js rename to app/soapbox/reducers/accounts.ts index de7926feb..cea4603af 100644 --- a/app/soapbox/reducers/accounts.js +++ b/app/soapbox/reducers/accounts.ts @@ -3,6 +3,7 @@ import { List as ImmutableList, fromJS, } from 'immutable'; +import { AnyAction } from 'redux'; import { ADMIN_USERS_FETCH_SUCCESS, @@ -37,20 +38,27 @@ import { ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP, } from '../actions/importer'; -const initialState = ImmutableMap(); +type AccountRecord = ReturnType; +type AccountMap = ImmutableMap; +type APIEntity = Record; +type APIEntities = Array; -const minifyAccount = account => { +type State = ImmutableMap; + +const initialState: State = ImmutableMap(); + +const minifyAccount = (account: AccountRecord): AccountRecord => { return account.mergeWith((o, n) => n || o, { moved: account.getIn(['moved', 'id']), }); }; -const fixAccount = (state, account) => { +const fixAccount = (state: State, account: APIEntity) => { const normalized = minifyAccount(normalizeAccount(account)); return state.set(account.id, normalized); }; -const normalizeAccounts = (state, accounts) => { +const normalizeAccounts = (state: State, accounts: ImmutableList) => { accounts.forEach(account => { state = fixAccount(state, account); }); @@ -58,33 +66,44 @@ const normalizeAccounts = (state, accounts) => { return state; }; -const importAccountFromChat = (state, chat) => fixAccount(state, chat.account); +const importAccountFromChat = ( + state: State, + chat: APIEntity, +): State => fixAccount(state, chat.account); -const importAccountsFromChats = (state, chats) => +const importAccountsFromChats = (state: State, chats: APIEntities): State => state.withMutations(mutable => chats.forEach(chat => importAccountFromChat(mutable, chat))); -const addTags = (state, accountIds, tags) => { +const addTags = ( + state: State, + accountIds: Array, + tags: Array, +): State => { return state.withMutations(state => { accountIds.forEach(id => { - state.updateIn([id, 'pleroma', 'tags'], ImmutableList(), v => + state.updateIn([id, 'pleroma', 'tags'], ImmutableList(), (v: ImmutableList) => v.toOrderedSet().union(tags).toList(), ); }); }); }; -const removeTags = (state, accountIds, tags) => { +const removeTags = ( + state: State, + accountIds: Array, + tags: Array, +): State => { return state.withMutations(state => { accountIds.forEach(id => { - state.updateIn([id, 'pleroma', 'tags'], ImmutableList(), v => + state.updateIn([id, 'pleroma', 'tags'], ImmutableList(), (v: ImmutableList) => v.toOrderedSet().subtract(tags).toList(), ); }); }); }; -const setActive = (state, accountIds, active) => { +const setActive = (state: State, accountIds: Array, active: boolean): State => { return state.withMutations(state => { accountIds.forEach(id => { state.setIn([id, 'pleroma', 'is_active'], active); @@ -92,12 +111,16 @@ const setActive = (state, accountIds, active) => { }); }; -const permissionGroupFields = { +const permissionGroupFields: Record = { admin: 'is_admin', moderator: 'is_moderator', }; -const addPermission = (state, accountIds, permissionGroup) => { +const addPermission = ( + state: State, + accountIds: Array, + permissionGroup: string, +): State => { const field = permissionGroupFields[permissionGroup]; if (!field) return state; @@ -108,7 +131,11 @@ const addPermission = (state, accountIds, permissionGroup) => { }); }; -const removePermission = (state, accountIds, permissionGroup) => { +const removePermission = ( + state: State, + accountIds: Array, + permissionGroup: string, +): State => { const field = permissionGroupFields[permissionGroup]; if (!field) return state; @@ -119,7 +146,7 @@ const removePermission = (state, accountIds, permissionGroup) => { }); }; -const buildAccount = adminUser => normalizeAccount({ +const buildAccount = (adminUser: ImmutableMap): AccountRecord => normalizeAccount({ id: adminUser.get('id'), username: adminUser.get('nickname').split('@')[0], acct: adminUser.get('nickname'), @@ -144,7 +171,10 @@ const buildAccount = adminUser => normalizeAccount({ should_refetch: true, }); -const mergeAdminUser = (account, adminUser) => { +const mergeAdminUser = ( + account: AccountRecord, + adminUser: ImmutableMap, +) => { return account.withMutations(account => { account.set('display_name', adminUser.get('display_name')); account.set('avatar', adminUser.get('avatar')); @@ -157,7 +187,7 @@ const mergeAdminUser = (account, adminUser) => { }); }; -const importAdminUser = (state, adminUser) => { +const importAdminUser = (state: State, adminUser: ImmutableMap): State => { const id = adminUser.get('id'); const account = state.get(id); @@ -168,15 +198,15 @@ const importAdminUser = (state, adminUser) => { } }; -const importAdminUsers = (state, adminUsers) => { - return state.withMutations(state => { +const importAdminUsers = (state: State, adminUsers: Array>): State => { + return state.withMutations((state: State) => { fromJS(adminUsers).forEach(adminUser => { - importAdminUser(state, adminUser); + importAdminUser(state, ImmutableMap(adminUser)); }); }); }; -const setSuggested = (state, accountIds, isSuggested) => { +const setSuggested = (state: State, accountIds: Array, isSuggested: boolean): State => { return state.withMutations(state => { accountIds.forEach(id => { state.setIn([id, 'pleroma', 'is_suggested'], isSuggested); @@ -184,16 +214,14 @@ const setSuggested = (state, accountIds, isSuggested) => { }); }; -export default function accounts(state = initialState, action) { +export default function accounts(state: State = initialState, action: AnyAction): State { switch(action.type) { case ACCOUNT_IMPORT: return fixAccount(state, action.account); case ACCOUNTS_IMPORT: return normalizeAccounts(state, action.accounts); case ACCOUNT_FETCH_FAIL_FOR_USERNAME_LOOKUP: - return state.set(-1, ImmutableMap({ - username: action.username, - })); + return state.set(-1, normalizeAccount({ username: action.username })); case CHATS_FETCH_SUCCESS: case CHATS_EXPAND_SUCCESS: return importAccountsFromChats(state, action.chats); diff --git a/app/soapbox/reducers/statuses.js b/app/soapbox/reducers/statuses.ts similarity index 64% rename from app/soapbox/reducers/statuses.js rename to app/soapbox/reducers/statuses.ts index 957640c9c..569cc7552 100644 --- a/app/soapbox/reducers/statuses.js +++ b/app/soapbox/reducers/statuses.ts @@ -1,10 +1,11 @@ import escapeTextContentForBrowser from 'escape-html'; -import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; +import { AnyAction } from 'redux'; import emojify from 'soapbox/features/emoji/emoji'; -import { normalizeStatus } from 'soapbox/normalizers/status'; +import { normalizeStatus } from 'soapbox/normalizers'; import { simulateEmojiReact, simulateUnEmojiReact } from 'soapbox/utils/emoji_reacts'; -import { stripCompatibilityFeatures } from 'soapbox/utils/html'; +import { stripCompatibilityFeatures, unescapeHTML } from 'soapbox/utils/html'; import { makeEmojiMap } from 'soapbox/utils/normalizers'; import { @@ -33,7 +34,13 @@ import { TIMELINE_DELETE } from '../actions/timelines'; const domParser = new DOMParser(); -const minifyStatus = status => { +type StatusRecord = ReturnType; +type APIEntity = Record; +type APIEntities = Array; + +type State = ImmutableMap; + +const minifyStatus = (status: StatusRecord): StatusRecord => { return status.mergeWith((o, n) => n || o, { account: status.getIn(['account', 'id']), reblog: status.getIn(['reblog', 'id']), @@ -42,48 +49,69 @@ const minifyStatus = status => { }); }; +// Gets titles of poll options from status +const getPollOptionTitles = (status: StatusRecord): Array => { + return status.poll?.options.map(({ title }: { title: string }) => title); +}; + +// Creates search text from the status +const buildSearchContent = (status: StatusRecord): string => { + const pollOptionTitles = getPollOptionTitles(status); + + const fields = ImmutableList([ + status.spoiler_text, + status.content, + ]).concat(pollOptionTitles); + + return unescapeHTML(fields.join('\n\n')); +}; + // Only calculate these values when status first encountered // Otherwise keep the ones already in the reducer -export const calculateStatus = (status, oldStatus, expandSpoilers = false) => { +const calculateStatus = ( + status: StatusRecord, + oldStatus: StatusRecord, + expandSpoilers: boolean = false, +): StatusRecord => { if (oldStatus) { return status.merge({ - search_index: oldStatus.get('search_index'), - contentHtml: oldStatus.get('contentHtml'), - spoilerHtml: oldStatus.get('spoilerHtml'), - hidden: oldStatus.get('hidden'), + search_index: oldStatus.search_index, + contentHtml: oldStatus.contentHtml, + spoilerHtml: oldStatus.spoilerHtml, + hidden: oldStatus.hidden, }); } else { - const spoilerText = status.get('spoiler_text') || ''; - const searchContent = (ImmutableList([spoilerText, status.get('content')]).concat(status.getIn(['poll', 'options'], ImmutableList()).map(option => option.get('title')))).join('\n\n').replace(//g, '\n').replace(/<\/p>

/g, '\n\n'); - const emojiMap = makeEmojiMap(status.get('emojis')); + const spoilerText = status.spoiler_text; + const searchContent = buildSearchContent(status); + const emojiMap = makeEmojiMap(status.emojis); return status.merge({ search_index: domParser.parseFromString(searchContent, 'text/html').documentElement.textContent, - contentHtml: stripCompatibilityFeatures(emojify(status.get('content'), emojiMap)), + contentHtml: stripCompatibilityFeatures(emojify(status.content, emojiMap)), spoilerHtml: emojify(escapeTextContentForBrowser(spoilerText), emojiMap), - hidden: expandSpoilers ? false : spoilerText.length > 0 || status.get('sensitive'), + hidden: expandSpoilers ? false : spoilerText.length > 0 || status.sensitive, }); } }; // Check whether a status is a quote by secondary characteristics -const isQuote = status => { - return Boolean(status.get('quote_id') || status.getIn(['pleroma', 'quote_url'])); +const isQuote = (status: StatusRecord) => { + return Boolean(status.getIn(['pleroma', 'quote_url'])); }; // Preserve quote if an existing status already has it -const fixQuote = (status, oldStatus) => { - if (oldStatus && !status.get('quote') && isQuote(status)) { +const fixQuote = (status: StatusRecord, oldStatus: StatusRecord): StatusRecord => { + if (oldStatus && !status.quote && isQuote(status)) { return status - .set('quote', oldStatus.get('quote')) + .set('quote', oldStatus.quote) .updateIn(['pleroma', 'quote_visible'], visible => visible || oldStatus.getIn(['pleroma', 'quote_visible'])); } else { return status; } }; -const fixStatus = (state, status, expandSpoilers) => { - const oldStatus = state.get(status.get('id')); +const fixStatus = (state: State, status: APIEntity, expandSpoilers: boolean): StatusRecord => { + const oldStatus: StatusRecord = state.get(status.id); return normalizeStatus(status).withMutations(status => { fixQuote(status, oldStatus); @@ -92,13 +120,13 @@ const fixStatus = (state, status, expandSpoilers) => { }); }; -const importStatus = (state, status, expandSpoilers) => - state.set(status.id, fixStatus(state, fromJS(status), expandSpoilers)); +const importStatus = (state: State, status: APIEntity, expandSpoilers: boolean): State => + state.set(status.id, fixStatus(state, status, expandSpoilers)); -const importStatuses = (state, statuses, expandSpoilers) => +const importStatuses = (state: State, statuses: APIEntities, expandSpoilers: boolean): State => state.withMutations(mutable => statuses.forEach(status => importStatus(mutable, status, expandSpoilers))); -const deleteStatus = (state, id, references) => { +const deleteStatus = (state: State, id: string, references: Array) => { references.forEach(ref => { state = deleteStatus(state, ref[0], []); }); @@ -106,25 +134,25 @@ const deleteStatus = (state, id, references) => { return state.delete(id); }; -const importPendingStatus = (state, { in_reply_to_id }) => { +const importPendingStatus = (state: State, { in_reply_to_id }: APIEntity) => { if (in_reply_to_id) { - return state.updateIn([in_reply_to_id, 'replies_count'], 0, count => count + 1); + return state.updateIn([in_reply_to_id, 'replies_count'], 0, (count: number) => count + 1); } else { return state; } }; -const deletePendingStatus = (state, { in_reply_to_id }) => { +const deletePendingStatus = (state: State, { in_reply_to_id }: APIEntity) => { if (in_reply_to_id) { - return state.updateIn([in_reply_to_id, 'replies_count'], 0, count => Math.max(0, count - 1)); + return state.updateIn([in_reply_to_id, 'replies_count'], 0, (count: number) => Math.max(0, count - 1)); } else { return state; } }; -const initialState = ImmutableMap(); +const initialState: State = ImmutableMap(); -export default function statuses(state = initialState, action) { +export default function statuses(state = initialState, action: AnyAction): State { switch(action.type) { case STATUS_IMPORT: return importStatus(state, action.status, action.expandSpoilers); @@ -172,7 +200,7 @@ export default function statuses(state = initialState, action) { return state.setIn([action.id, 'muted'], false); case STATUS_REVEAL: return state.withMutations(map => { - action.ids.forEach(id => { + action.ids.forEach((id: string) => { if (!(state.get(id) === undefined)) { map.setIn([id, 'hidden'], false); } @@ -180,7 +208,7 @@ export default function statuses(state = initialState, action) { }); case STATUS_HIDE: return state.withMutations(map => { - action.ids.forEach(id => { + action.ids.forEach((id: string) => { if (!(state.get(id) === undefined)) { map.setIn([id, 'hidden'], true); }