diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index f5105d460f7..f3b1bb1d875 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -1,9 +1,6 @@ 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'; @@ -27,17 +24,9 @@ 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'; @@ -99,10 +88,6 @@ const selectNotificationCountForRequest = (state, id) => { return thisRequest ? thisRequest.get('notifications_count') : 0; }; -export const loadPending = () => ({ - type: NOTIFICATIONS_LOAD_PENDING, -}); - export function updateNotifications(notification, intlMessages, intlLocale) { return (dispatch, getState) => { const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']); @@ -165,110 +150,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)); - fetchRelatedRelationships(dispatch, response.data); - 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, @@ -283,20 +166,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, }); diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js index 622f5e8e884..5ec7c34d92c 100644 --- a/app/javascript/mastodon/reducers/notifications.js +++ b/app/javascript/mastodon/reducers/notifications.js @@ -19,14 +19,8 @@ import { 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, @@ -39,7 +33,6 @@ const initialState = ImmutableMap({ items: ImmutableList(), hasMore: true, top: false, - mounted: 0, unread: 0, lastReadId: '0', readMarkerId: '0', @@ -87,94 +80,6 @@ const normalizeNotification = (state, notification, usePendingItems) => { }); }; -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); @@ -210,15 +115,6 @@ const deleteByStatus = (state, statusId) => { 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)) { @@ -231,12 +127,11 @@ const updateVisibility = (state, visibility) => { 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); + return !(isTabVisible && (ignoreScroll || isOnTop) && lastItemReached); }; const recountUnread = (state, last_read_id) => { @@ -259,28 +154,16 @@ 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: