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.

306 lines
9.4 KiB

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