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.

363 lines
12 KiB

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