mirror of
https://github.com/mastodon/mastodon.git
synced 2025-05-07 12:16:14 +00:00
Refactor context reducer to TypeScript (#34506)
This commit is contained in:
parent
bd9223f0b9
commit
17d8e2b6e3
|
@ -4,8 +4,11 @@ import api from '../api';
|
|||
|
||||
import { ensureComposeIsVisible, setComposeToStatus } from './compose';
|
||||
import { importFetchedStatus, importFetchedStatuses, importFetchedAccount } from './importer';
|
||||
import { fetchContext } from './statuses_typed';
|
||||
import { deleteFromTimelines } from './timelines';
|
||||
|
||||
export * from './statuses_typed';
|
||||
|
||||
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
|
||||
export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS';
|
||||
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_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_SUCCESS = 'STATUS_MUTE_SUCCESS';
|
||||
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;
|
||||
|
||||
if (alsoFetchContext) {
|
||||
dispatch(fetchContext(id));
|
||||
dispatch(fetchContext({ statusId: id }));
|
||||
}
|
||||
|
||||
if (skipLoading) {
|
||||
|
@ -178,50 +177,6 @@ export function deleteStatusFail(id, error) {
|
|||
export const updateStatus = status => dispatch =>
|
||||
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) {
|
||||
return (dispatch) => {
|
||||
dispatch(muteStatusRequest(id));
|
||||
|
|
18
app/javascript/mastodon/actions/statuses_typed.ts
Normal file
18
app/javascript/mastodon/actions/statuses_typed.ts
Normal 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,
|
||||
};
|
||||
},
|
||||
);
|
5
app/javascript/mastodon/api/statuses.ts
Normal file
5
app/javascript/mastodon/api/statuses.ts
Normal 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`);
|
|
@ -119,3 +119,8 @@ export interface ApiStatusJSON {
|
|||
card?: ApiPreviewCardJSON;
|
||||
poll?: ApiPollJSON;
|
||||
}
|
||||
|
||||
export interface ApiContextJSON {
|
||||
ancestors: ApiStatusJSON[];
|
||||
descendants: ApiStatusJSON[];
|
||||
}
|
||||
|
|
|
@ -39,9 +39,9 @@ export const NotificationMention: React.FC<{
|
|||
unread: boolean;
|
||||
}> = ({ notification, unread }) => {
|
||||
const [isDirect, isReply] = useAppSelector((state) => {
|
||||
const status = state.statuses.get(notification.statusId) as
|
||||
| Status
|
||||
| undefined;
|
||||
const status = notification.statusId
|
||||
? (state.statuses.get(notification.statusId) as Status | undefined)
|
||||
: undefined;
|
||||
|
||||
if (!status) return [false, false] as const;
|
||||
|
||||
|
|
|
@ -65,6 +65,7 @@ import { textForScreenReader, defaultMediaVisibility } from '../../components/st
|
|||
import StatusContainer from '../../containers/status_container';
|
||||
import { deleteModal } from '../../initial_state';
|
||||
import { makeGetStatus, makeGetPictureInPicture } from '../../selectors';
|
||||
import { getAncestorsIds, getDescendantsIds } from 'mastodon/selectors/contexts';
|
||||
import Column from '../ui/components/column';
|
||||
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen';
|
||||
|
||||
|
@ -83,69 +84,15 @@ const makeMapStateToProps = () => {
|
|||
const getStatus = makeGetStatus();
|
||||
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 status = getStatus(state, { id: props.params.statusId, contextType: 'detailed' });
|
||||
|
||||
let ancestorsIds = ImmutableList();
|
||||
let descendantsIds = ImmutableList();
|
||||
let ancestorsIds = [];
|
||||
let descendantsIds = [];
|
||||
|
||||
if (status) {
|
||||
ancestorsIds = getAncestorsIds(state, { id: status.get('in_reply_to_id') });
|
||||
descendantsIds = getDescendantsIds(state, { id: status.get('id') });
|
||||
ancestorsIds = getAncestorsIds(state, status.get('in_reply_to_id'));
|
||||
descendantsIds = getDescendantsIds(state, status.get('id'));
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -188,8 +135,8 @@ class Status extends ImmutablePureComponent {
|
|||
dispatch: PropTypes.func.isRequired,
|
||||
status: ImmutablePropTypes.map,
|
||||
isLoading: PropTypes.bool,
|
||||
ancestorsIds: ImmutablePropTypes.list.isRequired,
|
||||
descendantsIds: ImmutablePropTypes.list.isRequired,
|
||||
ancestorsIds: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
descendantsIds: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
askReplyConfirmation: PropTypes.bool,
|
||||
multiColumn: PropTypes.bool,
|
||||
|
@ -383,7 +330,7 @@ class Status extends ImmutablePureComponent {
|
|||
|
||||
handleToggleAll = () => {
|
||||
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')) {
|
||||
this.props.dispatch(revealStatus(statusIds));
|
||||
|
@ -482,13 +429,13 @@ class Status extends ImmutablePureComponent {
|
|||
const { status, ancestorsIds, descendantsIds } = this.props;
|
||||
|
||||
if (id === status.get('id')) {
|
||||
this._selectChild(ancestorsIds.size - 1, true);
|
||||
this._selectChild(ancestorsIds.length - 1, true);
|
||||
} else {
|
||||
let index = ancestorsIds.indexOf(id);
|
||||
|
||||
if (index === -1) {
|
||||
index = descendantsIds.indexOf(id);
|
||||
this._selectChild(ancestorsIds.size + index, true);
|
||||
this._selectChild(ancestorsIds.length + index, true);
|
||||
} else {
|
||||
this._selectChild(index - 1, true);
|
||||
}
|
||||
|
@ -499,13 +446,13 @@ class Status extends ImmutablePureComponent {
|
|||
const { status, ancestorsIds, descendantsIds } = this.props;
|
||||
|
||||
if (id === status.get('id')) {
|
||||
this._selectChild(ancestorsIds.size + 1, false);
|
||||
this._selectChild(ancestorsIds.length + 1, false);
|
||||
} else {
|
||||
let index = ancestorsIds.indexOf(id);
|
||||
|
||||
if (index === -1) {
|
||||
index = descendantsIds.indexOf(id);
|
||||
this._selectChild(ancestorsIds.size + index + 2, false);
|
||||
this._selectChild(ancestorsIds.length + index + 2, false);
|
||||
} else {
|
||||
this._selectChild(index + 1, false);
|
||||
}
|
||||
|
@ -536,8 +483,8 @@ class Status extends ImmutablePureComponent {
|
|||
onMoveUp={this.handleMoveUp}
|
||||
onMoveDown={this.handleMoveDown}
|
||||
contextType='thread'
|
||||
previousId={i > 0 ? list.get(i - 1) : undefined}
|
||||
nextId={list.get(i + 1) || (ancestors && statusId)}
|
||||
previousId={i > 0 ? list[i - 1] : undefined}
|
||||
nextId={list[i + 1] || (ancestors && statusId)}
|
||||
rootId={statusId}
|
||||
/>
|
||||
));
|
||||
|
@ -574,7 +521,7 @@ class Status extends ImmutablePureComponent {
|
|||
componentDidUpdate (prevProps) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -621,11 +568,11 @@ class Status extends ImmutablePureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
if (ancestorsIds && ancestorsIds.size > 0) {
|
||||
if (ancestorsIds && ancestorsIds.length > 0) {
|
||||
ancestors = <>{this.renderChildren(ancestorsIds, true)}</>;
|
||||
}
|
||||
|
||||
if (descendantsIds && descendantsIds.size > 0) {
|
||||
if (descendantsIds && descendantsIds.length > 0) {
|
||||
descendants = <>{this.renderChildren(descendantsIds)}</>;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
155
app/javascript/mastodon/reducers/contexts.ts
Normal file
155
app/javascript/mastodon/reducers/contexts.ts
Normal 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);
|
||||
},
|
||||
);
|
||||
});
|
|
@ -8,7 +8,7 @@ import accounts_map from './accounts_map';
|
|||
import { alertsReducer } from './alerts';
|
||||
import announcements from './announcements';
|
||||
import { composeReducer } from './compose';
|
||||
import contexts from './contexts';
|
||||
import { contextsReducer } from './contexts';
|
||||
import conversations from './conversations';
|
||||
import custom_emojis from './custom_emojis';
|
||||
import { dropdownMenuReducer } from './dropdown_menu';
|
||||
|
@ -55,7 +55,7 @@ const reducers = {
|
|||
settings,
|
||||
push_notifications,
|
||||
server,
|
||||
contexts,
|
||||
contexts: contextsReducer,
|
||||
compose: composeReducer,
|
||||
search: searchReducer,
|
||||
media_attachments,
|
||||
|
|
|
@ -64,6 +64,7 @@ const statusTranslateUndo = (state, id) => {
|
|||
});
|
||||
};
|
||||
|
||||
/** @type {ImmutableMap<string, ImmutableMap<string, any>>} */
|
||||
const initialState = ImmutableMap();
|
||||
|
||||
/** @type {import('@reduxjs/toolkit').Reducer<typeof initialState>} */
|
||||
|
|
94
app/javascript/mastodon/selectors/contexts.ts
Normal file
94
app/javascript/mastodon/selectors/contexts.ts
Normal 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;
|
||||
},
|
||||
);
|
|
@ -3,6 +3,7 @@ export type { GetState, AppDispatch, RootState } from './store';
|
|||
|
||||
export {
|
||||
createAppAsyncThunk,
|
||||
createAppSelector,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from './typed_functions';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
|
@ -24,6 +24,8 @@ export const createAppAsyncThunk = createAsyncThunk.withTypes<{
|
|||
rejectValue: AsyncThunkRejectValue;
|
||||
}>();
|
||||
|
||||
export const createAppSelector = createSelector.withTypes<RootState>();
|
||||
|
||||
interface AppThunkConfig {
|
||||
state: RootState;
|
||||
dispatch: AppDispatch;
|
||||
|
|
Loading…
Reference in New Issue
Block a user