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.

315 lines
9.7 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. 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. .update('items', Immutable.List(), list => (replace ? ids : list.unshift(...ids))));
  127. };
  128. const appendNormalizedAccountTimeline = (state, accountId, statuses) => {
  129. let moreIds = Immutable.List([]);
  130. statuses.forEach((status, i) => {
  131. state = normalizeStatus(state, status);
  132. moreIds = moreIds.set(i, status.get('id'));
  133. });
  134. return state.updateIn(['accounts_timelines', accountId], Immutable.Map(), map => map
  135. .set('isLoading', false)
  136. .update('items', list => list.push(...moreIds)));
  137. };
  138. const updateTimeline = (state, timeline, status, references) => {
  139. const top = state.getIn([timeline, 'top']);
  140. state = normalizeStatus(state, status);
  141. if (!top) {
  142. state = state.updateIn([timeline, 'unread'], unread => unread + 1);
  143. }
  144. state = state.updateIn([timeline, 'items'], Immutable.List(), list => {
  145. if (top && list.size > 40) {
  146. list = list.take(20);
  147. }
  148. if (list.includes(status.get('id'))) {
  149. return list;
  150. }
  151. const reblogOfId = status.getIn(['reblog', 'id'], null);
  152. if (reblogOfId !== null) {
  153. list = list.filterNot(itemId => references.includes(itemId));
  154. }
  155. return list.unshift(status.get('id'));
  156. });
  157. return state;
  158. };
  159. const deleteStatus = (state, id, accountId, references, reblogOf) => {
  160. if (reblogOf) {
  161. // If we are deleting a reblog, just replace reblog with its original
  162. return state.updateIn(['home', 'items'], list => list.map(item => item === id ? reblogOf : item));
  163. }
  164. // Remove references from timelines
  165. ['home', 'public', 'community', 'tag'].forEach(function (timeline) {
  166. state = state.updateIn([timeline, 'items'], list => list.filterNot(item => item === id));
  167. });
  168. // Remove references from account timelines
  169. state = state.updateIn(['accounts_timelines', accountId, 'items'], Immutable.List([]), list => list.filterNot(item => item === id));
  170. // Remove references from context
  171. state.getIn(['descendants', id], Immutable.List()).forEach(descendantId => {
  172. state = state.updateIn(['ancestors', descendantId], Immutable.List(), list => list.filterNot(itemId => itemId === id));
  173. });
  174. state.getIn(['ancestors', id], Immutable.List()).forEach(ancestorId => {
  175. state = state.updateIn(['descendants', ancestorId], Immutable.List(), list => list.filterNot(itemId => itemId === id));
  176. });
  177. state = state.deleteIn(['descendants', id]).deleteIn(['ancestors', id]);
  178. // Remove reblogs of deleted status
  179. references.forEach(ref => {
  180. state = deleteStatus(state, ref[0], ref[1], []);
  181. });
  182. return state;
  183. };
  184. const filterTimelines = (state, relationship, statuses) => {
  185. let references;
  186. statuses.forEach(status => {
  187. if (status.get('account') !== relationship.id) {
  188. return;
  189. }
  190. references = statuses.filter(item => item.get('reblog') === status.get('id')).map(item => [item.get('id'), item.get('account')]);
  191. state = deleteStatus(state, status.get('id'), status.get('account'), references);
  192. });
  193. return state;
  194. };
  195. const normalizeContext = (state, id, ancestors, descendants) => {
  196. const ancestorsIds = ancestors.map(ancestor => ancestor.get('id'));
  197. const descendantsIds = descendants.map(descendant => descendant.get('id'));
  198. return state.withMutations(map => {
  199. map.setIn(['ancestors', id], ancestorsIds);
  200. map.setIn(['descendants', id], descendantsIds);
  201. });
  202. };
  203. const resetTimeline = (state, timeline, id) => {
  204. if (timeline === 'tag' && typeof id !== 'undefined' && state.getIn([timeline, 'id']) !== id) {
  205. state = state.update(timeline, map => map
  206. .set('id', id)
  207. .set('isLoading', true)
  208. .set('loaded', false)
  209. .set('next', null)
  210. .set('top', true)
  211. .update('items', list => list.clear()));
  212. } else {
  213. state = state.setIn([timeline, 'isLoading'], true);
  214. }
  215. return state;
  216. };
  217. const updateTop = (state, timeline, top) => {
  218. if (top) {
  219. state = state.setIn([timeline, 'unread'], 0);
  220. }
  221. return state.setIn([timeline, 'top'], top);
  222. };
  223. export default function timelines(state = initialState, action) {
  224. switch(action.type) {
  225. case TIMELINE_REFRESH_REQUEST:
  226. case TIMELINE_EXPAND_REQUEST:
  227. return resetTimeline(state, action.timeline, action.id);
  228. case TIMELINE_REFRESH_FAIL:
  229. case TIMELINE_EXPAND_FAIL:
  230. return state.setIn([action.timeline, 'isLoading'], false);
  231. case TIMELINE_REFRESH_SUCCESS:
  232. return normalizeTimeline(state, action.timeline, Immutable.fromJS(action.statuses), action.next);
  233. case TIMELINE_EXPAND_SUCCESS:
  234. return appendNormalizedTimeline(state, action.timeline, Immutable.fromJS(action.statuses), action.next);
  235. case TIMELINE_UPDATE:
  236. return updateTimeline(state, action.timeline, Immutable.fromJS(action.status), action.references);
  237. case TIMELINE_DELETE:
  238. return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf);
  239. case CONTEXT_FETCH_SUCCESS:
  240. return normalizeContext(state, action.id, Immutable.fromJS(action.ancestors), Immutable.fromJS(action.descendants));
  241. case ACCOUNT_TIMELINE_FETCH_REQUEST:
  242. case ACCOUNT_TIMELINE_EXPAND_REQUEST:
  243. return state.updateIn(['accounts_timelines', action.id], Immutable.Map(), map => map.set('isLoading', true));
  244. case ACCOUNT_TIMELINE_FETCH_FAIL:
  245. case ACCOUNT_TIMELINE_EXPAND_FAIL:
  246. return state.updateIn(['accounts_timelines', action.id], Immutable.Map(), map => map.set('isLoading', false));
  247. case ACCOUNT_TIMELINE_FETCH_SUCCESS:
  248. return normalizeAccountTimeline(state, action.id, Immutable.fromJS(action.statuses), action.replace);
  249. case ACCOUNT_TIMELINE_EXPAND_SUCCESS:
  250. return appendNormalizedAccountTimeline(state, action.id, Immutable.fromJS(action.statuses));
  251. case ACCOUNT_BLOCK_SUCCESS:
  252. case ACCOUNT_MUTE_SUCCESS:
  253. return filterTimelines(state, action.relationship, action.statuses);
  254. case TIMELINE_SCROLL_TOP:
  255. return updateTop(state, action.timeline, action.top);
  256. case TIMELINE_CONNECT:
  257. return state.setIn([action.timeline, 'online'], true);
  258. case TIMELINE_DISCONNECT:
  259. return state.setIn([action.timeline, 'online'], false);
  260. default:
  261. return state;
  262. }
  263. };