about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--app/javascript/mastodon/actions/timelines.js20
-rw-r--r--app/javascript/mastodon/features/status/index.js47
-rw-r--r--app/javascript/mastodon/reducers/contexts.js94
3 files changed, 101 insertions, 60 deletions
diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js
index 8f54dfd8a..11a199db6 100644
--- a/app/javascript/mastodon/actions/timelines.js
+++ b/app/javascript/mastodon/actions/timelines.js
@@ -13,21 +13,9 @@ export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';
 
 export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
 
-export const TIMELINE_CONTEXT_UPDATE = 'CONTEXT_UPDATE';
-
 export function updateTimeline(timeline, status) {
   return (dispatch, getState) => {
     const references = status.reblog ? getState().get('statuses').filter((item, itemId) => (itemId === status.reblog.id || item.get('reblog') === status.reblog.id)).map((_, itemId) => itemId) : [];
-    const parents = [];
-
-    if (status.in_reply_to_id) {
-      let parent = getState().getIn(['statuses', status.in_reply_to_id]);
-
-      while (parent && parent.get('in_reply_to_id')) {
-        parents.push(parent.get('id'));
-        parent = getState().getIn(['statuses', parent.get('in_reply_to_id')]);
-      }
-    }
 
     dispatch(importFetchedStatus(status));
 
@@ -37,14 +25,6 @@ export function updateTimeline(timeline, status) {
       status,
       references,
     });
-
-    if (parents.length > 0) {
-      dispatch({
-        type: TIMELINE_CONTEXT_UPDATE,
-        status,
-        references: parents,
-      });
-    }
   };
 };
 
diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js
index d5af2a459..2e53dfa7e 100644
--- a/app/javascript/mastodon/features/status/index.js
+++ b/app/javascript/mastodon/features/status/index.js
@@ -1,3 +1,4 @@
+import Immutable from 'immutable';
 import React from 'react';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
@@ -54,11 +55,47 @@ const messages = defineMessages({
 const makeMapStateToProps = () => {
   const getStatus = makeGetStatus();
 
-  const mapStateToProps = (state, props) => ({
-    status: getStatus(state, props.params.statusId),
-    ancestorsIds: state.getIn(['contexts', 'ancestors', props.params.statusId]),
-    descendantsIds: state.getIn(['contexts', 'descendants', props.params.statusId]),
-  });
+  const mapStateToProps = (state, props) => {
+    const status = getStatus(state, props.params.statusId);
+    let ancestorsIds = Immutable.List();
+    let descendantsIds = Immutable.List();
+
+    if (status) {
+      ancestorsIds = ancestorsIds.withMutations(mutable => {
+        function addAncestor(id) {
+          if (id) {
+            const inReplyTo = state.getIn(['contexts', 'inReplyTos', id]);
+
+            mutable.unshift(id);
+            addAncestor(inReplyTo);
+          }
+        }
+
+        addAncestor(status.get('in_reply_to_id'));
+      });
+
+      descendantsIds = descendantsIds.withMutations(mutable => {
+        function addDescendantOf(id) {
+          const replies = state.getIn(['contexts', 'replies', id]);
+
+          if (replies) {
+            replies.forEach(reply => {
+              mutable.push(reply);
+              addDescendantOf(reply);
+            });
+          }
+        }
+
+        addDescendantOf(status.get('id'));
+      });
+    }
+
+    return {
+      status,
+      ancestorsIds,
+      descendantsIds,
+    };
+  };
 
   return mapStateToProps;
 };
diff --git a/app/javascript/mastodon/reducers/contexts.js b/app/javascript/mastodon/reducers/contexts.js
index ebd01e532..53e70b58e 100644
--- a/app/javascript/mastodon/reducers/contexts.js
+++ b/app/javascript/mastodon/reducers/contexts.js
@@ -3,38 +3,62 @@ import {
   ACCOUNT_MUTE_SUCCESS,
 } from '../actions/accounts';
 import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses';
-import { TIMELINE_DELETE, TIMELINE_CONTEXT_UPDATE } from '../actions/timelines';
+import { TIMELINE_DELETE, TIMELINE_UPDATE } from '../actions/timelines';
 import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 
 const initialState = ImmutableMap({
-  ancestors: ImmutableMap(),
-  descendants: ImmutableMap(),
+  inReplyTos: ImmutableMap(),
+  replies: ImmutableMap(),
 });
 
-const normalizeContext = (state, id, ancestors, descendants) => {
-  const ancestorsIds   = ImmutableList(ancestors.map(ancestor => ancestor.id));
-  const descendantsIds = ImmutableList(descendants.map(descendant => descendant.id));
+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) {
+          const siblings = replies.get(in_reply_to_id, ImmutableList());
 
-  return state.withMutations(map => {
-    map.setIn(['ancestors', id], ancestorsIds);
-    map.setIn(['descendants', id], descendantsIds);
-  });
-};
+          if (!siblings.includes(id)) {
+            const index = siblings.findLastIndex(sibling => sibling.id < id);
+            replies.set(in_reply_to_id, siblings.insert(index + 1, id));
+          }
+
+          inReplyTos.set(id, in_reply_to_id);
+        }
+      }
+
+      if (ancestors[0]) {
+        addReply({ id, in_reply_to_id: ancestors[0].id });
+      }
+
+      if (descendants[0]) {
+        addReply({ id: descendants[0].id, in_reply_to_id: id });
+      }
+
+      [ancestors, descendants].forEach(statuses => statuses.forEach(addReply));
+    }));
+  }));
+});
 
 const deleteFromContexts = (immutableState, ids) => immutableState.withMutations(state => {
-  state.update('ancestors', immutableAncestors => immutableAncestors.withMutations(ancestors => {
-    state.update('descendants', immutableDescendants => immutableDescendants.withMutations(descendants => {
+  state.update('inReplyTos', immutableAncestors => immutableAncestors.withMutations(inReplyTos => {
+    state.update('replies', immutableDescendants => immutableDescendants.withMutations(replies => {
       ids.forEach(id => {
-        descendants.get(id, ImmutableList()).forEach(descendantId => {
-          ancestors.update(descendantId, ImmutableList(), list => list.filterNot(itemId => itemId === id));
-        });
+        const inReplyToIdOfId = inReplyTos.get(id);
+        const repliesOfId = replies.get(id);
+        const siblings = replies.get(inReplyToIdOfId);
 
-        ancestors.get(id, ImmutableList()).forEach(ancestorId => {
-          descendants.update(ancestorId, ImmutableList(), list => list.filterNot(itemId => itemId === id));
-        });
+        if (siblings) {
+          replies.set(inReplyToIdOfId, siblings.filterNot(sibling => sibling === id));
+        }
+
+
+        if (repliesOfId) {
+          repliesOfId.forEach(reply => inReplyTos.delete(reply));
+        }
 
-        descendants.delete(id);
-        ancestors.delete(id);
+        inReplyTos.delete(id);
+        replies.delete(id);
       });
     }));
   }));
@@ -48,23 +72,23 @@ const filterContexts = (state, relationship, statuses) => {
   return deleteFromContexts(state, ownedStatusIds);
 };
 
-const updateContext = (state, status, references) => {
-  return state.update('descendants', map => {
-    references.forEach(parentId => {
-      map = map.update(parentId, ImmutableList(), list => {
-        if (list.includes(status.id)) {
-          return list;
-        }
+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());
 
-        return list.push(status.id);
-      });
+      mutable.setIn(['inReplyTos', status.id], status.in_reply_to_id);
+
+      if (!replies.includes(status.id)) {
+        mutable.setIn(['replies', status.id], replies.push(status.id));
+      }
     });
+  }
 
-    return map;
-  });
+  return state;
 };
 
-export default function contexts(state = initialState, action) {
+export default function replies(state = initialState, action) {
   switch(action.type) {
   case ACCOUNT_BLOCK_SUCCESS:
   case ACCOUNT_MUTE_SUCCESS:
@@ -73,8 +97,8 @@ export default function contexts(state = initialState, action) {
     return normalizeContext(state, action.id, action.ancestors, action.descendants);
   case TIMELINE_DELETE:
     return deleteFromContexts(state, [action.id]);
-  case TIMELINE_CONTEXT_UPDATE:
-    return updateContext(state, action.status, action.references);
+  case TIMELINE_UPDATE:
+    return updateContext(state, action.status);
   default:
     return state;
   }