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.

256 lines
9.5 KiB

  1. import {
  2. NOTIFICATIONS_UPDATE,
  3. NOTIFICATIONS_EXPAND_SUCCESS,
  4. NOTIFICATIONS_EXPAND_REQUEST,
  5. NOTIFICATIONS_EXPAND_FAIL,
  6. NOTIFICATIONS_FILTER_SET,
  7. NOTIFICATIONS_CLEAR,
  8. NOTIFICATIONS_SCROLL_TOP,
  9. NOTIFICATIONS_LOAD_PENDING,
  10. NOTIFICATIONS_MOUNT,
  11. NOTIFICATIONS_UNMOUNT,
  12. NOTIFICATIONS_MARK_AS_READ,
  13. NOTIFICATIONS_SET_BROWSER_SUPPORT,
  14. NOTIFICATIONS_SET_BROWSER_PERMISSION,
  15. } from '../actions/notifications';
  16. import {
  17. ACCOUNT_BLOCK_SUCCESS,
  18. ACCOUNT_MUTE_SUCCESS,
  19. FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
  20. FOLLOW_REQUEST_REJECT_SUCCESS,
  21. } from '../actions/accounts';
  22. import {
  23. MARKERS_FETCH_SUCCESS,
  24. } from '../actions/markers';
  25. import {
  26. APP_FOCUS,
  27. APP_UNFOCUS,
  28. } from '../actions/app';
  29. import { DOMAIN_BLOCK_SUCCESS } from 'mastodon/actions/domain_blocks';
  30. import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines';
  31. import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
  32. import compareId from '../compare_id';
  33. const initialState = ImmutableMap({
  34. pendingItems: ImmutableList(),
  35. items: ImmutableList(),
  36. hasMore: true,
  37. top: false,
  38. mounted: 0,
  39. unread: 0,
  40. lastReadId: '0',
  41. readMarkerId: '0',
  42. isTabVisible: true,
  43. isLoading: false,
  44. browserSupport: false,
  45. browserPermission: 'default',
  46. });
  47. const notificationToMap = notification => ImmutableMap({
  48. id: notification.id,
  49. type: notification.type,
  50. account: notification.account.id,
  51. created_at: notification.created_at,
  52. status: notification.status ? notification.status.id : null,
  53. });
  54. const normalizeNotification = (state, notification, usePendingItems) => {
  55. const top = state.get('top');
  56. if (usePendingItems || !state.get('pendingItems').isEmpty()) {
  57. return state.update('pendingItems', list => list.unshift(notificationToMap(notification))).update('unread', unread => unread + 1);
  58. }
  59. if (shouldCountUnreadNotifications(state)) {
  60. state = state.update('unread', unread => unread + 1);
  61. } else {
  62. state = state.set('lastReadId', notification.id);
  63. }
  64. return state.update('items', list => {
  65. if (top && list.size > 40) {
  66. list = list.take(20);
  67. }
  68. return list.unshift(notificationToMap(notification));
  69. });
  70. };
  71. const expandNormalizedNotifications = (state, notifications, next, isLoadingRecent, usePendingItems) => {
  72. const lastReadId = state.get('lastReadId');
  73. let items = ImmutableList();
  74. notifications.forEach((n, i) => {
  75. items = items.set(i, notificationToMap(n));
  76. });
  77. return state.withMutations(mutable => {
  78. if (!items.isEmpty()) {
  79. usePendingItems = isLoadingRecent && (usePendingItems || !mutable.get('pendingItems').isEmpty());
  80. mutable.update(usePendingItems ? 'pendingItems' : 'items', list => {
  81. const lastIndex = 1 + list.findLastIndex(
  82. item => item !== null && (compareId(item.get('id'), items.last().get('id')) > 0 || item.get('id') === items.last().get('id')),
  83. );
  84. const firstIndex = 1 + list.take(lastIndex).findLastIndex(
  85. item => item !== null && compareId(item.get('id'), items.first().get('id')) > 0,
  86. );
  87. return list.take(firstIndex).concat(items, list.skip(lastIndex));
  88. });
  89. }
  90. if (!next) {
  91. mutable.set('hasMore', false);
  92. }
  93. if (shouldCountUnreadNotifications(state)) {
  94. mutable.set('unread', mutable.get('pendingItems').count(item => item !== null) + mutable.get('items').count(item => item && compareId(item.get('id'), lastReadId) > 0));
  95. } else {
  96. const mostRecent = items.find(item => item !== null);
  97. if (mostRecent && compareId(lastReadId, mostRecent.get('id')) < 0) {
  98. mutable.set('lastReadId', mostRecent.get('id'));
  99. }
  100. }
  101. mutable.set('isLoading', false);
  102. });
  103. };
  104. const filterNotifications = (state, accountIds, type) => {
  105. const helper = list => list.filterNot(item => item !== null && accountIds.includes(item.get('account')) && (type === undefined || type === item.get('type')));
  106. return state.update('items', helper).update('pendingItems', helper);
  107. };
  108. const clearUnread = (state) => {
  109. state = state.set('unread', state.get('pendingItems').size);
  110. const lastNotification = state.get('items').find(item => item !== null);
  111. return state.set('lastReadId', lastNotification ? lastNotification.get('id') : '0');
  112. };
  113. const updateTop = (state, top) => {
  114. state = state.set('top', top);
  115. if (!shouldCountUnreadNotifications(state)) {
  116. state = clearUnread(state);
  117. }
  118. return state;
  119. };
  120. const deleteByStatus = (state, statusId) => {
  121. const lastReadId = state.get('lastReadId');
  122. if (shouldCountUnreadNotifications(state)) {
  123. const deletedUnread = state.get('items').filter(item => item !== null && item.get('status') === statusId && compareId(item.get('id'), lastReadId) > 0);
  124. state = state.update('unread', unread => unread - deletedUnread.size);
  125. }
  126. const helper = list => list.filterNot(item => item !== null && item.get('status') === statusId);
  127. const deletedUnread = state.get('pendingItems').filter(item => item !== null && item.get('status') === statusId && compareId(item.get('id'), lastReadId) > 0);
  128. state = state.update('unread', unread => unread - deletedUnread.size);
  129. return state.update('items', helper).update('pendingItems', helper);
  130. };
  131. const updateMounted = (state) => {
  132. state = state.update('mounted', count => count + 1);
  133. if (!shouldCountUnreadNotifications(state, state.get('mounted') === 1)) {
  134. state = state.set('readMarkerId', state.get('lastReadId'));
  135. state = clearUnread(state);
  136. }
  137. return state;
  138. };
  139. const updateVisibility = (state, visibility) => {
  140. state = state.set('isTabVisible', visibility);
  141. if (!shouldCountUnreadNotifications(state)) {
  142. state = state.set('readMarkerId', state.get('lastReadId'));
  143. state = clearUnread(state);
  144. }
  145. return state;
  146. };
  147. const shouldCountUnreadNotifications = (state, ignoreScroll = false) => {
  148. const isTabVisible = state.get('isTabVisible');
  149. const isOnTop = state.get('top');
  150. const isMounted = state.get('mounted') > 0;
  151. const lastReadId = state.get('lastReadId');
  152. const lastItem = state.get('items').findLast(item => item !== null);
  153. const lastItemReached = !state.get('hasMore') || lastReadId === '0' || (lastItem && compareId(lastItem.get('id'), lastReadId) <= 0);
  154. return !(isTabVisible && (ignoreScroll || isOnTop) && isMounted && lastItemReached);
  155. };
  156. const recountUnread = (state, last_read_id) => {
  157. return state.withMutations(mutable => {
  158. if (compareId(last_read_id, mutable.get('lastReadId')) > 0) {
  159. mutable.set('lastReadId', last_read_id);
  160. }
  161. if (compareId(last_read_id, mutable.get('readMarkerId')) > 0) {
  162. mutable.set('readMarkerId', last_read_id);
  163. }
  164. if (state.get('unread') > 0 || shouldCountUnreadNotifications(state)) {
  165. mutable.set('unread', mutable.get('pendingItems').count(item => item !== null) + mutable.get('items').count(item => item && compareId(item.get('id'), last_read_id) > 0));
  166. }
  167. });
  168. };
  169. export default function notifications(state = initialState, action) {
  170. switch(action.type) {
  171. case MARKERS_FETCH_SUCCESS:
  172. return action.markers.notifications ? recountUnread(state, action.markers.notifications.last_read_id) : state;
  173. case NOTIFICATIONS_MOUNT:
  174. return updateMounted(state);
  175. case NOTIFICATIONS_UNMOUNT:
  176. return state.update('mounted', count => count - 1);
  177. case APP_FOCUS:
  178. return updateVisibility(state, true);
  179. case APP_UNFOCUS:
  180. return updateVisibility(state, false);
  181. case NOTIFICATIONS_LOAD_PENDING:
  182. return state.update('items', list => state.get('pendingItems').concat(list.take(40))).set('pendingItems', ImmutableList()).set('unread', 0);
  183. case NOTIFICATIONS_EXPAND_REQUEST:
  184. return state.set('isLoading', true);
  185. case NOTIFICATIONS_EXPAND_FAIL:
  186. return state.set('isLoading', false);
  187. case NOTIFICATIONS_FILTER_SET:
  188. return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', true);
  189. case NOTIFICATIONS_SCROLL_TOP:
  190. return updateTop(state, action.top);
  191. case NOTIFICATIONS_UPDATE:
  192. return normalizeNotification(state, action.notification, action.usePendingItems);
  193. case NOTIFICATIONS_EXPAND_SUCCESS:
  194. return expandNormalizedNotifications(state, action.notifications, action.next, action.isLoadingRecent, action.usePendingItems);
  195. case ACCOUNT_BLOCK_SUCCESS:
  196. return filterNotifications(state, [action.relationship.id]);
  197. case ACCOUNT_MUTE_SUCCESS:
  198. return action.relationship.muting_notifications ? filterNotifications(state, [action.relationship.id]) : state;
  199. case DOMAIN_BLOCK_SUCCESS:
  200. return filterNotifications(state, action.accounts);
  201. case FOLLOW_REQUEST_AUTHORIZE_SUCCESS:
  202. case FOLLOW_REQUEST_REJECT_SUCCESS:
  203. return filterNotifications(state, [action.id], 'follow_request');
  204. case ACCOUNT_MUTE_SUCCESS:
  205. return action.relationship.muting_notifications ? filterNotifications(state, [action.relationship.id]) : state;
  206. case NOTIFICATIONS_CLEAR:
  207. return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', false);
  208. case TIMELINE_DELETE:
  209. return deleteByStatus(state, action.id);
  210. case TIMELINE_DISCONNECT:
  211. return action.timeline === 'home' ?
  212. state.update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items) :
  213. state;
  214. case NOTIFICATIONS_MARK_AS_READ:
  215. const lastNotification = state.get('items').find(item => item !== null);
  216. return lastNotification ? recountUnread(state, lastNotification.get('id')) : state;
  217. case NOTIFICATIONS_SET_BROWSER_SUPPORT:
  218. return state.set('browserSupport', action.value);
  219. case NOTIFICATIONS_SET_BROWSER_PERMISSION:
  220. return state.set('browserPermission', action.value);
  221. default:
  222. return state;
  223. }
  224. };