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.

272 lines
8.5 KiB

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