Use Immutable `Record` for accounts in Redux state (#26559)

This commit is contained in:
Renaud Chaput 2023-11-03 16:00:03 +01:00 committed by GitHub
parent 9d799d40ba
commit 3bf2a7296e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
65 changed files with 765 additions and 662 deletions

View File

@ -1,3 +1,4 @@
import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships';
import { createAppAsyncThunk } from 'mastodon/store/typed_functions'; import { createAppAsyncThunk } from 'mastodon/store/typed_functions';
import api from '../api'; import api from '../api';
@ -5,8 +6,7 @@ import api from '../api';
export const submitAccountNote = createAppAsyncThunk( export const submitAccountNote = createAppAsyncThunk(
'account_note/submit', 'account_note/submit',
async (args: { id: string; value: string }, { getState }) => { async (args: { id: string; value: string }, { getState }) => {
// TODO: replace `unknown` with `ApiRelationshipJSON` when it is merged const response = await api(getState).post<ApiRelationshipJSON>(
const response = await api(getState).post<unknown>(
`/api/v1/accounts/${args.id}/note`, `/api/v1/accounts/${args.id}/note`,
{ {
comment: args.value, comment: args.value,

View File

@ -1,5 +1,15 @@
import api, { getLinks } from '../api'; 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'; import { importFetchedAccount, importFetchedAccounts } from './importer';
export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST'; 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_SUCCESS = 'ACCOUNT_LOOKUP_SUCCESS';
export const ACCOUNT_LOOKUP_FAIL = 'ACCOUNT_LOOKUP_FAIL'; 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_REQUEST = 'ACCOUNT_BLOCK_REQUEST';
export const ACCOUNT_BLOCK_SUCCESS = 'ACCOUNT_BLOCK_SUCCESS';
export const ACCOUNT_BLOCK_FAIL = 'ACCOUNT_BLOCK_FAIL'; export const ACCOUNT_BLOCK_FAIL = 'ACCOUNT_BLOCK_FAIL';
export const ACCOUNT_UNBLOCK_REQUEST = 'ACCOUNT_UNBLOCK_REQUEST'; 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_UNBLOCK_FAIL = 'ACCOUNT_UNBLOCK_FAIL';
export const ACCOUNT_MUTE_REQUEST = 'ACCOUNT_MUTE_REQUEST'; 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_MUTE_FAIL = 'ACCOUNT_MUTE_FAIL';
export const ACCOUNT_UNMUTE_REQUEST = 'ACCOUNT_UNMUTE_REQUEST'; 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_UNMUTE_FAIL = 'ACCOUNT_UNMUTE_FAIL';
export const ACCOUNT_PIN_REQUEST = 'ACCOUNT_PIN_REQUEST'; 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_PIN_FAIL = 'ACCOUNT_PIN_FAIL';
export const ACCOUNT_UNPIN_REQUEST = 'ACCOUNT_UNPIN_REQUEST'; 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 ACCOUNT_UNPIN_FAIL = 'ACCOUNT_UNPIN_FAIL';
export const FOLLOWERS_FETCH_REQUEST = 'FOLLOWERS_FETCH_REQUEST'; 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 FOLLOWING_EXPAND_FAIL = 'FOLLOWING_EXPAND_FAIL';
export const RELATIONSHIPS_FETCH_REQUEST = 'RELATIONSHIPS_FETCH_REQUEST'; 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 RELATIONSHIPS_FETCH_FAIL = 'RELATIONSHIPS_FETCH_FAIL';
export const FOLLOW_REQUESTS_FETCH_REQUEST = 'FOLLOW_REQUESTS_FETCH_REQUEST'; 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_REQUESTS_EXPAND_FAIL = 'FOLLOW_REQUESTS_EXPAND_FAIL';
export const FOLLOW_REQUEST_AUTHORIZE_REQUEST = 'FOLLOW_REQUEST_AUTHORIZE_REQUEST'; 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_AUTHORIZE_FAIL = 'FOLLOW_REQUEST_AUTHORIZE_FAIL';
export const FOLLOW_REQUEST_REJECT_REQUEST = 'FOLLOW_REQUEST_REJECT_REQUEST'; 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 FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL';
export const ACCOUNT_REVEAL = 'ACCOUNT_REVEAL'; export const ACCOUNT_REVEAL = 'ACCOUNT_REVEAL';
export * from './accounts_typed';
export function fetchAccount(id) { export function fetchAccount(id) {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch(fetchRelationships([id])); dispatch(fetchRelationships([id]));
@ -149,12 +144,12 @@ export function followAccount(id, options = { reblogs: true }) {
const alreadyFollowing = getState().getIn(['relationships', id, 'following']); const alreadyFollowing = getState().getIn(['relationships', id, 'following']);
const locked = getState().getIn(['accounts', id, 'locked'], false); 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 => { api(getState).post(`/api/v1/accounts/${id}/follow`, options).then(response => {
dispatch(followAccountSuccess(response.data, alreadyFollowing)); dispatch(followAccountSuccess({relationship: response.data, alreadyFollowing}));
}).catch(error => { }).catch(error => {
dispatch(followAccountFail(error, locked)); dispatch(followAccountFail({ id, error, locked }));
}); });
}; };
} }
@ -164,74 +159,22 @@ export function unfollowAccount(id) {
dispatch(unfollowAccountRequest(id)); dispatch(unfollowAccountRequest(id));
api(getState).post(`/api/v1/accounts/${id}/unfollow`).then(response => { 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 => { }).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) { export function blockAccount(id) {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch(blockAccountRequest(id)); dispatch(blockAccountRequest(id));
api(getState).post(`/api/v1/accounts/${id}/block`).then(response => { 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 // 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 => { }).catch(error => {
dispatch(blockAccountFail(id, error)); dispatch(blockAccountFail({ id, error }));
}); });
}; };
} }
@ -241,9 +184,9 @@ export function unblockAccount(id) {
dispatch(unblockAccountRequest(id)); dispatch(unblockAccountRequest(id));
api(getState).post(`/api/v1/accounts/${id}/unblock`).then(response => { api(getState).post(`/api/v1/accounts/${id}/unblock`).then(response => {
dispatch(unblockAccountSuccess(response.data)); dispatch(unblockAccountSuccess({ relationship: response.data }));
}).catch(error => { }).catch(error => {
dispatch(unblockAccountFail(id, error)); dispatch(unblockAccountFail({ id, error }));
}); });
}; };
} }
@ -254,15 +197,6 @@ export function blockAccountRequest(id) {
id, id,
}; };
} }
export function blockAccountSuccess(relationship, statuses) {
return {
type: ACCOUNT_BLOCK_SUCCESS,
relationship,
statuses,
};
}
export function blockAccountFail(error) { export function blockAccountFail(error) {
return { return {
type: ACCOUNT_BLOCK_FAIL, 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) { export function unblockAccountFail(error) {
return { return {
type: ACCOUNT_UNBLOCK_FAIL, 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 => { 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 // 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 => { }).catch(error => {
dispatch(muteAccountFail(id, error)); dispatch(muteAccountFail({ id, error }));
}); });
}; };
} }
@ -310,9 +237,9 @@ export function unmuteAccount(id) {
dispatch(unmuteAccountRequest(id)); dispatch(unmuteAccountRequest(id));
api(getState).post(`/api/v1/accounts/${id}/unmute`).then(response => { api(getState).post(`/api/v1/accounts/${id}/unmute`).then(response => {
dispatch(unmuteAccountSuccess(response.data)); dispatch(unmuteAccountSuccess({ relationship: response.data }));
}).catch(error => { }).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) { export function muteAccountFail(error) {
return { return {
type: ACCOUNT_MUTE_FAIL, 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) { export function unmuteAccountFail(error) {
return { return {
type: ACCOUNT_UNMUTE_FAIL, type: ACCOUNT_UNMUTE_FAIL,
@ -549,7 +461,7 @@ export function fetchRelationships(accountIds) {
dispatch(fetchRelationshipsRequest(newAccountIds)); dispatch(fetchRelationshipsRequest(newAccountIds));
api(getState).get(`/api/v1/accounts/relationships?${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => { 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 => { }).catch(error => {
dispatch(fetchRelationshipsFail(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) { export function fetchRelationshipsFail(error) {
return { return {
type: RELATIONSHIPS_FETCH_FAIL, type: RELATIONSHIPS_FETCH_FAIL,
@ -659,7 +563,7 @@ export function authorizeFollowRequest(id) {
api(getState) api(getState)
.post(`/api/v1/follow_requests/${id}/authorize`) .post(`/api/v1/follow_requests/${id}/authorize`)
.then(() => dispatch(authorizeFollowRequestSuccess(id))) .then(() => dispatch(authorizeFollowRequestSuccess({ id })))
.catch(error => dispatch(authorizeFollowRequestFail(id, error))); .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) { export function authorizeFollowRequestFail(id, error) {
return { return {
type: FOLLOW_REQUEST_AUTHORIZE_FAIL, type: FOLLOW_REQUEST_AUTHORIZE_FAIL,
@ -693,7 +590,7 @@ export function rejectFollowRequest(id) {
api(getState) api(getState)
.post(`/api/v1/follow_requests/${id}/reject`) .post(`/api/v1/follow_requests/${id}/reject`)
.then(() => dispatch(rejectFollowRequestSuccess(id))) .then(() => dispatch(rejectFollowRequestSuccess({ id })))
.catch(error => dispatch(rejectFollowRequestFail(id, error))); .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) { export function rejectFollowRequestFail(id, error) {
return { return {
type: FOLLOW_REQUEST_REJECT_FAIL, type: FOLLOW_REQUEST_REJECT_FAIL,
@ -725,7 +615,7 @@ export function pinAccount(id) {
dispatch(pinAccountRequest(id)); dispatch(pinAccountRequest(id));
api(getState).post(`/api/v1/accounts/${id}/pin`).then(response => { api(getState).post(`/api/v1/accounts/${id}/pin`).then(response => {
dispatch(pinAccountSuccess(response.data)); dispatch(pinAccountSuccess({ relationship: response.data }));
}).catch(error => { }).catch(error => {
dispatch(pinAccountFail(error)); dispatch(pinAccountFail(error));
}); });
@ -737,7 +627,7 @@ export function unpinAccount(id) {
dispatch(unpinAccountRequest(id)); dispatch(unpinAccountRequest(id));
api(getState).post(`/api/v1/accounts/${id}/unpin`).then(response => { api(getState).post(`/api/v1/accounts/${id}/unpin`).then(response => {
dispatch(unpinAccountSuccess(response.data)); dispatch(unpinAccountSuccess({ relationship: response.data }));
}).catch(error => { }).catch(error => {
dispatch(unpinAccountFail(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) { export function pinAccountFail(error) {
return { return {
type: ACCOUNT_PIN_FAIL, 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) { export function unpinAccountFail(error) {
return { return {
type: ACCOUNT_UNPIN_FAIL, type: ACCOUNT_UNPIN_FAIL,
error, error,
}; };
} }
export const revealAccount = id => ({
type: ACCOUNT_REVEAL,
id,
});

View File

@ -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 extends object>(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[] }>,
);

View File

@ -1,11 +1,13 @@
import api, { getLinks } from '../api'; 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_REQUEST = 'DOMAIN_BLOCK_REQUEST';
export const DOMAIN_BLOCK_SUCCESS = 'DOMAIN_BLOCK_SUCCESS';
export const DOMAIN_BLOCK_FAIL = 'DOMAIN_BLOCK_FAIL'; export const DOMAIN_BLOCK_FAIL = 'DOMAIN_BLOCK_FAIL';
export const DOMAIN_UNBLOCK_REQUEST = 'DOMAIN_UNBLOCK_REQUEST'; 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_UNBLOCK_FAIL = 'DOMAIN_UNBLOCK_FAIL';
export const DOMAIN_BLOCKS_FETCH_REQUEST = 'DOMAIN_BLOCKS_FETCH_REQUEST'; export const DOMAIN_BLOCKS_FETCH_REQUEST = 'DOMAIN_BLOCKS_FETCH_REQUEST';
@ -24,7 +26,7 @@ export function blockDomain(domain) {
const at_domain = '@' + domain; const at_domain = '@' + domain;
const accounts = getState().get('accounts').filter(item => item.get('acct').endsWith(at_domain)).valueSeq().map(item => item.get('id')); 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 => { }).catch(err => {
dispatch(blockDomainFail(domain, 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) { export function blockDomainFail(domain, error) {
return { return {
type: DOMAIN_BLOCK_FAIL, type: DOMAIN_BLOCK_FAIL,
@ -61,7 +55,7 @@ export function unblockDomain(domain) {
api(getState).delete('/api/v1/domain_blocks', { params: { domain } }).then(() => { api(getState).delete('/api/v1/domain_blocks', { params: { domain } }).then(() => {
const at_domain = '@' + domain; const at_domain = '@' + domain;
const accounts = getState().get('accounts').filter(item => item.get('acct').endsWith(at_domain)).valueSeq().map(item => item.get('id')); 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 => { }).catch(err => {
dispatch(unblockDomainFail(domain, 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) { export function unblockDomainFail(domain, error) {
return { return {
type: DOMAIN_UNBLOCK_FAIL, type: DOMAIN_UNBLOCK_FAIL,

View File

@ -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');

View File

@ -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 STATUS_IMPORT = 'STATUS_IMPORT';
export const STATUSES_IMPORT = 'STATUSES_IMPORT'; export const STATUSES_IMPORT = 'STATUSES_IMPORT';
export const POLLS_IMPORT = 'POLLS_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) { export function importStatus(status) {
return { type: STATUS_IMPORT, status }; return { type: STATUS_IMPORT, status };
} }
@ -45,7 +37,7 @@ export function importFetchedAccounts(accounts) {
const normalAccounts = []; const normalAccounts = [];
function processAccount(account) { function processAccount(account) {
pushUnique(normalAccounts, normalizeAccount(account)); pushUnique(normalAccounts, account);
if (account.moved) { if (account.moved) {
processAccount(account.moved); processAccount(account.moved);
@ -54,7 +46,7 @@ export function importFetchedAccounts(accounts) {
accounts.forEach(processAccount); accounts.forEach(processAccount);
return importAccounts(normalAccounts); return importAccounts({ accounts: normalAccounts });
} }
export function importFetchedStatus(status) { export function importFetchedStatus(status) {

View File

@ -2,7 +2,6 @@ import escapeTextContentForBrowser from 'escape-html';
import emojify from '../../features/emoji/emoji'; import emojify from '../../features/emoji/emoji';
import { expandSpoilers } from '../../initial_state'; import { expandSpoilers } from '../../initial_state';
import { unescapeHTML } from '../../utils/html';
const domParser = new DOMParser(); const domParser = new DOMParser();
@ -17,32 +16,6 @@ export function searchTextFromRawStatus (status) {
return domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; 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) { export function normalizeFilterResult(result) {
const normalResult = { ...result }; const normalResult = { ...result };

View File

@ -18,10 +18,12 @@ import {
importFetchedStatuses, importFetchedStatuses,
} from './importer'; } from './importer';
import { submitMarkers } from './markers'; import { submitMarkers } from './markers';
import { notificationsUpdate } from "./notifications_typed";
import { register as registerPushNotifications } from './push_notifications'; import { register as registerPushNotifications } from './push_notifications';
import { saveSettings } from './settings'; 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_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';
export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST'; 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(importFetchedAccount(notification.report.target_account));
} }
dispatch({
type: NOTIFICATIONS_UPDATE, dispatch(notificationsUpdate(notification, preferPendingItems, playSound && !filtered));
notification,
usePendingItems: preferPendingItems,
meta: (playSound && !filtered) ? { sound: 'boop' } : undefined,
});
fetchRelatedRelationships(dispatch, [notification]); fetchRelatedRelationships(dispatch, [notification]);
} else if (playSound && !filtered) { } else if (playSound && !filtered) {

View File

@ -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 },
}),
);

View File

@ -11,6 +11,7 @@ const convertState = rawState =>
fromJS(rawState, (k, v) => fromJS(rawState, (k, v) =>
Iterable.isIndexed(v) ? v.toList() : v.toMap()); Iterable.isIndexed(v) ? v.toList() : v.toMap());
export function hydrateStore(rawState) { export function hydrateStore(rawState) {
return dispatch => { return dispatch => {
const state = convertState(rawState); const state = convertState(rawState);

View File

@ -31,9 +31,9 @@ export interface ApiAccountJSON {
id: string; id: string;
last_status_at: string; last_status_at: string;
locked: boolean; locked: boolean;
noindex: boolean; noindex?: boolean;
note: string; note: string;
roles: ApiAccountJSON[]; roles?: ApiAccountJSON[];
statuses_count: number; statuses_count: number;
uri: string; uri: string;
url: string; url: string;

View File

@ -36,7 +36,7 @@ class Account extends ImmutablePureComponent {
static propTypes = { static propTypes = {
size: PropTypes.number, size: PropTypes.number,
account: ImmutablePropTypes.map, account: ImmutablePropTypes.record,
onFollow: PropTypes.func.isRequired, onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired,
onMute: PropTypes.func.isRequired, onMute: PropTypes.func.isRequired,

View File

@ -1,7 +1,8 @@
import classNames from 'classnames'; import classNames from 'classnames';
import type { Account } from 'mastodon/models/account';
import { useHovering } from '../../hooks/useHovering'; import { useHovering } from '../../hooks/useHovering';
import type { Account } from '../../types/resources';
import { autoPlayGif } from '../initial_state'; import { autoPlayGif } from '../initial_state';
interface Props { interface Props {

View File

@ -1,5 +1,6 @@
import type { Account } from 'mastodon/models/account';
import { useHovering } from '../../hooks/useHovering'; import { useHovering } from '../../hooks/useHovering';
import type { Account } from '../../types/resources';
import { autoPlayGif } from '../initial_state'; import { autoPlayGif } from '../initial_state';
interface Props { interface Props {

View File

@ -2,7 +2,8 @@ import React from 'react';
import type { List } from 'immutable'; import type { List } from 'immutable';
import type { Account } from '../../types/resources'; import type { Account } from 'mastodon/models/account';
import { autoPlayGif } from '../initial_state'; import { autoPlayGif } from '../initial_state';
import { Skeleton } from './skeleton'; import { Skeleton } from './skeleton';

View File

@ -19,7 +19,7 @@ const makeMapStateToProps = () => {
class InlineAccount extends PureComponent { class InlineAccount extends PureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map.isRequired, account: ImmutablePropTypes.record.isRequired,
}; };
render () { render () {

View File

@ -80,7 +80,7 @@ class Status extends ImmutablePureComponent {
static propTypes = { static propTypes = {
status: ImmutablePropTypes.map, status: ImmutablePropTypes.map,
account: ImmutablePropTypes.map, account: ImmutablePropTypes.record,
previousId: PropTypes.string, previousId: PropTypes.string,
nextInReplyToId: PropTypes.string, nextInReplyToId: PropTypes.string,
rootId: PropTypes.string, rootId: PropTypes.string,

View File

@ -49,7 +49,7 @@ class InlineAlert extends PureComponent {
class AccountNote extends ImmutablePureComponent { class AccountNote extends ImmutablePureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map.isRequired, account: ImmutablePropTypes.record.isRequired,
value: PropTypes.string, value: PropTypes.string,
onSave: PropTypes.func.isRequired, onSave: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,

View File

@ -15,7 +15,7 @@ const messages = defineMessages({
class FeaturedTags extends ImmutablePureComponent { class FeaturedTags extends ImmutablePureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map, account: ImmutablePropTypes.record,
featuredTags: ImmutablePropTypes.list, featuredTags: ImmutablePropTypes.list,
tagged: PropTypes.string, tagged: PropTypes.string,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,

View File

@ -11,7 +11,7 @@ import { Icon } from 'mastodon/components/icon';
export default class FollowRequestNote extends ImmutablePureComponent { export default class FollowRequestNote extends ImmutablePureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map.isRequired, account: ImmutablePropTypes.record.isRequired,
}; };
render () { render () {

View File

@ -91,7 +91,7 @@ const dateFormatOptions = {
class Header extends ImmutablePureComponent { class Header extends ImmutablePureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map, account: ImmutablePropTypes.record,
identity_props: ImmutablePropTypes.list, identity_props: ImmutablePropTypes.list,
onFollow: PropTypes.func.isRequired, onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired,

View File

@ -17,7 +17,7 @@ import MovedNote from './moved_note';
class Header extends ImmutablePureComponent { class Header extends ImmutablePureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map, account: ImmutablePropTypes.record,
onFollow: PropTypes.func.isRequired, onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired,

View File

@ -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 (
<div className='limited-account-hint'>
<p><FormattedMessage id='limited_account_hint.title' defaultMessage='This profile has been hidden by the moderators of {domain}.' values={{ domain }} /></p>
<Button onClick={reveal}><FormattedMessage id='limited_account_hint.action' defaultMessage='Show profile anyway' /></Button>
</div>
);
}
}
export default connect(() => {}, mapDispatchToProps)(LimitedAccountHint);

View File

@ -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 (
<div className='limited-account-hint'>
<p>
<FormattedMessage
id='limited_account_hint.title'
defaultMessage='This profile has been hidden by the moderators of {domain}.'
values={{ domain }}
/>
</p>
<Button onClick={reveal}>
<FormattedMessage
id='limited_account_hint.action'
defaultMessage='Show profile anyway'
/>
</Button>
</div>
);
};

View File

@ -21,7 +21,7 @@ import { LoadingIndicator } from '../../components/loading_indicator';
import StatusList from '../../components/status_list'; import StatusList from '../../components/status_list';
import Column from '../ui/components/column'; 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'; import HeaderContainer from './containers/header_container';
const emptyList = ImmutableList(); const emptyList = ImmutableList();

View File

@ -28,7 +28,7 @@ const messages = defineMessages({
class ActionBar extends PureComponent { class ActionBar extends PureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map.isRequired, account: ImmutablePropTypes.record.isRequired,
onLogout: PropTypes.func.isRequired, onLogout: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };

View File

@ -7,7 +7,7 @@ import { DisplayName } from '../../../components/display_name';
export default class AutosuggestAccount extends ImmutablePureComponent { export default class AutosuggestAccount extends ImmutablePureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map.isRequired, account: ImmutablePropTypes.record.isRequired,
}; };
render () { render () {

View File

@ -14,7 +14,7 @@ import ActionBar from './action_bar';
export default class NavigationBar extends ImmutablePureComponent { export default class NavigationBar extends ImmutablePureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map.isRequired, account: ImmutablePropTypes.record.isRequired,
onLogout: PropTypes.func.isRequired, onLogout: PropTypes.func.isRequired,
onClose: PropTypes.func, onClose: PropTypes.func,
}; };

View File

@ -102,7 +102,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
class AccountCard extends ImmutablePureComponent { class AccountCard extends ImmutablePureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map.isRequired, account: ImmutablePropTypes.record.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
onFollow: PropTypes.func.isRequired, onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired,

View File

@ -22,7 +22,7 @@ const messages = defineMessages({
class AccountAuthorize extends ImmutablePureComponent { class AccountAuthorize extends ImmutablePureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map.isRequired, account: ImmutablePropTypes.record.isRequired,
onAuthorize: PropTypes.func.isRequired, onAuthorize: PropTypes.func.isRequired,
onReject: PropTypes.func.isRequired, onReject: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,

View File

@ -23,7 +23,7 @@ import { ColumnBackButton } from '../../components/column_back_button';
import { LoadingIndicator } from '../../components/loading_indicator'; import { LoadingIndicator } from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list'; import ScrollableList from '../../components/scrollable_list';
import AccountContainer from '../../containers/account_container'; 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 HeaderContainer from '../account_timeline/containers/header_container';
import Column from '../ui/components/column'; import Column from '../ui/components/column';

View File

@ -23,7 +23,7 @@ import { ColumnBackButton } from '../../components/column_back_button';
import { LoadingIndicator } from '../../components/loading_indicator'; import { LoadingIndicator } from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list'; import ScrollableList from '../../components/scrollable_list';
import AccountContainer from '../../containers/account_container'; 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 HeaderContainer from '../account_timeline/containers/header_container';
import Column from '../ui/components/column'; import Column from '../ui/components/column';

View File

@ -21,7 +21,7 @@ const makeMapStateToProps = () => {
class Account extends ImmutablePureComponent { class Account extends ImmutablePureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map.isRequired, account: ImmutablePropTypes.record.isRequired,
}; };
render () { render () {

View File

@ -39,7 +39,7 @@ const mapDispatchToProps = (dispatch, { accountId }) => ({
class Account extends ImmutablePureComponent { class Account extends ImmutablePureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map.isRequired, account: ImmutablePropTypes.record.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
onRemove: PropTypes.func.isRequired, onRemove: PropTypes.func.isRequired,
onAdd: PropTypes.func.isRequired, onAdd: PropTypes.func.isRequired,

View File

@ -22,7 +22,7 @@ const messages = defineMessages({
class FollowRequest extends ImmutablePureComponent { class FollowRequest extends ImmutablePureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map.isRequired, account: ImmutablePropTypes.record.isRequired,
onAuthorize: PropTypes.func.isRequired, onAuthorize: PropTypes.func.isRequired,
onReject: PropTypes.func.isRequired, onReject: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,

View File

@ -20,7 +20,7 @@ const messages = defineMessages({
class Report extends ImmutablePureComponent { class Report extends ImmutablePureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map.isRequired, account: ImmutablePropTypes.record.isRequired,
report: ImmutablePropTypes.map.isRequired, report: ImmutablePropTypes.map.isRequired,
hidden: PropTypes.bool, hidden: PropTypes.bool,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,

View File

@ -46,7 +46,7 @@ const mapStateToProps = () => {
class Onboarding extends ImmutablePureComponent { class Onboarding extends ImmutablePureComponent {
static propTypes = { static propTypes = {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
account: ImmutablePropTypes.map, account: ImmutablePropTypes.record,
...WithRouterPropTypes, ...WithRouterPropTypes,
}; };

View File

@ -145,7 +145,7 @@ class Share extends PureComponent {
static propTypes = { static propTypes = {
onBack: PropTypes.func, onBack: PropTypes.func,
account: ImmutablePropTypes.map, account: ImmutablePropTypes.record,
intl: PropTypes.object, intl: PropTypes.object,
}; };

View File

@ -27,7 +27,7 @@ class Header extends ImmutablePureComponent {
static propTypes = { static propTypes = {
accountId: PropTypes.string.isRequired, accountId: PropTypes.string.isRequired,
statusId: PropTypes.string.isRequired, statusId: PropTypes.string.isRequired,
account: ImmutablePropTypes.map.isRequired, account: ImmutablePropTypes.record.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };

View File

@ -20,7 +20,7 @@ class Thanks extends PureComponent {
static propTypes = { static propTypes = {
submitted: PropTypes.bool, submitted: PropTypes.bool,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
account: ImmutablePropTypes.map.isRequired, account: ImmutablePropTypes.record.isRequired,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
}; };

View File

@ -110,7 +110,7 @@ class FocalPointModal extends ImmutablePureComponent {
static propTypes = { static propTypes = {
media: ImmutablePropTypes.map.isRequired, media: ImmutablePropTypes.map.isRequired,
account: ImmutablePropTypes.map.isRequired, account: ImmutablePropTypes.record.isRequired,
isUploadingThumbnail: PropTypes.bool, isUploadingThumbnail: PropTypes.bool,
onSave: PropTypes.func.isRequired, onSave: PropTypes.func.isRequired,
onChangeDescription: PropTypes.func.isRequired, onChangeDescription: PropTypes.func.isRequired,

View File

@ -41,7 +41,7 @@ class ReportModal extends ImmutablePureComponent {
statusId: PropTypes.string, statusId: PropTypes.string,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
account: ImmutablePropTypes.map.isRequired, account: ImmutablePropTypes.record.isRequired,
}; };
state = { state = {

View File

@ -1,43 +1,5 @@
// @ts-check // @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 * @typedef {[code: string, name: string, localName: string]} InitialStateLanguage
@ -85,7 +47,7 @@
/** /**
* @typedef InitialState * @typedef InitialState
* @property {Record<string, Account>} accounts * @property {Record<string, import("./api_types/accounts").ApiAccountJSON>} accounts
* @property {InitialStateLanguage[]} languages * @property {InitialStateLanguage[]} languages
* @property {boolean=} critical_updates_pending * @property {boolean=} critical_updates_pending
* @property {InitialStateMeta} meta * @property {InitialStateMeta} meta

View File

@ -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<ApiAccountFieldJSON> {
name_emojified: string;
value_emojified: string;
value_plain: string | null;
}
type AccountField = RecordOf<AccountFieldShape>;
const AccountFieldFactory = ImmutableRecord<AccountFieldShape>({
name: '',
value: '',
verified_at: null,
name_emojified: '',
value_emojified: '',
value_plain: null,
});
// AccountRole
export type AccountRoleShape = ApiAccountRoleJSON;
export type AccountRole = RecordOf<AccountRoleShape>;
const AccountRoleFactory = ImmutableRecord<AccountRoleShape>({
color: '',
id: '',
name: '',
});
// Account
export interface AccountShape
extends Required<
Omit<ApiAccountJSON, 'emojis' | 'fields' | 'roles' | 'moved'>
> {
emojis: List<CustomEmoji>;
fields: List<AccountField>;
roles: List<AccountRole>;
display_name_html: string;
note_emojified: string;
note_plain: string | null;
hidden: boolean;
moved: string | null;
}
export type Account = RecordOf<AccountShape>;
export const accountDefaultValues: AccountShape = {
acct: '',
avatar: '',
avatar_static: '',
bot: false,
created_at: '',
discoverable: false,
display_name: '',
display_name_html: '',
emojis: List<CustomEmoji>(),
fields: List<AccountField>(),
group: false,
header: '',
header_static: '',
id: '',
last_status_at: '',
locked: false,
noindex: false,
note: '',
note_emojified: '',
note_plain: 'string',
roles: List<AccountRole>(),
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<AccountShape>(accountDefaultValues);
type EmojiMap = Record<string, ApiCustomEmojiJSON>;
function makeEmojiMap(emojis: ApiCustomEmojiJSON[]) {
return emojis.reduce<EmojiMap>((obj, emoji) => {
obj[`:${emoji.shortcode}:`] = emoji;
return obj;
}, {});
}
function createAccountField(
jsonField: ApiAccountFieldJSON,
emojiMap: EmojiMap,
) {
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),
});
}

View File

@ -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<ApiCustomEmojiJSON>; // no changes from server shape
export type CustomEmoji = RecordOf<CustomEmojiShape>;
export const CustomEmojiFactory = Record<CustomEmojiShape>({
shortcode: '',
static_url: '',
url: '',
category: '',
visible_in_picker: false,
});

View File

@ -0,0 +1,29 @@
import type { RecordOf } from 'immutable';
import { Record } from 'immutable';
import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships';
type RelationshipShape = Required<ApiRelationshipJSON>; // no changes from server shape
export type Relationship = RecordOf<RelationshipShape>;
const RelationshipFactory = Record<RelationshipShape>({
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<RelationshipShape>) {
return RelationshipFactory(attributes);
}

View File

@ -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;
}
}

View File

@ -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<string, Account>();
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<typeof initialState> = (
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;
};

View File

@ -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;
}
}

View File

@ -1,7 +1,7 @@
import { Map as ImmutableMap } from 'immutable'; import { Map as ImmutableMap } from 'immutable';
import { ACCOUNT_LOOKUP_FAIL } from '../actions/accounts'; 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(); export const normalizeForLookup = str => str.toLowerCase();
@ -11,10 +11,8 @@ export default function accountsMap(state = initialState, action) {
switch(action.type) { switch(action.type) {
case ACCOUNT_LOOKUP_FAIL: case ACCOUNT_LOOKUP_FAIL:
return action.error?.response?.status === 404 ? state.set(normalizeForLookup(action.acct), null) : state; return action.error?.response?.status === 404 ? state.set(normalizeForLookup(action.acct), null) : state;
case ACCOUNT_IMPORT: case importAccounts.type:
return state.set(normalizeForLookup(action.account.acct), action.account.id); return state.withMutations(map => action.payload.accounts.forEach(account => map.set(normalizeForLookup(account.acct), account.id)));
case ACCOUNTS_IMPORT:
return state.withMutations(map => action.accounts.forEach(account => map.set(normalizeForLookup(account.acct), account.id)));
default: default:
return state; return state;
} }

View File

@ -1,8 +1,8 @@
import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { import {
ACCOUNT_BLOCK_SUCCESS, blockAccountSuccess,
ACCOUNT_MUTE_SUCCESS, muteAccountSuccess,
} from '../actions/accounts'; } from '../actions/accounts';
import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses'; import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses';
import { TIMELINE_DELETE, TIMELINE_UPDATE } from '../actions/timelines'; import { TIMELINE_DELETE, TIMELINE_UPDATE } from '../actions/timelines';
@ -92,9 +92,9 @@ const updateContext = (state, status) => {
export default function replies(state = initialState, action) { export default function replies(state = initialState, action) {
switch(action.type) { switch(action.type) {
case ACCOUNT_BLOCK_SUCCESS: case blockAccountSuccess.type:
case ACCOUNT_MUTE_SUCCESS: case muteAccountSuccess.type:
return filterContexts(state, action.relationship, action.statuses); return filterContexts(state, action.payload.relationship, action.payload.statuses);
case CONTEXT_FETCH_SUCCESS: case CONTEXT_FETCH_SUCCESS:
return normalizeContext(state, action.id, action.ancestors, action.descendants); return normalizeContext(state, action.id, action.ancestors, action.descendants);
case TIMELINE_DELETE: case TIMELINE_DELETE:

View File

@ -1,7 +1,7 @@
import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS } from 'mastodon/actions/accounts'; import { blockAccountSuccess, muteAccountSuccess } from 'mastodon/actions/accounts';
import { DOMAIN_BLOCK_SUCCESS } from 'mastodon/actions/domain_blocks'; import { blockDomainSuccess } from 'mastodon/actions/domain_blocks';
import { import {
CONVERSATIONS_MOUNT, CONVERSATIONS_MOUNT,
@ -105,11 +105,11 @@ export default function conversations(state = initialState, action) {
return item; return item;
})); }));
case ACCOUNT_BLOCK_SUCCESS: case blockAccountSuccess.type:
case ACCOUNT_MUTE_SUCCESS: case muteAccountSuccess.type:
return filterConversations(state, [action.relationship.id]); return filterConversations(state, [action.payload.relationship.id]);
case DOMAIN_BLOCK_SUCCESS: case blockDomainSuccess.type:
return filterConversations(state, action.accounts); return filterConversations(state, action.payload.accounts);
case CONVERSATIONS_DELETE_SUCCESS: case CONVERSATIONS_DELETE_SUCCESS:
return state.update('items', list => list.filterNot(item => item.get('id') === action.id)); return state.update('items', list => list.filterNot(item => item.get('id') === action.id));
default: default:

View File

@ -3,7 +3,7 @@ import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutabl
import { import {
DOMAIN_BLOCKS_FETCH_SUCCESS, DOMAIN_BLOCKS_FETCH_SUCCESS,
DOMAIN_BLOCKS_EXPAND_SUCCESS, DOMAIN_BLOCKS_EXPAND_SUCCESS,
DOMAIN_UNBLOCK_SUCCESS, unblockDomainSuccess
} from '../actions/domain_blocks'; } from '../actions/domain_blocks';
const initialState = ImmutableMap({ 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); return state.setIn(['blocks', 'items'], ImmutableOrderedSet(action.domains)).setIn(['blocks', 'next'], action.next);
case DOMAIN_BLOCKS_EXPAND_SUCCESS: case DOMAIN_BLOCKS_EXPAND_SUCCESS:
return state.updateIn(['blocks', 'items'], set => set.union(action.domains)).setIn(['blocks', 'next'], action.next); return state.updateIn(['blocks', 'items'], set => set.union(action.domains)).setIn(['blocks', 'next'], action.next);
case DOMAIN_UNBLOCK_SUCCESS: case unblockDomainSuccess.type:
return state.updateIn(['blocks', 'items'], set => set.delete(action.domain)); return state.updateIn(['blocks', 'items'], set => set.delete(action.payload.domain));
default: default:
return state; return state;
} }

View File

@ -3,8 +3,7 @@ import { Record as ImmutableRecord } from 'immutable';
import { loadingBarReducer } from 'react-redux-loading-bar'; import { loadingBarReducer } from 'react-redux-loading-bar';
import { combineReducers } from 'redux-immutable'; import { combineReducers } from 'redux-immutable';
import accounts from './accounts'; import { accountsReducer } from './accounts';
import accounts_counters from './accounts_counters';
import accounts_map from './accounts_map'; import accounts_map from './accounts_map';
import alerts from './alerts'; import alerts from './alerts';
import announcements from './announcements'; import announcements from './announcements';
@ -32,7 +31,7 @@ import notifications from './notifications';
import picture_in_picture from './picture_in_picture'; import picture_in_picture from './picture_in_picture';
import polls from './polls'; import polls from './polls';
import push_notifications from './push_notifications'; import push_notifications from './push_notifications';
import relationships from './relationships'; import { relationshipsReducer } from './relationships';
import search from './search'; import search from './search';
import server from './server'; import server from './server';
import settings from './settings'; import settings from './settings';
@ -55,11 +54,10 @@ const reducers = {
user_lists, user_lists,
domain_lists, domain_lists,
status_lists, status_lists,
accounts, accounts: accountsReducer,
accounts_counters,
accounts_map, accounts_map,
statuses, statuses,
relationships, relationships: relationshipsReducer,
settings, settings,
push_notifications, push_notifications,
mutes, mutes,

View File

@ -1,12 +1,12 @@
import { fromJS, Map as ImmutableMap, List as ImmutableList } from 'immutable'; 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 { import {
ACCOUNT_BLOCK_SUCCESS, authorizeFollowRequestSuccess,
ACCOUNT_MUTE_SUCCESS, blockAccountSuccess,
FOLLOW_REQUEST_AUTHORIZE_SUCCESS, muteAccountSuccess,
FOLLOW_REQUEST_REJECT_SUCCESS, rejectFollowRequestSuccess,
} from '../actions/accounts'; } from '../actions/accounts';
import { import {
focusApp, focusApp,
@ -16,7 +16,7 @@ import {
MARKERS_FETCH_SUCCESS, MARKERS_FETCH_SUCCESS,
} from '../actions/markers'; } from '../actions/markers';
import { import {
NOTIFICATIONS_UPDATE, notificationsUpdate,
NOTIFICATIONS_EXPAND_SUCCESS, NOTIFICATIONS_EXPAND_SUCCESS,
NOTIFICATIONS_EXPAND_REQUEST, NOTIFICATIONS_EXPAND_REQUEST,
NOTIFICATIONS_EXPAND_FAIL, 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); return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', true);
case NOTIFICATIONS_SCROLL_TOP: case NOTIFICATIONS_SCROLL_TOP:
return updateTop(state, action.top); return updateTop(state, action.top);
case NOTIFICATIONS_UPDATE: case notificationsUpdate.type:
return normalizeNotification(state, action.notification, action.usePendingItems); return normalizeNotification(state, action.payload.notification, action.payload.usePendingItems);
case NOTIFICATIONS_EXPAND_SUCCESS: case NOTIFICATIONS_EXPAND_SUCCESS:
return expandNormalizedNotifications(state, action.notifications, action.next, action.isLoadingMore, action.isLoadingRecent, action.usePendingItems); return expandNormalizedNotifications(state, action.notifications, action.next, action.isLoadingMore, action.isLoadingRecent, action.usePendingItems);
case ACCOUNT_BLOCK_SUCCESS: case blockAccountSuccess.type:
return filterNotifications(state, [action.relationship.id]); return filterNotifications(state, [action.payload.relationship.id]);
case ACCOUNT_MUTE_SUCCESS: case muteAccountSuccess.type:
return action.relationship.muting_notifications ? filterNotifications(state, [action.relationship.id]) : state; return action.relationship.muting_notifications ? filterNotifications(state, [action.payload.relationship.id]) : state;
case DOMAIN_BLOCK_SUCCESS: case blockDomainSuccess.type:
return filterNotifications(state, action.accounts); return filterNotifications(state, action.payload.accounts);
case FOLLOW_REQUEST_AUTHORIZE_SUCCESS: case authorizeFollowRequestSuccess.type:
case FOLLOW_REQUEST_REJECT_SUCCESS: case rejectFollowRequestSuccess.type:
return filterNotifications(state, [action.id], 'follow_request'); return filterNotifications(state, [action.payload.id], 'follow_request');
case NOTIFICATIONS_CLEAR: case NOTIFICATIONS_CLEAR:
return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', false); return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', false);
case TIMELINE_DELETE: case TIMELINE_DELETE:

View File

@ -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;
}
}

View File

@ -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<string, Relationship>();
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> = (
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;
};

View File

@ -1,8 +1,8 @@
import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable'; import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable';
import { import {
ACCOUNT_BLOCK_SUCCESS, blockAccountSuccess,
ACCOUNT_MUTE_SUCCESS, muteAccountSuccess,
} from '../actions/accounts'; } from '../actions/accounts';
import { import {
BOOKMARKED_STATUSES_FETCH_REQUEST, BOOKMARKED_STATUSES_FETCH_REQUEST,
@ -142,9 +142,9 @@ export default function statusLists(state = initialState, action) {
return prependOneToList(state, 'pins', action.status); return prependOneToList(state, 'pins', action.status);
case UNPIN_SUCCESS: case UNPIN_SUCCESS:
return removeOneFromList(state, 'pins', action.status); return removeOneFromList(state, 'pins', action.status);
case ACCOUNT_BLOCK_SUCCESS: case blockAccountSuccess.type:
case ACCOUNT_MUTE_SUCCESS: case muteAccountSuccess.type:
return state.updateIn(['trending', 'items'], ImmutableOrderedSet(), list => list.filterNot(statusId => action.statuses.getIn([statusId, 'account']) === action.relationship.id)); return state.updateIn(['trending', 'items'], ImmutableOrderedSet(), list => list.filterNot(statusId => action.payload.statuses.getIn([statusId, 'account']) === action.payload.relationship.id));
default: default:
return state; return state;
} }

View File

@ -1,7 +1,7 @@
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS } from 'mastodon/actions/accounts'; import { blockAccountSuccess, muteAccountSuccess } from 'mastodon/actions/accounts';
import { DOMAIN_BLOCK_SUCCESS } from 'mastodon/actions/domain_blocks'; import { blockDomainSuccess } from 'mastodon/actions/domain_blocks';
import { import {
SUGGESTIONS_FETCH_REQUEST, SUGGESTIONS_FETCH_REQUEST,
@ -29,11 +29,11 @@ export default function suggestionsReducer(state = initialState, action) {
return state.set('isLoading', false); return state.set('isLoading', false);
case SUGGESTIONS_DISMISS: case SUGGESTIONS_DISMISS:
return state.update('items', list => list.filterNot(x => x.account === action.id)); return state.update('items', list => list.filterNot(x => x.account === action.id));
case ACCOUNT_BLOCK_SUCCESS: case blockAccountSuccess.type:
case ACCOUNT_MUTE_SUCCESS: case muteAccountSuccess.type:
return state.update('items', list => list.filterNot(x => x.account === action.relationship.id)); return state.update('items', list => list.filterNot(x => x.account === action.payload.relationship.id));
case DOMAIN_BLOCK_SUCCESS: case blockDomainSuccess.type:
return state.update('items', list => list.filterNot(x => action.accounts.includes(x.account))); return state.update('items', list => list.filterNot(x => action.payload.accounts.includes(x.account)));
default: default:
return state; return state;
} }

View File

@ -1,9 +1,9 @@
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable'; import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
import { import {
ACCOUNT_BLOCK_SUCCESS, blockAccountSuccess,
ACCOUNT_MUTE_SUCCESS, muteAccountSuccess,
ACCOUNT_UNFOLLOW_SUCCESS, unfollowAccountSuccess
} from '../actions/accounts'; } from '../actions/accounts';
import { import {
TIMELINE_UPDATE, TIMELINE_UPDATE,
@ -200,11 +200,11 @@ export default function timelines(state = initialState, action) {
return deleteStatus(state, action.id, action.references, action.reblogOf); return deleteStatus(state, action.id, action.references, action.reblogOf);
case TIMELINE_CLEAR: case TIMELINE_CLEAR:
return clearTimeline(state, action.timeline); return clearTimeline(state, action.timeline);
case ACCOUNT_BLOCK_SUCCESS: case blockAccountSuccess.type:
case ACCOUNT_MUTE_SUCCESS: case muteAccountSuccess.type:
return filterTimelines(state, action.relationship, action.statuses); return filterTimelines(state, action.payload.relationship, action.payload.statuses);
case ACCOUNT_UNFOLLOW_SUCCESS: case unfollowAccountSuccess.type:
return filterTimeline('home', state, action.relationship, action.statuses); return filterTimeline('home', state, action.payload.relationship, action.payload.statuses);
case TIMELINE_SCROLL_TOP: case TIMELINE_SCROLL_TOP:
return updateTop(state, action.timeline, action.top); return updateTop(state, action.timeline, action.top);
case TIMELINE_CONNECT: case TIMELINE_CONNECT:

View File

@ -33,8 +33,8 @@ import {
FOLLOW_REQUESTS_EXPAND_REQUEST, FOLLOW_REQUESTS_EXPAND_REQUEST,
FOLLOW_REQUESTS_EXPAND_SUCCESS, FOLLOW_REQUESTS_EXPAND_SUCCESS,
FOLLOW_REQUESTS_EXPAND_FAIL, FOLLOW_REQUESTS_EXPAND_FAIL,
FOLLOW_REQUEST_AUTHORIZE_SUCCESS, authorizeFollowRequestSuccess,
FOLLOW_REQUEST_REJECT_SUCCESS, rejectFollowRequestSuccess,
} from '../actions/accounts'; } from '../actions/accounts';
import { import {
BLOCKS_FETCH_REQUEST, BLOCKS_FETCH_REQUEST,
@ -66,11 +66,7 @@ import {
MUTES_EXPAND_SUCCESS, MUTES_EXPAND_SUCCESS,
MUTES_EXPAND_FAIL, MUTES_EXPAND_FAIL,
} from '../actions/mutes'; } from '../actions/mutes';
import { import { notificationsUpdate } from '../actions/notifications';
NOTIFICATIONS_UPDATE,
} from '../actions/notifications';
const initialListState = ImmutableMap({ const initialListState = ImmutableMap({
next: null, next: null,
@ -163,8 +159,8 @@ export default function userLists(state = initialState, action) {
case FAVOURITES_FETCH_FAIL: case FAVOURITES_FETCH_FAIL:
case FAVOURITES_EXPAND_FAIL: case FAVOURITES_EXPAND_FAIL:
return state.setIn(['favourited_by', action.id, 'isLoading'], false); return state.setIn(['favourited_by', action.id, 'isLoading'], false);
case NOTIFICATIONS_UPDATE: case notificationsUpdate.type:
return action.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.notification) : state; return action.payload.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.payload.notification) : state;
case FOLLOW_REQUESTS_FETCH_SUCCESS: case FOLLOW_REQUESTS_FETCH_SUCCESS:
return normalizeList(state, ['follow_requests'], action.accounts, action.next); return normalizeList(state, ['follow_requests'], action.accounts, action.next);
case FOLLOW_REQUESTS_EXPAND_SUCCESS: case FOLLOW_REQUESTS_EXPAND_SUCCESS:
@ -175,9 +171,9 @@ export default function userLists(state = initialState, action) {
case FOLLOW_REQUESTS_FETCH_FAIL: case FOLLOW_REQUESTS_FETCH_FAIL:
case FOLLOW_REQUESTS_EXPAND_FAIL: case FOLLOW_REQUESTS_EXPAND_FAIL:
return state.setIn(['follow_requests', 'isLoading'], false); return state.setIn(['follow_requests', 'isLoading'], false);
case FOLLOW_REQUEST_AUTHORIZE_SUCCESS: case authorizeFollowRequestSuccess.type:
case FOLLOW_REQUEST_REJECT_SUCCESS: case rejectFollowRequestSuccess.type:
return state.updateIn(['follow_requests', 'items'], list => list.filterNot(item => item === action.id)); return state.updateIn(['follow_requests', 'items'], list => list.filterNot(item => item === action.payload.id));
case BLOCKS_FETCH_SUCCESS: case BLOCKS_FETCH_SUCCESS:
return normalizeList(state, ['blocks'], action.accounts, action.next); return normalizeList(state, ['blocks'], action.accounts, action.next);
case BLOCKS_EXPAND_SUCCESS: case BLOCKS_EXPAND_SUCCESS:

View File

@ -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<AccountShape, 'moved'> {
relationship: Relationship | null;
moved: Account | null;
}
const FullAccountFactory = ImmutableRecord<FullAccountShape>({
...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);
},
);
}

View File

@ -5,23 +5,7 @@ import { toServerSideType } from 'mastodon/utils/filters';
import { me } from '../initial_state'; import { me } from '../initial_state';
const getAccountBase = (state, id) => state.getIn(['accounts', id], null); export { makeGetAccount } from "./accounts";
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);
});
});
};
const getFilters = (state, { contextType }) => { const getFilters = (state, { contextType }) => {
if (!contextType) return null; if (!contextType) return null;

View File

@ -35,6 +35,5 @@ export const store = configureStore({
// Infer the `RootState` and `AppDispatch` types from the store itself // Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof rootReducer>; export type RootState = ReturnType<typeof rootReducer>;
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch; export type AppDispatch = typeof store.dispatch;
export type GetState = typeof store.getState; export type GetState = typeof store.getState;

View File

@ -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
>;