about summary refs log tree commit diff
path: root/app/javascript/mastodon/reducers/notifications.js
diff options
context:
space:
mode:
authorThibG <thib@sitedethib.com>2020-09-26 20:57:07 +0200
committerGitHub <noreply@github.com>2020-09-26 20:57:07 +0200
commitff89025979abf3dae40b8e0745b2584cc3feb744 (patch)
treefd7650bee5e553a42b5160ecf5e645587c41e4dc /app/javascript/mastodon/reducers/notifications.js
parent03b6b034b95b9ee2ce4a80c3b9c724273dcb0072 (diff)
Add unread notification markers (#14818)
* Add unread notification markers

Fixes #14804

* Allow IntersectionObserverArticle's children to be updated
Diffstat (limited to 'app/javascript/mastodon/reducers/notifications.js')
-rw-r--r--app/javascript/mastodon/reducers/notifications.js111
1 files changed, 102 insertions, 9 deletions
diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js
index ed1ba0272..b01db806f 100644
--- a/app/javascript/mastodon/reducers/notifications.js
+++ b/app/javascript/mastodon/reducers/notifications.js
@@ -9,6 +9,7 @@ import {
   NOTIFICATIONS_LOAD_PENDING,
   NOTIFICATIONS_MOUNT,
   NOTIFICATIONS_UNMOUNT,
+  NOTIFICATIONS_MARK_AS_READ,
 } from '../actions/notifications';
 import {
   ACCOUNT_BLOCK_SUCCESS,
@@ -16,6 +17,13 @@ import {
   FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
   FOLLOW_REQUEST_REJECT_SUCCESS,
 } from '../actions/accounts';
+import {
+  MARKERS_FETCH_SUCCESS,
+} from '../actions/markers';
+import {
+  APP_FOCUS,
+  APP_UNFOCUS,
+} from '../actions/app';
 import { DOMAIN_BLOCK_SUCCESS } from 'mastodon/actions/domain_blocks';
 import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines';
 import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
@@ -26,8 +34,11 @@ const initialState = ImmutableMap({
   items: ImmutableList(),
   hasMore: true,
   top: false,
-  mounted: false,
+  mounted: 0,
   unread: 0,
+  lastReadId: '0',
+  readMarkerId: '0',
+  isTabVisible: true,
   isLoading: false,
 });
 
@@ -46,8 +57,10 @@ const normalizeNotification = (state, notification, usePendingItems) => {
     return state.update('pendingItems', list => list.unshift(notificationToMap(notification))).update('unread', unread => unread + 1);
   }
 
-  if (!top) {
+  if (shouldCountUnreadNotifications(state)) {
     state = state.update('unread', unread => unread + 1);
+  } else {
+    state = state.set('lastReadId', notification.id);
   }
 
   return state.update('items', list => {
@@ -60,6 +73,7 @@ const normalizeNotification = (state, notification, usePendingItems) => {
 };
 
 const expandNormalizedNotifications = (state, notifications, next, isLoadingRecent, usePendingItems) => {
+  const lastReadId = state.get('lastReadId');
   let items = ImmutableList();
 
   notifications.forEach((n, i) => {
@@ -87,6 +101,15 @@ const expandNormalizedNotifications = (state, notifications, next, isLoadingRece
       mutable.set('hasMore', false);
     }
 
+    if (shouldCountUnreadNotifications(state)) {
+      mutable.update('unread', unread => unread + items.count(item => compareId(item.get('id'), lastReadId) > 0));
+    } else {
+      const mostRecent = items.find(item => item !== null);
+      if (mostRecent && compareId(lastReadId, mostRecent.get('id')) < 0) {
+        mutable.set('lastReadId', mostRecent.get('id'));
+      }
+    }
+
     mutable.set('isLoading', false);
   });
 };
@@ -96,21 +119,92 @@ const filterNotifications = (state, accountIds, type) => {
   return state.update('items', helper).update('pendingItems', helper);
 };
 
+const clearUnread = (state) => {
+  state = state.set('unread', state.get('pendingItems').size);
+  const lastNotification = state.get('items').find(item => item !== null);
+  return state.set('lastReadId', lastNotification ? lastNotification.get('id') : '0');
+};
+
 const updateTop = (state, top) => {
-  if (top) {
-    state = state.set('unread', state.get('pendingItems').size);
+  state = state.set('top', top);
+
+  if (!shouldCountUnreadNotifications(state)) {
+    state = clearUnread(state);
   }
 
-  return state.set('top', top);
+  return state;
 };
 
 const deleteByStatus = (state, statusId) => {
+  const lastReadId = state.get('lastReadId');
+
+  if (shouldCountUnreadNotifications(state)) {
+    const deletedUnread = state.get('items').filter(item => item !== null && item.get('status') === statusId && compareId(item.get('id'), lastReadId) > 0);
+    state = state.update('unread', unread => unread - deletedUnread.size);
+  }
+
   const helper = list => list.filterNot(item => item !== null && item.get('status') === statusId);
+  const deletedUnread = state.get('pendingItems').filter(item => item !== null && item.get('status') === statusId && compareId(item.get('id'), lastReadId) > 0);
+  state = state.update('unread', unread => unread - deletedUnread.size);
   return state.update('items', helper).update('pendingItems', helper);
 };
 
+const updateMounted = (state) => {
+  state = state.update('mounted', count => count + 1);
+  if (!shouldCountUnreadNotifications(state)) {
+    state = state.set('readMarkerId', state.get('lastReadId'));
+    state = clearUnread(state);
+  }
+  return state;
+};
+
+const updateVisibility = (state, visibility) => {
+  state = state.set('isTabVisible', visibility);
+  if (!shouldCountUnreadNotifications(state)) {
+    state = state.set('readMarkerId', state.get('lastReadId'));
+    state = clearUnread(state);
+  }
+  return state;
+};
+
+const shouldCountUnreadNotifications = (state) => {
+  const isTabVisible   = state.get('isTabVisible');
+  const isOnTop        = state.get('top');
+  const isMounted      = state.get('mounted') > 0;
+  const lastReadId     = state.get('lastReadId');
+  const lastItemReached = !state.get('hasMore') || lastReadId === '0' || (!state.get('items').isEmpty() && compareId(state.get('items').last().get('id'), lastReadId) <= 0);
+
+  return !(isTabVisible && isOnTop && isMounted && lastItemReached);
+};
+
+const recountUnread = (state, last_read_id) => {
+  return state.withMutations(mutable => {
+    if (compareId(last_read_id, mutable.get('lastReadId')) > 0) {
+      mutable.set('lastReadId', last_read_id);
+    }
+
+    if (compareId(last_read_id, mutable.get('readMarkerId')) > 0) {
+      mutable.set('readMarkerId', last_read_id);
+    }
+
+    if (state.get('unread') > 0 || shouldCountUnreadNotifications(state)) {
+      mutable.set('unread', mutable.get('pendingItems').count(item => item !== null) + mutable.get('items').count(item => item && compareId(item.get('id'), last_read_id) > 0));
+    }
+  });
+};
+
 export default function notifications(state = initialState, action) {
   switch(action.type) {
+  case MARKERS_FETCH_SUCCESS:
+    return action.markers.notifications ? recountUnread(state, action.markers.notifications.last_read_id) : state;
+  case NOTIFICATIONS_MOUNT:
+    return updateMounted(state);
+  case NOTIFICATIONS_UNMOUNT:
+    return state.update('mounted', count => count - 1);
+  case APP_FOCUS:
+    return updateVisibility(state, true);
+  case APP_UNFOCUS:
+    return updateVisibility(state, false);
   case NOTIFICATIONS_LOAD_PENDING:
     return state.update('items', list => state.get('pendingItems').concat(list.take(40))).set('pendingItems', ImmutableList()).set('unread', 0);
   case NOTIFICATIONS_EXPAND_REQUEST:
@@ -144,10 +238,9 @@ export default function notifications(state = initialState, action) {
     return action.timeline === 'home' ?
       state.update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items) :
       state;
-  case NOTIFICATIONS_MOUNT:
-    return state.set('mounted', true);
-  case NOTIFICATIONS_UNMOUNT:
-    return state.set('mounted', false);
+  case NOTIFICATIONS_MARK_AS_READ:
+    const lastNotification = state.get('items').find(item => item !== null);
+    return lastNotification ? recountUnread(state, lastNotification.get('id')) : state;
   default:
     return state;
   }