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.

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