mirror of
https://github.com/mastodon/mastodon
synced 2024-11-27 14:28:24 +09:00
Add a new experimental notifications route
This commit is contained in:
parent
bb516ba1e7
commit
62088f51bd
@ -7,7 +7,7 @@ import { importFetchedAccounts, importFetchedStatuses } from './importer';
|
||||
|
||||
export const fetchNotifications = createDataLoadingThunk(
|
||||
'notificationGroups/fetch',
|
||||
apiFetchNotifications,
|
||||
() => apiFetchNotifications(),
|
||||
(notifications, { dispatch }) => {
|
||||
const fetchedAccounts: ApiAccountJSON[] = [];
|
||||
const fetchedStatuses: ApiStatusJSON[] = [];
|
||||
@ -21,8 +21,8 @@ export const fetchNotifications = createDataLoadingThunk(
|
||||
// fetchedAccounts.push(...notification.report.target_account);
|
||||
// }
|
||||
|
||||
if ('target_status' in notification) {
|
||||
fetchedStatuses.push(notification.target_status);
|
||||
if ('status' in notification) {
|
||||
fetchedStatuses.push(notification.status);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -178,7 +178,7 @@ const noOp = () => {};
|
||||
|
||||
let expandNotificationsController = new AbortController();
|
||||
|
||||
export function expandNotifications({ maxId, forceLoad } = {}, done = noOp) {
|
||||
export function expandNotifications({ maxId, forceLoad = false } = {}, done = noOp) {
|
||||
return (dispatch, getState) => {
|
||||
const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']);
|
||||
const notifications = getState().get('notifications');
|
||||
|
@ -33,7 +33,7 @@ export interface BaseNotificationGroupJSON {
|
||||
|
||||
interface NotificationGroupWithStatusJSON extends BaseNotificationGroupJSON {
|
||||
type: NotificationWithStatusType;
|
||||
target_status: ApiStatusJSON;
|
||||
status: ApiStatusJSON;
|
||||
}
|
||||
|
||||
interface ReportNotificationGroupJSON extends BaseNotificationGroupJSON {
|
||||
|
@ -0,0 +1,42 @@
|
||||
import type { NotificationGroup as NotificationGroupModel } from 'mastodon/models/notification_group';
|
||||
import { useAppSelector } from 'mastodon/store';
|
||||
|
||||
import { NotificationReblog } from './notification_reblog';
|
||||
|
||||
export const NotificationGroup: React.FC<{
|
||||
notificationGroupId: NotificationGroupModel['group_key'];
|
||||
unread: boolean;
|
||||
onMoveUp: unknown;
|
||||
onMoveDown: unknown;
|
||||
}> = ({ notificationGroupId }) => {
|
||||
const notificationGroup = useAppSelector((state) =>
|
||||
state.notificationsGroups.groups.find(
|
||||
(item) => item.type !== 'gap' && item.group_key === notificationGroupId,
|
||||
),
|
||||
);
|
||||
|
||||
if (!notificationGroup || notificationGroup.type === 'gap') return null;
|
||||
|
||||
switch (notificationGroup.type) {
|
||||
case 'reblog':
|
||||
return <NotificationReblog notification={notificationGroup} />;
|
||||
case 'follow':
|
||||
case 'follow_request':
|
||||
case 'favourite':
|
||||
case 'mention':
|
||||
case 'poll':
|
||||
case 'status':
|
||||
case 'update':
|
||||
case 'admin.sign_up':
|
||||
case 'admin.report':
|
||||
case 'moderation_warning':
|
||||
case 'severed_relationships':
|
||||
default:
|
||||
return (
|
||||
<div>
|
||||
<pre>{JSON.stringify(notificationGroup, undefined, 2)}</pre>
|
||||
<hr />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
@ -0,0 +1,7 @@
|
||||
import type { NotificationGroupReblog } from 'mastodon/models/notification_group';
|
||||
|
||||
export const NotificationReblog: React.FC<{
|
||||
notification: NotificationGroupReblog;
|
||||
}> = ({ notification }) => {
|
||||
return <div>reblog {notification.group_key}</div>;
|
||||
};
|
397
app/javascript/mastodon/features/notifications_v2/index.tsx
Normal file
397
app/javascript/mastodon/features/notifications_v2/index.tsx
Normal file
@ -0,0 +1,397 @@
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import type { Map as ImmutableMap } from 'immutable';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
import DoneAllIcon from '@/material-icons/400-24px/done_all.svg?react';
|
||||
import NotificationsIcon from '@/material-icons/400-24px/notifications-fill.svg?react';
|
||||
import { fetchNotifications } from 'mastodon/actions/notification_groups';
|
||||
import { compareId } from 'mastodon/compare_id';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator';
|
||||
import { useIdentity } from 'mastodon/identity_context';
|
||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
import type { RootState } from 'mastodon/store';
|
||||
|
||||
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
||||
import { submitMarkers } from '../../actions/markers';
|
||||
import {
|
||||
expandNotifications,
|
||||
scrollTopNotifications,
|
||||
loadPending,
|
||||
mountNotifications,
|
||||
unmountNotifications,
|
||||
markNotificationsAsRead,
|
||||
} from '../../actions/notifications';
|
||||
import Column from '../../components/column';
|
||||
import ColumnHeader from '../../components/column_header';
|
||||
import { LoadGap } from '../../components/load_gap';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
import { FilteredNotificationsBanner } from '../notifications/components/filtered_notifications_banner';
|
||||
import NotificationsPermissionBanner from '../notifications/components/notifications_permission_banner';
|
||||
import ColumnSettingsContainer from '../notifications/containers/column_settings_container';
|
||||
import FilterBarContainer from '../notifications/containers/filter_bar_container';
|
||||
|
||||
import { NotificationGroup } from './components/notification_group';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.notifications', defaultMessage: 'Notifications' },
|
||||
markAsRead: {
|
||||
id: 'notifications.mark_as_read',
|
||||
defaultMessage: 'Mark every notification as read',
|
||||
},
|
||||
});
|
||||
|
||||
/* 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(
|
||||
[
|
||||
selectSettingsNotificationsQuickFilterShow,
|
||||
selectSettingsNotificationsQuickFilterActive,
|
||||
getExcludedTypes,
|
||||
(state: RootState) => state.notificationsGroups.groups,
|
||||
],
|
||||
(showFilterBar, allowedType, excludedTypes, notifications) => {
|
||||
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.filter(
|
||||
(item) => item.type !== 'gap' || !excludedTypes.includes(item.type),
|
||||
);
|
||||
}
|
||||
return notifications.filter(
|
||||
(item) => item.type !== 'gap' || allowedType === item.type,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// const mapStateToProps = (state) => ({
|
||||
// isUnread:
|
||||
// state.getIn(['notifications', 'unread']) > 0 ||
|
||||
// state.getIn(['notifications', 'pendingItems']).size > 0,
|
||||
// numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList())
|
||||
// .size,
|
||||
// canMarkAsRead:
|
||||
// state.getIn(['settings', 'notifications', 'showUnread']) &&
|
||||
// state.getIn(['notifications', 'readMarkerId']) !== '0' &&
|
||||
// getNotifications(state).some(
|
||||
// (item) =>
|
||||
// item !== null &&
|
||||
// compareId(
|
||||
// item.get('id'),
|
||||
// state.getIn(['notifications', 'readMarkerId']),
|
||||
// ) > 0,
|
||||
// ),
|
||||
// });
|
||||
|
||||
export const Notifications: React.FC<{
|
||||
columnId?: string;
|
||||
isUnread?: boolean;
|
||||
multiColumn?: boolean;
|
||||
numPending: number;
|
||||
}> = ({ isUnread, columnId, multiColumn, numPending }) => {
|
||||
const intl = useIntl();
|
||||
const notifications = useAppSelector(getNotifications);
|
||||
const dispatch = useAppDispatch();
|
||||
const isLoading = useAppSelector((s) => s.notificationsGroups.isLoading);
|
||||
const hasMore = useAppSelector((s) => s.notificationsGroups.hasMore);
|
||||
const readMarkerId = useAppSelector(
|
||||
(s) => s.notificationsGroups.readMarkerId,
|
||||
);
|
||||
const lastReadId = useAppSelector((s) =>
|
||||
selectSettingsNotificationsShowUnread(s)
|
||||
? s.notificationsGroups.readMarkerId
|
||||
: '0',
|
||||
);
|
||||
const canMarkAsRead = useAppSelector(
|
||||
(s) =>
|
||||
selectSettingsNotificationsShowUnread(s) &&
|
||||
s.notificationsGroups.readMarkerId !== '0' &&
|
||||
notifications.some(
|
||||
(item) =>
|
||||
item.type !== 'gap' && compareId(item.group_key, readMarkerId) > 0,
|
||||
),
|
||||
);
|
||||
const needsNotificationPermission = useAppSelector(
|
||||
selectNeedsNotificationPermission,
|
||||
);
|
||||
|
||||
const columnRef = useRef<Column>(null);
|
||||
|
||||
const selectChild = useCallback((index: number, alignTop: boolean) => {
|
||||
const container = columnRef.current?.node as HTMLElement | undefined;
|
||||
|
||||
if (!container) return;
|
||||
|
||||
const element = container.querySelector<HTMLElement>(
|
||||
`article:nth-of-type(${index + 1}) .focusable`,
|
||||
);
|
||||
|
||||
if (element) {
|
||||
if (alignTop && container.scrollTop > element.offsetTop) {
|
||||
element.scrollIntoView(true);
|
||||
} else if (
|
||||
!alignTop &&
|
||||
container.scrollTop + container.clientHeight <
|
||||
element.offsetTop + element.offsetHeight
|
||||
) {
|
||||
element.scrollIntoView(false);
|
||||
}
|
||||
element.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(mountNotifications());
|
||||
|
||||
// FIXME: remove once this becomes the main implementation
|
||||
void dispatch(fetchNotifications());
|
||||
|
||||
return () => {
|
||||
dispatch(unmountNotifications());
|
||||
dispatch(scrollTopNotifications(false));
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
const handleLoadGap = useCallback(
|
||||
(maxId: string) => {
|
||||
dispatch(expandNotifications({ maxId }));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
// TODO: fix this, probably incorrect
|
||||
const handleLoadOlder = useDebouncedCallback(
|
||||
() => {
|
||||
const last = notifications[notifications.length - 1];
|
||||
if (last && last.type !== 'gap')
|
||||
dispatch(expandNotifications({ maxId: last.group_key }));
|
||||
},
|
||||
300,
|
||||
{ leading: true },
|
||||
);
|
||||
|
||||
const handleLoadPending = useCallback(() => {
|
||||
dispatch(loadPending());
|
||||
}, [dispatch]);
|
||||
|
||||
const handleScrollToTop = useDebouncedCallback(() => {
|
||||
dispatch(scrollTopNotifications(true));
|
||||
}, 100);
|
||||
|
||||
const handleScroll = useDebouncedCallback(() => {
|
||||
dispatch(scrollTopNotifications(false));
|
||||
}, 100);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
handleLoadOlder.cancel();
|
||||
handleScrollToTop.cancel();
|
||||
handleScroll.cancel();
|
||||
};
|
||||
}, [handleLoadOlder, handleScrollToTop, handleScroll]);
|
||||
|
||||
const handlePin = useCallback(() => {
|
||||
if (columnId) {
|
||||
dispatch(removeColumn(columnId));
|
||||
} else {
|
||||
dispatch(addColumn('NOTIFICATIONS', {}));
|
||||
}
|
||||
}, [columnId, dispatch]);
|
||||
|
||||
const handleMove = useCallback(
|
||||
(dir: unknown) => {
|
||||
dispatch(moveColumn(columnId, dir));
|
||||
},
|
||||
[dispatch, columnId],
|
||||
);
|
||||
|
||||
const handleHeaderClick = useCallback(() => {
|
||||
columnRef.current?.scrollTop();
|
||||
}, []);
|
||||
|
||||
const handleMoveUp = useCallback(
|
||||
(id: string) => {
|
||||
const elementIndex =
|
||||
notifications.findIndex(
|
||||
(item) => item.type !== 'gap' && item.group_key === id,
|
||||
) - 1;
|
||||
selectChild(elementIndex, true);
|
||||
},
|
||||
[notifications, selectChild],
|
||||
);
|
||||
|
||||
const handleMoveDown = useCallback(
|
||||
(id: string) => {
|
||||
const elementIndex =
|
||||
notifications.findIndex(
|
||||
(item) => item.type !== 'gap' && item.group_key === id,
|
||||
) + 1;
|
||||
selectChild(elementIndex, false);
|
||||
},
|
||||
[notifications, selectChild],
|
||||
);
|
||||
|
||||
const handleMarkAsRead = useCallback(() => {
|
||||
dispatch(markNotificationsAsRead());
|
||||
void dispatch(submitMarkers({ immediate: true }));
|
||||
}, [dispatch]);
|
||||
|
||||
const pinned = !!columnId;
|
||||
const emptyMessage = (
|
||||
<FormattedMessage
|
||||
id='empty_column.notifications'
|
||||
defaultMessage="You don't have any notifications yet. When other people interact with you, you will see it here."
|
||||
/>
|
||||
);
|
||||
|
||||
const { signedIn } = useIdentity();
|
||||
|
||||
const filterBarContainer = signedIn ? <FilterBarContainer /> : null;
|
||||
|
||||
const scrollableContent = useMemo(() => {
|
||||
if (notifications.length === 0 && !hasMore) return null;
|
||||
|
||||
return notifications.map((item) =>
|
||||
item.type === 'gap' ? (
|
||||
<LoadGap
|
||||
key={item.id}
|
||||
disabled={isLoading}
|
||||
maxId={item.maxId}
|
||||
onClick={handleLoadGap}
|
||||
/>
|
||||
) : (
|
||||
<NotificationGroup
|
||||
key={item.group_key}
|
||||
notificationGroupId={item.group_key}
|
||||
onMoveUp={handleMoveUp}
|
||||
onMoveDown={handleMoveDown}
|
||||
unread={
|
||||
lastReadId !== '0' && compareId(item.group_key, lastReadId) > 0
|
||||
}
|
||||
/>
|
||||
),
|
||||
);
|
||||
}, [
|
||||
notifications,
|
||||
isLoading,
|
||||
hasMore,
|
||||
lastReadId,
|
||||
handleLoadGap,
|
||||
handleMoveUp,
|
||||
handleMoveDown,
|
||||
]);
|
||||
|
||||
const scrollContainer = signedIn ? (
|
||||
<ScrollableList
|
||||
scrollKey={`notifications-${columnId}`}
|
||||
trackScroll={!pinned}
|
||||
isLoading={isLoading}
|
||||
showLoading={isLoading && notifications.length === 0}
|
||||
hasMore={hasMore}
|
||||
numPending={numPending}
|
||||
prepend={needsNotificationPermission && <NotificationsPermissionBanner />}
|
||||
alwaysPrepend
|
||||
emptyMessage={emptyMessage}
|
||||
onLoadMore={handleLoadOlder}
|
||||
onLoadPending={handleLoadPending}
|
||||
onScrollToTop={handleScrollToTop}
|
||||
onScroll={handleScroll}
|
||||
bindToDocument={!multiColumn}
|
||||
>
|
||||
{scrollableContent}
|
||||
</ScrollableList>
|
||||
) : (
|
||||
<NotSignedInIndicator />
|
||||
);
|
||||
|
||||
const extraButton = canMarkAsRead ? (
|
||||
<button
|
||||
aria-label={intl.formatMessage(messages.markAsRead)}
|
||||
title={intl.formatMessage(messages.markAsRead)}
|
||||
onClick={handleMarkAsRead}
|
||||
className='column-header__button'
|
||||
>
|
||||
<Icon id='done-all' icon={DoneAllIcon} />
|
||||
</button>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<Column
|
||||
bindToDocument={!multiColumn}
|
||||
ref={columnRef}
|
||||
label={intl.formatMessage(messages.title)}
|
||||
>
|
||||
{/* @ts-expect-error This component is not yet Typescript */}
|
||||
<ColumnHeader
|
||||
icon='bell'
|
||||
iconComponent={NotificationsIcon}
|
||||
active={isUnread}
|
||||
title={intl.formatMessage(messages.title)}
|
||||
onPin={handlePin}
|
||||
onMove={handleMove}
|
||||
onClick={handleHeaderClick}
|
||||
pinned={pinned}
|
||||
multiColumn={multiColumn}
|
||||
extraButton={extraButton}
|
||||
>
|
||||
<ColumnSettingsContainer />
|
||||
</ColumnHeader>
|
||||
|
||||
{filterBarContainer}
|
||||
|
||||
<FilteredNotificationsBanner />
|
||||
|
||||
{scrollContainer}
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.title)}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default Notifications;
|
@ -49,6 +49,7 @@ import {
|
||||
DirectTimeline,
|
||||
HashtagTimeline,
|
||||
Notifications,
|
||||
Notifications_v2,
|
||||
NotificationRequests,
|
||||
NotificationRequest,
|
||||
FollowRequests,
|
||||
@ -203,6 +204,7 @@ class SwitchingColumnsArea extends PureComponent {
|
||||
<WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
|
||||
<WrappedRoute path='/lists/:id' component={ListTimeline} content={children} />
|
||||
<WrappedRoute path='/notifications' component={Notifications} content={children} exact />
|
||||
<WrappedRoute path='/notifications_v2' component={Notifications_v2} content={children} exact />
|
||||
<WrappedRoute path='/notifications/requests' component={NotificationRequests} content={children} exact />
|
||||
<WrappedRoute path='/notifications/requests/:id' component={NotificationRequest} content={children} exact />
|
||||
<WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} />
|
||||
|
@ -10,6 +10,10 @@ export function Notifications () {
|
||||
return import(/* webpackChunkName: "features/notifications" */'../../notifications');
|
||||
}
|
||||
|
||||
export function Notifications_v2 () {
|
||||
return import(/* webpackChunkName: "features/notifications_v2" */'../../notifications_v2');
|
||||
}
|
||||
|
||||
export function HomeTimeline () {
|
||||
return import(/* webpackChunkName: "features/home_timeline" */'../../home_timeline');
|
||||
}
|
||||
|
@ -4,14 +4,16 @@ import type {
|
||||
NotificationType,
|
||||
NotificationWithStatusType,
|
||||
} from 'mastodon/api_types/notifications';
|
||||
import type { ApiStatusJSON } from 'mastodon/api_types/statuses';
|
||||
|
||||
type BaseNotificationGroup = BaseNotificationGroupJSON;
|
||||
interface BaseNotificationGroup
|
||||
extends Omit<BaseNotificationGroupJSON, 'sample_accounts'> {
|
||||
sampleAccountsIds: string[];
|
||||
}
|
||||
|
||||
interface BaseNotificationWithStatus<Type extends NotificationWithStatusType>
|
||||
extends BaseNotificationGroup {
|
||||
type: Type;
|
||||
status: ApiStatusJSON;
|
||||
statusId: string;
|
||||
}
|
||||
|
||||
interface BaseNotification<Type extends NotificationType>
|
||||
@ -54,6 +56,20 @@ export type NotificationGroup =
|
||||
export function createNotificationGroupFromJSON(
|
||||
groupJson: NotificationGroupJSON,
|
||||
): NotificationGroup {
|
||||
// @ts-expect-error -- FIXME: properly convert the special notifications here
|
||||
return groupJson;
|
||||
const { sample_accounts, ...group } = groupJson;
|
||||
const sampleAccountsIds = sample_accounts.map((account) => account.id);
|
||||
|
||||
if ('status' in group) {
|
||||
const { status, ...groupWithoutStatus } = group;
|
||||
return {
|
||||
statusId: status.id,
|
||||
sampleAccountsIds,
|
||||
...groupWithoutStatus,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
sampleAccountsIds,
|
||||
...group,
|
||||
};
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ import { modalReducer } from './modal';
|
||||
import { notificationPolicyReducer } from './notification_policy';
|
||||
import { notificationRequestsReducer } from './notification_requests';
|
||||
import notifications from './notifications';
|
||||
import { notificationGroupsReducer } from './notifications_groups';
|
||||
import { pictureInPictureReducer } from './picture_in_picture';
|
||||
import polls from './polls';
|
||||
import push_notifications from './push_notifications';
|
||||
@ -65,6 +66,7 @@ const reducers = {
|
||||
search,
|
||||
media_attachments,
|
||||
notifications,
|
||||
notificationsGroups: notificationGroupsReducer,
|
||||
height_cache,
|
||||
custom_emojis,
|
||||
lists,
|
||||
|
@ -4,23 +4,44 @@ import { fetchNotifications } from 'mastodon/actions/notification_groups';
|
||||
import { createNotificationGroupFromJSON } from 'mastodon/models/notification_group';
|
||||
import type { NotificationGroup } from 'mastodon/models/notification_group';
|
||||
|
||||
interface Gap {
|
||||
type: 'gap';
|
||||
id: string;
|
||||
maxId: string;
|
||||
}
|
||||
|
||||
interface NotificationGroupsState {
|
||||
groups: NotificationGroup[];
|
||||
groups: (NotificationGroup | Gap)[];
|
||||
unread: number;
|
||||
isLoading: boolean;
|
||||
hasMore: boolean;
|
||||
readMarkerId: string;
|
||||
}
|
||||
|
||||
const initialState: NotificationGroupsState = {
|
||||
groups: [],
|
||||
unread: 0,
|
||||
isLoading: false,
|
||||
hasMore: false,
|
||||
readMarkerId: '0',
|
||||
};
|
||||
|
||||
export const notificationGroupsReducer = createReducer(
|
||||
export const notificationGroupsReducer = createReducer<NotificationGroupsState>(
|
||||
initialState,
|
||||
(builder) => {
|
||||
builder.addCase(fetchNotifications.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
});
|
||||
|
||||
builder.addCase(fetchNotifications.fulfilled, (state, action) => {
|
||||
state.groups = action.payload.map((json) =>
|
||||
createNotificationGroupFromJSON(json),
|
||||
);
|
||||
state.isLoading = false;
|
||||
});
|
||||
|
||||
builder.addCase(fetchNotifications.rejected, (state) => {
|
||||
state.isLoading = false;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
@ -28,6 +28,7 @@ Rails.application.routes.draw do
|
||||
/conversations
|
||||
/lists/(*any)
|
||||
/notifications/(*any)
|
||||
/notifications_v2/(*any)
|
||||
/favourites
|
||||
/bookmarks
|
||||
/pinned
|
||||
|
@ -123,6 +123,7 @@
|
||||
"tesseract.js": "^2.1.5",
|
||||
"tiny-queue": "^0.2.1",
|
||||
"twitter-text": "3.1.0",
|
||||
"use-debounce": "^10.0.0",
|
||||
"webpack": "^4.47.0",
|
||||
"webpack-assets-manifest": "^4.0.6",
|
||||
"webpack-bundle-analyzer": "^4.8.0",
|
||||
|
10
yarn.lock
10
yarn.lock
@ -2887,6 +2887,7 @@ __metadata:
|
||||
tiny-queue: "npm:^0.2.1"
|
||||
twitter-text: "npm:3.1.0"
|
||||
typescript: "npm:^5.0.4"
|
||||
use-debounce: "npm:^10.0.0"
|
||||
webpack: "npm:^4.47.0"
|
||||
webpack-assets-manifest: "npm:^4.0.6"
|
||||
webpack-bundle-analyzer: "npm:^4.8.0"
|
||||
@ -17385,6 +17386,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"use-debounce@npm:^10.0.0":
|
||||
version: 10.0.0
|
||||
resolution: "use-debounce@npm:10.0.0"
|
||||
peerDependencies:
|
||||
react: ">=16.8.0"
|
||||
checksum: 10c0/c1166cba52dedeab17e3e29275af89c57a3e8981b75f6e38ae2896ac36ecd4ed7d8fff5f882ba4b2f91eac7510d5ae0dd89fa4f7d081622ed436c3c89eda5cd1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"use-isomorphic-layout-effect@npm:^1.1.1, use-isomorphic-layout-effect@npm:^1.1.2":
|
||||
version: 1.1.2
|
||||
resolution: "use-isomorphic-layout-effect@npm:1.1.2"
|
||||
|
Loading…
Reference in New Issue
Block a user