Join some notification filters, add filter for event-related notifications

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2024-05-03 15:00:42 +02:00
parent 3d12df2c37
commit 5d28e9609a
8 changed files with 60 additions and 70 deletions

View file

@ -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,26 +181,33 @@ 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<string, any> = {}, done: () => any = noOp) =>
let abortExpandNotifications = new AbortController();
const expandNotifications = ({ maxId }: Record<string, any> = {}, 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) {
if (abort) {
abortExpandNotifications.abort();
abortExpandNotifications = new AbortController();
} else {
done();
return dispatch(noOp);
}
}
const params: Record<string, any> = {
max_id: maxId,
@ -200,10 +220,11 @@ const expandNotifications = ({ maxId }: Record<string, any> = {}, 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<string, any> = {}, 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,

View file

@ -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,

View file

@ -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: <Icon className='h-4 w-4' src={require('@tabler/icons/outline/bell-ringing.svg')} />,
title: intl.formatMessage(messages.statuses),
action: onClick('status'),
name: 'status',
});
items.push({
text: <Icon className='h-4 w-4' src={require('@tabler/icons/outline/heart.svg')} />,
title: intl.formatMessage(messages.favourites),
action: onClick('favourite'),
name: 'favourite',
});
if (features.emojiReacts) items.push({
text: <Icon className='h-4 w-4' src={require('@tabler/icons/outline/mood-smile.svg')} />,
title: intl.formatMessage(messages.emoji_reacts),
action: onClick('pleroma:emoji_reaction'),
name: 'pleroma:emoji_reaction',
});
items.push({
text: <Icon className='h-4 w-4' src={require('@tabler/icons/outline/repeat.svg')} />,
title: intl.formatMessage(messages.boosts),
@ -74,11 +80,11 @@ const NotificationFilterBar = () => {
action: onClick('poll'),
name: 'poll',
});
if (features.accountNotifies || features.accountSubscriptions) items.push({
text: <Icon className='h-4 w-4' src={require('@tabler/icons/outline/bell-ringing.svg')} />,
title: intl.formatMessage(messages.statuses),
action: onClick('status'),
name: 'status',
if (features.events) items.push({
text: <Icon className='h-4 w-4' src={require('@tabler/icons/outline/calendar.svg')} />,
title: intl.formatMessage(messages.events),
action: onClick('events'),
name: 'events',
});
items.push({
text: <Icon className='h-4 w-4' src={require('@tabler/icons/outline/user-plus.svg')} />,

View file

@ -43,9 +43,7 @@ const icons: Record<NotificationType, string> = {
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<NotificationType, MessageDescriptor> = 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<INotification> = (props) => {
/>
) : null;
case 'favourite':
case 'group_favourite':
case 'mention':
case 'reblog':
case 'group_reblog':
case 'status':
case 'poll':
case 'update':

View file

@ -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<string, boolean>).filter(item => !item).keys()),
(state: RootState) => state.notifications.items.toList(),
], (showFilterBar, allowedType, excludedTypes, notifications: ImmutableList<NotificationEntity>) => {
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<JSX.Element>}

View file

@ -8,7 +8,7 @@ import PlaceholderStatusContent from './placeholder-status-content';
/** Fake notification to display while data is loading. */
const PlaceholderNotification = () => (
<div className='bg-white px-4 py-6 black:bg-black sm:p-6 dark:bg-primary-900'>
<div className='bg-white p-4 black:bg-black dark:bg-primary-900'>
<div className='w-full animate-pulse'>
<div className='mb-2'>
<PlaceholderStatusContent minLength={20} maxLength={20} />

View file

@ -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);

View file

@ -5,8 +5,6 @@ const NOTIFICATION_TYPES = [
'mention',
'reblog',
'favourite',
'group_favourite',
'group_reblog',
'poll',
'status',
'move',