diff --git a/app/javascript/mastodon/actions/account_notes.ts b/app/javascript/mastodon/actions/account_notes.ts index eeef23e36..e524e5235 100644 --- a/app/javascript/mastodon/actions/account_notes.ts +++ b/app/javascript/mastodon/actions/account_notes.ts @@ -1,3 +1,4 @@ +import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships'; import { createAppAsyncThunk } from 'mastodon/store/typed_functions'; import api from '../api'; @@ -5,8 +6,7 @@ import api from '../api'; export const submitAccountNote = createAppAsyncThunk( 'account_note/submit', async (args: { id: string; value: string }, { getState }) => { - // TODO: replace `unknown` with `ApiRelationshipJSON` when it is merged - const response = await api(getState).post( + const response = await api(getState).post( `/api/v1/accounts/${args.id}/note`, { comment: args.value, diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js index 3a85393d6..4a985a41e 100644 --- a/app/javascript/mastodon/actions/accounts.js +++ b/app/javascript/mastodon/actions/accounts.js @@ -1,5 +1,15 @@ import api, { getLinks } from '../api'; +import { + followAccountSuccess, unfollowAccountSuccess, + authorizeFollowRequestSuccess, rejectFollowRequestSuccess, + followAccountRequest, followAccountFail, + unfollowAccountRequest, unfollowAccountFail, + muteAccountSuccess, unmuteAccountSuccess, + blockAccountSuccess, unblockAccountSuccess, + pinAccountSuccess, unpinAccountSuccess, + fetchRelationshipsSuccess, +} from './accounts_typed'; import { importFetchedAccount, importFetchedAccounts } from './importer'; export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST'; @@ -10,36 +20,22 @@ export const ACCOUNT_LOOKUP_REQUEST = 'ACCOUNT_LOOKUP_REQUEST'; export const ACCOUNT_LOOKUP_SUCCESS = 'ACCOUNT_LOOKUP_SUCCESS'; export const ACCOUNT_LOOKUP_FAIL = 'ACCOUNT_LOOKUP_FAIL'; -export const ACCOUNT_FOLLOW_REQUEST = 'ACCOUNT_FOLLOW_REQUEST'; -export const ACCOUNT_FOLLOW_SUCCESS = 'ACCOUNT_FOLLOW_SUCCESS'; -export const ACCOUNT_FOLLOW_FAIL = 'ACCOUNT_FOLLOW_FAIL'; - -export const ACCOUNT_UNFOLLOW_REQUEST = 'ACCOUNT_UNFOLLOW_REQUEST'; -export const ACCOUNT_UNFOLLOW_SUCCESS = 'ACCOUNT_UNFOLLOW_SUCCESS'; -export const ACCOUNT_UNFOLLOW_FAIL = 'ACCOUNT_UNFOLLOW_FAIL'; - export const ACCOUNT_BLOCK_REQUEST = 'ACCOUNT_BLOCK_REQUEST'; -export const ACCOUNT_BLOCK_SUCCESS = 'ACCOUNT_BLOCK_SUCCESS'; export const ACCOUNT_BLOCK_FAIL = 'ACCOUNT_BLOCK_FAIL'; export const ACCOUNT_UNBLOCK_REQUEST = 'ACCOUNT_UNBLOCK_REQUEST'; -export const ACCOUNT_UNBLOCK_SUCCESS = 'ACCOUNT_UNBLOCK_SUCCESS'; export const ACCOUNT_UNBLOCK_FAIL = 'ACCOUNT_UNBLOCK_FAIL'; export const ACCOUNT_MUTE_REQUEST = 'ACCOUNT_MUTE_REQUEST'; -export const ACCOUNT_MUTE_SUCCESS = 'ACCOUNT_MUTE_SUCCESS'; export const ACCOUNT_MUTE_FAIL = 'ACCOUNT_MUTE_FAIL'; export const ACCOUNT_UNMUTE_REQUEST = 'ACCOUNT_UNMUTE_REQUEST'; -export const ACCOUNT_UNMUTE_SUCCESS = 'ACCOUNT_UNMUTE_SUCCESS'; export const ACCOUNT_UNMUTE_FAIL = 'ACCOUNT_UNMUTE_FAIL'; export const ACCOUNT_PIN_REQUEST = 'ACCOUNT_PIN_REQUEST'; -export const ACCOUNT_PIN_SUCCESS = 'ACCOUNT_PIN_SUCCESS'; export const ACCOUNT_PIN_FAIL = 'ACCOUNT_PIN_FAIL'; export const ACCOUNT_UNPIN_REQUEST = 'ACCOUNT_UNPIN_REQUEST'; -export const ACCOUNT_UNPIN_SUCCESS = 'ACCOUNT_UNPIN_SUCCESS'; export const ACCOUNT_UNPIN_FAIL = 'ACCOUNT_UNPIN_FAIL'; export const FOLLOWERS_FETCH_REQUEST = 'FOLLOWERS_FETCH_REQUEST'; @@ -59,7 +55,6 @@ export const FOLLOWING_EXPAND_SUCCESS = 'FOLLOWING_EXPAND_SUCCESS'; export const FOLLOWING_EXPAND_FAIL = 'FOLLOWING_EXPAND_FAIL'; export const RELATIONSHIPS_FETCH_REQUEST = 'RELATIONSHIPS_FETCH_REQUEST'; -export const RELATIONSHIPS_FETCH_SUCCESS = 'RELATIONSHIPS_FETCH_SUCCESS'; export const RELATIONSHIPS_FETCH_FAIL = 'RELATIONSHIPS_FETCH_FAIL'; export const FOLLOW_REQUESTS_FETCH_REQUEST = 'FOLLOW_REQUESTS_FETCH_REQUEST'; @@ -71,15 +66,15 @@ export const FOLLOW_REQUESTS_EXPAND_SUCCESS = 'FOLLOW_REQUESTS_EXPAND_SUCCESS'; export const FOLLOW_REQUESTS_EXPAND_FAIL = 'FOLLOW_REQUESTS_EXPAND_FAIL'; export const FOLLOW_REQUEST_AUTHORIZE_REQUEST = 'FOLLOW_REQUEST_AUTHORIZE_REQUEST'; -export const FOLLOW_REQUEST_AUTHORIZE_SUCCESS = 'FOLLOW_REQUEST_AUTHORIZE_SUCCESS'; export const FOLLOW_REQUEST_AUTHORIZE_FAIL = 'FOLLOW_REQUEST_AUTHORIZE_FAIL'; export const FOLLOW_REQUEST_REJECT_REQUEST = 'FOLLOW_REQUEST_REJECT_REQUEST'; -export const FOLLOW_REQUEST_REJECT_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS'; export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL'; export const ACCOUNT_REVEAL = 'ACCOUNT_REVEAL'; +export * from './accounts_typed'; + export function fetchAccount(id) { return (dispatch, getState) => { dispatch(fetchRelationships([id])); @@ -149,12 +144,12 @@ export function followAccount(id, options = { reblogs: true }) { const alreadyFollowing = getState().getIn(['relationships', id, 'following']); const locked = getState().getIn(['accounts', id, 'locked'], false); - dispatch(followAccountRequest(id, locked)); + dispatch(followAccountRequest({ id, locked })); api(getState).post(`/api/v1/accounts/${id}/follow`, options).then(response => { - dispatch(followAccountSuccess(response.data, alreadyFollowing)); + dispatch(followAccountSuccess({relationship: response.data, alreadyFollowing})); }).catch(error => { - dispatch(followAccountFail(error, locked)); + dispatch(followAccountFail({ id, error, locked })); }); }; } @@ -164,74 +159,22 @@ export function unfollowAccount(id) { dispatch(unfollowAccountRequest(id)); api(getState).post(`/api/v1/accounts/${id}/unfollow`).then(response => { - dispatch(unfollowAccountSuccess(response.data, getState().get('statuses'))); + dispatch(unfollowAccountSuccess({relationship: response.data, statuses: getState().get('statuses')})); }).catch(error => { - dispatch(unfollowAccountFail(error)); + dispatch(unfollowAccountFail({ id, error })); }); }; } -export function followAccountRequest(id, locked) { - return { - type: ACCOUNT_FOLLOW_REQUEST, - id, - locked, - skipLoading: true, - }; -} - -export function followAccountSuccess(relationship, alreadyFollowing) { - return { - type: ACCOUNT_FOLLOW_SUCCESS, - relationship, - alreadyFollowing, - skipLoading: true, - }; -} - -export function followAccountFail(error, locked) { - return { - type: ACCOUNT_FOLLOW_FAIL, - error, - locked, - skipLoading: true, - }; -} - -export function unfollowAccountRequest(id) { - return { - type: ACCOUNT_UNFOLLOW_REQUEST, - id, - skipLoading: true, - }; -} - -export function unfollowAccountSuccess(relationship, statuses) { - return { - type: ACCOUNT_UNFOLLOW_SUCCESS, - relationship, - statuses, - skipLoading: true, - }; -} - -export function unfollowAccountFail(error) { - return { - type: ACCOUNT_UNFOLLOW_FAIL, - error, - skipLoading: true, - }; -} - export function blockAccount(id) { return (dispatch, getState) => { dispatch(blockAccountRequest(id)); api(getState).post(`/api/v1/accounts/${id}/block`).then(response => { // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers - dispatch(blockAccountSuccess(response.data, getState().get('statuses'))); + dispatch(blockAccountSuccess({ relationship: response.data, statuses: getState().get('statuses') })); }).catch(error => { - dispatch(blockAccountFail(id, error)); + dispatch(blockAccountFail({ id, error })); }); }; } @@ -241,9 +184,9 @@ export function unblockAccount(id) { dispatch(unblockAccountRequest(id)); api(getState).post(`/api/v1/accounts/${id}/unblock`).then(response => { - dispatch(unblockAccountSuccess(response.data)); + dispatch(unblockAccountSuccess({ relationship: response.data })); }).catch(error => { - dispatch(unblockAccountFail(id, error)); + dispatch(unblockAccountFail({ id, error })); }); }; } @@ -254,15 +197,6 @@ export function blockAccountRequest(id) { id, }; } - -export function blockAccountSuccess(relationship, statuses) { - return { - type: ACCOUNT_BLOCK_SUCCESS, - relationship, - statuses, - }; -} - export function blockAccountFail(error) { return { type: ACCOUNT_BLOCK_FAIL, @@ -277,13 +211,6 @@ export function unblockAccountRequest(id) { }; } -export function unblockAccountSuccess(relationship) { - return { - type: ACCOUNT_UNBLOCK_SUCCESS, - relationship, - }; -} - export function unblockAccountFail(error) { return { type: ACCOUNT_UNBLOCK_FAIL, @@ -298,9 +225,9 @@ export function muteAccount(id, notifications, duration=0) { api(getState).post(`/api/v1/accounts/${id}/mute`, { notifications, duration }).then(response => { // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers - dispatch(muteAccountSuccess(response.data, getState().get('statuses'))); + dispatch(muteAccountSuccess({ relationship: response.data, statuses: getState().get('statuses') })); }).catch(error => { - dispatch(muteAccountFail(id, error)); + dispatch(muteAccountFail({ id, error })); }); }; } @@ -310,9 +237,9 @@ export function unmuteAccount(id) { dispatch(unmuteAccountRequest(id)); api(getState).post(`/api/v1/accounts/${id}/unmute`).then(response => { - dispatch(unmuteAccountSuccess(response.data)); + dispatch(unmuteAccountSuccess({ relationship: response.data })); }).catch(error => { - dispatch(unmuteAccountFail(id, error)); + dispatch(unmuteAccountFail({ id, error })); }); }; } @@ -324,14 +251,6 @@ export function muteAccountRequest(id) { }; } -export function muteAccountSuccess(relationship, statuses) { - return { - type: ACCOUNT_MUTE_SUCCESS, - relationship, - statuses, - }; -} - export function muteAccountFail(error) { return { type: ACCOUNT_MUTE_FAIL, @@ -346,13 +265,6 @@ export function unmuteAccountRequest(id) { }; } -export function unmuteAccountSuccess(relationship) { - return { - type: ACCOUNT_UNMUTE_SUCCESS, - relationship, - }; -} - export function unmuteAccountFail(error) { return { type: ACCOUNT_UNMUTE_FAIL, @@ -549,7 +461,7 @@ export function fetchRelationships(accountIds) { dispatch(fetchRelationshipsRequest(newAccountIds)); api(getState).get(`/api/v1/accounts/relationships?${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => { - dispatch(fetchRelationshipsSuccess(response.data)); + dispatch(fetchRelationshipsSuccess({ relationships: response.data })); }).catch(error => { dispatch(fetchRelationshipsFail(error)); }); @@ -564,14 +476,6 @@ export function fetchRelationshipsRequest(ids) { }; } -export function fetchRelationshipsSuccess(relationships) { - return { - type: RELATIONSHIPS_FETCH_SUCCESS, - relationships, - skipLoading: true, - }; -} - export function fetchRelationshipsFail(error) { return { type: RELATIONSHIPS_FETCH_FAIL, @@ -659,7 +563,7 @@ export function authorizeFollowRequest(id) { api(getState) .post(`/api/v1/follow_requests/${id}/authorize`) - .then(() => dispatch(authorizeFollowRequestSuccess(id))) + .then(() => dispatch(authorizeFollowRequestSuccess({ id }))) .catch(error => dispatch(authorizeFollowRequestFail(id, error))); }; } @@ -671,13 +575,6 @@ export function authorizeFollowRequestRequest(id) { }; } -export function authorizeFollowRequestSuccess(id) { - return { - type: FOLLOW_REQUEST_AUTHORIZE_SUCCESS, - id, - }; -} - export function authorizeFollowRequestFail(id, error) { return { type: FOLLOW_REQUEST_AUTHORIZE_FAIL, @@ -693,7 +590,7 @@ export function rejectFollowRequest(id) { api(getState) .post(`/api/v1/follow_requests/${id}/reject`) - .then(() => dispatch(rejectFollowRequestSuccess(id))) + .then(() => dispatch(rejectFollowRequestSuccess({ id }))) .catch(error => dispatch(rejectFollowRequestFail(id, error))); }; } @@ -705,13 +602,6 @@ export function rejectFollowRequestRequest(id) { }; } -export function rejectFollowRequestSuccess(id) { - return { - type: FOLLOW_REQUEST_REJECT_SUCCESS, - id, - }; -} - export function rejectFollowRequestFail(id, error) { return { type: FOLLOW_REQUEST_REJECT_FAIL, @@ -725,7 +615,7 @@ export function pinAccount(id) { dispatch(pinAccountRequest(id)); api(getState).post(`/api/v1/accounts/${id}/pin`).then(response => { - dispatch(pinAccountSuccess(response.data)); + dispatch(pinAccountSuccess({ relationship: response.data })); }).catch(error => { dispatch(pinAccountFail(error)); }); @@ -737,7 +627,7 @@ export function unpinAccount(id) { dispatch(unpinAccountRequest(id)); api(getState).post(`/api/v1/accounts/${id}/unpin`).then(response => { - dispatch(unpinAccountSuccess(response.data)); + dispatch(unpinAccountSuccess({ relationship: response.data })); }).catch(error => { dispatch(unpinAccountFail(error)); }); @@ -751,13 +641,6 @@ export function pinAccountRequest(id) { }; } -export function pinAccountSuccess(relationship) { - return { - type: ACCOUNT_PIN_SUCCESS, - relationship, - }; -} - export function pinAccountFail(error) { return { type: ACCOUNT_PIN_FAIL, @@ -772,21 +655,9 @@ export function unpinAccountRequest(id) { }; } -export function unpinAccountSuccess(relationship) { - return { - type: ACCOUNT_UNPIN_SUCCESS, - relationship, - }; -} - export function unpinAccountFail(error) { return { type: ACCOUNT_UNPIN_FAIL, error, }; } - -export const revealAccount = id => ({ - type: ACCOUNT_REVEAL, - id, -}); diff --git a/app/javascript/mastodon/actions/accounts_typed.ts b/app/javascript/mastodon/actions/accounts_typed.ts new file mode 100644 index 000000000..b908e7528 --- /dev/null +++ b/app/javascript/mastodon/actions/accounts_typed.ts @@ -0,0 +1,97 @@ +import { createAction } from '@reduxjs/toolkit'; + +import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; +import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships'; + +export const revealAccount = createAction<{ + id: string; +}>('accounts/revealAccount'); + +export const importAccounts = createAction<{ accounts: ApiAccountJSON[] }>( + 'accounts/importAccounts', +); + +function actionWithSkipLoadingTrue(args: Args) { + return { + payload: { + ...args, + skipLoading: true, + }, + }; +} + +export const followAccountSuccess = createAction( + 'accounts/followAccountSuccess', + actionWithSkipLoadingTrue<{ + relationship: ApiRelationshipJSON; + alreadyFollowing: boolean; + }>, +); + +export const unfollowAccountSuccess = createAction( + 'accounts/unfollowAccountSuccess', + actionWithSkipLoadingTrue<{ + relationship: ApiRelationshipJSON; + statuses: unknown; + alreadyFollowing?: boolean; + }>, +); + +export const authorizeFollowRequestSuccess = createAction<{ id: string }>( + 'accounts/followRequestAuthorizeSuccess', +); + +export const rejectFollowRequestSuccess = createAction<{ id: string }>( + 'accounts/followRequestRejectSuccess', +); + +export const followAccountRequest = createAction( + 'accounts/followRequest', + actionWithSkipLoadingTrue<{ id: string; locked: boolean }>, +); + +export const followAccountFail = createAction( + 'accounts/followFail', + actionWithSkipLoadingTrue<{ id: string; error: string; locked: boolean }>, +); + +export const unfollowAccountRequest = createAction( + 'accounts/unfollowRequest', + actionWithSkipLoadingTrue<{ id: string }>, +); + +export const unfollowAccountFail = createAction( + 'accounts/unfollowFail', + actionWithSkipLoadingTrue<{ id: string; error: string }>, +); + +export const blockAccountSuccess = createAction<{ + relationship: ApiRelationshipJSON; + statuses: unknown; +}>('accounts/blockSuccess'); + +export const unblockAccountSuccess = createAction<{ + relationship: ApiRelationshipJSON; +}>('accounts/unblockSuccess'); + +export const muteAccountSuccess = createAction<{ + relationship: ApiRelationshipJSON; + statuses: unknown; +}>('accounts/muteSuccess'); + +export const unmuteAccountSuccess = createAction<{ + relationship: ApiRelationshipJSON; +}>('accounts/unmuteSuccess'); + +export const pinAccountSuccess = createAction<{ + relationship: ApiRelationshipJSON; +}>('accounts/pinSuccess'); + +export const unpinAccountSuccess = createAction<{ + relationship: ApiRelationshipJSON; +}>('accounts/unpinSuccess'); + +export const fetchRelationshipsSuccess = createAction( + 'relationships/fetchSuccess', + actionWithSkipLoadingTrue<{ relationships: ApiRelationshipJSON[] }>, +); diff --git a/app/javascript/mastodon/actions/domain_blocks.js b/app/javascript/mastodon/actions/domain_blocks.js index d06de20a2..718002613 100644 --- a/app/javascript/mastodon/actions/domain_blocks.js +++ b/app/javascript/mastodon/actions/domain_blocks.js @@ -1,11 +1,13 @@ import api, { getLinks } from '../api'; +import { blockDomainSuccess, unblockDomainSuccess } from "./domain_blocks_typed"; + +export * from "./domain_blocks_typed"; + export const DOMAIN_BLOCK_REQUEST = 'DOMAIN_BLOCK_REQUEST'; -export const DOMAIN_BLOCK_SUCCESS = 'DOMAIN_BLOCK_SUCCESS'; export const DOMAIN_BLOCK_FAIL = 'DOMAIN_BLOCK_FAIL'; export const DOMAIN_UNBLOCK_REQUEST = 'DOMAIN_UNBLOCK_REQUEST'; -export const DOMAIN_UNBLOCK_SUCCESS = 'DOMAIN_UNBLOCK_SUCCESS'; export const DOMAIN_UNBLOCK_FAIL = 'DOMAIN_UNBLOCK_FAIL'; export const DOMAIN_BLOCKS_FETCH_REQUEST = 'DOMAIN_BLOCKS_FETCH_REQUEST'; @@ -24,7 +26,7 @@ export function blockDomain(domain) { const at_domain = '@' + domain; const accounts = getState().get('accounts').filter(item => item.get('acct').endsWith(at_domain)).valueSeq().map(item => item.get('id')); - dispatch(blockDomainSuccess(domain, accounts)); + dispatch(blockDomainSuccess({ domain, accounts })); }).catch(err => { dispatch(blockDomainFail(domain, err)); }); @@ -38,14 +40,6 @@ export function blockDomainRequest(domain) { }; } -export function blockDomainSuccess(domain, accounts) { - return { - type: DOMAIN_BLOCK_SUCCESS, - domain, - accounts, - }; -} - export function blockDomainFail(domain, error) { return { type: DOMAIN_BLOCK_FAIL, @@ -61,7 +55,7 @@ export function unblockDomain(domain) { api(getState).delete('/api/v1/domain_blocks', { params: { domain } }).then(() => { const at_domain = '@' + domain; const accounts = getState().get('accounts').filter(item => item.get('acct').endsWith(at_domain)).valueSeq().map(item => item.get('id')); - dispatch(unblockDomainSuccess(domain, accounts)); + dispatch(unblockDomainSuccess({ domain, accounts })); }).catch(err => { dispatch(unblockDomainFail(domain, err)); }); @@ -75,14 +69,6 @@ export function unblockDomainRequest(domain) { }; } -export function unblockDomainSuccess(domain, accounts) { - return { - type: DOMAIN_UNBLOCK_SUCCESS, - domain, - accounts, - }; -} - export function unblockDomainFail(domain, error) { return { type: DOMAIN_UNBLOCK_FAIL, diff --git a/app/javascript/mastodon/actions/domain_blocks_typed.ts b/app/javascript/mastodon/actions/domain_blocks_typed.ts new file mode 100644 index 000000000..08e0b4a17 --- /dev/null +++ b/app/javascript/mastodon/actions/domain_blocks_typed.ts @@ -0,0 +1,13 @@ +import { createAction } from '@reduxjs/toolkit'; + +import type { Account } from 'mastodon/models/account'; + +export const blockDomainSuccess = createAction<{ + domain: string; + accounts: Account[]; +}>('domain_blocks/blockSuccess'); + +export const unblockDomainSuccess = createAction<{ + domain: string; + accounts: Account[]; +}>('domain_blocks/unblockSuccess'); diff --git a/app/javascript/mastodon/actions/importer/index.js b/app/javascript/mastodon/actions/importer/index.js index 369be6b8f..16f191b58 100644 --- a/app/javascript/mastodon/actions/importer/index.js +++ b/app/javascript/mastodon/actions/importer/index.js @@ -1,7 +1,7 @@ -import { normalizeAccount, normalizeStatus, normalizePoll } from './normalizer'; +import { importAccounts } from '../accounts_typed'; + +import { normalizeStatus, normalizePoll } from './normalizer'; -export const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT'; -export const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT'; export const STATUS_IMPORT = 'STATUS_IMPORT'; export const STATUSES_IMPORT = 'STATUSES_IMPORT'; export const POLLS_IMPORT = 'POLLS_IMPORT'; @@ -13,14 +13,6 @@ function pushUnique(array, object) { } } -export function importAccount(account) { - return { type: ACCOUNT_IMPORT, account }; -} - -export function importAccounts(accounts) { - return { type: ACCOUNTS_IMPORT, accounts }; -} - export function importStatus(status) { return { type: STATUS_IMPORT, status }; } @@ -45,7 +37,7 @@ export function importFetchedAccounts(accounts) { const normalAccounts = []; function processAccount(account) { - pushUnique(normalAccounts, normalizeAccount(account)); + pushUnique(normalAccounts, account); if (account.moved) { processAccount(account.moved); @@ -54,7 +46,7 @@ export function importFetchedAccounts(accounts) { accounts.forEach(processAccount); - return importAccounts(normalAccounts); + return importAccounts({ accounts: normalAccounts }); } export function importFetchedStatus(status) { diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js index a72142a86..b5a30343e 100644 --- a/app/javascript/mastodon/actions/importer/normalizer.js +++ b/app/javascript/mastodon/actions/importer/normalizer.js @@ -2,7 +2,6 @@ import escapeTextContentForBrowser from 'escape-html'; import emojify from '../../features/emoji/emoji'; import { expandSpoilers } from '../../initial_state'; -import { unescapeHTML } from '../../utils/html'; const domParser = new DOMParser(); @@ -17,32 +16,6 @@ export function searchTextFromRawStatus (status) { return domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; } -export function normalizeAccount(account) { - account = { ...account }; - - const emojiMap = makeEmojiMap(account.emojis); - const displayName = account.display_name.trim().length === 0 ? account.username : account.display_name; - - account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap); - account.note_emojified = emojify(account.note, emojiMap); - account.note_plain = unescapeHTML(account.note); - - if (account.fields) { - account.fields = account.fields.map(pair => ({ - ...pair, - name_emojified: emojify(escapeTextContentForBrowser(pair.name), emojiMap), - value_emojified: emojify(pair.value, emojiMap), - value_plain: unescapeHTML(pair.value), - })); - } - - if (account.moved) { - account.moved = account.moved.id; - } - - return account; -} - export function normalizeFilterResult(result) { const normalResult = { ...result }; diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index 02fe10ba5..878ff5d89 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -18,10 +18,12 @@ import { importFetchedStatuses, } from './importer'; import { submitMarkers } from './markers'; +import { notificationsUpdate } from "./notifications_typed"; import { register as registerPushNotifications } from './push_notifications'; import { saveSettings } from './settings'; -export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; +export * from "./notifications_typed"; + export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP'; export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST'; @@ -95,12 +97,8 @@ export function updateNotifications(notification, intlMessages, intlLocale) { dispatch(importFetchedAccount(notification.report.target_account)); } - dispatch({ - type: NOTIFICATIONS_UPDATE, - notification, - usePendingItems: preferPendingItems, - meta: (playSound && !filtered) ? { sound: 'boop' } : undefined, - }); + + dispatch(notificationsUpdate(notification, preferPendingItems, playSound && !filtered)); fetchRelatedRelationships(dispatch, [notification]); } else if (playSound && !filtered) { diff --git a/app/javascript/mastodon/actions/notifications_typed.ts b/app/javascript/mastodon/actions/notifications_typed.ts new file mode 100644 index 000000000..7e51fa51e --- /dev/null +++ b/app/javascript/mastodon/actions/notifications_typed.ts @@ -0,0 +1,23 @@ +import { createAction } from '@reduxjs/toolkit'; + +import type { ApiAccountJSON } from '../api_types/accounts'; +// To be replaced once ApiNotificationJSON type exists +interface FakeApiNotificationJSON { + type: string; + account: ApiAccountJSON; +} + +export const notificationsUpdate = createAction( + 'notifications/update', + ({ + playSound, + ...args + }: { + notification: FakeApiNotificationJSON; + usePendingItems: boolean; + playSound: boolean; + }) => ({ + payload: args, + meta: { playSound: playSound ? { sound: 'boop' } : undefined }, + }), +); diff --git a/app/javascript/mastodon/actions/store.js b/app/javascript/mastodon/actions/store.js index 682b0f5db..8ab75cdc4 100644 --- a/app/javascript/mastodon/actions/store.js +++ b/app/javascript/mastodon/actions/store.js @@ -11,6 +11,7 @@ const convertState = rawState => fromJS(rawState, (k, v) => Iterable.isIndexed(v) ? v.toList() : v.toMap()); + export function hydrateStore(rawState) { return dispatch => { const state = convertState(rawState); diff --git a/app/javascript/mastodon/api_types/accounts.ts b/app/javascript/mastodon/api_types/accounts.ts index 9d740c96d..ce55dc604 100644 --- a/app/javascript/mastodon/api_types/accounts.ts +++ b/app/javascript/mastodon/api_types/accounts.ts @@ -31,9 +31,9 @@ export interface ApiAccountJSON { id: string; last_status_at: string; locked: boolean; - noindex: boolean; + noindex?: boolean; note: string; - roles: ApiAccountJSON[]; + roles?: ApiAccountJSON[]; statuses_count: number; uri: string; url: string; diff --git a/app/javascript/mastodon/components/account.jsx b/app/javascript/mastodon/components/account.jsx index e4e3c88b6..aa18ce79a 100644 --- a/app/javascript/mastodon/components/account.jsx +++ b/app/javascript/mastodon/components/account.jsx @@ -36,7 +36,7 @@ class Account extends ImmutablePureComponent { static propTypes = { size: PropTypes.number, - account: ImmutablePropTypes.map, + account: ImmutablePropTypes.record, onFollow: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired, onMute: PropTypes.func.isRequired, diff --git a/app/javascript/mastodon/components/avatar.tsx b/app/javascript/mastodon/components/avatar.tsx index 5f9bb390e..77bd42340 100644 --- a/app/javascript/mastodon/components/avatar.tsx +++ b/app/javascript/mastodon/components/avatar.tsx @@ -1,7 +1,8 @@ import classNames from 'classnames'; +import type { Account } from 'mastodon/models/account'; + import { useHovering } from '../../hooks/useHovering'; -import type { Account } from '../../types/resources'; import { autoPlayGif } from '../initial_state'; interface Props { diff --git a/app/javascript/mastodon/components/avatar_overlay.tsx b/app/javascript/mastodon/components/avatar_overlay.tsx index 61de9d0be..f98cfcc38 100644 --- a/app/javascript/mastodon/components/avatar_overlay.tsx +++ b/app/javascript/mastodon/components/avatar_overlay.tsx @@ -1,5 +1,6 @@ +import type { Account } from 'mastodon/models/account'; + import { useHovering } from '../../hooks/useHovering'; -import type { Account } from '../../types/resources'; import { autoPlayGif } from '../initial_state'; interface Props { diff --git a/app/javascript/mastodon/components/display_name.tsx b/app/javascript/mastodon/components/display_name.tsx index 82a42bb02..840924482 100644 --- a/app/javascript/mastodon/components/display_name.tsx +++ b/app/javascript/mastodon/components/display_name.tsx @@ -2,7 +2,8 @@ import React from 'react'; import type { List } from 'immutable'; -import type { Account } from '../../types/resources'; +import type { Account } from 'mastodon/models/account'; + import { autoPlayGif } from '../initial_state'; import { Skeleton } from './skeleton'; diff --git a/app/javascript/mastodon/components/inline_account.jsx b/app/javascript/mastodon/components/inline_account.jsx index f9767c29d..792c41228 100644 --- a/app/javascript/mastodon/components/inline_account.jsx +++ b/app/javascript/mastodon/components/inline_account.jsx @@ -19,7 +19,7 @@ const makeMapStateToProps = () => { class InlineAccount extends PureComponent { static propTypes = { - account: ImmutablePropTypes.map.isRequired, + account: ImmutablePropTypes.record.isRequired, }; render () { diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx index e065b4816..c141ee2da 100644 --- a/app/javascript/mastodon/components/status.jsx +++ b/app/javascript/mastodon/components/status.jsx @@ -80,7 +80,7 @@ class Status extends ImmutablePureComponent { static propTypes = { status: ImmutablePropTypes.map, - account: ImmutablePropTypes.map, + account: ImmutablePropTypes.record, previousId: PropTypes.string, nextInReplyToId: PropTypes.string, rootId: PropTypes.string, diff --git a/app/javascript/mastodon/features/account/components/account_note.jsx b/app/javascript/mastodon/features/account/components/account_note.jsx index bab523acf..272a4ee31 100644 --- a/app/javascript/mastodon/features/account/components/account_note.jsx +++ b/app/javascript/mastodon/features/account/components/account_note.jsx @@ -49,7 +49,7 @@ class InlineAlert extends PureComponent { class AccountNote extends ImmutablePureComponent { static propTypes = { - account: ImmutablePropTypes.map.isRequired, + account: ImmutablePropTypes.record.isRequired, value: PropTypes.string, onSave: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, diff --git a/app/javascript/mastodon/features/account/components/featured_tags.jsx b/app/javascript/mastodon/features/account/components/featured_tags.jsx index bbbec1b86..4d7dd8656 100644 --- a/app/javascript/mastodon/features/account/components/featured_tags.jsx +++ b/app/javascript/mastodon/features/account/components/featured_tags.jsx @@ -15,7 +15,7 @@ const messages = defineMessages({ class FeaturedTags extends ImmutablePureComponent { static propTypes = { - account: ImmutablePropTypes.map, + account: ImmutablePropTypes.record, featuredTags: ImmutablePropTypes.list, tagged: PropTypes.string, intl: PropTypes.object.isRequired, diff --git a/app/javascript/mastodon/features/account/components/follow_request_note.jsx b/app/javascript/mastodon/features/account/components/follow_request_note.jsx index 0e597a705..685c282df 100644 --- a/app/javascript/mastodon/features/account/components/follow_request_note.jsx +++ b/app/javascript/mastodon/features/account/components/follow_request_note.jsx @@ -11,7 +11,7 @@ import { Icon } from 'mastodon/components/icon'; export default class FollowRequestNote extends ImmutablePureComponent { static propTypes = { - account: ImmutablePropTypes.map.isRequired, + account: ImmutablePropTypes.record.isRequired, }; render () { diff --git a/app/javascript/mastodon/features/account/components/header.jsx b/app/javascript/mastodon/features/account/components/header.jsx index 76074225a..e546c7569 100644 --- a/app/javascript/mastodon/features/account/components/header.jsx +++ b/app/javascript/mastodon/features/account/components/header.jsx @@ -91,7 +91,7 @@ const dateFormatOptions = { class Header extends ImmutablePureComponent { static propTypes = { - account: ImmutablePropTypes.map, + account: ImmutablePropTypes.record, identity_props: ImmutablePropTypes.list, onFollow: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired, diff --git a/app/javascript/mastodon/features/account_timeline/components/header.jsx b/app/javascript/mastodon/features/account_timeline/components/header.jsx index aede7e495..7de8d3771 100644 --- a/app/javascript/mastodon/features/account_timeline/components/header.jsx +++ b/app/javascript/mastodon/features/account_timeline/components/header.jsx @@ -17,7 +17,7 @@ import MovedNote from './moved_note'; class Header extends ImmutablePureComponent { static propTypes = { - account: ImmutablePropTypes.map, + account: ImmutablePropTypes.record, onFollow: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired, diff --git a/app/javascript/mastodon/features/account_timeline/components/limited_account_hint.jsx b/app/javascript/mastodon/features/account_timeline/components/limited_account_hint.jsx deleted file mode 100644 index 59b735823..000000000 --- a/app/javascript/mastodon/features/account_timeline/components/limited_account_hint.jsx +++ /dev/null @@ -1,40 +0,0 @@ -import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -import { FormattedMessage } from 'react-intl'; - -import { connect } from 'react-redux'; - -import { revealAccount } from 'mastodon/actions/accounts'; -import { Button } from 'mastodon/components/button'; -import { domain } from 'mastodon/initial_state'; - -const mapDispatchToProps = (dispatch, { accountId }) => ({ - - reveal () { - dispatch(revealAccount(accountId)); - }, - -}); - -class LimitedAccountHint extends PureComponent { - - static propTypes = { - accountId: PropTypes.string.isRequired, - reveal: PropTypes.func, - }; - - render () { - const { reveal } = this.props; - - return ( -
-

- -
- ); - } - -} - -export default connect(() => {}, mapDispatchToProps)(LimitedAccountHint); diff --git a/app/javascript/mastodon/features/account_timeline/components/limited_account_hint.tsx b/app/javascript/mastodon/features/account_timeline/components/limited_account_hint.tsx new file mode 100644 index 000000000..f06bf574a --- /dev/null +++ b/app/javascript/mastodon/features/account_timeline/components/limited_account_hint.tsx @@ -0,0 +1,35 @@ +import { useCallback } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import { revealAccount } from 'mastodon/actions/accounts_typed'; +import { Button } from 'mastodon/components/button'; +import { domain } from 'mastodon/initial_state'; +import { useAppDispatch } from 'mastodon/store'; + +export const LimitedAccountHint: React.FC<{ accountId: string }> = ({ + accountId, +}) => { + const dispatch = useAppDispatch(); + const reveal = useCallback(() => { + dispatch(revealAccount({ id: accountId })); + }, [dispatch, accountId]); + + return ( +
+

+ +

+ +
+ ); +}; diff --git a/app/javascript/mastodon/features/account_timeline/index.jsx b/app/javascript/mastodon/features/account_timeline/index.jsx index 5dae66b46..5ec029593 100644 --- a/app/javascript/mastodon/features/account_timeline/index.jsx +++ b/app/javascript/mastodon/features/account_timeline/index.jsx @@ -21,7 +21,7 @@ import { LoadingIndicator } from '../../components/loading_indicator'; import StatusList from '../../components/status_list'; import Column from '../ui/components/column'; -import LimitedAccountHint from './components/limited_account_hint'; +import { LimitedAccountHint } from './components/limited_account_hint'; import HeaderContainer from './containers/header_container'; const emptyList = ImmutableList(); diff --git a/app/javascript/mastodon/features/compose/components/action_bar.jsx b/app/javascript/mastodon/features/compose/components/action_bar.jsx index f7488cf55..3e2109092 100644 --- a/app/javascript/mastodon/features/compose/components/action_bar.jsx +++ b/app/javascript/mastodon/features/compose/components/action_bar.jsx @@ -28,7 +28,7 @@ const messages = defineMessages({ class ActionBar extends PureComponent { static propTypes = { - account: ImmutablePropTypes.map.isRequired, + account: ImmutablePropTypes.record.isRequired, onLogout: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, }; diff --git a/app/javascript/mastodon/features/compose/components/autosuggest_account.jsx b/app/javascript/mastodon/features/compose/components/autosuggest_account.jsx index ebda0590e..0a73bc102 100644 --- a/app/javascript/mastodon/features/compose/components/autosuggest_account.jsx +++ b/app/javascript/mastodon/features/compose/components/autosuggest_account.jsx @@ -7,7 +7,7 @@ import { DisplayName } from '../../../components/display_name'; export default class AutosuggestAccount extends ImmutablePureComponent { static propTypes = { - account: ImmutablePropTypes.map.isRequired, + account: ImmutablePropTypes.record.isRequired, }; render () { diff --git a/app/javascript/mastodon/features/compose/components/navigation_bar.jsx b/app/javascript/mastodon/features/compose/components/navigation_bar.jsx index e842ab1f8..2f0bb79f8 100644 --- a/app/javascript/mastodon/features/compose/components/navigation_bar.jsx +++ b/app/javascript/mastodon/features/compose/components/navigation_bar.jsx @@ -14,7 +14,7 @@ import ActionBar from './action_bar'; export default class NavigationBar extends ImmutablePureComponent { static propTypes = { - account: ImmutablePropTypes.map.isRequired, + account: ImmutablePropTypes.record.isRequired, onLogout: PropTypes.func.isRequired, onClose: PropTypes.func, }; diff --git a/app/javascript/mastodon/features/directory/components/account_card.jsx b/app/javascript/mastodon/features/directory/components/account_card.jsx index a5e5717b3..ff1f8a653 100644 --- a/app/javascript/mastodon/features/directory/components/account_card.jsx +++ b/app/javascript/mastodon/features/directory/components/account_card.jsx @@ -102,7 +102,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ class AccountCard extends ImmutablePureComponent { static propTypes = { - account: ImmutablePropTypes.map.isRequired, + account: ImmutablePropTypes.record.isRequired, intl: PropTypes.object.isRequired, onFollow: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired, diff --git a/app/javascript/mastodon/features/follow_requests/components/account_authorize.jsx b/app/javascript/mastodon/features/follow_requests/components/account_authorize.jsx index b5dfe510e..ca2b45414 100644 --- a/app/javascript/mastodon/features/follow_requests/components/account_authorize.jsx +++ b/app/javascript/mastodon/features/follow_requests/components/account_authorize.jsx @@ -22,7 +22,7 @@ const messages = defineMessages({ class AccountAuthorize extends ImmutablePureComponent { static propTypes = { - account: ImmutablePropTypes.map.isRequired, + account: ImmutablePropTypes.record.isRequired, onAuthorize: PropTypes.func.isRequired, onReject: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, diff --git a/app/javascript/mastodon/features/followers/index.jsx b/app/javascript/mastodon/features/followers/index.jsx index fc0ce8ab3..e50b2171a 100644 --- a/app/javascript/mastodon/features/followers/index.jsx +++ b/app/javascript/mastodon/features/followers/index.jsx @@ -23,7 +23,7 @@ import { ColumnBackButton } from '../../components/column_back_button'; import { LoadingIndicator } from '../../components/loading_indicator'; import ScrollableList from '../../components/scrollable_list'; import AccountContainer from '../../containers/account_container'; -import LimitedAccountHint from '../account_timeline/components/limited_account_hint'; +import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint'; import HeaderContainer from '../account_timeline/containers/header_container'; import Column from '../ui/components/column'; diff --git a/app/javascript/mastodon/features/following/index.jsx b/app/javascript/mastodon/features/following/index.jsx index fb02d1795..73e77aadd 100644 --- a/app/javascript/mastodon/features/following/index.jsx +++ b/app/javascript/mastodon/features/following/index.jsx @@ -23,7 +23,7 @@ import { ColumnBackButton } from '../../components/column_back_button'; import { LoadingIndicator } from '../../components/loading_indicator'; import ScrollableList from '../../components/scrollable_list'; import AccountContainer from '../../containers/account_container'; -import LimitedAccountHint from '../account_timeline/components/limited_account_hint'; +import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint'; import HeaderContainer from '../account_timeline/containers/header_container'; import Column from '../ui/components/column'; diff --git a/app/javascript/mastodon/features/list_adder/components/account.jsx b/app/javascript/mastodon/features/list_adder/components/account.jsx index 31a2e9637..94a90726e 100644 --- a/app/javascript/mastodon/features/list_adder/components/account.jsx +++ b/app/javascript/mastodon/features/list_adder/components/account.jsx @@ -21,7 +21,7 @@ const makeMapStateToProps = () => { class Account extends ImmutablePureComponent { static propTypes = { - account: ImmutablePropTypes.map.isRequired, + account: ImmutablePropTypes.record.isRequired, }; render () { diff --git a/app/javascript/mastodon/features/list_editor/components/account.jsx b/app/javascript/mastodon/features/list_editor/components/account.jsx index f38c7d93a..18d5e905c 100644 --- a/app/javascript/mastodon/features/list_editor/components/account.jsx +++ b/app/javascript/mastodon/features/list_editor/components/account.jsx @@ -39,7 +39,7 @@ const mapDispatchToProps = (dispatch, { accountId }) => ({ class Account extends ImmutablePureComponent { static propTypes = { - account: ImmutablePropTypes.map.isRequired, + account: ImmutablePropTypes.record.isRequired, intl: PropTypes.object.isRequired, onRemove: PropTypes.func.isRequired, onAdd: PropTypes.func.isRequired, diff --git a/app/javascript/mastodon/features/notifications/components/follow_request.jsx b/app/javascript/mastodon/features/notifications/components/follow_request.jsx index c10633bee..03420b6c0 100644 --- a/app/javascript/mastodon/features/notifications/components/follow_request.jsx +++ b/app/javascript/mastodon/features/notifications/components/follow_request.jsx @@ -22,7 +22,7 @@ const messages = defineMessages({ class FollowRequest extends ImmutablePureComponent { static propTypes = { - account: ImmutablePropTypes.map.isRequired, + account: ImmutablePropTypes.record.isRequired, onAuthorize: PropTypes.func.isRequired, onReject: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, diff --git a/app/javascript/mastodon/features/notifications/components/report.jsx b/app/javascript/mastodon/features/notifications/components/report.jsx index cb50b62cd..52d6bfee9 100644 --- a/app/javascript/mastodon/features/notifications/components/report.jsx +++ b/app/javascript/mastodon/features/notifications/components/report.jsx @@ -20,7 +20,7 @@ const messages = defineMessages({ class Report extends ImmutablePureComponent { static propTypes = { - account: ImmutablePropTypes.map.isRequired, + account: ImmutablePropTypes.record.isRequired, report: ImmutablePropTypes.map.isRequired, hidden: PropTypes.bool, intl: PropTypes.object.isRequired, diff --git a/app/javascript/mastodon/features/onboarding/index.jsx b/app/javascript/mastodon/features/onboarding/index.jsx index 7b8a41faa..51d4b71f2 100644 --- a/app/javascript/mastodon/features/onboarding/index.jsx +++ b/app/javascript/mastodon/features/onboarding/index.jsx @@ -46,7 +46,7 @@ const mapStateToProps = () => { class Onboarding extends ImmutablePureComponent { static propTypes = { dispatch: PropTypes.func.isRequired, - account: ImmutablePropTypes.map, + account: ImmutablePropTypes.record, ...WithRouterPropTypes, }; diff --git a/app/javascript/mastodon/features/onboarding/share.jsx b/app/javascript/mastodon/features/onboarding/share.jsx index 8e01701eb..334924422 100644 --- a/app/javascript/mastodon/features/onboarding/share.jsx +++ b/app/javascript/mastodon/features/onboarding/share.jsx @@ -145,7 +145,7 @@ class Share extends PureComponent { static propTypes = { onBack: PropTypes.func, - account: ImmutablePropTypes.map, + account: ImmutablePropTypes.record, intl: PropTypes.object, }; diff --git a/app/javascript/mastodon/features/picture_in_picture/components/header.jsx b/app/javascript/mastodon/features/picture_in_picture/components/header.jsx index b7fd9d127..80a13bd2e 100644 --- a/app/javascript/mastodon/features/picture_in_picture/components/header.jsx +++ b/app/javascript/mastodon/features/picture_in_picture/components/header.jsx @@ -27,7 +27,7 @@ class Header extends ImmutablePureComponent { static propTypes = { accountId: PropTypes.string.isRequired, statusId: PropTypes.string.isRequired, - account: ImmutablePropTypes.map.isRequired, + account: ImmutablePropTypes.record.isRequired, onClose: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, }; diff --git a/app/javascript/mastodon/features/report/thanks.jsx b/app/javascript/mastodon/features/report/thanks.jsx index 146d4b389..904c44770 100644 --- a/app/javascript/mastodon/features/report/thanks.jsx +++ b/app/javascript/mastodon/features/report/thanks.jsx @@ -20,7 +20,7 @@ class Thanks extends PureComponent { static propTypes = { submitted: PropTypes.bool, onClose: PropTypes.func.isRequired, - account: ImmutablePropTypes.map.isRequired, + account: ImmutablePropTypes.record.isRequired, dispatch: PropTypes.func.isRequired, }; diff --git a/app/javascript/mastodon/features/ui/components/focal_point_modal.jsx b/app/javascript/mastodon/features/ui/components/focal_point_modal.jsx index 40481e2c8..5353ebdb8 100644 --- a/app/javascript/mastodon/features/ui/components/focal_point_modal.jsx +++ b/app/javascript/mastodon/features/ui/components/focal_point_modal.jsx @@ -110,7 +110,7 @@ class FocalPointModal extends ImmutablePureComponent { static propTypes = { media: ImmutablePropTypes.map.isRequired, - account: ImmutablePropTypes.map.isRequired, + account: ImmutablePropTypes.record.isRequired, isUploadingThumbnail: PropTypes.bool, onSave: PropTypes.func.isRequired, onChangeDescription: PropTypes.func.isRequired, diff --git a/app/javascript/mastodon/features/ui/components/report_modal.jsx b/app/javascript/mastodon/features/ui/components/report_modal.jsx index 2b6f04207..3fd8ff127 100644 --- a/app/javascript/mastodon/features/ui/components/report_modal.jsx +++ b/app/javascript/mastodon/features/ui/components/report_modal.jsx @@ -41,7 +41,7 @@ class ReportModal extends ImmutablePureComponent { statusId: PropTypes.string, dispatch: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, - account: ImmutablePropTypes.map.isRequired, + account: ImmutablePropTypes.record.isRequired, }; state = { diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index bf5ce556e..596c9ca49 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -1,43 +1,5 @@ // @ts-check -/** - * @typedef Emoji - * @property {string} shortcode - * @property {string} static_url - * @property {string} url - */ - -/** - * @typedef AccountField - * @property {string} name - * @property {string} value - * @property {string} verified_at - */ - -/** - * @typedef Account - * @property {string} acct - * @property {string} avatar - * @property {string} avatar_static - * @property {boolean} bot - * @property {string} created_at - * @property {boolean=} discoverable - * @property {string} display_name - * @property {Emoji[]} emojis - * @property {AccountField[]} fields - * @property {number} followers_count - * @property {number} following_count - * @property {boolean} group - * @property {string} header - * @property {string} header_static - * @property {string} id - * @property {string=} last_status_at - * @property {boolean} locked - * @property {string} note - * @property {number} statuses_count - * @property {string} url - * @property {string} username - */ /** * @typedef {[code: string, name: string, localName: string]} InitialStateLanguage @@ -85,7 +47,7 @@ /** * @typedef InitialState - * @property {Record} accounts + * @property {Record} accounts * @property {InitialStateLanguage[]} languages * @property {boolean=} critical_updates_pending * @property {InitialStateMeta} meta diff --git a/app/javascript/mastodon/models/account.ts b/app/javascript/mastodon/models/account.ts new file mode 100644 index 000000000..f20d2a2d3 --- /dev/null +++ b/app/javascript/mastodon/models/account.ts @@ -0,0 +1,149 @@ +import type { RecordOf } from 'immutable'; +import { List, Record as ImmutableRecord } from 'immutable'; + +import escapeTextContentForBrowser from 'escape-html'; + +import type { + ApiAccountFieldJSON, + 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'; + +// AccountField +interface AccountFieldShape extends Required { + name_emojified: string; + value_emojified: string; + value_plain: string | null; +} + +type AccountField = RecordOf; + +const AccountFieldFactory = ImmutableRecord({ + name: '', + value: '', + verified_at: null, + name_emojified: '', + value_emojified: '', + value_plain: null, +}); + +// AccountRole +export type AccountRoleShape = ApiAccountRoleJSON; +export type AccountRole = RecordOf; + +const AccountRoleFactory = ImmutableRecord({ + color: '', + id: '', + name: '', +}); + +// Account +export interface AccountShape + extends Required< + Omit + > { + emojis: List; + fields: List; + roles: List; + display_name_html: string; + note_emojified: string; + note_plain: string | null; + hidden: boolean; + moved: string | null; +} + +export type Account = RecordOf; + +export const accountDefaultValues: AccountShape = { + acct: '', + avatar: '', + avatar_static: '', + bot: false, + created_at: '', + discoverable: false, + display_name: '', + display_name_html: '', + emojis: List(), + fields: List(), + group: false, + header: '', + header_static: '', + id: '', + last_status_at: '', + locked: false, + noindex: false, + note: '', + note_emojified: '', + note_plain: 'string', + roles: List(), + uri: '', + url: '', + username: '', + followers_count: 0, + following_count: 0, + statuses_count: 0, + hidden: false, + suspended: false, + memorial: false, + limited: false, + moved: null, +}; + +const AccountFactory = ImmutableRecord(accountDefaultValues); + +type EmojiMap = Record; + +function makeEmojiMap(emojis: ApiCustomEmojiJSON[]) { + return emojis.reduce((obj, emoji) => { + obj[`:${emoji.shortcode}:`] = emoji; + return obj; + }, {}); +} + +function createAccountField( + jsonField: ApiAccountFieldJSON, + emojiMap: EmojiMap, +) { + return AccountFieldFactory({ + ...jsonField, + name_emojified: emojify( + escapeTextContentForBrowser(jsonField.name), + emojiMap, + ), + value_emojified: emojify(jsonField.value, emojiMap), + value_plain: unescapeHTML(jsonField.value), + }); +} + +export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) { + const { moved, ...accountJSON } = serverJSON; + + const emojiMap = makeEmojiMap(accountJSON.emojis); + + const displayName = + accountJSON.display_name.trim().length === 0 + ? accountJSON.username + : accountJSON.display_name; + + return AccountFactory({ + ...accountJSON, + moved: moved?.id, + fields: List( + serverJSON.fields.map((field) => createAccountField(field, emojiMap)), + ), + emojis: List(serverJSON.emojis.map((emoji) => CustomEmojiFactory(emoji))), + roles: List(serverJSON.roles?.map((role) => AccountRoleFactory(role))), + display_name_html: emojify( + escapeTextContentForBrowser(displayName), + emojiMap, + ), + note_emojified: emojify(accountJSON.note, emojiMap), + note_plain: unescapeHTML(accountJSON.note), + }); +} diff --git a/app/javascript/mastodon/models/custom_emoji.ts b/app/javascript/mastodon/models/custom_emoji.ts new file mode 100644 index 000000000..76479f3ae --- /dev/null +++ b/app/javascript/mastodon/models/custom_emoji.ts @@ -0,0 +1,15 @@ +import type { RecordOf } from 'immutable'; +import { Record } from 'immutable'; + +import type { ApiCustomEmojiJSON } from 'mastodon/api_types/custom_emoji'; + +type CustomEmojiShape = Required; // no changes from server shape +export type CustomEmoji = RecordOf; + +export const CustomEmojiFactory = Record({ + shortcode: '', + static_url: '', + url: '', + category: '', + visible_in_picker: false, +}); diff --git a/app/javascript/mastodon/models/relationship.ts b/app/javascript/mastodon/models/relationship.ts new file mode 100644 index 000000000..115b27873 --- /dev/null +++ b/app/javascript/mastodon/models/relationship.ts @@ -0,0 +1,29 @@ +import type { RecordOf } from 'immutable'; +import { Record } from 'immutable'; + +import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships'; + +type RelationshipShape = Required; // no changes from server shape +export type Relationship = RecordOf; + +const RelationshipFactory = Record({ + blocked_by: false, + blocking: false, + domain_blocking: false, + endorsed: false, + followed_by: false, + following: false, + id: '', + languages: null, + muting_notifications: false, + muting: false, + note: '', + notifying: false, + requested_by: false, + requested: false, + showing_reblogs: false, +}); + +export function createRelationship(attributes: Partial) { + return RelationshipFactory(attributes); +} diff --git a/app/javascript/mastodon/reducers/accounts.js b/app/javascript/mastodon/reducers/accounts.js deleted file mode 100644 index 76122cc63..000000000 --- a/app/javascript/mastodon/reducers/accounts.js +++ /dev/null @@ -1,39 +0,0 @@ -import { Map as ImmutableMap, fromJS } from 'immutable'; - -import { ACCOUNT_REVEAL } from 'mastodon/actions/accounts'; -import { ACCOUNT_IMPORT, ACCOUNTS_IMPORT } from 'mastodon/actions/importer'; - -const initialState = ImmutableMap(); - -const normalizeAccount = (state, account) => { - account = { ...account }; - - delete account.followers_count; - delete account.following_count; - delete account.statuses_count; - - account.hidden = state.getIn([account.id, 'hidden']) === false ? false : account.limited; - - return state.set(account.id, fromJS(account)); -}; - -const normalizeAccounts = (state, accounts) => { - accounts.forEach(account => { - state = normalizeAccount(state, account); - }); - - return state; -}; - -export default function accounts(state = initialState, action) { - switch(action.type) { - case ACCOUNT_IMPORT: - return normalizeAccount(state, action.account); - case ACCOUNTS_IMPORT: - return normalizeAccounts(state, action.accounts); - case ACCOUNT_REVEAL: - return state.setIn([action.id, 'hidden'], false); - default: - return state; - } -} diff --git a/app/javascript/mastodon/reducers/accounts.ts b/app/javascript/mastodon/reducers/accounts.ts new file mode 100644 index 000000000..4a00f21f3 --- /dev/null +++ b/app/javascript/mastodon/reducers/accounts.ts @@ -0,0 +1,82 @@ +import { Map as ImmutableMap } from 'immutable'; + +import type { Reducer } from 'redux'; + +import { + followAccountSuccess, + unfollowAccountSuccess, + importAccounts, + revealAccount, +} from 'mastodon/actions/accounts_typed'; +import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; +import { me } from 'mastodon/initial_state'; +import type { Account } from 'mastodon/models/account'; +import { createAccountFromServerJSON } from 'mastodon/models/account'; + +const initialState = ImmutableMap(); + +const normalizeAccount = ( + state: typeof initialState, + account: ApiAccountJSON, +) => { + return state.set( + account.id, + createAccountFromServerJSON(account).set( + 'hidden', + state.get(account.id)?.hidden === false + ? false + : account.limited || false, + ), + ); +}; + +const normalizeAccounts = ( + state: typeof initialState, + accounts: ApiAccountJSON[], +) => { + accounts.forEach((account) => { + state = normalizeAccount(state, account); + }); + + return state; +}; + +export const accountsReducer: Reducer = ( + state = initialState, + action, +) => { + const currentUserId = me; + + if (!currentUserId) + throw new Error( + 'No current user (me) defined when calling `accountsReducer`', + ); + + if (revealAccount.match(action)) + return state.setIn([action.payload.id, 'hidden'], false); + else if (importAccounts.match(action)) + return normalizeAccounts(state, action.payload.accounts); + else if (followAccountSuccess.match(action)) + return state + .update( + action.payload.relationship.id, + (account) => account?.update('followers_count', (n) => n + 1), + ) + .update( + currentUserId, + (account) => account?.update('following_count', (n) => n + 1), + ); + else if (unfollowAccountSuccess.match(action)) + return state + .update( + action.payload.relationship.id, + (account) => + account?.update('followers_count', (n) => Math.max(0, n - 1)), + ) + .update( + currentUserId, + (account) => + account?.update('following_count', (n) => Math.max(0, n - 1)), + ); + else return state; +}; diff --git a/app/javascript/mastodon/reducers/accounts_counters.js b/app/javascript/mastodon/reducers/accounts_counters.js deleted file mode 100644 index eb7878deb..000000000 --- a/app/javascript/mastodon/reducers/accounts_counters.js +++ /dev/null @@ -1,49 +0,0 @@ -import { Map as ImmutableMap, fromJS } from 'immutable'; - -import { me } from 'mastodon/initial_state'; - -import { - ACCOUNT_FOLLOW_SUCCESS, - ACCOUNT_UNFOLLOW_SUCCESS, -} from '../actions/accounts'; -import { ACCOUNT_IMPORT, ACCOUNTS_IMPORT } from '../actions/importer'; - -const normalizeAccount = (state, account) => state.set(account.id, fromJS({ - followers_count: account.followers_count, - following_count: account.following_count, - statuses_count: account.statuses_count, -})); - -const normalizeAccounts = (state, accounts) => { - accounts.forEach(account => { - state = normalizeAccount(state, account); - }); - - return state; -}; - -const incrementFollowers = (state, accountId) => - state.updateIn([accountId, 'followers_count'], num => num + 1) - .updateIn([me, 'following_count'], num => num + 1); - -const decrementFollowers = (state, accountId) => - state.updateIn([accountId, 'followers_count'], num => Math.max(0, num - 1)) - .updateIn([me, 'following_count'], num => Math.max(0, num - 1)); - -const initialState = ImmutableMap(); - -export default function accountsCounters(state = initialState, action) { - switch(action.type) { - case ACCOUNT_IMPORT: - return normalizeAccount(state, action.account); - case ACCOUNTS_IMPORT: - return normalizeAccounts(state, action.accounts); - case ACCOUNT_FOLLOW_SUCCESS: - return action.alreadyFollowing ? state : - incrementFollowers(state, action.relationship.id); - case ACCOUNT_UNFOLLOW_SUCCESS: - return decrementFollowers(state, action.relationship.id); - default: - return state; - } -} diff --git a/app/javascript/mastodon/reducers/accounts_map.js b/app/javascript/mastodon/reducers/accounts_map.js index fca0e3ce1..d5ecad7db 100644 --- a/app/javascript/mastodon/reducers/accounts_map.js +++ b/app/javascript/mastodon/reducers/accounts_map.js @@ -1,7 +1,7 @@ import { Map as ImmutableMap } from 'immutable'; import { ACCOUNT_LOOKUP_FAIL } from '../actions/accounts'; -import { ACCOUNT_IMPORT, ACCOUNTS_IMPORT } from '../actions/importer'; +import { importAccounts } from '../actions/accounts_typed'; export const normalizeForLookup = str => str.toLowerCase(); @@ -11,10 +11,8 @@ export default function accountsMap(state = initialState, action) { switch(action.type) { case ACCOUNT_LOOKUP_FAIL: return action.error?.response?.status === 404 ? state.set(normalizeForLookup(action.acct), null) : state; - case ACCOUNT_IMPORT: - return state.set(normalizeForLookup(action.account.acct), action.account.id); - case ACCOUNTS_IMPORT: - return state.withMutations(map => action.accounts.forEach(account => map.set(normalizeForLookup(account.acct), account.id))); + case importAccounts.type: + return state.withMutations(map => action.payload.accounts.forEach(account => map.set(normalizeForLookup(account.acct), account.id))); default: return state; } diff --git a/app/javascript/mastodon/reducers/contexts.js b/app/javascript/mastodon/reducers/contexts.js index 32e194dd4..f7d7419a4 100644 --- a/app/javascript/mastodon/reducers/contexts.js +++ b/app/javascript/mastodon/reducers/contexts.js @@ -1,8 +1,8 @@ import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { - ACCOUNT_BLOCK_SUCCESS, - ACCOUNT_MUTE_SUCCESS, + blockAccountSuccess, + muteAccountSuccess, } from '../actions/accounts'; import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses'; import { TIMELINE_DELETE, TIMELINE_UPDATE } from '../actions/timelines'; @@ -92,9 +92,9 @@ const updateContext = (state, status) => { export default function replies(state = initialState, action) { switch(action.type) { - case ACCOUNT_BLOCK_SUCCESS: - case ACCOUNT_MUTE_SUCCESS: - return filterContexts(state, action.relationship, action.statuses); + case blockAccountSuccess.type: + case muteAccountSuccess.type: + return filterContexts(state, action.payload.relationship, action.payload.statuses); case CONTEXT_FETCH_SUCCESS: return normalizeContext(state, action.id, action.ancestors, action.descendants); case TIMELINE_DELETE: diff --git a/app/javascript/mastodon/reducers/conversations.js b/app/javascript/mastodon/reducers/conversations.js index 247e8a597..3e99e680e 100644 --- a/app/javascript/mastodon/reducers/conversations.js +++ b/app/javascript/mastodon/reducers/conversations.js @@ -1,7 +1,7 @@ import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; -import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS } from 'mastodon/actions/accounts'; -import { DOMAIN_BLOCK_SUCCESS } from 'mastodon/actions/domain_blocks'; +import { blockAccountSuccess, muteAccountSuccess } from 'mastodon/actions/accounts'; +import { blockDomainSuccess } from 'mastodon/actions/domain_blocks'; import { CONVERSATIONS_MOUNT, @@ -105,11 +105,11 @@ export default function conversations(state = initialState, action) { return item; })); - case ACCOUNT_BLOCK_SUCCESS: - case ACCOUNT_MUTE_SUCCESS: - return filterConversations(state, [action.relationship.id]); - case DOMAIN_BLOCK_SUCCESS: - return filterConversations(state, action.accounts); + case blockAccountSuccess.type: + case muteAccountSuccess.type: + return filterConversations(state, [action.payload.relationship.id]); + case blockDomainSuccess.type: + return filterConversations(state, action.payload.accounts); case CONVERSATIONS_DELETE_SUCCESS: return state.update('items', list => list.filterNot(item => item.get('id') === action.id)); default: diff --git a/app/javascript/mastodon/reducers/domain_lists.js b/app/javascript/mastodon/reducers/domain_lists.js index 8cdd3ba37..5f63c77f5 100644 --- a/app/javascript/mastodon/reducers/domain_lists.js +++ b/app/javascript/mastodon/reducers/domain_lists.js @@ -3,7 +3,7 @@ import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutabl import { DOMAIN_BLOCKS_FETCH_SUCCESS, DOMAIN_BLOCKS_EXPAND_SUCCESS, - DOMAIN_UNBLOCK_SUCCESS, + unblockDomainSuccess } from '../actions/domain_blocks'; const initialState = ImmutableMap({ @@ -18,8 +18,8 @@ export default function domainLists(state = initialState, action) { return state.setIn(['blocks', 'items'], ImmutableOrderedSet(action.domains)).setIn(['blocks', 'next'], action.next); case DOMAIN_BLOCKS_EXPAND_SUCCESS: return state.updateIn(['blocks', 'items'], set => set.union(action.domains)).setIn(['blocks', 'next'], action.next); - case DOMAIN_UNBLOCK_SUCCESS: - return state.updateIn(['blocks', 'items'], set => set.delete(action.domain)); + case unblockDomainSuccess.type: + return state.updateIn(['blocks', 'items'], set => set.delete(action.payload.domain)); default: return state; } diff --git a/app/javascript/mastodon/reducers/index.ts b/app/javascript/mastodon/reducers/index.ts index 722f04f37..ecef63387 100644 --- a/app/javascript/mastodon/reducers/index.ts +++ b/app/javascript/mastodon/reducers/index.ts @@ -3,8 +3,7 @@ import { Record as ImmutableRecord } from 'immutable'; import { loadingBarReducer } from 'react-redux-loading-bar'; import { combineReducers } from 'redux-immutable'; -import accounts from './accounts'; -import accounts_counters from './accounts_counters'; +import { accountsReducer } from './accounts'; import accounts_map from './accounts_map'; import alerts from './alerts'; import announcements from './announcements'; @@ -32,7 +31,7 @@ import notifications from './notifications'; import picture_in_picture from './picture_in_picture'; import polls from './polls'; import push_notifications from './push_notifications'; -import relationships from './relationships'; +import { relationshipsReducer } from './relationships'; import search from './search'; import server from './server'; import settings from './settings'; @@ -55,11 +54,10 @@ const reducers = { user_lists, domain_lists, status_lists, - accounts, - accounts_counters, + accounts: accountsReducer, accounts_map, statuses, - relationships, + relationships: relationshipsReducer, settings, push_notifications, mutes, diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js index 66870bec5..4a7a822a3 100644 --- a/app/javascript/mastodon/reducers/notifications.js +++ b/app/javascript/mastodon/reducers/notifications.js @@ -1,12 +1,12 @@ import { fromJS, Map as ImmutableMap, List as ImmutableList } from 'immutable'; -import { DOMAIN_BLOCK_SUCCESS } from 'mastodon/actions/domain_blocks'; +import { blockDomainSuccess } from 'mastodon/actions/domain_blocks'; import { - ACCOUNT_BLOCK_SUCCESS, - ACCOUNT_MUTE_SUCCESS, - FOLLOW_REQUEST_AUTHORIZE_SUCCESS, - FOLLOW_REQUEST_REJECT_SUCCESS, + authorizeFollowRequestSuccess, + blockAccountSuccess, + muteAccountSuccess, + rejectFollowRequestSuccess, } from '../actions/accounts'; import { focusApp, @@ -16,7 +16,7 @@ import { MARKERS_FETCH_SUCCESS, } from '../actions/markers'; import { - NOTIFICATIONS_UPDATE, + notificationsUpdate, NOTIFICATIONS_EXPAND_SUCCESS, NOTIFICATIONS_EXPAND_REQUEST, NOTIFICATIONS_EXPAND_FAIL, @@ -274,19 +274,19 @@ export default function notifications(state = initialState, action) { return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', true); case NOTIFICATIONS_SCROLL_TOP: return updateTop(state, action.top); - case NOTIFICATIONS_UPDATE: - return normalizeNotification(state, action.notification, action.usePendingItems); + case notificationsUpdate.type: + return normalizeNotification(state, action.payload.notification, action.payload.usePendingItems); case NOTIFICATIONS_EXPAND_SUCCESS: return expandNormalizedNotifications(state, action.notifications, action.next, action.isLoadingMore, action.isLoadingRecent, action.usePendingItems); - case ACCOUNT_BLOCK_SUCCESS: - return filterNotifications(state, [action.relationship.id]); - case ACCOUNT_MUTE_SUCCESS: - return action.relationship.muting_notifications ? filterNotifications(state, [action.relationship.id]) : state; - case DOMAIN_BLOCK_SUCCESS: - return filterNotifications(state, action.accounts); - case FOLLOW_REQUEST_AUTHORIZE_SUCCESS: - case FOLLOW_REQUEST_REJECT_SUCCESS: - return filterNotifications(state, [action.id], 'follow_request'); + case blockAccountSuccess.type: + return filterNotifications(state, [action.payload.relationship.id]); + case muteAccountSuccess.type: + return action.relationship.muting_notifications ? filterNotifications(state, [action.payload.relationship.id]) : state; + case blockDomainSuccess.type: + return filterNotifications(state, action.payload.accounts); + case authorizeFollowRequestSuccess.type: + case rejectFollowRequestSuccess.type: + return filterNotifications(state, [action.payload.id], 'follow_request'); case NOTIFICATIONS_CLEAR: return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', false); case TIMELINE_DELETE: diff --git a/app/javascript/mastodon/reducers/relationships.js b/app/javascript/mastodon/reducers/relationships.js deleted file mode 100644 index 32b4b4f37..000000000 --- a/app/javascript/mastodon/reducers/relationships.js +++ /dev/null @@ -1,88 +0,0 @@ -import { Map as ImmutableMap, fromJS } from 'immutable'; - -import { - submitAccountNote, -} from '../actions/account_notes'; -import { - ACCOUNT_FOLLOW_SUCCESS, - ACCOUNT_FOLLOW_REQUEST, - ACCOUNT_FOLLOW_FAIL, - ACCOUNT_UNFOLLOW_SUCCESS, - ACCOUNT_UNFOLLOW_REQUEST, - ACCOUNT_UNFOLLOW_FAIL, - ACCOUNT_BLOCK_SUCCESS, - ACCOUNT_UNBLOCK_SUCCESS, - ACCOUNT_MUTE_SUCCESS, - ACCOUNT_UNMUTE_SUCCESS, - ACCOUNT_PIN_SUCCESS, - ACCOUNT_UNPIN_SUCCESS, - RELATIONSHIPS_FETCH_SUCCESS, - FOLLOW_REQUEST_AUTHORIZE_SUCCESS, - FOLLOW_REQUEST_REJECT_SUCCESS, -} from '../actions/accounts'; -import { - DOMAIN_BLOCK_SUCCESS, - DOMAIN_UNBLOCK_SUCCESS, -} from '../actions/domain_blocks'; -import { - NOTIFICATIONS_UPDATE, -} from '../actions/notifications'; - - -const normalizeRelationship = (state, relationship) => state.set(relationship.id, fromJS(relationship)); - -const normalizeRelationships = (state, relationships) => { - relationships.forEach(relationship => { - state = normalizeRelationship(state, relationship); - }); - - return state; -}; - -const setDomainBlocking = (state, accounts, blocking) => { - return state.withMutations(map => { - accounts.forEach(id => { - map.setIn([id, 'domain_blocking'], blocking); - }); - }); -}; - -const initialState = ImmutableMap(); - -export default function relationships(state = initialState, action) { - switch(action.type) { - case FOLLOW_REQUEST_AUTHORIZE_SUCCESS: - return state.setIn([action.id, 'followed_by'], true).setIn([action.id, 'requested_by'], false); - case FOLLOW_REQUEST_REJECT_SUCCESS: - return state.setIn([action.id, 'followed_by'], false).setIn([action.id, 'requested_by'], false); - case NOTIFICATIONS_UPDATE: - return action.notification.type === 'follow_request' ? state.setIn([action.notification.account.id, 'requested_by'], true) : state; - case ACCOUNT_FOLLOW_REQUEST: - return state.getIn([action.id, 'following']) ? state : state.setIn([action.id, action.locked ? 'requested' : 'following'], true); - case ACCOUNT_FOLLOW_FAIL: - return state.setIn([action.id, action.locked ? 'requested' : 'following'], false); - case ACCOUNT_UNFOLLOW_REQUEST: - return state.setIn([action.id, 'following'], false); - case ACCOUNT_UNFOLLOW_FAIL: - return state.setIn([action.id, 'following'], true); - case ACCOUNT_FOLLOW_SUCCESS: - case ACCOUNT_UNFOLLOW_SUCCESS: - case ACCOUNT_BLOCK_SUCCESS: - case ACCOUNT_UNBLOCK_SUCCESS: - case ACCOUNT_MUTE_SUCCESS: - case ACCOUNT_UNMUTE_SUCCESS: - case ACCOUNT_PIN_SUCCESS: - case ACCOUNT_UNPIN_SUCCESS: - return normalizeRelationship(state, action.relationship); - case RELATIONSHIPS_FETCH_SUCCESS: - return normalizeRelationships(state, action.relationships); - case submitAccountNote.fulfilled: - return normalizeRelationship(state, action.payload.relationship); - case DOMAIN_BLOCK_SUCCESS: - return setDomainBlocking(state, action.accounts, true); - case DOMAIN_UNBLOCK_SUCCESS: - return setDomainBlocking(state, action.accounts, false); - default: - return state; - } -} diff --git a/app/javascript/mastodon/reducers/relationships.ts b/app/javascript/mastodon/reducers/relationships.ts new file mode 100644 index 000000000..2ba61839c --- /dev/null +++ b/app/javascript/mastodon/reducers/relationships.ts @@ -0,0 +1,123 @@ +import { Map as ImmutableMap } from 'immutable'; + +import { isFulfilled } from '@reduxjs/toolkit'; +import type { Reducer } from 'redux'; + +import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships'; +import type { Account } from 'mastodon/models/account'; +import { createRelationship } from 'mastodon/models/relationship'; +import type { Relationship } from 'mastodon/models/relationship'; + +import { submitAccountNote } from '../actions/account_notes'; +import { + followAccountSuccess, + unfollowAccountSuccess, + authorizeFollowRequestSuccess, + rejectFollowRequestSuccess, + followAccountRequest, + followAccountFail, + unfollowAccountRequest, + unfollowAccountFail, + blockAccountSuccess, + unblockAccountSuccess, + muteAccountSuccess, + unmuteAccountSuccess, + pinAccountSuccess, + unpinAccountSuccess, + fetchRelationshipsSuccess, +} from '../actions/accounts_typed'; +import { + blockDomainSuccess, + unblockDomainSuccess, +} from '../actions/domain_blocks_typed'; +import { notificationsUpdate } from '../actions/notifications_typed'; + +const initialState = ImmutableMap(); +type State = typeof initialState; + +const normalizeRelationship = ( + state: State, + relationship: ApiRelationshipJSON, +) => state.set(relationship.id, createRelationship(relationship)); + +const normalizeRelationships = ( + state: State, + relationships: ApiRelationshipJSON[], +) => { + relationships.forEach((relationship) => { + state = normalizeRelationship(state, relationship); + }); + + return state; +}; + +const setDomainBlocking = ( + state: State, + accounts: Account[], + blocking: boolean, +) => { + return state.withMutations((map) => { + accounts.forEach((id) => { + map.setIn([id, 'domain_blocking'], blocking); + }); + }); +}; + +export const relationshipsReducer: Reducer = ( + state = initialState, + action, +) => { + if (authorizeFollowRequestSuccess.match(action)) + return state + .setIn([action.payload.id, 'followed_by'], true) + .setIn([action.payload.id, 'requested_by'], false); + else if (rejectFollowRequestSuccess.match(action)) + return state + .setIn([action.payload.id, 'followed_by'], false) + .setIn([action.payload.id, 'requested_by'], false); + else if (notificationsUpdate.match(action)) + return action.payload.notification.type === 'follow_request' + ? state.setIn( + [action.payload.notification.account.id, 'requested_by'], + true, + ) + : state; + else if (followAccountRequest.match(action)) + return state.getIn([action.payload.id, 'following']) + ? state + : state.setIn( + [ + action.payload.id, + action.payload.locked ? 'requested' : 'following', + ], + true, + ); + else if (followAccountFail.match(action)) + return state.setIn( + [action.payload.id, action.payload.locked ? 'requested' : 'following'], + false, + ); + else if (unfollowAccountRequest.match(action)) + return state.setIn([action.payload.id, 'following'], false); + else if (unfollowAccountFail.match(action)) + return state.setIn([action.payload.id, 'following'], true); + else if ( + followAccountSuccess.match(action) || + unfollowAccountSuccess.match(action) || + blockAccountSuccess.match(action) || + unblockAccountSuccess.match(action) || + muteAccountSuccess.match(action) || + unmuteAccountSuccess.match(action) || + pinAccountSuccess.match(action) || + unpinAccountSuccess.match(action) || + isFulfilled(submitAccountNote)(action) + ) + return normalizeRelationship(state, action.payload.relationship); + else if (fetchRelationshipsSuccess.match(action)) + return normalizeRelationships(state, action.payload.relationships); + else if (blockDomainSuccess.match(action)) + return setDomainBlocking(state, action.payload.accounts, true); + else if (unblockDomainSuccess.match(action)) + return setDomainBlocking(state, action.payload.accounts, false); + else return state; +}; diff --git a/app/javascript/mastodon/reducers/status_lists.js b/app/javascript/mastodon/reducers/status_lists.js index 41cc07341..6cb6a937b 100644 --- a/app/javascript/mastodon/reducers/status_lists.js +++ b/app/javascript/mastodon/reducers/status_lists.js @@ -1,8 +1,8 @@ import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable'; import { - ACCOUNT_BLOCK_SUCCESS, - ACCOUNT_MUTE_SUCCESS, + blockAccountSuccess, + muteAccountSuccess, } from '../actions/accounts'; import { BOOKMARKED_STATUSES_FETCH_REQUEST, @@ -142,9 +142,9 @@ export default function statusLists(state = initialState, action) { return prependOneToList(state, 'pins', action.status); case UNPIN_SUCCESS: return removeOneFromList(state, 'pins', action.status); - case ACCOUNT_BLOCK_SUCCESS: - case ACCOUNT_MUTE_SUCCESS: - return state.updateIn(['trending', 'items'], ImmutableOrderedSet(), list => list.filterNot(statusId => action.statuses.getIn([statusId, 'account']) === action.relationship.id)); + case blockAccountSuccess.type: + case muteAccountSuccess.type: + return state.updateIn(['trending', 'items'], ImmutableOrderedSet(), list => list.filterNot(statusId => action.payload.statuses.getIn([statusId, 'account']) === action.payload.relationship.id)); default: return state; } diff --git a/app/javascript/mastodon/reducers/suggestions.js b/app/javascript/mastodon/reducers/suggestions.js index ce1bcc774..0f224ff4b 100644 --- a/app/javascript/mastodon/reducers/suggestions.js +++ b/app/javascript/mastodon/reducers/suggestions.js @@ -1,7 +1,7 @@ import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; -import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS } from 'mastodon/actions/accounts'; -import { DOMAIN_BLOCK_SUCCESS } from 'mastodon/actions/domain_blocks'; +import { blockAccountSuccess, muteAccountSuccess } from 'mastodon/actions/accounts'; +import { blockDomainSuccess } from 'mastodon/actions/domain_blocks'; import { SUGGESTIONS_FETCH_REQUEST, @@ -29,11 +29,11 @@ export default function suggestionsReducer(state = initialState, action) { return state.set('isLoading', false); case SUGGESTIONS_DISMISS: return state.update('items', list => list.filterNot(x => x.account === action.id)); - case ACCOUNT_BLOCK_SUCCESS: - case ACCOUNT_MUTE_SUCCESS: - return state.update('items', list => list.filterNot(x => x.account === action.relationship.id)); - case DOMAIN_BLOCK_SUCCESS: - return state.update('items', list => list.filterNot(x => action.accounts.includes(x.account))); + case blockAccountSuccess.type: + case muteAccountSuccess.type: + return state.update('items', list => list.filterNot(x => x.account === action.payload.relationship.id)); + case blockDomainSuccess.type: + return state.update('items', list => list.filterNot(x => action.payload.accounts.includes(x.account))); default: return state; } diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js index cb3da5072..43dedd6e6 100644 --- a/app/javascript/mastodon/reducers/timelines.js +++ b/app/javascript/mastodon/reducers/timelines.js @@ -1,9 +1,9 @@ import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable'; import { - ACCOUNT_BLOCK_SUCCESS, - ACCOUNT_MUTE_SUCCESS, - ACCOUNT_UNFOLLOW_SUCCESS, + blockAccountSuccess, + muteAccountSuccess, + unfollowAccountSuccess } from '../actions/accounts'; import { TIMELINE_UPDATE, @@ -200,11 +200,11 @@ export default function timelines(state = initialState, action) { return deleteStatus(state, action.id, action.references, action.reblogOf); case TIMELINE_CLEAR: return clearTimeline(state, action.timeline); - case ACCOUNT_BLOCK_SUCCESS: - case ACCOUNT_MUTE_SUCCESS: - return filterTimelines(state, action.relationship, action.statuses); - case ACCOUNT_UNFOLLOW_SUCCESS: - return filterTimeline('home', state, action.relationship, action.statuses); + case blockAccountSuccess.type: + case muteAccountSuccess.type: + return filterTimelines(state, action.payload.relationship, action.payload.statuses); + case unfollowAccountSuccess.type: + return filterTimeline('home', state, action.payload.relationship, action.payload.statuses); case TIMELINE_SCROLL_TOP: return updateTop(state, action.timeline, action.top); case TIMELINE_CONNECT: diff --git a/app/javascript/mastodon/reducers/user_lists.js b/app/javascript/mastodon/reducers/user_lists.js index 089899398..2f17fed5f 100644 --- a/app/javascript/mastodon/reducers/user_lists.js +++ b/app/javascript/mastodon/reducers/user_lists.js @@ -33,8 +33,8 @@ import { FOLLOW_REQUESTS_EXPAND_REQUEST, FOLLOW_REQUESTS_EXPAND_SUCCESS, FOLLOW_REQUESTS_EXPAND_FAIL, - FOLLOW_REQUEST_AUTHORIZE_SUCCESS, - FOLLOW_REQUEST_REJECT_SUCCESS, + authorizeFollowRequestSuccess, + rejectFollowRequestSuccess, } from '../actions/accounts'; import { BLOCKS_FETCH_REQUEST, @@ -66,11 +66,7 @@ import { MUTES_EXPAND_SUCCESS, MUTES_EXPAND_FAIL, } from '../actions/mutes'; -import { - NOTIFICATIONS_UPDATE, -} from '../actions/notifications'; - - +import { notificationsUpdate } from '../actions/notifications'; const initialListState = ImmutableMap({ next: null, @@ -163,8 +159,8 @@ export default function userLists(state = initialState, action) { case FAVOURITES_FETCH_FAIL: case FAVOURITES_EXPAND_FAIL: return state.setIn(['favourited_by', action.id, 'isLoading'], false); - case NOTIFICATIONS_UPDATE: - return action.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.notification) : state; + case notificationsUpdate.type: + return action.payload.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.payload.notification) : state; case FOLLOW_REQUESTS_FETCH_SUCCESS: return normalizeList(state, ['follow_requests'], action.accounts, action.next); case FOLLOW_REQUESTS_EXPAND_SUCCESS: @@ -175,9 +171,9 @@ export default function userLists(state = initialState, action) { case FOLLOW_REQUESTS_FETCH_FAIL: case FOLLOW_REQUESTS_EXPAND_FAIL: return state.setIn(['follow_requests', 'isLoading'], false); - case FOLLOW_REQUEST_AUTHORIZE_SUCCESS: - case FOLLOW_REQUEST_REJECT_SUCCESS: - return state.updateIn(['follow_requests', 'items'], list => list.filterNot(item => item === action.id)); + case authorizeFollowRequestSuccess.type: + case rejectFollowRequestSuccess.type: + return state.updateIn(['follow_requests', 'items'], list => list.filterNot(item => item === action.payload.id)); case BLOCKS_FETCH_SUCCESS: return normalizeList(state, ['blocks'], action.accounts, action.next); case BLOCKS_EXPAND_SUCCESS: diff --git a/app/javascript/mastodon/selectors/accounts.ts b/app/javascript/mastodon/selectors/accounts.ts new file mode 100644 index 000000000..66193136c --- /dev/null +++ b/app/javascript/mastodon/selectors/accounts.ts @@ -0,0 +1,47 @@ +import { Record as ImmutableRecord } from 'immutable'; +import { createSelector } from 'reselect'; + +import { accountDefaultValues } from 'mastodon/models/account'; +import type { Account, AccountShape } from 'mastodon/models/account'; +import type { Relationship } from 'mastodon/models/relationship'; +import type { RootState } from 'mastodon/store'; + +const getAccountBase = (state: RootState, id: string) => + state.accounts.get(id, null); + +const getAccountRelationship = (state: RootState, id: string) => + state.relationships.get(id, null); + +const getAccountMoved = (state: RootState, id: string) => { + const movedToId = state.accounts.get(id)?.moved; + + if (!movedToId) return undefined; + + return state.accounts.get(movedToId); +}; + +interface FullAccountShape extends Omit { + relationship: Relationship | null; + moved: Account | null; +} + +const FullAccountFactory = ImmutableRecord({ + ...accountDefaultValues, + moved: null, + relationship: null, +}); + +export function makeGetAccount() { + return createSelector( + [getAccountBase, getAccountRelationship, getAccountMoved], + (base, relationship, moved) => { + if (base === null) { + return null; + } + + return FullAccountFactory(base) + .set('relationship', relationship) + .set('moved', moved ?? null); + }, + ); +} diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js index 0968fb090..8a07ba774 100644 --- a/app/javascript/mastodon/selectors/index.js +++ b/app/javascript/mastodon/selectors/index.js @@ -5,23 +5,7 @@ import { toServerSideType } from 'mastodon/utils/filters'; import { me } from '../initial_state'; -const getAccountBase = (state, id) => state.getIn(['accounts', id], null); -const getAccountCounters = (state, id) => state.getIn(['accounts_counters', id], null); -const getAccountRelationship = (state, id) => state.getIn(['relationships', id], null); -const getAccountMoved = (state, id) => state.getIn(['accounts', state.getIn(['accounts', id, 'moved'])]); - -export const makeGetAccount = () => { - return createSelector([getAccountBase, getAccountCounters, getAccountRelationship, getAccountMoved], (base, counters, relationship, moved) => { - if (base === null) { - return null; - } - - return base.merge(counters).withMutations(map => { - map.set('relationship', relationship); - map.set('moved', moved); - }); - }); -}; +export { makeGetAccount } from "./accounts"; const getFilters = (state, { contextType }) => { if (!contextType) return null; diff --git a/app/javascript/mastodon/store/store.ts b/app/javascript/mastodon/store/store.ts index 635088568..9f43f58a4 100644 --- a/app/javascript/mastodon/store/store.ts +++ b/app/javascript/mastodon/store/store.ts @@ -35,6 +35,5 @@ export const store = configureStore({ // Infer the `RootState` and `AppDispatch` types from the store itself export type RootState = ReturnType; -// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState} export type AppDispatch = typeof store.dispatch; export type GetState = typeof store.getState; diff --git a/app/javascript/types/resources.ts b/app/javascript/types/resources.ts deleted file mode 100644 index f3901ad15..000000000 --- a/app/javascript/types/resources.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { Record } from 'immutable'; - -type CustomEmoji = Record<{ - shortcode: string; - static_url: string; - url: string; -}>; - -type AccountField = Record<{ - name: string; - value: string; - verified_at: string | null; -}>; - -interface AccountApiResponseValues { - acct: string; - avatar: string; - avatar_static: string; - bot: boolean; - created_at: string; - discoverable: boolean; - display_name: string; - emojis: CustomEmoji[]; - fields: AccountField[]; - followers_count: number; - following_count: number; - group: boolean; - header: string; - header_static: string; - id: string; - last_status_at: string; - locked: boolean; - note: string; - statuses_count: number; - url: string; - uri: string; - username: string; -} - -type NormalizedAccountField = Record<{ - name_emojified: string; - value_emojified: string; - value_plain: string; -}>; - -interface NormalizedAccountValues { - display_name_html: string; - fields: NormalizedAccountField[]; - note_emojified: string; - note_plain: string; -} - -export type Account = Record< - AccountApiResponseValues & NormalizedAccountValues ->;