diff --git a/src/actions/notifications.ts b/src/actions/notifications.ts index fb70a717bf..f5af37eb0b 100644 --- a/src/actions/notifications.ts +++ b/src/actions/notifications.ts @@ -45,6 +45,19 @@ const NOTIFICATIONS_MARK_READ_FAIL = 'NOTIFICATIONS_MARK_READ_FAIL'; const MAX_QUEUED_NOTIFICATIONS = 40; +const FILTER_TYPES = { + all: undefined, + mention: ['mention'], + favourite: ['favourite', 'pleroma:emoji_reaction'], + reblog: ['reblog'], + poll: ['poll'], + status: ['status'], + follow: ['follow', 'follow_request'], + events: ['pleroma:event_reminder', 'pleroma:participation_request', 'pleroma:participation_accepted'], +}; + +type FilterType = keyof typeof FILTER_TYPES; + defineMessages({ mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' }, group: { id: 'notifications.group', defaultMessage: '{count, plural, one {# notification} other {# notifications}}' }, @@ -168,25 +181,32 @@ const dequeueNotifications = () => dispatch(markReadNotifications()); }; -const excludeTypesFromFilter = (filter: string) => { - return NOTIFICATION_TYPES.filter(item => item !== filter); +const excludeTypesFromFilter = (filters: string[]) => { + return NOTIFICATION_TYPES.filter(item => !filters.includes(item)); }; const noOp = () => new Promise(f => f(undefined)); -const expandNotifications = ({ maxId }: Record = {}, done: () => any = noOp) => +let abortExpandNotifications = new AbortController(); + +const expandNotifications = ({ maxId }: Record = {}, done: () => any = noOp, abort?: boolean) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return dispatch(noOp); const state = getState(); const features = getFeatures(state.instance); - const activeFilter = getSettings(state).getIn(['notifications', 'quickFilter', 'active']) as string; + const activeFilter = getSettings(state).getIn(['notifications', 'quickFilter', 'active']) as FilterType; const notifications = state.notifications; const isLoadingMore = !!maxId; if (notifications.isLoading) { - done(); - return dispatch(noOp); + if (abort) { + abortExpandNotifications.abort(); + abortExpandNotifications = new AbortController(); + } else { + done(); + return dispatch(noOp); + } } const params: Record = { @@ -200,10 +220,11 @@ const expandNotifications = ({ maxId }: Record = {}, done: () => an params.exclude_types = EXCLUDE_TYPES; } } else { + const filtered = FILTER_TYPES[activeFilter] || [activeFilter]; if (features.notificationsIncludeTypes) { - params.types = [activeFilter]; + params.types = filtered; } else { - params.exclude_types = excludeTypesFromFilter(activeFilter); + params.exclude_types = excludeTypesFromFilter(filtered); } } @@ -213,7 +234,7 @@ const expandNotifications = ({ maxId }: Record = {}, done: () => an dispatch(expandNotificationsRequest(isLoadingMore)); - return api(getState).get('/api/v1/notifications', { params }).then(response => { + return api(getState).get('/api/v1/notifications', { params, signal: abortExpandNotifications.signal }).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); const entries = (response.data as APIEntity[]).reduce((acc, item) => { @@ -286,14 +307,14 @@ const scrollTopNotifications = (top: boolean) => dispatch(markReadNotifications()); }; -const setFilter = (filterType: string) => +const setFilter = (filterType: FilterType, abort?: boolean) => (dispatch: AppDispatch) => { dispatch({ type: NOTIFICATIONS_FILTER_SET, path: ['notifications', 'quickFilter', 'active'], value: filterType, }); - dispatch(expandNotifications()); + dispatch(expandNotifications(undefined, undefined, abort)); dispatch(saveSettings()); }; @@ -343,6 +364,7 @@ export { NOTIFICATIONS_MARK_READ_SUCCESS, NOTIFICATIONS_MARK_READ_FAIL, MAX_QUEUED_NOTIFICATIONS, + type FilterType, updateNotifications, updateNotificationsQueue, dequeueNotifications, diff --git a/src/actions/settings.ts b/src/actions/settings.ts index 792ed0aaaf..436e008fc8 100644 --- a/src/actions/settings.ts +++ b/src/actions/settings.ts @@ -81,17 +81,6 @@ const defaultSettings = ImmutableMap({ advanced: false, }), - shows: ImmutableMap({ - follow: true, - follow_request: true, - favourite: true, - reblog: true, - mention: true, - poll: true, - move: true, - 'pleroma:emoji_reaction': true, - }), - sounds: ImmutableMap({ follow: false, follow_request: false, diff --git a/src/features/notifications/components/filter-bar.tsx b/src/features/notifications/components/filter-bar.tsx index 4b508a911f..4fd2f75f45 100644 --- a/src/features/notifications/components/filter-bar.tsx +++ b/src/features/notifications/components/filter-bar.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { setFilter } from 'soapbox/actions/notifications'; +import { type FilterType, setFilter } from 'soapbox/actions/notifications'; import { Icon, Tabs } from 'soapbox/components/ui'; import { useAppDispatch, useFeatures, useSettings } from 'soapbox/hooks'; @@ -10,12 +10,12 @@ import type { Item } from 'soapbox/components/ui/tabs/tabs'; const messages = defineMessages({ all: { id: 'notifications.filter.all', defaultMessage: 'All' }, mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' }, + statuses: { id: 'notifications.filter.statuses', defaultMessage: 'Updates from people you follow' }, favourites: { id: 'notifications.filter.favourites', defaultMessage: 'Likes' }, boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Reposts' }, polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' }, + events: { id: 'notifications.filter.events', defaultMessage: 'Events' }, follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' }, - emoji_reacts: { id: 'notifications.filter.emoji_reacts', defaultMessage: 'Emoji reacts' }, - statuses: { id: 'notifications.filter.statuses', defaultMessage: 'Updates from people you follow' }, }); const NotificationFilterBar = () => { @@ -27,7 +27,13 @@ const NotificationFilterBar = () => { const selectedFilter = settings.notifications.quickFilter.active; const advancedMode = settings.notifications.quickFilter.advanced; - const onClick = (notificationType: string) => () => dispatch(setFilter(notificationType)); + const onClick = (notificationType: FilterType) => () => { + try { + dispatch(setFilter(notificationType, true)); + } catch (e) { + console.error(e); + } + }; const items: Item[] = [ { @@ -50,18 +56,18 @@ const NotificationFilterBar = () => { action: onClick('mention'), name: 'mention', }); + if (features.accountNotifies || features.accountSubscriptions) items.push({ + text: , + title: intl.formatMessage(messages.statuses), + action: onClick('status'), + name: 'status', + }); items.push({ text: , title: intl.formatMessage(messages.favourites), action: onClick('favourite'), name: 'favourite', }); - if (features.emojiReacts) items.push({ - text: , - title: intl.formatMessage(messages.emoji_reacts), - action: onClick('pleroma:emoji_reaction'), - name: 'pleroma:emoji_reaction', - }); items.push({ text: , title: intl.formatMessage(messages.boosts), @@ -74,11 +80,11 @@ const NotificationFilterBar = () => { action: onClick('poll'), name: 'poll', }); - if (features.accountNotifies || features.accountSubscriptions) items.push({ - text: , - title: intl.formatMessage(messages.statuses), - action: onClick('status'), - name: 'status', + if (features.events) items.push({ + text: , + title: intl.formatMessage(messages.events), + action: onClick('events'), + name: 'events', }); items.push({ text: , diff --git a/src/features/notifications/components/notification.tsx b/src/features/notifications/components/notification.tsx index b429fc899c..2a9e4325ac 100644 --- a/src/features/notifications/components/notification.tsx +++ b/src/features/notifications/components/notification.tsx @@ -43,9 +43,7 @@ const icons: Record = { follow_request: require('@tabler/icons/outline/user-plus.svg'), mention: require('@tabler/icons/outline/at.svg'), favourite: require('@tabler/icons/outline/heart.svg'), - group_favourite: require('@tabler/icons/outline/heart.svg'), reblog: require('@tabler/icons/outline/repeat.svg'), - group_reblog: require('@tabler/icons/outline/repeat.svg'), status: require('@tabler/icons/outline/bell-ringing.svg'), poll: require('@tabler/icons/outline/chart-bar.svg'), move: require('@tabler/icons/outline/briefcase.svg'), @@ -75,18 +73,10 @@ const messages: Record = defineMessages({ id: 'notification.favourite', defaultMessage: '{name} liked your post', }, - group_favourite: { - id: 'notification.group_favourite', - defaultMessage: '{name} liked your group post', - }, reblog: { id: 'notification.reblog', defaultMessage: '{name} reposted your post', }, - group_reblog: { - id: 'notification.group_reblog', - defaultMessage: '{name} reposted your group post', - }, status: { id: 'notification.status', defaultMessage: '{name} just posted', @@ -307,10 +297,8 @@ const Notification: React.FC = (props) => { /> ) : null; case 'favourite': - case 'group_favourite': case 'mention': case 'reblog': - case 'group_reblog': case 'status': case 'poll': case 'update': diff --git a/src/features/notifications/index.tsx b/src/features/notifications/index.tsx index 870ce8ad19..e4133195a5 100644 --- a/src/features/notifications/index.tsx +++ b/src/features/notifications/index.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx'; -import { List as ImmutableList, Map as ImmutableMap } from 'immutable'; +import { List as ImmutableList } from 'immutable'; import debounce from 'lodash/debounce'; import React, { useCallback, useEffect, useRef } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; @@ -10,7 +10,6 @@ import { scrollTopNotifications, dequeueNotifications, } from 'soapbox/actions/notifications'; -import { getSettings } from 'soapbox/actions/settings'; import PullToRefresh from 'soapbox/components/pull-to-refresh'; import ScrollTopButton from 'soapbox/components/scroll-top-button'; import ScrollableList from 'soapbox/components/scrollable-list'; @@ -23,7 +22,6 @@ import Notification from './components/notification'; import type { VirtuosoHandle } from 'react-virtuoso'; import type { RootState } from 'soapbox/store'; -import type { Notification as NotificationEntity } from 'soapbox/types/entities'; const messages = defineMessages({ title: { id: 'column.notifications', defaultMessage: 'Notifications' }, @@ -31,19 +29,8 @@ const messages = defineMessages({ }); const getNotifications = createSelector([ - state => getSettings(state).getIn(['notifications', 'quickFilter', 'show']), - state => getSettings(state).getIn(['notifications', 'quickFilter', 'active']), - state => ImmutableList((getSettings(state).getIn(['notifications', 'shows']) as ImmutableMap).filter(item => !item).keys()), (state: RootState) => state.notifications.items.toList(), -], (showFilterBar, allowedType, excludedTypes, notifications: ImmutableList) => { - if (!showFilterBar || allowedType === 'all') { - // used if user changed the notification settings after loading the notifications from the server - // otherwise a list of notifications will come pre-filtered from the backend - // we need to turn it off for FilterBar in order not to block ourselves from seeing a specific category - return notifications.filterNot(item => item !== null && excludedTypes.includes(item.get('type'))); - } - return notifications.filter(item => item !== null && allowedType === item.get('type')); -}); +], (notifications) => notifications.filter(item => item !== null)); const Notifications = () => { const dispatch = useAppDispatch(); @@ -164,9 +151,8 @@ const Notifications = () => { onLoadMore={handleLoadOlder} onScrollToTop={handleScrollToTop} onScroll={handleScroll} - listClassName={clsx({ - 'divide-y divide-gray-200 black:divide-gray-800 dark:divide-primary-800 divide-solid': notifications.size > 0, - 'space-y-2': notifications.size === 0, + listClassName={clsx('divide-y divide-solid divide-gray-200 black:divide-gray-800 dark:divide-primary-800', { + 'animate-pulse': notifications.size === 0, })} > {scrollableContent as ImmutableList} diff --git a/src/features/placeholder/components/placeholder-notification.tsx b/src/features/placeholder/components/placeholder-notification.tsx index 8e66b08a1e..2cc4368fec 100644 --- a/src/features/placeholder/components/placeholder-notification.tsx +++ b/src/features/placeholder/components/placeholder-notification.tsx @@ -8,7 +8,7 @@ import PlaceholderStatusContent from './placeholder-status-content'; /** Fake notification to display while data is loading. */ const PlaceholderNotification = () => ( -
+
diff --git a/src/reducers/notifications.ts b/src/reducers/notifications.ts index f2147b2cfb..f32e5af6ea 100644 --- a/src/reducers/notifications.ts +++ b/src/reducers/notifications.ts @@ -208,6 +208,7 @@ export default function notifications(state: State = ReducerRecord(), action: An case NOTIFICATIONS_EXPAND_REQUEST: return state.set('isLoading', true); case NOTIFICATIONS_EXPAND_FAIL: + if (action.error?.message === 'canceled') return state; return state.set('isLoading', false); case NOTIFICATIONS_FILTER_SET: return state.set('items', ImmutableOrderedMap()).set('hasMore', true); diff --git a/src/utils/notification.ts b/src/utils/notification.ts index 43e73be113..9faeb6e364 100644 --- a/src/utils/notification.ts +++ b/src/utils/notification.ts @@ -5,8 +5,6 @@ const NOTIFICATION_TYPES = [ 'mention', 'reblog', 'favourite', - 'group_favourite', - 'group_reblog', 'poll', 'status', 'move',