Refactor context reducer to TypeScript (#34506)

This commit is contained in:
Eugen Rochko 2025-04-28 15:38:40 +02:00 committed by GitHub
parent bd9223f0b9
commit 17d8e2b6e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 308 additions and 234 deletions

View File

@ -4,8 +4,11 @@ import api from '../api';
import { ensureComposeIsVisible, setComposeToStatus } from './compose'; import { ensureComposeIsVisible, setComposeToStatus } from './compose';
import { importFetchedStatus, importFetchedStatuses, importFetchedAccount } from './importer'; import { importFetchedStatus, importFetchedStatuses, importFetchedAccount } from './importer';
import { fetchContext } from './statuses_typed';
import { deleteFromTimelines } from './timelines'; import { deleteFromTimelines } from './timelines';
export * from './statuses_typed';
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST'; export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS'; export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS';
export const STATUS_FETCH_FAIL = 'STATUS_FETCH_FAIL'; export const STATUS_FETCH_FAIL = 'STATUS_FETCH_FAIL';
@ -14,10 +17,6 @@ export const STATUS_DELETE_REQUEST = 'STATUS_DELETE_REQUEST';
export const STATUS_DELETE_SUCCESS = 'STATUS_DELETE_SUCCESS'; export const STATUS_DELETE_SUCCESS = 'STATUS_DELETE_SUCCESS';
export const STATUS_DELETE_FAIL = 'STATUS_DELETE_FAIL'; export const STATUS_DELETE_FAIL = 'STATUS_DELETE_FAIL';
export const CONTEXT_FETCH_REQUEST = 'CONTEXT_FETCH_REQUEST';
export const CONTEXT_FETCH_SUCCESS = 'CONTEXT_FETCH_SUCCESS';
export const CONTEXT_FETCH_FAIL = 'CONTEXT_FETCH_FAIL';
export const STATUS_MUTE_REQUEST = 'STATUS_MUTE_REQUEST'; export const STATUS_MUTE_REQUEST = 'STATUS_MUTE_REQUEST';
export const STATUS_MUTE_SUCCESS = 'STATUS_MUTE_SUCCESS'; export const STATUS_MUTE_SUCCESS = 'STATUS_MUTE_SUCCESS';
export const STATUS_MUTE_FAIL = 'STATUS_MUTE_FAIL'; export const STATUS_MUTE_FAIL = 'STATUS_MUTE_FAIL';
@ -54,7 +53,7 @@ export function fetchStatus(id, forceFetch = false, alsoFetchContext = true) {
const skipLoading = !forceFetch && getState().getIn(['statuses', id], null) !== null; const skipLoading = !forceFetch && getState().getIn(['statuses', id], null) !== null;
if (alsoFetchContext) { if (alsoFetchContext) {
dispatch(fetchContext(id)); dispatch(fetchContext({ statusId: id }));
} }
if (skipLoading) { if (skipLoading) {
@ -178,50 +177,6 @@ export function deleteStatusFail(id, error) {
export const updateStatus = status => dispatch => export const updateStatus = status => dispatch =>
dispatch(importFetchedStatus(status)); dispatch(importFetchedStatus(status));
export function fetchContext(id) {
return (dispatch) => {
dispatch(fetchContextRequest(id));
api().get(`/api/v1/statuses/${id}/context`).then(response => {
dispatch(importFetchedStatuses(response.data.ancestors.concat(response.data.descendants)));
dispatch(fetchContextSuccess(id, response.data.ancestors, response.data.descendants));
}).catch(error => {
if (error.response && error.response.status === 404) {
dispatch(deleteFromTimelines(id));
}
dispatch(fetchContextFail(id, error));
});
};
}
export function fetchContextRequest(id) {
return {
type: CONTEXT_FETCH_REQUEST,
id,
};
}
export function fetchContextSuccess(id, ancestors, descendants) {
return {
type: CONTEXT_FETCH_SUCCESS,
id,
ancestors,
descendants,
statuses: ancestors.concat(descendants),
};
}
export function fetchContextFail(id, error) {
return {
type: CONTEXT_FETCH_FAIL,
id,
error,
skipAlert: true,
};
}
export function muteStatus(id) { export function muteStatus(id) {
return (dispatch) => { return (dispatch) => {
dispatch(muteStatusRequest(id)); dispatch(muteStatusRequest(id));

View File

@ -0,0 +1,18 @@
import { apiGetContext } from 'mastodon/api/statuses';
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
import { importFetchedStatuses } from './importer';
export const fetchContext = createDataLoadingThunk(
'status/context',
({ statusId }: { statusId: string }) => apiGetContext(statusId),
(context, { dispatch }) => {
const statuses = context.ancestors.concat(context.descendants);
dispatch(importFetchedStatuses(statuses));
return {
context,
};
},
);

View File

@ -0,0 +1,5 @@
import { apiRequestGet } from 'mastodon/api';
import type { ApiContextJSON } from 'mastodon/api_types/statuses';
export const apiGetContext = (statusId: string) =>
apiRequestGet<ApiContextJSON>(`v1/statuses/${statusId}/context`);

View File

@ -119,3 +119,8 @@ export interface ApiStatusJSON {
card?: ApiPreviewCardJSON; card?: ApiPreviewCardJSON;
poll?: ApiPollJSON; poll?: ApiPollJSON;
} }
export interface ApiContextJSON {
ancestors: ApiStatusJSON[];
descendants: ApiStatusJSON[];
}

View File

@ -39,9 +39,9 @@ export const NotificationMention: React.FC<{
unread: boolean; unread: boolean;
}> = ({ notification, unread }) => { }> = ({ notification, unread }) => {
const [isDirect, isReply] = useAppSelector((state) => { const [isDirect, isReply] = useAppSelector((state) => {
const status = state.statuses.get(notification.statusId) as const status = notification.statusId
| Status ? (state.statuses.get(notification.statusId) as Status | undefined)
| undefined; : undefined;
if (!status) return [false, false] as const; if (!status) return [false, false] as const;

View File

@ -65,6 +65,7 @@ import { textForScreenReader, defaultMediaVisibility } from '../../components/st
import StatusContainer from '../../containers/status_container'; import StatusContainer from '../../containers/status_container';
import { deleteModal } from '../../initial_state'; import { deleteModal } from '../../initial_state';
import { makeGetStatus, makeGetPictureInPicture } from '../../selectors'; import { makeGetStatus, makeGetPictureInPicture } from '../../selectors';
import { getAncestorsIds, getDescendantsIds } from 'mastodon/selectors/contexts';
import Column from '../ui/components/column'; import Column from '../ui/components/column';
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen'; import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen';
@ -83,69 +84,15 @@ const makeMapStateToProps = () => {
const getStatus = makeGetStatus(); const getStatus = makeGetStatus();
const getPictureInPicture = makeGetPictureInPicture(); const getPictureInPicture = makeGetPictureInPicture();
const getAncestorsIds = createSelector([
(_, { id }) => id,
state => state.getIn(['contexts', 'inReplyTos']),
], (statusId, inReplyTos) => {
let ancestorsIds = ImmutableList();
ancestorsIds = ancestorsIds.withMutations(mutable => {
let id = statusId;
while (id && !mutable.includes(id)) {
mutable.unshift(id);
id = inReplyTos.get(id);
}
});
return ancestorsIds;
});
const getDescendantsIds = createSelector([
(_, { id }) => id,
state => state.getIn(['contexts', 'replies']),
state => state.get('statuses'),
], (statusId, contextReplies, statuses) => {
let descendantsIds = [];
const ids = [statusId];
while (ids.length > 0) {
let id = ids.pop();
const replies = contextReplies.get(id);
if (statusId !== id) {
descendantsIds.push(id);
}
if (replies) {
replies.reverse().forEach(reply => {
if (!ids.includes(reply) && !descendantsIds.includes(reply) && statusId !== reply) ids.push(reply);
});
}
}
let insertAt = descendantsIds.findIndex((id) => statuses.get(id).get('in_reply_to_account_id') !== statuses.get(id).get('account'));
if (insertAt !== -1) {
descendantsIds.forEach((id, idx) => {
if (idx > insertAt && statuses.get(id).get('in_reply_to_account_id') === statuses.get(id).get('account')) {
descendantsIds.splice(idx, 1);
descendantsIds.splice(insertAt, 0, id);
insertAt += 1;
}
});
}
return ImmutableList(descendantsIds);
});
const mapStateToProps = (state, props) => { const mapStateToProps = (state, props) => {
const status = getStatus(state, { id: props.params.statusId, contextType: 'detailed' }); const status = getStatus(state, { id: props.params.statusId, contextType: 'detailed' });
let ancestorsIds = ImmutableList(); let ancestorsIds = [];
let descendantsIds = ImmutableList(); let descendantsIds = [];
if (status) { if (status) {
ancestorsIds = getAncestorsIds(state, { id: status.get('in_reply_to_id') }); ancestorsIds = getAncestorsIds(state, status.get('in_reply_to_id'));
descendantsIds = getDescendantsIds(state, { id: status.get('id') }); descendantsIds = getDescendantsIds(state, status.get('id'));
} }
return { return {
@ -188,8 +135,8 @@ class Status extends ImmutablePureComponent {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
status: ImmutablePropTypes.map, status: ImmutablePropTypes.map,
isLoading: PropTypes.bool, isLoading: PropTypes.bool,
ancestorsIds: ImmutablePropTypes.list.isRequired, ancestorsIds: PropTypes.arrayOf(PropTypes.string).isRequired,
descendantsIds: ImmutablePropTypes.list.isRequired, descendantsIds: PropTypes.arrayOf(PropTypes.string).isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
askReplyConfirmation: PropTypes.bool, askReplyConfirmation: PropTypes.bool,
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
@ -383,7 +330,7 @@ class Status extends ImmutablePureComponent {
handleToggleAll = () => { handleToggleAll = () => {
const { status, ancestorsIds, descendantsIds } = this.props; const { status, ancestorsIds, descendantsIds } = this.props;
const statusIds = [status.get('id')].concat(ancestorsIds.toJS(), descendantsIds.toJS()); const statusIds = [status.get('id')].concat(ancestorsIds, descendantsIds);
if (status.get('hidden')) { if (status.get('hidden')) {
this.props.dispatch(revealStatus(statusIds)); this.props.dispatch(revealStatus(statusIds));
@ -482,13 +429,13 @@ class Status extends ImmutablePureComponent {
const { status, ancestorsIds, descendantsIds } = this.props; const { status, ancestorsIds, descendantsIds } = this.props;
if (id === status.get('id')) { if (id === status.get('id')) {
this._selectChild(ancestorsIds.size - 1, true); this._selectChild(ancestorsIds.length - 1, true);
} else { } else {
let index = ancestorsIds.indexOf(id); let index = ancestorsIds.indexOf(id);
if (index === -1) { if (index === -1) {
index = descendantsIds.indexOf(id); index = descendantsIds.indexOf(id);
this._selectChild(ancestorsIds.size + index, true); this._selectChild(ancestorsIds.length + index, true);
} else { } else {
this._selectChild(index - 1, true); this._selectChild(index - 1, true);
} }
@ -499,13 +446,13 @@ class Status extends ImmutablePureComponent {
const { status, ancestorsIds, descendantsIds } = this.props; const { status, ancestorsIds, descendantsIds } = this.props;
if (id === status.get('id')) { if (id === status.get('id')) {
this._selectChild(ancestorsIds.size + 1, false); this._selectChild(ancestorsIds.length + 1, false);
} else { } else {
let index = ancestorsIds.indexOf(id); let index = ancestorsIds.indexOf(id);
if (index === -1) { if (index === -1) {
index = descendantsIds.indexOf(id); index = descendantsIds.indexOf(id);
this._selectChild(ancestorsIds.size + index + 2, false); this._selectChild(ancestorsIds.length + index + 2, false);
} else { } else {
this._selectChild(index + 1, false); this._selectChild(index + 1, false);
} }
@ -536,8 +483,8 @@ class Status extends ImmutablePureComponent {
onMoveUp={this.handleMoveUp} onMoveUp={this.handleMoveUp}
onMoveDown={this.handleMoveDown} onMoveDown={this.handleMoveDown}
contextType='thread' contextType='thread'
previousId={i > 0 ? list.get(i - 1) : undefined} previousId={i > 0 ? list[i - 1] : undefined}
nextId={list.get(i + 1) || (ancestors && statusId)} nextId={list[i + 1] || (ancestors && statusId)}
rootId={statusId} rootId={statusId}
/> />
)); ));
@ -574,7 +521,7 @@ class Status extends ImmutablePureComponent {
componentDidUpdate (prevProps) { componentDidUpdate (prevProps) {
const { status, ancestorsIds } = this.props; const { status, ancestorsIds } = this.props;
if (status && (ancestorsIds.size > prevProps.ancestorsIds.size || prevProps.status?.get('id') !== status.get('id'))) { if (status && (ancestorsIds.length > prevProps.ancestorsIds.length || prevProps.status?.get('id') !== status.get('id'))) {
this._scrollStatusIntoView(); this._scrollStatusIntoView();
} }
} }
@ -621,11 +568,11 @@ class Status extends ImmutablePureComponent {
); );
} }
if (ancestorsIds && ancestorsIds.size > 0) { if (ancestorsIds && ancestorsIds.length > 0) {
ancestors = <>{this.renderChildren(ancestorsIds, true)}</>; ancestors = <>{this.renderChildren(ancestorsIds, true)}</>;
} }
if (descendantsIds && descendantsIds.size > 0) { if (descendantsIds && descendantsIds.length > 0) {
descendants = <>{this.renderChildren(descendantsIds)}</>; descendants = <>{this.renderChildren(descendantsIds)}</>;
} }

View File

@ -1,109 +0,0 @@
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { timelineDelete } from 'mastodon/actions/timelines_typed';
import {
blockAccountSuccess,
muteAccountSuccess,
} from '../actions/accounts';
import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses';
import { TIMELINE_UPDATE } from '../actions/timelines';
import { compareId } from '../compare_id';
const initialState = ImmutableMap({
inReplyTos: ImmutableMap(),
replies: ImmutableMap(),
});
const normalizeContext = (immutableState, id, ancestors, descendants) => immutableState.withMutations(state => {
state.update('inReplyTos', immutableAncestors => immutableAncestors.withMutations(inReplyTos => {
state.update('replies', immutableDescendants => immutableDescendants.withMutations(replies => {
function addReply({ id, in_reply_to_id }) {
if (in_reply_to_id && !inReplyTos.has(id)) {
replies.update(in_reply_to_id, ImmutableList(), siblings => {
const index = siblings.findLastIndex(sibling => compareId(sibling, id) < 0);
return siblings.insert(index + 1, id);
});
inReplyTos.set(id, in_reply_to_id);
}
}
// We know in_reply_to_id of statuses but `id` itself.
// So we assume that the status of the id replies to last ancestors.
ancestors.forEach(addReply);
if (ancestors[0]) {
addReply({ id, in_reply_to_id: ancestors[ancestors.length - 1].id });
}
descendants.forEach(addReply);
}));
}));
});
const deleteFromContexts = (immutableState, ids) => immutableState.withMutations(state => {
state.update('inReplyTos', immutableAncestors => immutableAncestors.withMutations(inReplyTos => {
state.update('replies', immutableDescendants => immutableDescendants.withMutations(replies => {
ids.forEach(id => {
const inReplyToIdOfId = inReplyTos.get(id);
const repliesOfId = replies.get(id);
const siblings = replies.get(inReplyToIdOfId);
if (siblings) {
replies.set(inReplyToIdOfId, siblings.filterNot(sibling => sibling === id));
}
if (repliesOfId) {
repliesOfId.forEach(reply => inReplyTos.delete(reply));
}
inReplyTos.delete(id);
replies.delete(id);
});
}));
}));
});
const filterContexts = (state, relationship, statuses) => {
const ownedStatusIds = statuses
.filter(status => status.get('account') === relationship.id)
.map(status => status.get('id'));
return deleteFromContexts(state, ownedStatusIds);
};
const updateContext = (state, status) => {
if (status.in_reply_to_id) {
return state.withMutations(mutable => {
const replies = mutable.getIn(['replies', status.in_reply_to_id], ImmutableList());
mutable.setIn(['inReplyTos', status.id], status.in_reply_to_id);
if (!replies.includes(status.id)) {
mutable.setIn(['replies', status.in_reply_to_id], replies.push(status.id));
}
});
}
return state;
};
export default function replies(state = initialState, action) {
switch(action.type) {
case blockAccountSuccess.type:
case muteAccountSuccess.type:
return filterContexts(state, action.payload.relationship, action.payload.statuses);
case CONTEXT_FETCH_SUCCESS:
return normalizeContext(state, action.id, action.ancestors, action.descendants);
case timelineDelete.type:
return deleteFromContexts(state, [action.payload.statusId]);
case TIMELINE_UPDATE:
return updateContext(state, action.status);
default:
return state;
}
}

View File

@ -0,0 +1,155 @@
/* eslint-disable @typescript-eslint/no-dynamic-delete */
import { createReducer } from '@reduxjs/toolkit';
import type { Draft, UnknownAction } from '@reduxjs/toolkit';
import type { List as ImmutableList } from 'immutable';
import { timelineDelete } from 'mastodon/actions/timelines_typed';
import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships';
import type {
ApiStatusJSON,
ApiContextJSON,
} from 'mastodon/api_types/statuses';
import type { Status } from 'mastodon/models/status';
import { blockAccountSuccess, muteAccountSuccess } from '../actions/accounts';
import { fetchContext } from '../actions/statuses';
import { TIMELINE_UPDATE } from '../actions/timelines';
import { compareId } from '../compare_id';
interface TimelineUpdateAction extends UnknownAction {
timeline: string;
status: ApiStatusJSON;
usePendingItems: boolean;
}
interface State {
inReplyTos: Record<string, string>;
replies: Record<string, string[]>;
}
const initialState: State = {
inReplyTos: {},
replies: {},
};
const normalizeContext = (
state: Draft<State>,
id: string,
{ ancestors, descendants }: ApiContextJSON,
): void => {
const addReply = ({
id,
in_reply_to_id,
}: {
id: string;
in_reply_to_id?: string;
}) => {
if (!in_reply_to_id) {
return;
}
if (!state.inReplyTos[id]) {
const siblings = (state.replies[in_reply_to_id] ??= []);
const index = siblings.findIndex((sibling) => compareId(sibling, id) < 0);
siblings.splice(index + 1, 0, id);
state.inReplyTos[id] = in_reply_to_id;
}
};
// We know in_reply_to_id of statuses but `id` itself.
// So we assume that the status of the id replies to last ancestors.
ancestors.forEach(addReply);
if (ancestors[0]) {
addReply({
id,
in_reply_to_id: ancestors[ancestors.length - 1]?.id,
});
}
descendants.forEach(addReply);
};
const deleteFromContexts = (state: Draft<State>, ids: string[]): void => {
ids.forEach((id) => {
const inReplyToIdOfId = state.inReplyTos[id];
const repliesOfId = state.replies[id];
if (inReplyToIdOfId) {
const siblings = state.replies[inReplyToIdOfId];
if (siblings) {
state.replies[inReplyToIdOfId] = siblings.filter(
(sibling) => sibling !== id,
);
}
}
if (repliesOfId) {
repliesOfId.forEach((reply) => {
delete state.inReplyTos[reply];
});
}
delete state.inReplyTos[id];
delete state.replies[id];
});
};
const filterContexts = (
state: Draft<State>,
relationship: ApiRelationshipJSON,
statuses: ImmutableList<Status>,
): void => {
const ownedStatusIds = statuses
.filter((status) => (status.get('account') as string) === relationship.id)
.map((status) => status.get('id') as string);
deleteFromContexts(state, ownedStatusIds.toArray());
};
const updateContext = (state: Draft<State>, status: ApiStatusJSON): void => {
if (!status.in_reply_to_id) {
return;
}
const siblings = (state.replies[status.in_reply_to_id] ??= []);
state.inReplyTos[status.id] = status.in_reply_to_id;
if (!siblings.includes(status.id)) {
siblings.push(status.id);
}
};
export const contextsReducer = createReducer(initialState, (builder) => {
builder
.addCase(fetchContext.fulfilled, (state, action) => {
normalizeContext(state, action.meta.arg.statusId, action.payload.context);
})
.addCase(blockAccountSuccess, (state, action) => {
filterContexts(
state,
action.payload.relationship,
action.payload.statuses as ImmutableList<Status>,
);
})
.addCase(muteAccountSuccess, (state, action) => {
filterContexts(
state,
action.payload.relationship,
action.payload.statuses as ImmutableList<Status>,
);
})
.addCase(timelineDelete, (state, action) => {
deleteFromContexts(state, [action.payload.statusId]);
})
.addMatcher(
(action: UnknownAction): action is TimelineUpdateAction =>
action.type === TIMELINE_UPDATE,
(state, action) => {
updateContext(state, action.status);
},
);
});

View File

@ -8,7 +8,7 @@ import accounts_map from './accounts_map';
import { alertsReducer } from './alerts'; import { alertsReducer } from './alerts';
import announcements from './announcements'; import announcements from './announcements';
import { composeReducer } from './compose'; import { composeReducer } from './compose';
import contexts from './contexts'; import { contextsReducer } from './contexts';
import conversations from './conversations'; import conversations from './conversations';
import custom_emojis from './custom_emojis'; import custom_emojis from './custom_emojis';
import { dropdownMenuReducer } from './dropdown_menu'; import { dropdownMenuReducer } from './dropdown_menu';
@ -55,7 +55,7 @@ const reducers = {
settings, settings,
push_notifications, push_notifications,
server, server,
contexts, contexts: contextsReducer,
compose: composeReducer, compose: composeReducer,
search: searchReducer, search: searchReducer,
media_attachments, media_attachments,

View File

@ -64,6 +64,7 @@ const statusTranslateUndo = (state, id) => {
}); });
}; };
/** @type {ImmutableMap<string, ImmutableMap<string, any>>} */
const initialState = ImmutableMap(); const initialState = ImmutableMap();
/** @type {import('@reduxjs/toolkit').Reducer<typeof initialState>} */ /** @type {import('@reduxjs/toolkit').Reducer<typeof initialState>} */

View File

@ -0,0 +1,94 @@
import { createAppSelector } from 'mastodon/store';
export const getAncestorsIds = createAppSelector(
[(_, id: string) => id, (state) => state.contexts.inReplyTos],
(statusId, inReplyTos) => {
const ancestorsIds: string[] = [];
let currentId: string | undefined = statusId;
while (currentId && !ancestorsIds.includes(currentId)) {
ancestorsIds.unshift(currentId);
currentId = inReplyTos[currentId];
}
return ancestorsIds;
},
);
export const getDescendantsIds = createAppSelector(
[
(_, id: string) => id,
(state) => state.contexts.replies,
(state) => state.statuses,
],
(statusId, contextReplies, statuses) => {
const descendantsIds: string[] = [];
const visitIds = [statusId];
while (visitIds.length > 0) {
const id = visitIds.pop();
if (!id) {
break;
}
const replies = contextReplies[id];
if (statusId !== id) {
descendantsIds.push(id);
}
if (replies) {
replies.reverse().forEach((replyId) => {
if (
!visitIds.includes(replyId) &&
!descendantsIds.includes(replyId) &&
statusId !== replyId
) {
visitIds.push(replyId);
}
});
}
}
let insertAt = descendantsIds.findIndex((id) => {
const status = statuses.get(id);
if (!status) {
return false;
}
const inReplyToAccountId = status.get('in_reply_to_account_id') as
| string
| null;
const accountId = status.get('account') as string;
return inReplyToAccountId !== accountId;
});
if (insertAt !== -1) {
descendantsIds.forEach((id, idx) => {
const status = statuses.get(id);
if (!status) {
return;
}
const inReplyToAccountId = status.get('in_reply_to_account_id') as
| string
| null;
const accountId = status.get('account') as string;
if (idx > insertAt && inReplyToAccountId === accountId) {
descendantsIds.splice(idx, 1);
descendantsIds.splice(insertAt, 0, id);
insertAt += 1;
}
});
}
return descendantsIds;
},
);

View File

@ -3,6 +3,7 @@ export type { GetState, AppDispatch, RootState } from './store';
export { export {
createAppAsyncThunk, createAppAsyncThunk,
createAppSelector,
useAppDispatch, useAppDispatch,
useAppSelector, useAppSelector,
} from './typed_functions'; } from './typed_functions';

View File

@ -1,5 +1,5 @@
import type { GetThunkAPI } from '@reduxjs/toolkit'; import type { GetThunkAPI } from '@reduxjs/toolkit';
import { createAsyncThunk } from '@reduxjs/toolkit'; import { createAsyncThunk, createSelector } from '@reduxjs/toolkit';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports // eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
@ -24,6 +24,8 @@ export const createAppAsyncThunk = createAsyncThunk.withTypes<{
rejectValue: AsyncThunkRejectValue; rejectValue: AsyncThunkRejectValue;
}>(); }>();
export const createAppSelector = createSelector.withTypes<RootState>();
interface AppThunkConfig { interface AppThunkConfig {
state: RootState; state: RootState;
dispatch: AppDispatch; dispatch: AppDispatch;