mirror of https://github.com/mastodon/mastodon.git
Handle filters, and add basic pagination management
parent
c0d302a65e
commit
47d5ffc62a
|
@ -1,17 +1,49 @@
|
||||||
import { apiFetchNotifications } from 'mastodon/api/notifications';
|
import { apiFetchNotifications } from 'mastodon/api/notifications';
|
||||||
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
|
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
|
||||||
|
import type { NotificationGroupJSON } from 'mastodon/api_types/notifications';
|
||||||
|
import { allNotificationTypes } from 'mastodon/api_types/notifications';
|
||||||
import type { ApiStatusJSON } from 'mastodon/api_types/statuses';
|
import type { ApiStatusJSON } from 'mastodon/api_types/statuses';
|
||||||
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
|
import type { NotificationGap } from 'mastodon/reducers/notifications_groups';
|
||||||
|
import {
|
||||||
|
selectSettingsNotificationsExcludedTypes,
|
||||||
|
selectSettingsNotificationsQuickFilterActive,
|
||||||
|
} from 'mastodon/selectors/settings';
|
||||||
|
import {
|
||||||
|
createAppAsyncThunk,
|
||||||
|
createDataLoadingThunk,
|
||||||
|
} from 'mastodon/store/typed_functions';
|
||||||
|
|
||||||
import { importFetchedAccounts, importFetchedStatuses } from './importer';
|
import { importFetchedAccounts, importFetchedStatuses } from './importer';
|
||||||
|
import { NOTIFICATIONS_FILTER_SET } from './notifications';
|
||||||
|
import { saveSettings } from './settings';
|
||||||
|
|
||||||
|
function excludeAllTypesExcept(filter: string) {
|
||||||
|
return allNotificationTypes.filter((item) => item !== filter);
|
||||||
|
}
|
||||||
|
|
||||||
export const fetchNotifications = createDataLoadingThunk(
|
export const fetchNotifications = createDataLoadingThunk(
|
||||||
'notificationGroups/fetch',
|
'notificationGroups/fetch',
|
||||||
() => apiFetchNotifications(),
|
async (params: { url?: string } | undefined, { getState }) => {
|
||||||
(notifications, { dispatch }) => {
|
if (params?.url) return apiFetchNotifications({}, params.url);
|
||||||
|
|
||||||
|
const activeFilter =
|
||||||
|
selectSettingsNotificationsQuickFilterActive(getState());
|
||||||
|
|
||||||
|
return apiFetchNotifications({
|
||||||
|
exclude_types:
|
||||||
|
activeFilter === 'all'
|
||||||
|
? selectSettingsNotificationsExcludedTypes(getState())
|
||||||
|
: excludeAllTypesExcept(activeFilter),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
({ notifications, links }, { dispatch }) => {
|
||||||
const fetchedAccounts: ApiAccountJSON[] = [];
|
const fetchedAccounts: ApiAccountJSON[] = [];
|
||||||
const fetchedStatuses: ApiStatusJSON[] = [];
|
const fetchedStatuses: ApiStatusJSON[] = [];
|
||||||
|
|
||||||
|
// We ignore the previous link, as it will always be here but we know there are no more
|
||||||
|
// recent notifications when doing the initial load
|
||||||
|
const nextLink = links.refs.find((link) => link.rel === 'next');
|
||||||
|
|
||||||
notifications.forEach((notification) => {
|
notifications.forEach((notification) => {
|
||||||
if ('sample_accounts' in notification) {
|
if ('sample_accounts' in notification) {
|
||||||
fetchedAccounts.push(...notification.sample_accounts);
|
fetchedAccounts.push(...notification.sample_accounts);
|
||||||
|
@ -36,6 +68,25 @@ export const fetchNotifications = createDataLoadingThunk(
|
||||||
if (fetchedStatuses.length > 0)
|
if (fetchedStatuses.length > 0)
|
||||||
dispatch(importFetchedStatuses(fetchedStatuses));
|
dispatch(importFetchedStatuses(fetchedStatuses));
|
||||||
|
|
||||||
|
const payload: (NotificationGroupJSON | NotificationGap)[] = notifications;
|
||||||
|
|
||||||
|
if (nextLink) payload.push({ type: 'gap', loadUrl: nextLink.uri });
|
||||||
|
|
||||||
|
return payload;
|
||||||
// dispatch(submitMarkers());
|
// dispatch(submitMarkers());
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const setNotificationsFilter = createAppAsyncThunk(
|
||||||
|
'notifications/filter/set',
|
||||||
|
({ filterType }: { filterType: string }, { dispatch }) => {
|
||||||
|
dispatch({
|
||||||
|
type: NOTIFICATIONS_FILTER_SET,
|
||||||
|
path: ['notifications', 'quickFilter', 'active'],
|
||||||
|
value: filterType,
|
||||||
|
});
|
||||||
|
// dispatch(expandNotifications({ forceLoad: true }));
|
||||||
|
void dispatch(fetchNotifications());
|
||||||
|
dispatch(saveSettings());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
|
@ -1,6 +1,17 @@
|
||||||
import { apiRequest } from 'mastodon/api';
|
import api, { getLinks } from 'mastodon/api';
|
||||||
import type { NotificationGroupJSON } from 'mastodon/api_types/notifications';
|
import type { NotificationGroupJSON } from 'mastodon/api_types/notifications';
|
||||||
|
|
||||||
export const apiFetchNotifications = () => {
|
export const apiFetchNotifications = async (
|
||||||
return apiRequest<NotificationGroupJSON[]>('GET', '/v2_alpha/notifications');
|
params?: {
|
||||||
|
exclude_types?: string[];
|
||||||
|
},
|
||||||
|
forceUrl?: string,
|
||||||
|
) => {
|
||||||
|
const response = await api().request<NotificationGroupJSON[]>({
|
||||||
|
method: 'GET',
|
||||||
|
url: forceUrl ?? '/api/v2_alpha/notifications',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { notifications: response.data, links: getLinks(response) };
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,6 +7,21 @@ import type { ApiReportJSON } from './reports';
|
||||||
import type { ApiStatusJSON } from './statuses';
|
import type { ApiStatusJSON } from './statuses';
|
||||||
|
|
||||||
// See app/model/notification.rb
|
// See app/model/notification.rb
|
||||||
|
export const allNotificationTypes = [
|
||||||
|
'follow',
|
||||||
|
'follow_request',
|
||||||
|
'favourite',
|
||||||
|
'reblog',
|
||||||
|
'mention',
|
||||||
|
'poll',
|
||||||
|
'status',
|
||||||
|
'update',
|
||||||
|
'admin.sign_up',
|
||||||
|
'admin.report',
|
||||||
|
'moderation_warning',
|
||||||
|
'severed_relationships',
|
||||||
|
];
|
||||||
|
|
||||||
export type NotificationWithStatusType =
|
export type NotificationWithStatusType =
|
||||||
| 'favourite'
|
| 'favourite'
|
||||||
| 'reblog'
|
| 'reblog'
|
||||||
|
|
|
@ -0,0 +1,145 @@
|
||||||
|
import type { PropsWithChildren } from 'react';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react';
|
||||||
|
import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react';
|
||||||
|
import PersonAddIcon from '@/material-icons/400-24px/person_add.svg?react';
|
||||||
|
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
|
||||||
|
import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
|
||||||
|
import StarIcon from '@/material-icons/400-24px/star.svg?react';
|
||||||
|
import { setNotificationsFilter } from 'mastodon/actions/notification_groups';
|
||||||
|
import { Icon } from 'mastodon/components/icon';
|
||||||
|
import {
|
||||||
|
selectSettingsNotificationsQuickFilterActive,
|
||||||
|
selectSettingsNotificationsQuickFilterAdvanced,
|
||||||
|
} from 'mastodon/selectors/settings';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
|
const tooltips = defineMessages({
|
||||||
|
mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' },
|
||||||
|
favourites: {
|
||||||
|
id: 'notifications.filter.favourites',
|
||||||
|
defaultMessage: 'Favorites',
|
||||||
|
},
|
||||||
|
boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' },
|
||||||
|
polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' },
|
||||||
|
follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' },
|
||||||
|
statuses: {
|
||||||
|
id: 'notifications.filter.statuses',
|
||||||
|
defaultMessage: 'Updates from people you follow',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const BarButton: React.FC<
|
||||||
|
PropsWithChildren<{
|
||||||
|
selectedFilter: string;
|
||||||
|
type: string;
|
||||||
|
title?: string;
|
||||||
|
}>
|
||||||
|
> = ({ selectedFilter, type, title, children }) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const onClick = useCallback(() => {
|
||||||
|
void dispatch(setNotificationsFilter({ filterType: type }));
|
||||||
|
}, [dispatch, type]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={selectedFilter === type ? 'active' : ''}
|
||||||
|
onClick={onClick}
|
||||||
|
title={title}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FilterBar: React.FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const selectedFilter = useAppSelector(
|
||||||
|
selectSettingsNotificationsQuickFilterActive,
|
||||||
|
);
|
||||||
|
const advancedMode = useAppSelector(
|
||||||
|
selectSettingsNotificationsQuickFilterAdvanced,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (advancedMode)
|
||||||
|
return (
|
||||||
|
<div className='notification__filter-bar'>
|
||||||
|
<BarButton selectedFilter={selectedFilter} type='all' key='all'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='notifications.filter.all'
|
||||||
|
defaultMessage='All'
|
||||||
|
/>
|
||||||
|
</BarButton>
|
||||||
|
<BarButton
|
||||||
|
selectedFilter={selectedFilter}
|
||||||
|
type='mention'
|
||||||
|
key='mention'
|
||||||
|
title={intl.formatMessage(tooltips.mentions)}
|
||||||
|
>
|
||||||
|
<Icon id='reply-all' icon={ReplyAllIcon} />
|
||||||
|
</BarButton>
|
||||||
|
<BarButton
|
||||||
|
selectedFilter={selectedFilter}
|
||||||
|
type='favourite'
|
||||||
|
key='favourite'
|
||||||
|
title={intl.formatMessage(tooltips.favourites)}
|
||||||
|
>
|
||||||
|
<Icon id='star' icon={StarIcon} />
|
||||||
|
</BarButton>
|
||||||
|
<BarButton
|
||||||
|
selectedFilter={selectedFilter}
|
||||||
|
type='reblog'
|
||||||
|
key='reblog'
|
||||||
|
title={intl.formatMessage(tooltips.boosts)}
|
||||||
|
>
|
||||||
|
<Icon id='retweet' icon={RepeatIcon} />
|
||||||
|
</BarButton>
|
||||||
|
<BarButton
|
||||||
|
selectedFilter={selectedFilter}
|
||||||
|
type='poll'
|
||||||
|
key='poll'
|
||||||
|
title={intl.formatMessage(tooltips.polls)}
|
||||||
|
>
|
||||||
|
<Icon id='tasks' icon={InsertChartIcon} />
|
||||||
|
</BarButton>
|
||||||
|
<BarButton
|
||||||
|
selectedFilter={selectedFilter}
|
||||||
|
type='status'
|
||||||
|
key='status'
|
||||||
|
title={intl.formatMessage(tooltips.statuses)}
|
||||||
|
>
|
||||||
|
<Icon id='home' icon={HomeIcon} />
|
||||||
|
</BarButton>
|
||||||
|
<BarButton
|
||||||
|
selectedFilter={selectedFilter}
|
||||||
|
type='follow'
|
||||||
|
key='follow'
|
||||||
|
title={intl.formatMessage(tooltips.follows)}
|
||||||
|
>
|
||||||
|
<Icon id='user-plus' icon={PersonAddIcon} />
|
||||||
|
</BarButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
else
|
||||||
|
return (
|
||||||
|
<div className='notification__filter-bar'>
|
||||||
|
<BarButton selectedFilter={selectedFilter} type='all' key='all'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='notifications.filter.all'
|
||||||
|
defaultMessage='All'
|
||||||
|
/>
|
||||||
|
</BarButton>
|
||||||
|
<BarButton selectedFilter={selectedFilter} type='mention' key='mention'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='notifications.filter.mentions'
|
||||||
|
defaultMessage='Mentions'
|
||||||
|
/>
|
||||||
|
</BarButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -5,8 +5,6 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import type { Map as ImmutableMap } from 'immutable';
|
|
||||||
import { List as ImmutableList } from 'immutable';
|
|
||||||
|
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
|
||||||
|
@ -17,6 +15,13 @@ import { compareId } from 'mastodon/compare_id';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator';
|
import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator';
|
||||||
import { useIdentity } from 'mastodon/identity_context';
|
import { useIdentity } from 'mastodon/identity_context';
|
||||||
|
import {
|
||||||
|
selectNeedsNotificationPermission,
|
||||||
|
selectSettingsNotificationsExcludedTypes,
|
||||||
|
selectSettingsNotificationsQuickFilterActive,
|
||||||
|
selectSettingsNotificationsQuickFilterShow,
|
||||||
|
selectSettingsNotificationsShowUnread,
|
||||||
|
} from 'mastodon/selectors/settings';
|
||||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
import type { RootState } from 'mastodon/store';
|
import type { RootState } from 'mastodon/store';
|
||||||
|
|
||||||
|
@ -37,9 +42,9 @@ import ScrollableList from '../../components/scrollable_list';
|
||||||
import { FilteredNotificationsBanner } from '../notifications/components/filtered_notifications_banner';
|
import { FilteredNotificationsBanner } from '../notifications/components/filtered_notifications_banner';
|
||||||
import NotificationsPermissionBanner from '../notifications/components/notifications_permission_banner';
|
import NotificationsPermissionBanner from '../notifications/components/notifications_permission_banner';
|
||||||
import ColumnSettingsContainer from '../notifications/containers/column_settings_container';
|
import ColumnSettingsContainer from '../notifications/containers/column_settings_container';
|
||||||
import FilterBarContainer from '../notifications/containers/filter_bar_container';
|
|
||||||
|
|
||||||
import { NotificationGroup } from './components/notification_group';
|
import { NotificationGroup } from './components/notification_group';
|
||||||
|
import { FilterBar } from './filter_bar';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'column.notifications', defaultMessage: 'Notifications' },
|
title: { id: 'column.notifications', defaultMessage: 'Notifications' },
|
||||||
|
@ -49,46 +54,11 @@ const messages = defineMessages({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */
|
|
||||||
// state.settings is not yet typed, so we disable some ESLint checks for those selectors
|
|
||||||
const selectSettingsNotificationsShow = (state: RootState) =>
|
|
||||||
state.settings.getIn(['notifications', 'shows']) as ImmutableMap<
|
|
||||||
string,
|
|
||||||
boolean
|
|
||||||
>;
|
|
||||||
|
|
||||||
const selectSettingsNotificationsQuickFilterShow = (state: RootState) =>
|
|
||||||
state.settings.getIn(['notifications', 'quickFilter', 'show']) as boolean;
|
|
||||||
|
|
||||||
const selectSettingsNotificationsQuickFilterActive = (state: RootState) =>
|
|
||||||
state.settings.getIn(['notifications', 'quickFilter', 'active']) as string;
|
|
||||||
|
|
||||||
const selectSettingsNotificationsShowUnread = (state: RootState) =>
|
|
||||||
state.settings.getIn(['notifications', 'showUnread']) as boolean;
|
|
||||||
|
|
||||||
const selectNeedsNotificationPermission = (state: RootState) =>
|
|
||||||
(state.settings.getIn(['notifications', 'alerts']).includes(true) &&
|
|
||||||
state.notifications.get('browserSupport') &&
|
|
||||||
state.notifications.get('browserPermission') === 'default' &&
|
|
||||||
!state.settings.getIn([
|
|
||||||
'notifications',
|
|
||||||
'dismissPermissionBanner',
|
|
||||||
])) as boolean;
|
|
||||||
|
|
||||||
/* eslint-enable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */
|
|
||||||
|
|
||||||
const getExcludedTypes = createSelector(
|
|
||||||
[selectSettingsNotificationsShow],
|
|
||||||
(shows) => {
|
|
||||||
return ImmutableList(shows.filter((item) => !item).keys());
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const getNotifications = createSelector(
|
const getNotifications = createSelector(
|
||||||
[
|
[
|
||||||
selectSettingsNotificationsQuickFilterShow,
|
selectSettingsNotificationsQuickFilterShow,
|
||||||
selectSettingsNotificationsQuickFilterActive,
|
selectSettingsNotificationsQuickFilterActive,
|
||||||
getExcludedTypes,
|
selectSettingsNotificationsExcludedTypes,
|
||||||
(state: RootState) => state.notificationsGroups.groups,
|
(state: RootState) => state.notificationsGroups.groups,
|
||||||
],
|
],
|
||||||
(showFilterBar, allowedType, excludedTypes, notifications) => {
|
(showFilterBar, allowedType, excludedTypes, notifications) => {
|
||||||
|
@ -97,11 +67,11 @@ const getNotifications = createSelector(
|
||||||
// otherwise a list of notifications will come pre-filtered from the backend
|
// 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
|
// we need to turn it off for FilterBar in order not to block ourselves from seeing a specific category
|
||||||
return notifications.filter(
|
return notifications.filter(
|
||||||
(item) => item.type !== 'gap' || !excludedTypes.includes(item.type),
|
(item) => item.type === 'gap' || !excludedTypes.includes(item.type),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return notifications.filter(
|
return notifications.filter(
|
||||||
(item) => item.type !== 'gap' || allowedType === item.type,
|
(item) => item.type === 'gap' || allowedType === item.type,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -195,8 +165,9 @@ export const Notifications: React.FC<{
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
const handleLoadGap = useCallback(
|
const handleLoadGap = useCallback(
|
||||||
(maxId: string) => {
|
(loadUrl: string) => {
|
||||||
dispatch(expandNotifications({ maxId }));
|
// TODO: this should not be fetch (as this overrides the existing notifications), but expand?
|
||||||
|
void dispatch(fetchNotifications({ url: loadUrl }));
|
||||||
},
|
},
|
||||||
[dispatch],
|
[dispatch],
|
||||||
);
|
);
|
||||||
|
@ -288,7 +259,7 @@ export const Notifications: React.FC<{
|
||||||
|
|
||||||
const { signedIn } = useIdentity();
|
const { signedIn } = useIdentity();
|
||||||
|
|
||||||
const filterBarContainer = signedIn ? <FilterBarContainer /> : null;
|
const filterBar = signedIn ? <FilterBar /> : null;
|
||||||
|
|
||||||
const scrollableContent = useMemo(() => {
|
const scrollableContent = useMemo(() => {
|
||||||
if (notifications.length === 0 && !hasMore) return null;
|
if (notifications.length === 0 && !hasMore) return null;
|
||||||
|
@ -296,9 +267,9 @@ export const Notifications: React.FC<{
|
||||||
return notifications.map((item) =>
|
return notifications.map((item) =>
|
||||||
item.type === 'gap' ? (
|
item.type === 'gap' ? (
|
||||||
<LoadGap
|
<LoadGap
|
||||||
key={item.id}
|
key={item.loadUrl}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
maxId={item.maxId}
|
maxId={item.loadUrl}
|
||||||
onClick={handleLoadGap}
|
onClick={handleLoadGap}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
@ -379,7 +350,7 @@ export const Notifications: React.FC<{
|
||||||
<ColumnSettingsContainer />
|
<ColumnSettingsContainer />
|
||||||
</ColumnHeader>
|
</ColumnHeader>
|
||||||
|
|
||||||
{filterBarContainer}
|
{filterBar}
|
||||||
|
|
||||||
<FilteredNotificationsBanner />
|
<FilteredNotificationsBanner />
|
||||||
|
|
||||||
|
|
|
@ -4,14 +4,13 @@ import { fetchNotifications } from 'mastodon/actions/notification_groups';
|
||||||
import { createNotificationGroupFromJSON } from 'mastodon/models/notification_group';
|
import { createNotificationGroupFromJSON } from 'mastodon/models/notification_group';
|
||||||
import type { NotificationGroup } from 'mastodon/models/notification_group';
|
import type { NotificationGroup } from 'mastodon/models/notification_group';
|
||||||
|
|
||||||
interface Gap {
|
export interface NotificationGap {
|
||||||
type: 'gap';
|
type: 'gap';
|
||||||
id: string;
|
loadUrl: string;
|
||||||
maxId: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NotificationGroupsState {
|
interface NotificationGroupsState {
|
||||||
groups: (NotificationGroup | Gap)[];
|
groups: (NotificationGroup | NotificationGap)[];
|
||||||
unread: number;
|
unread: number;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
hasMore: boolean;
|
hasMore: boolean;
|
||||||
|
@ -35,7 +34,7 @@ export const notificationGroupsReducer = createReducer<NotificationGroupsState>(
|
||||||
|
|
||||||
builder.addCase(fetchNotifications.fulfilled, (state, action) => {
|
builder.addCase(fetchNotifications.fulfilled, (state, action) => {
|
||||||
state.groups = action.payload.map((json) =>
|
state.groups = action.payload.map((json) =>
|
||||||
createNotificationGroupFromJSON(json),
|
json.type === 'gap' ? json : createNotificationGroupFromJSON(json),
|
||||||
);
|
);
|
||||||
state.isLoading = false;
|
state.isLoading = false;
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
import type { RootState } from 'mastodon/store';
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */
|
||||||
|
// state.settings is not yet typed, so we disable some ESLint checks for those selectors
|
||||||
|
export const selectSettingsNotificationsShows = (state: RootState) =>
|
||||||
|
state.settings.getIn(['notifications', 'shows']).toJS() as Record<
|
||||||
|
string,
|
||||||
|
boolean
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const selectSettingsNotificationsExcludedTypes = (state: RootState) =>
|
||||||
|
Object.entries(selectSettingsNotificationsShows(state))
|
||||||
|
.filter(([_type, enabled]) => !enabled)
|
||||||
|
.map(([type, _enabled]) => type);
|
||||||
|
|
||||||
|
export const selectSettingsNotificationsQuickFilterShow = (state: RootState) =>
|
||||||
|
state.settings.getIn(['notifications', 'quickFilter', 'show']) as boolean;
|
||||||
|
|
||||||
|
export const selectSettingsNotificationsQuickFilterActive = (
|
||||||
|
state: RootState,
|
||||||
|
) => state.settings.getIn(['notifications', 'quickFilter', 'active']) as string;
|
||||||
|
|
||||||
|
export const selectSettingsNotificationsQuickFilterAdvanced = (
|
||||||
|
state: RootState,
|
||||||
|
) =>
|
||||||
|
state.settings.getIn(['notifications', 'quickFilter', 'advanced']) as boolean;
|
||||||
|
|
||||||
|
export const selectSettingsNotificationsShowUnread = (state: RootState) =>
|
||||||
|
state.settings.getIn(['notifications', 'showUnread']) as boolean;
|
||||||
|
|
||||||
|
export const selectNeedsNotificationPermission = (state: RootState) =>
|
||||||
|
(state.settings.getIn(['notifications', 'alerts']).includes(true) &&
|
||||||
|
state.notifications.get('browserSupport') &&
|
||||||
|
state.notifications.get('browserPermission') === 'default' &&
|
||||||
|
!state.settings.getIn([
|
||||||
|
'notifications',
|
||||||
|
'dismissPermissionBanner',
|
||||||
|
])) as boolean;
|
||||||
|
|
||||||
|
/* eslint-enable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */
|
Loading…
Reference in New Issue