Refactor accounts map reducer to TypeScript

This commit is contained in:
Eugen Rochko 2025-03-07 11:46:17 +01:00
parent db269a4c0a
commit fbe899fe14
13 changed files with 193 additions and 162 deletions

View File

@ -92,34 +92,6 @@ export function fetchAccount(id) {
};
}
export const lookupAccount = acct => (dispatch) => {
dispatch(lookupAccountRequest(acct));
api().get('/api/v1/accounts/lookup', { params: { acct } }).then(response => {
dispatch(fetchRelationships([response.data.id]));
dispatch(importFetchedAccount(response.data));
dispatch(lookupAccountSuccess());
}).catch(error => {
dispatch(lookupAccountFail(acct, error));
});
};
export const lookupAccountRequest = (acct) => ({
type: ACCOUNT_LOOKUP_REQUEST,
acct,
});
export const lookupAccountSuccess = () => ({
type: ACCOUNT_LOOKUP_SUCCESS,
});
export const lookupAccountFail = (acct, error) => ({
type: ACCOUNT_LOOKUP_FAIL,
acct,
error,
skipAlert: true,
});
export function fetchAccountRequest(id) {
return {
type: ACCOUNT_FETCH_REQUEST,

View File

@ -1,16 +1,15 @@
import { createAction } from '@reduxjs/toolkit';
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
import { apiLookupAccount } from 'mastodon/api/accounts';
import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships';
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
import { importFetchedAccount } from './importer';
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: {
@ -95,3 +94,14 @@ export const fetchRelationshipsSuccess = createAction(
'relationships/fetch/SUCCESS',
actionWithSkipLoadingTrue<{ relationships: ApiRelationshipJSON[] }>,
);
export const lookupAccount = createDataLoadingThunk(
'accounts/lookup',
({ acct }: { acct: string }) => apiLookupAccount(acct),
(data, { dispatch }) => {
dispatch(importFetchedAccount(data));
//dispatch(fetchRelationships([data.id]));
return data;
},
);

View File

@ -1,89 +0,0 @@
import { createPollFromServerJSON } from 'mastodon/models/poll';
import { importAccounts } from '../accounts_typed';
import { normalizeStatus } from './normalizer';
import { importPolls } from './polls';
export const STATUS_IMPORT = 'STATUS_IMPORT';
export const STATUSES_IMPORT = 'STATUSES_IMPORT';
export const FILTERS_IMPORT = 'FILTERS_IMPORT';
function pushUnique(array, object) {
if (array.every(element => element.id !== object.id)) {
array.push(object);
}
}
export function importStatus(status) {
return { type: STATUS_IMPORT, status };
}
export function importStatuses(statuses) {
return { type: STATUSES_IMPORT, statuses };
}
export function importFilters(filters) {
return { type: FILTERS_IMPORT, filters };
}
export function importFetchedAccount(account) {
return importFetchedAccounts([account]);
}
export function importFetchedAccounts(accounts) {
const normalAccounts = [];
function processAccount(account) {
pushUnique(normalAccounts, account);
if (account.moved) {
processAccount(account.moved);
}
}
accounts.forEach(processAccount);
return importAccounts({ accounts: normalAccounts });
}
export function importFetchedStatus(status) {
return importFetchedStatuses([status]);
}
export function importFetchedStatuses(statuses) {
return (dispatch, getState) => {
const accounts = [];
const normalStatuses = [];
const polls = [];
const filters = [];
function processStatus(status) {
pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id])));
pushUnique(accounts, status.account);
if (status.filtered) {
status.filtered.forEach(result => pushUnique(filters, result.filter));
}
if (status.reblog?.id) {
processStatus(status.reblog);
}
if (status.poll?.id) {
pushUnique(polls, createPollFromServerJSON(status.poll, getState().polls.get(status.poll.id)));
}
if (status.card) {
status.card.authors.forEach(author => author.account && pushUnique(accounts, author.account));
}
}
statuses.forEach(processStatus);
dispatch(importPolls({ polls }));
dispatch(importFetchedAccounts(accounts));
dispatch(importStatuses(normalStatuses));
dispatch(importFilters(filters));
};
}

View File

@ -0,0 +1,117 @@
import { createAction } from '@reduxjs/toolkit';
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
import type { ApiStatusJSON, ApiFilterJSON } from 'mastodon/api_types/statuses';
import type { Poll } from 'mastodon/models/poll';
import { createPollFromServerJSON } from 'mastodon/models/poll';
import type { AppDispatch, RootState } from 'mastodon/store';
import { normalizeStatus } from './normalizer';
import { importPolls } from './polls';
export const STATUS_IMPORT = 'STATUS_IMPORT';
export const STATUSES_IMPORT = 'STATUSES_IMPORT';
export const FILTERS_IMPORT = 'FILTERS_IMPORT';
export const importAccounts = createAction<{ accounts: ApiAccountJSON[] }>(
'accounts/importAccounts',
);
interface Identifiable {
id: string;
}
const pushUnique = (array: Identifiable[], object: Identifiable) => {
if (array.every((element) => element.id !== object.id)) {
array.push(object);
}
};
export const importStatus = (status: ApiStatusJSON) => {
return { type: STATUS_IMPORT, status };
};
export const importStatuses = (statuses: ApiStatusJSON[]) => {
return { type: STATUSES_IMPORT, statuses };
};
export const importFilters = (filters: unknown[]) => {
return { type: FILTERS_IMPORT, filters };
};
export const importFetchedAccount = (account: ApiAccountJSON) => {
return importFetchedAccounts([account]);
};
export const importFetchedAccounts = (accounts: ApiAccountJSON[]) => {
const normalAccounts: ApiAccountJSON[] = [];
function processAccount(account: ApiAccountJSON) {
pushUnique(normalAccounts, account);
if (account.moved) {
processAccount(account.moved);
}
}
accounts.forEach(processAccount);
return importAccounts({ accounts: normalAccounts });
};
export const importFetchedStatus = (status: ApiStatusJSON) => {
return importFetchedStatuses([status]);
};
export const importFetchedStatuses = (statuses: ApiStatusJSON[]) => {
return (dispatch: AppDispatch, getState: () => RootState) => {
const accounts: ApiAccountJSON[] = [];
const normalStatuses: ApiStatusJSON[] = [];
const polls: Poll[] = [];
const filters: ApiFilterJSON[] = [];
function processStatus(status: ApiStatusJSON) {
pushUnique(
normalStatuses,
normalizeStatus(
status,
getState().statuses.get(status.id),
) as ApiStatusJSON,
);
pushUnique(accounts, status.account);
if (status.filtered) {
status.filtered.forEach((result) => {
pushUnique(filters, result.filter);
});
}
if (status.reblog?.id) {
processStatus(status.reblog);
}
if (status.poll?.id) {
pushUnique(
polls,
createPollFromServerJSON(
status.poll,
getState().polls.get(status.poll.id),
),
);
}
if (status.card) {
status.card.authors.forEach((author) => {
if (author.account) pushUnique(accounts, author.account);
});
}
}
statuses.forEach(processStatus);
dispatch(importPolls({ polls }));
dispatch(importFetchedAccounts(accounts));
dispatch(importStatuses(normalStatuses));
dispatch(importFilters(filters));
};
};

View File

@ -1,4 +1,5 @@
import { apiRequestPost } from 'mastodon/api';
import { apiRequestPost, apiRequestGet } from 'mastodon/api';
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships';
export const apiSubmitAccountNote = (id: string, value: string) =>
@ -18,3 +19,6 @@ export const apiFollowAccount = (
export const apiUnfollowAccount = (id: string) =>
apiRequestPost<ApiRelationshipJSON>(`v1/accounts/${id}/unfollow`);
export const apiLookupAccount = (acct: string) =>
apiRequestGet<ApiAccountJSON>('v1/accounts/lookup', { acct });

View File

@ -8,7 +8,8 @@ import { createSelector } from '@reduxjs/toolkit';
import type { Map as ImmutableMap } from 'immutable';
import { List as ImmutableList } from 'immutable';
import { lookupAccount, fetchAccount } from 'mastodon/actions/accounts';
import { fetchAccount } from 'mastodon/actions/accounts';
import { lookupAccount } from 'mastodon/actions/accounts_typed';
import { openModal } from 'mastodon/actions/modal';
import { expandAccountMediaTimeline } from 'mastodon/actions/timelines';
import { ColumnBackButton } from 'mastodon/components/column_back_button';
@ -101,7 +102,10 @@ export const AccountGallery: React.FC<{
const accountId = useAppSelector(
(state) =>
id ??
(state.accounts_map.get(normalizeForLookup(acct)) as string | undefined),
(acct &&
(state.accounts_map.get(normalizeForLookup(acct)) as
| string
| undefined)),
);
const attachments = useAppSelector((state) =>
accountId
@ -140,8 +144,8 @@ export const AccountGallery: React.FC<{
| undefined;
useEffect(() => {
if (!accountId) {
dispatch(lookupAccount(acct));
if (!accountId && acct) {
void dispatch(lookupAccount({ acct }));
}
}, [dispatch, accountId, acct]);

View File

@ -7,6 +7,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import { lookupAccount } from 'mastodon/actions/accounts_typed';
import { TimelineHint } from 'mastodon/components/timeline_hint';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
import { me } from 'mastodon/initial_state';
@ -14,7 +15,7 @@ import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
import { getAccountHidden } from 'mastodon/selectors/accounts';
import { useAppSelector } from 'mastodon/store';
import { lookupAccount, fetchAccount } from '../../actions/accounts';
import { fetchAccount } from '../../actions/accounts';
import { fetchFeaturedTags } from '../../actions/featured_tags';
import { expandAccountFeaturedTimeline, expandAccountTimeline, connectTimeline, disconnectTimeline } from '../../actions/timelines';
import { ColumnBackButton } from '../../components/column_back_button';
@ -125,7 +126,7 @@ class AccountTimeline extends ImmutablePureComponent {
if (accountId) {
this._load();
} else {
dispatch(lookupAccount(acct));
dispatch(lookupAccount({ acct }));
}
}
@ -135,7 +136,7 @@ class AccountTimeline extends ImmutablePureComponent {
if (prevProps.accountId !== accountId && accountId) {
this._load();
} else if (prevProps.params.acct !== acct) {
dispatch(lookupAccount(acct));
dispatch(lookupAccount({ acct }));
} else if (prevProps.params.tagged !== tagged) {
if (!withReplies) {
dispatch(expandAccountFeaturedTimeline(accountId, { tagged }));

View File

@ -8,6 +8,7 @@ import { connect } from 'react-redux';
import { debounce } from 'lodash';
import { lookupAccount } from 'mastodon/actions/accounts_typed';
import { Account } from 'mastodon/components/account';
import { TimelineHint } from 'mastodon/components/timeline_hint';
import { AccountHeader } from 'mastodon/features/account_timeline/components/account_header';
@ -17,7 +18,6 @@ import { getAccountHidden } from 'mastodon/selectors/accounts';
import { useAppSelector } from 'mastodon/store';
import {
lookupAccount,
fetchAccount,
fetchFollowers,
expandFollowers,
@ -104,7 +104,7 @@ class Followers extends ImmutablePureComponent {
if (accountId) {
this._load();
} else {
dispatch(lookupAccount(acct));
dispatch(lookupAccount({ acct }));
}
}
@ -114,7 +114,7 @@ class Followers extends ImmutablePureComponent {
if (prevProps.accountId !== accountId && accountId) {
this._load();
} else if (prevProps.params.acct !== acct) {
dispatch(lookupAccount(acct));
dispatch(lookupAccount({ acct }));
}
}

View File

@ -8,6 +8,7 @@ import { connect } from 'react-redux';
import { debounce } from 'lodash';
import { lookupAccount } from 'mastodon/actions/accounts_typed';
import { Account } from 'mastodon/components/account';
import { TimelineHint } from 'mastodon/components/timeline_hint';
import { AccountHeader } from 'mastodon/features/account_timeline/components/account_header';
@ -17,7 +18,6 @@ import { getAccountHidden } from 'mastodon/selectors/accounts';
import { useAppSelector } from 'mastodon/store';
import {
lookupAccount,
fetchAccount,
fetchFollowing,
expandFollowing,
@ -104,7 +104,7 @@ class Following extends ImmutablePureComponent {
if (accountId) {
this._load();
} else {
dispatch(lookupAccount(acct));
dispatch(lookupAccount({ acct }));
}
}
@ -114,7 +114,7 @@ class Following extends ImmutablePureComponent {
if (prevProps.accountId !== accountId && accountId) {
this._load();
} else if (prevProps.params.acct !== acct) {
dispatch(lookupAccount(acct));
dispatch(lookupAccount({ acct }));
}
}

View File

@ -4,9 +4,9 @@ import { Map as ImmutableMap } from 'immutable';
import {
followAccountSuccess,
unfollowAccountSuccess,
importAccounts,
revealAccount,
} from 'mastodon/actions/accounts_typed';
import { importAccounts } from 'mastodon/actions/importer';
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
import { me } from 'mastodon/initial_state';
import type { Account } from 'mastodon/models/account';

View File

@ -1,23 +0,0 @@
import { Map as ImmutableMap } from 'immutable';
import { ACCOUNT_LOOKUP_FAIL } from '../actions/accounts';
import { importAccounts } from '../actions/accounts_typed';
import { domain } from '../initial_state';
const pattern = new RegExp(`@${domain}$`, 'gi');
export const normalizeForLookup = str =>
str.toLowerCase().replace(pattern, '');
const initialState = ImmutableMap();
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 importAccounts.type:
return state.withMutations(map => action.payload.accounts.forEach(account => map.set(normalizeForLookup(account.acct), account.id)));
default:
return state;
}
}

View File

@ -0,0 +1,35 @@
import type { Reducer } from '@reduxjs/toolkit';
import { Map as ImmutableMap } from 'immutable';
import { AxiosError } from 'axios';
import { lookupAccount } from 'mastodon/actions/accounts_typed';
import { importAccounts } from 'mastodon/actions/importer';
import { domain } from 'mastodon/initial_state';
const pattern = new RegExp(`@${domain}$`, 'gi');
export const normalizeForLookup = (str: string) =>
str.toLowerCase().replace(pattern, '');
const initialState = ImmutableMap<string, string | null>();
export const accountsMapReducer: Reducer<typeof initialState> = (
state = initialState,
action,
) => {
if (lookupAccount.rejected.match(action)) {
return action.error instanceof AxiosError &&
action.error.response?.status === 404
? state.set(normalizeForLookup(action.meta.arg.acct), null)
: state;
} else if (importAccounts.match(action)) {
return state.withMutations((map) => {
action.payload.accounts.forEach((account) =>
map.set(normalizeForLookup(account.acct), account.id),
);
});
} else {
return state;
}
};

View File

@ -4,7 +4,7 @@ import { loadingBarReducer } from 'react-redux-loading-bar';
import { combineReducers } from 'redux-immutable';
import { accountsReducer } from './accounts';
import accounts_map from './accounts_map';
import { accountsMapReducer } from './accounts_map';
import alerts from './alerts';
import announcements from './announcements';
import { composeReducer } from './compose';
@ -52,7 +52,7 @@ const reducers = {
domain_lists,
status_lists,
accounts: accountsReducer,
accounts_map,
accounts_map: accountsMapReducer,
statuses,
relationships: relationshipsReducer,
settings,