Change threads to retain sorting returned by the API in web UI

This commit is contained in:
Eugen Rochko 2023-12-04 21:59:24 +01:00
parent 456597dae5
commit 587e3a049e
2 changed files with 33 additions and 134 deletions

View File

@ -6,11 +6,10 @@ import classNames from 'classnames';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import Immutable from 'immutable'; import { List as ImmutableList } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { ReactComponent as VisibilityIcon } from '@material-symbols/svg-600/outlined/visibility.svg'; import { ReactComponent as VisibilityIcon } from '@material-symbols/svg-600/outlined/visibility.svg';
import { ReactComponent as VisibilityOffIcon } from '@material-symbols/svg-600/outlined/visibility_off.svg'; import { ReactComponent as VisibilityOffIcon } from '@material-symbols/svg-600/outlined/visibility_off.svg';
@ -72,7 +71,6 @@ import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from
import ActionBar from './components/action_bar'; import ActionBar from './components/action_bar';
import DetailedStatus from './components/detailed_status'; import DetailedStatus from './components/detailed_status';
const messages = defineMessages({ const messages = defineMessages({
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' }, deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
@ -91,83 +89,19 @@ const makeMapStateToProps = () => {
const getStatus = makeGetStatus(); const getStatus = makeGetStatus();
const getPictureInPicture = makeGetPictureInPicture(); const getPictureInPicture = makeGetPictureInPicture();
const getAncestorsIds = createSelector([ return (state, props) => {
(_, { id }) => id,
state => state.getIn(['contexts', 'inReplyTos']),
], (statusId, inReplyTos) => {
let ancestorsIds = Immutable.List();
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 Immutable.List(descendantsIds);
});
const mapStateToProps = (state, props) => {
const status = getStatus(state, { id: props.params.statusId }); const status = getStatus(state, { id: props.params.statusId });
let ancestorsIds = Immutable.List();
let descendantsIds = Immutable.List();
if (status) {
ancestorsIds = getAncestorsIds(state, { id: status.get('in_reply_to_id') });
descendantsIds = getDescendantsIds(state, { id: status.get('id') });
}
return { return {
isLoading: state.getIn(['statuses', props.params.statusId, 'isLoading']), isLoading: state.getIn(['statuses', props.params.statusId, 'isLoading']),
status, status,
ancestorsIds, ancestorsIds: state.getIn(['contexts', props.params.statusId, 'ancestors'], ImmutableList()),
descendantsIds, descendantsIds: state.getIn(['contexts', props.params.statusId, 'descendants'], ImmutableList()),
askReplyConfirmation: state.getIn(['compose', 'text']).trim().length !== 0, askReplyConfirmation: state.getIn(['compose', 'text']).trim().length !== 0,
domain: state.getIn(['meta', 'domain']), domain: state.getIn(['meta', 'domain']),
pictureInPicture: getPictureInPicture(state, { id: props.params.statusId }), pictureInPicture: getPictureInPicture(state, { id: props.params.statusId }),
}; };
}; };
return mapStateToProps;
}; };
const truncate = (str, num) => { const truncate = (str, num) => {

View File

@ -6,65 +6,20 @@ import {
} from '../actions/accounts'; } from '../actions/accounts';
import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses'; import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses';
import { TIMELINE_DELETE, TIMELINE_UPDATE } from '../actions/timelines'; import { TIMELINE_DELETE, TIMELINE_UPDATE } from '../actions/timelines';
import { compareId } from '../compare_id';
const initialState = ImmutableMap({ const initialState = ImmutableMap();
inReplyTos: ImmutableMap(),
replies: ImmutableMap(),
});
const normalizeContext = (immutableState, id, ancestors, descendants) => immutableState.withMutations(state => { const normalizeContext = (state, id, ancestors, descendants) => state.set(id, ImmutableMap({
state.update('inReplyTos', immutableAncestors => immutableAncestors.withMutations(inReplyTos => { ancestors: ImmutableList(ancestors.map(x => x.id)),
state.update('replies', immutableDescendants => immutableDescendants.withMutations(replies => { descendants: ImmutableList(descendants.map(x => x.id)),
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 => { const deleteFromContexts = (state, deletedIds) => state.update(contexts =>
state.update('inReplyTos', immutableAncestors => immutableAncestors.withMutations(inReplyTos => { contexts.map(context =>
state.update('replies', immutableDescendants => immutableDescendants.withMutations(replies => { context.update(map => ImmutableMap({
ids.forEach(id => { ancestors: map.get('ancestors').filterNot(id => deletedIds.includes(id)),
const inReplyToIdOfId = inReplyTos.get(id); descendants: map.get('descendants').filterNot(id => deletedIds.includes(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 filterContexts = (state, relationship, statuses) => {
const ownedStatusIds = statuses const ownedStatusIds = statuses
@ -75,16 +30,26 @@ const filterContexts = (state, relationship, statuses) => {
}; };
const updateContext = (state, status) => { const updateContext = (state, status) => {
if (status.in_reply_to_id) { const inReplyToId = 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 (inReplyToId) {
return state.update(contexts => contexts.map((context, rootStatusId) => {
if (!replies.includes(status.id)) { if (context.get('descendants').includes(status.id)) {
mutable.setIn(['replies', status.in_reply_to_id], replies.push(status.id)); return context;
} }
});
if (rootStatusId === inReplyToId) {
return context.update('descendants', list => list.push(status.id));
}
const ancestorIndex = context.get('descendants').indexOf(inReplyToId);
if (ancestorIndex !== -1) {
return context.update('descendants', list => list.insert(ancestorIndex + 1, status.id));
}
return context;
}));
} }
return state; return state;