diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index 4c6e27cd5f..3266df5a59 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -1,21 +1,13 @@ import { IntlMessageFormat } from 'intl-messageformat'; import { defineMessages } from 'react-intl'; -import { List as ImmutableList } from 'immutable'; - -import { compareId } from 'mastodon/compare_id'; -import { usePendingItems as preferPendingItems } from 'mastodon/initial_state'; - -import api, { getLinks } from '../api'; import { unescapeHTML } from '../utils/html'; import { requestNotificationPermission } from '../utils/notifications'; import { fetchFollowRequests } from './accounts'; import { importFetchedAccount, - importFetchedAccounts, importFetchedStatus, - importFetchedStatuses, } from './importer'; import { submitMarkers } from './markers'; import { notificationsUpdate } from "./notifications_typed"; @@ -26,27 +18,11 @@ export * from "./notifications_typed"; export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP'; -export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST'; -export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS'; -export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL'; - export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET'; -export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP'; -export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING'; - -export const NOTIFICATIONS_MOUNT = 'NOTIFICATIONS_MOUNT'; -export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT'; - -export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ'; - export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT'; export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION'; -export const NOTIFICATION_REQUESTS_ACCEPT_REQUEST = 'NOTIFICATION_REQUESTS_ACCEPT_REQUEST'; -export const NOTIFICATION_REQUESTS_ACCEPT_SUCCESS = 'NOTIFICATION_REQUESTS_ACCEPT_SUCCESS'; -export const NOTIFICATION_REQUESTS_ACCEPT_FAIL = 'NOTIFICATION_REQUESTS_ACCEPT_FAIL'; - export const NOTIFICATION_REQUESTS_DISMISS_REQUEST = 'NOTIFICATION_REQUESTS_DISMISS_REQUEST'; export const NOTIFICATION_REQUESTS_DISMISS_SUCCESS = 'NOTIFICATION_REQUESTS_DISMISS_SUCCESS'; export const NOTIFICATION_REQUESTS_DISMISS_FAIL = 'NOTIFICATION_REQUESTS_DISMISS_FAIL'; @@ -56,10 +32,6 @@ defineMessages({ group: { id: 'notifications.group', defaultMessage: '{count} notifications' }, }); -export const loadPending = () => ({ - type: NOTIFICATIONS_LOAD_PENDING, -}); - export function updateNotifications(notification, intlMessages, intlLocale) { return (dispatch, getState) => { const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']); @@ -96,8 +68,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) { dispatch(importFetchedAccount(notification.report.target_account)); } - - dispatch(notificationsUpdate({ notification, preferPendingItems, playSound: playSound && !filtered})); + dispatch(notificationsUpdate({ notification, playSound: playSound && !filtered})); } else if (playSound && !filtered) { dispatch({ type: NOTIFICATIONS_UPDATE_NOOP, @@ -120,116 +91,8 @@ export function updateNotifications(notification, intlMessages, intlLocale) { }; } -const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS(); - -const excludeTypesFromFilter = filter => { - const allTypes = ImmutableList([ - 'follow', - 'follow_request', - 'favourite', - 'reblog', - 'mention', - 'poll', - 'status', - 'update', - 'admin.sign_up', - 'admin.report', - ]); - - return allTypes.filterNot(item => item === filter).toJS(); -}; - const noOp = () => {}; -let expandNotificationsController = new AbortController(); - -export function expandNotifications({ maxId = undefined, forceLoad = false }) { - return async (dispatch, getState) => { - const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']); - const notifications = getState().get('notifications'); - const isLoadingMore = !!maxId; - - if (notifications.get('isLoading')) { - if (forceLoad) { - expandNotificationsController.abort(); - expandNotificationsController = new AbortController(); - } else { - return; - } - } - - const params = { - max_id: maxId, - exclude_types: activeFilter === 'all' - ? excludeTypesFromSettings(getState()) - : excludeTypesFromFilter(activeFilter), - }; - - if (!params.max_id && (notifications.get('items', ImmutableList()).size + notifications.get('pendingItems', ImmutableList()).size) > 0) { - const a = notifications.getIn(['pendingItems', 0, 'id']); - const b = notifications.getIn(['items', 0, 'id']); - - if (a && b && compareId(a, b) > 0) { - params.since_id = a; - } else { - params.since_id = b || a; - } - } - - const isLoadingRecent = !!params.since_id; - - dispatch(expandNotificationsRequest(isLoadingMore)); - - try { - const response = await api().get('/api/v1/notifications', { params, signal: expandNotificationsController.signal }); - const next = getLinks(response).refs.find(link => link.rel === 'next'); - - dispatch(importFetchedAccounts(response.data.map(item => item.account))); - dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status))); - dispatch(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account))); - - dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent, isLoadingRecent && preferPendingItems)); - dispatch(submitMarkers()); - } catch(error) { - dispatch(expandNotificationsFail(error, isLoadingMore)); - } - }; -} - -export function expandNotificationsRequest(isLoadingMore) { - return { - type: NOTIFICATIONS_EXPAND_REQUEST, - skipLoading: !isLoadingMore, - }; -} - -export function expandNotificationsSuccess(notifications, next, isLoadingMore, isLoadingRecent, usePendingItems) { - return { - type: NOTIFICATIONS_EXPAND_SUCCESS, - notifications, - next, - isLoadingRecent: isLoadingRecent, - usePendingItems, - skipLoading: !isLoadingMore, - }; -} - -export function expandNotificationsFail(error, isLoadingMore) { - return { - type: NOTIFICATIONS_EXPAND_FAIL, - error, - skipLoading: !isLoadingMore, - skipAlert: !isLoadingMore || error.name === 'AbortError', - }; -} - -export function scrollTopNotifications(top) { - return { - type: NOTIFICATIONS_SCROLL_TOP, - top, - }; -} - export function setFilter (filterType) { return dispatch => { dispatch({ @@ -237,24 +100,10 @@ export function setFilter (filterType) { path: ['notifications', 'quickFilter', 'active'], value: filterType, }); - dispatch(expandNotifications({ forceLoad: true })); dispatch(saveSettings()); }; } -export const mountNotifications = () => ({ - type: NOTIFICATIONS_MOUNT, -}); - -export const unmountNotifications = () => ({ - type: NOTIFICATIONS_UNMOUNT, -}); - - -export const markNotificationsAsRead = () => ({ - type: NOTIFICATIONS_MARK_AS_READ, -}); - // Browser support export function setupBrowserNotifications() { return dispatch => { diff --git a/app/javascript/mastodon/actions/notifications_migration.tsx b/app/javascript/mastodon/actions/notifications_migration.tsx deleted file mode 100644 index cd9f5ca3d6..0000000000 --- a/app/javascript/mastodon/actions/notifications_migration.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { createAppAsyncThunk } from 'mastodon/store'; - -import { fetchNotifications } from './notification_groups'; - -export const initializeNotifications = createAppAsyncThunk( - 'notifications/initialize', - (_, { dispatch }) => { - void dispatch(fetchNotifications()); - }, -); diff --git a/app/javascript/mastodon/actions/notifications_typed.ts b/app/javascript/mastodon/actions/notifications_typed.ts index 88d942d45e..3eb1230666 100644 --- a/app/javascript/mastodon/actions/notifications_typed.ts +++ b/app/javascript/mastodon/actions/notifications_typed.ts @@ -9,7 +9,6 @@ export const notificationsUpdate = createAction( ...args }: { notification: ApiNotificationJSON; - usePendingItems: boolean; playSound: boolean; }) => ({ payload: args, diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js index 30e643363a..478e0cae45 100644 --- a/app/javascript/mastodon/actions/streaming.js +++ b/app/javascript/mastodon/actions/streaming.js @@ -11,7 +11,7 @@ import { } from './announcements'; import { updateConversations } from './conversations'; import { processNewNotificationForGroups, refreshStaleNotificationGroups, pollRecentNotifications as pollRecentGroupNotifications } from './notification_groups'; -import { updateNotifications, expandNotifications } from './notifications'; +import { updateNotifications } from './notifications'; import { updateStatus } from './statuses'; import { updateTimeline, @@ -107,9 +107,6 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti break; } case 'notifications_merged': { - const state = getState(); - if (state.notifications.top || !state.notifications.mounted) - dispatch(expandNotifications({ forceLoad: true, maxId: undefined })); dispatch(refreshStaleNotificationGroups()); break; } diff --git a/app/javascript/mastodon/features/notifications/containers/column_settings_container.js b/app/javascript/mastodon/features/notifications/containers/column_settings_container.js index 4ac6cfa629..f061060ec7 100644 --- a/app/javascript/mastodon/features/notifications/containers/column_settings_container.js +++ b/app/javascript/mastodon/features/notifications/containers/column_settings_container.js @@ -3,7 +3,7 @@ import { defineMessages, injectIntl } from 'react-intl'; import { connect } from 'react-redux'; import { openModal } from 'mastodon/actions/modal'; -import { initializeNotifications } from 'mastodon/actions/notifications_migration'; +import { fetchNotifications } from 'mastodon/actions/notification_groups'; import { showAlert } from '../../../actions/alerts'; import { setFilter, requestBrowserPermission } from '../../../actions/notifications'; @@ -60,7 +60,7 @@ const mapDispatchToProps = (dispatch) => ({ dispatch(changeSetting(['notifications', ...path], checked)); if(path[0] === 'group' && path[1] === 'follow') { - dispatch(initializeNotifications()); + dispatch(fetchNotifications()); } } }, diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index bfc540a331..1ecc52b6bd 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -13,7 +13,7 @@ import { HotKeys } from 'react-hotkeys'; import { focusApp, unfocusApp, changeLayout } from 'mastodon/actions/app'; import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'mastodon/actions/markers'; -import { initializeNotifications } from 'mastodon/actions/notifications_migration'; +import { fetchNotifications } from 'mastodon/actions/notification_groups'; import { INTRODUCTION_VERSION } from 'mastodon/actions/onboarding'; import { HoverCardController } from 'mastodon/components/hover_card_controller'; import { PictureInPicture } from 'mastodon/features/picture_in_picture'; @@ -418,7 +418,7 @@ class UI extends PureComponent { if (signedIn) { this.props.dispatch(fetchMarkers()); this.props.dispatch(expandHomeTimeline()); - this.props.dispatch(initializeNotifications()); + this.props.dispatch(fetchNotifications()); this.props.dispatch(fetchServerTranslationLanguages()); setTimeout(() => this.props.dispatch(fetchServer()), 3000); diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js index c99a619b52..b47e24e397 100644 --- a/app/javascript/mastodon/reducers/notifications.js +++ b/app/javascript/mastodon/reducers/notifications.js @@ -1,50 +1,11 @@ -import { fromJS, Map as ImmutableMap, List as ImmutableList } from 'immutable'; - -import { blockDomainSuccess } from 'mastodon/actions/domain_blocks'; -import { timelineDelete } from 'mastodon/actions/timelines_typed'; +import { fromJS, Map as ImmutableMap } from 'immutable'; import { - authorizeFollowRequestSuccess, - blockAccountSuccess, - muteAccountSuccess, - rejectFollowRequestSuccess, -} from '../actions/accounts'; -import { - focusApp, - unfocusApp, -} from '../actions/app'; -import { - fetchMarkers, -} from '../actions/markers'; -import { clearNotifications } from '../actions/notification_groups'; -import { - notificationsUpdate, - NOTIFICATIONS_EXPAND_SUCCESS, - NOTIFICATIONS_EXPAND_REQUEST, - NOTIFICATIONS_EXPAND_FAIL, - NOTIFICATIONS_FILTER_SET, - NOTIFICATIONS_SCROLL_TOP, - NOTIFICATIONS_LOAD_PENDING, - NOTIFICATIONS_MOUNT, - NOTIFICATIONS_UNMOUNT, - NOTIFICATIONS_MARK_AS_READ, NOTIFICATIONS_SET_BROWSER_SUPPORT, NOTIFICATIONS_SET_BROWSER_PERMISSION, } from '../actions/notifications'; -import { disconnectTimeline } from '../actions/timelines'; -import { compareId } from '../compare_id'; const initialState = ImmutableMap({ - pendingItems: ImmutableList(), - items: ImmutableList(), - hasMore: true, - top: false, - mounted: 0, - unread: 0, - lastReadId: '0', - readMarkerId: '0', - isTabVisible: true, - isLoading: 0, browserSupport: false, browserPermission: 'default', }); @@ -60,248 +21,8 @@ export const notificationToMap = notification => ImmutableMap({ moderation_warning: notification.moderation_warning ? fromJS(notification.moderation_warning) : null, }); -const normalizeNotification = (state, notification, usePendingItems) => { - const top = state.get('top'); - - // Under currently unknown conditions, the client may receive duplicates from the server - if (state.get('pendingItems').some((item) => item?.get('id') === notification.id) || state.get('items').some((item) => item?.get('id') === notification.id)) { - return state; - } - - if (usePendingItems || !state.get('pendingItems').isEmpty()) { - return state.update('pendingItems', list => list.unshift(notificationToMap(notification))).update('unread', unread => unread + 1); - } - - if (shouldCountUnreadNotifications(state)) { - state = state.update('unread', unread => unread + 1); - } else { - state = state.set('lastReadId', notification.id); - } - - return state.update('items', list => { - if (top && list.size > 40) { - list = list.take(20); - } - - return list.unshift(notificationToMap(notification)); - }); -}; - -const expandNormalizedNotifications = (state, notifications, next, isLoadingMore, isLoadingRecent, usePendingItems) => { - // This method is pretty tricky because: - // - existing notifications might be out of order - // - the existing notifications may have gaps, most often explicitly noted with a `null` item - // - ideally, we don't want it to reorder existing items - // - `notifications` may include items that are already included - // - this function can be called either to fill in a gap, or load newer items - - const lastReadId = state.get('lastReadId'); - const newItems = ImmutableList(notifications.map(notificationToMap)); - - return state.withMutations(mutable => { - if (!newItems.isEmpty()) { - usePendingItems = isLoadingRecent && (usePendingItems || !mutable.get('pendingItems').isEmpty()); - - mutable.update(usePendingItems ? 'pendingItems' : 'items', oldItems => { - // If called to poll *new* notifications, we just need to add them on top without duplicates - if (isLoadingRecent) { - const idsToCheck = oldItems.map(item => item?.get('id')).toSet(); - const insertedItems = newItems.filterNot(item => idsToCheck.includes(item.get('id'))); - return insertedItems.concat(oldItems); - } - - // If called to expand more (presumably older than any known to the WebUI), we just have to - // add them to the bottom without duplicates - if (isLoadingMore) { - const idsToCheck = oldItems.map(item => item?.get('id')).toSet(); - const insertedItems = newItems.filterNot(item => idsToCheck.includes(item.get('id'))); - return oldItems.concat(insertedItems); - } - - // Now this gets tricky, as we don't necessarily know for sure where the gap to fill is, - // and some items in the timeline may not be properly ordered. - - // However, we know that `newItems.last()` is the oldest item that was requested and that - // there is no “hole” between `newItems.last()` and `newItems.first()`. - - // First, find the furthest (if properly sorted, oldest) item in the notifications that is - // newer than the oldest fetched one, as it's most likely that it delimits the gap. - // Start the gap *after* that item. - const lastIndex = oldItems.findLastIndex(item => item !== null && compareId(item.get('id'), newItems.last().get('id')) >= 0) + 1; - - // Then, try to find the furthest (if properly sorted, oldest) item in the notifications that - // is newer than the most recent fetched one, as it delimits a section comprised of only - // items older or within `newItems` (or that were deleted from the server, so should be removed - // anyway). - // Stop the gap *after* that item. - const firstIndex = oldItems.take(lastIndex).findLastIndex(item => item !== null && compareId(item.get('id'), newItems.first().get('id')) > 0) + 1; - - // At this point: - // - no `oldItems` after `firstIndex` is newer than any of the `newItems` - // - all `oldItems` after `lastIndex` are older than every of the `newItems` - // - it is possible for items in the replaced slice to be older than every `newItems` - // - it is possible for items before `firstIndex` to be in the `newItems` range - // Therefore: - // - to avoid losing items, items from the replaced slice that are older than `newItems` - // should be added in the back. - // - to avoid duplicates, `newItems` should be checked the first `firstIndex` items of - // `oldItems` - const idsToCheck = oldItems.take(firstIndex).map(item => item?.get('id')).toSet(); - const insertedItems = newItems.filterNot(item => idsToCheck.includes(item.get('id'))); - const olderItems = oldItems.slice(firstIndex, lastIndex).filter(item => item !== null && compareId(item.get('id'), newItems.last().get('id')) < 0); - - return oldItems.take(firstIndex).concat( - insertedItems, - olderItems, - oldItems.skip(lastIndex), - ); - }); - } - - if (!next) { - mutable.set('hasMore', false); - } - - if (shouldCountUnreadNotifications(state)) { - mutable.set('unread', mutable.get('pendingItems').count(item => item !== null) + mutable.get('items').count(item => item && compareId(item.get('id'), lastReadId) > 0)); - } else { - const mostRecent = newItems.find(item => item !== null); - if (mostRecent && compareId(lastReadId, mostRecent.get('id')) < 0) { - mutable.set('lastReadId', mostRecent.get('id')); - } - } - - mutable.update('isLoading', (nbLoading) => nbLoading - 1); - }); -}; - -const filterNotifications = (state, accountIds, type) => { - const helper = list => list.filterNot(item => item !== null && accountIds.includes(item.get('account')) && (type === undefined || type === item.get('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) => { - state = state.set('top', top); - - if (!shouldCountUnreadNotifications(state)) { - state = clearUnread(state); - } - - 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 fetchMarkers.fulfilled.type: - return action.payload.markers.notifications ? recountUnread(state, action.payload.markers.notifications.last_read_id) : state; - case NOTIFICATIONS_MOUNT: - return updateMounted(state); - case NOTIFICATIONS_UNMOUNT: - return state.update('mounted', count => count - 1); - case focusApp.type: - return updateVisibility(state, true); - case unfocusApp.type: - 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: - return state.update('isLoading', (nbLoading) => nbLoading + 1); - case NOTIFICATIONS_EXPAND_FAIL: - return state.update('isLoading', (nbLoading) => nbLoading - 1); - case NOTIFICATIONS_FILTER_SET: - return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', true); - case NOTIFICATIONS_SCROLL_TOP: - return updateTop(state, action.top); - case notificationsUpdate.type: - return normalizeNotification(state, action.payload.notification, action.payload.usePendingItems); - case NOTIFICATIONS_EXPAND_SUCCESS: - return expandNormalizedNotifications(state, action.notifications, action.next, action.isLoadingMore, action.isLoadingRecent, action.usePendingItems); - case blockAccountSuccess.type: - return filterNotifications(state, [action.payload.relationship.id]); - case muteAccountSuccess.type: - return action.payload.relationship.muting_notifications ? filterNotifications(state, [action.payload.relationship.id]) : state; - case blockDomainSuccess.type: - return filterNotifications(state, action.payload.accounts); - case authorizeFollowRequestSuccess.type: - case rejectFollowRequestSuccess.type: - return filterNotifications(state, [action.payload.id], 'follow_request'); - case clearNotifications.pending.type: - return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', false); - case timelineDelete.type: - return deleteByStatus(state, action.payload.statusId); - case disconnectTimeline.type: - return action.payload.timeline === 'home' ? - state.update(action.payload.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items) : - state; - 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: