You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

242 lines
7.4 KiB

8 years ago
8 years ago
8 years ago
  1. import {
  2. TIMELINE_REFRESH_REQUEST,
  3. TIMELINE_REFRESH_SUCCESS,
  4. TIMELINE_UPDATE,
  5. TIMELINE_DELETE,
  6. TIMELINE_EXPAND_SUCCESS,
  7. TIMELINE_SCROLL_TOP
  8. } from '../actions/timelines';
  9. import {
  10. REBLOG_SUCCESS,
  11. UNREBLOG_SUCCESS,
  12. FAVOURITE_SUCCESS,
  13. UNFAVOURITE_SUCCESS
  14. } from '../actions/interactions';
  15. import {
  16. ACCOUNT_FETCH_SUCCESS,
  17. ACCOUNT_TIMELINE_FETCH_SUCCESS,
  18. ACCOUNT_TIMELINE_EXPAND_SUCCESS,
  19. ACCOUNT_BLOCK_SUCCESS
  20. } from '../actions/accounts';
  21. import {
  22. STATUS_FETCH_SUCCESS,
  23. CONTEXT_FETCH_SUCCESS
  24. } from '../actions/statuses';
  25. import Immutable from 'immutable';
  26. const initialState = Immutable.Map({
  27. home: Immutable.Map({
  28. loaded: false,
  29. top: true,
  30. items: Immutable.List()
  31. }),
  32. mentions: Immutable.Map({
  33. loaded: false,
  34. top: true,
  35. items: Immutable.List()
  36. }),
  37. public: Immutable.Map({
  38. loaded: false,
  39. top: true,
  40. items: Immutable.List()
  41. }),
  42. tag: Immutable.Map({
  43. id: null,
  44. loaded: false,
  45. top: true,
  46. items: Immutable.List()
  47. }),
  48. accounts_timelines: Immutable.Map(),
  49. ancestors: Immutable.Map(),
  50. descendants: Immutable.Map()
  51. });
  52. const normalizeStatus = (state, status) => {
  53. const replyToId = status.get('in_reply_to_id');
  54. const id = status.get('id');
  55. if (replyToId) {
  56. if (!state.getIn(['descendants', replyToId], Immutable.List()).includes(id)) {
  57. state = state.updateIn(['descendants', replyToId], Immutable.List(), set => set.push(id));
  58. }
  59. if (!state.getIn(['ancestors', id], Immutable.List()).includes(replyToId)) {
  60. state = state.updateIn(['ancestors', id], Immutable.List(), set => set.push(replyToId));
  61. }
  62. }
  63. return state;
  64. };
  65. const normalizeTimeline = (state, timeline, statuses, replace = false) => {
  66. let ids = Immutable.List();
  67. const loaded = state.getIn([timeline, 'loaded']);
  68. statuses.forEach((status, i) => {
  69. state = normalizeStatus(state, status);
  70. ids = ids.set(i, status.get('id'));
  71. });
  72. state = state.setIn([timeline, 'loaded'], true);
  73. return state.updateIn([timeline, 'items'], Immutable.List(), list => (loaded ? list.unshift(...ids) : ids));
  74. };
  75. const appendNormalizedTimeline = (state, timeline, statuses) => {
  76. let moreIds = Immutable.List();
  77. statuses.forEach((status, i) => {
  78. state = normalizeStatus(state, status);
  79. moreIds = moreIds.set(i, status.get('id'));
  80. });
  81. return state.updateIn([timeline, 'items'], Immutable.List(), list => list.push(...moreIds));
  82. };
  83. const normalizeAccountTimeline = (state, accountId, statuses, replace = false) => {
  84. let ids = Immutable.List();
  85. statuses.forEach((status, i) => {
  86. state = normalizeStatus(state, status);
  87. ids = ids.set(i, status.get('id'));
  88. });
  89. return state.updateIn(['accounts_timelines', accountId], Immutable.List([]), list => (replace ? ids : list.unshift(...ids)));
  90. };
  91. const appendNormalizedAccountTimeline = (state, accountId, statuses) => {
  92. let moreIds = Immutable.List([]);
  93. statuses.forEach((status, i) => {
  94. state = normalizeStatus(state, status);
  95. moreIds = moreIds.set(i, status.get('id'));
  96. });
  97. return state.updateIn(['accounts_timelines', accountId], Immutable.List([]), list => list.push(...moreIds));
  98. };
  99. const updateTimeline = (state, timeline, status, references) => {
  100. const top = state.getIn([timeline, 'top']);
  101. state = normalizeStatus(state, status);
  102. state = state.updateIn([timeline, 'items'], Immutable.List(), list => {
  103. if (top && list.size > 40) {
  104. list = list.take(20);
  105. }
  106. if (list.includes(status.get('id'))) {
  107. return list;
  108. }
  109. const reblogOfId = status.getIn(['reblog', 'id'], null);
  110. if (reblogOfId !== null) {
  111. list = list.filterNot(itemId => references.includes(itemId));
  112. }
  113. return list.unshift(status.get('id'));
  114. });
  115. return state;
  116. };
  117. const deleteStatus = (state, id, accountId, references, reblogOf) => {
  118. if (reblogOf) {
  119. // If we are deleting a reblog, just replace reblog with its original
  120. return state.updateIn(['home', 'items'], list => list.map(item => item === id ? reblogOf : item));
  121. }
  122. // Remove references from timelines
  123. ['home', 'mentions', 'public', 'tag'].forEach(function (timeline) {
  124. state = state.updateIn([timeline, 'items'], list => list.filterNot(item => item === id));
  125. });
  126. // Remove references from account timelines
  127. state = state.updateIn(['accounts_timelines', accountId], Immutable.List([]), list => list.filterNot(item => item === id));
  128. // Remove references from context
  129. state.getIn(['descendants', id], Immutable.List()).forEach(descendantId => {
  130. state = state.updateIn(['ancestors', descendantId], Immutable.List(), list => list.filterNot(itemId => itemId === id));
  131. });
  132. state.getIn(['ancestors', id], Immutable.List()).forEach(ancestorId => {
  133. state = state.updateIn(['descendants', ancestorId], Immutable.List(), list => list.filterNot(itemId => itemId === id));
  134. });
  135. state = state.deleteIn(['descendants', id]).deleteIn(['ancestors', id]);
  136. // Remove reblogs of deleted status
  137. references.forEach(ref => {
  138. state = deleteStatus(state, ref[0], ref[1], []);
  139. });
  140. return state;
  141. };
  142. const filterTimelines = (state, relationship, statuses) => {
  143. let references;
  144. statuses.forEach(status => {
  145. if (status.get('account') !== relationship.id) {
  146. return;
  147. }
  148. references = statuses.filter(item => item.get('reblog') === status.get('id')).map(item => [item.get('id'), item.get('account')]);
  149. state = deleteStatus(state, status.get('id'), status.get('account'), references);
  150. });
  151. return state;
  152. };
  153. const normalizeContext = (state, id, ancestors, descendants) => {
  154. const ancestorsIds = ancestors.map(ancestor => ancestor.get('id'));
  155. const descendantsIds = descendants.map(descendant => descendant.get('id'));
  156. return state.withMutations(map => {
  157. map.setIn(['ancestors', id], ancestorsIds);
  158. map.setIn(['descendants', id], descendantsIds);
  159. });
  160. };
  161. const resetTimeline = (state, timeline, id) => {
  162. if (timeline === 'tag' && state.getIn([timeline, 'id']) !== id) {
  163. state = state.update(timeline, map => map
  164. .set('id', id)
  165. .set('loaded', false)
  166. .update('items', list => list.clear()));
  167. }
  168. return state;
  169. };
  170. export default function timelines(state = initialState, action) {
  171. switch(action.type) {
  172. case TIMELINE_REFRESH_REQUEST:
  173. return resetTimeline(state, action.timeline, action.id);
  174. case TIMELINE_REFRESH_SUCCESS:
  175. return normalizeTimeline(state, action.timeline, Immutable.fromJS(action.statuses));
  176. case TIMELINE_EXPAND_SUCCESS:
  177. return appendNormalizedTimeline(state, action.timeline, Immutable.fromJS(action.statuses));
  178. case TIMELINE_UPDATE:
  179. return updateTimeline(state, action.timeline, Immutable.fromJS(action.status), action.references);
  180. case TIMELINE_DELETE:
  181. return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf);
  182. case CONTEXT_FETCH_SUCCESS:
  183. return normalizeContext(state, action.id, Immutable.fromJS(action.ancestors), Immutable.fromJS(action.descendants));
  184. case ACCOUNT_TIMELINE_FETCH_SUCCESS:
  185. return normalizeAccountTimeline(state, action.id, Immutable.fromJS(action.statuses), action.replace);
  186. case ACCOUNT_TIMELINE_EXPAND_SUCCESS:
  187. return appendNormalizedAccountTimeline(state, action.id, Immutable.fromJS(action.statuses));
  188. case ACCOUNT_BLOCK_SUCCESS:
  189. return filterTimelines(state, action.relationship, action.statuses);
  190. case TIMELINE_SCROLL_TOP:
  191. return state.setIn([action.timeline, 'top'], action.top);
  192. default:
  193. return state;
  194. }
  195. };