aboutsummaryrefslogtreecommitdiff
path: root/app/javascript/mastodon/reducers/notifications.js
diff options
context:
space:
mode:
Diffstat (limited to 'app/javascript/mastodon/reducers/notifications.js')
-rw-r--r--app/javascript/mastodon/reducers/notifications.js120
1 files changed, 111 insertions, 9 deletions
diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js
index ed1ba0272..1d4874717 100644
--- a/app/javascript/mastodon/reducers/notifications.js
+++ b/app/javascript/mastodon/reducers/notifications.js
@@ -9,6 +9,9 @@ import {
NOTIFICATIONS_LOAD_PENDING,
NOTIFICATIONS_MOUNT,
NOTIFICATIONS_UNMOUNT,
+ NOTIFICATIONS_MARK_AS_READ,
+ NOTIFICATIONS_SET_BROWSER_SUPPORT,
+ NOTIFICATIONS_SET_BROWSER_PERMISSION,
} from '../actions/notifications';
import {
ACCOUNT_BLOCK_SUCCESS,
@@ -16,6 +19,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,9 +36,14 @@ const initialState = ImmutableMap({
items: ImmutableList(),
hasMore: true,
top: false,
- mounted: false,
+ mounted: 0,
unread: 0,
+ lastReadId: '0',
+ readMarkerId: '0',
+ isTabVisible: true,
isLoading: false,
+ browserSupport: false,
+ browserPermission: 'default',
});
const notificationToMap = notification => ImmutableMap({
@@ -46,8 +61,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 +77,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 +105,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 +123,93 @@ 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.get('mounted') === 1)) {
+ 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, ignoreScroll = false) => {
+ const isTabVisible = state.get('isTabVisible');
+ const isOnTop = state.get('top');
+ const isMounted = state.get('mounted') > 0;
+ const lastReadId = state.get('lastReadId');
+ const lastItem = state.get('items').findLast(item => item !== null);
+ const lastItemReached = !state.get('hasMore') || lastReadId === '0' || (lastItem && compareId(lastItem.get('id'), lastReadId) <= 0);
+
+ return !(isTabVisible && (ignoreScroll || 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 +243,13 @@ 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;
+ case NOTIFICATIONS_SET_BROWSER_SUPPORT:
+ return state.set('browserSupport', action.value);
+ case NOTIFICATIONS_SET_BROWSER_PERMISSION:
+ return state.set('browserPermission', action.value);
default:
return state;
}