mirror of
https://github.com/mastodon/mastodon.git
synced 2024-12-22 17:35:40 +01:00
Change threads to retain sorting returned by the API in web UI
This commit is contained in:
parent
456597dae5
commit
587e3a049e
2 changed files with 33 additions and 134 deletions
|
@ -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) => {
|
||||||
|
|
|
@ -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 deleteFromContexts = (state, deletedIds) => state.update(contexts =>
|
||||||
const index = siblings.findLastIndex(sibling => compareId(sibling, id) < 0);
|
contexts.map(context =>
|
||||||
return siblings.insert(index + 1, id);
|
context.update(map => ImmutableMap({
|
||||||
});
|
ancestors: map.get('ancestors').filterNot(id => deletedIds.includes(id)),
|
||||||
|
descendants: map.get('descendants').filterNot(id => deletedIds.includes(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 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;
|
||||||
|
|
Loading…
Reference in a new issue