Refactor context reducer to TypeScript (#34506)
This commit is contained in:
		@@ -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));
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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;
 | 
					  card?: ApiPreviewCardJSON;
 | 
				
			||||||
  poll?: ApiPollJSON;
 | 
					  poll?: ApiPollJSON;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface ApiContextJSON {
 | 
				
			||||||
 | 
					  ancestors: ApiStatusJSON[];
 | 
				
			||||||
 | 
					  descendants: ApiStatusJSON[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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)}</>;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 { 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,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>} */
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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 {
 | 
					export {
 | 
				
			||||||
  createAppAsyncThunk,
 | 
					  createAppAsyncThunk,
 | 
				
			||||||
 | 
					  createAppSelector,
 | 
				
			||||||
  useAppDispatch,
 | 
					  useAppDispatch,
 | 
				
			||||||
  useAppSelector,
 | 
					  useAppSelector,
 | 
				
			||||||
} from './typed_functions';
 | 
					} from './typed_functions';
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user