mirror of https://github.com/mastodon/mastodon.git
Support streaming notifications
parent
006ef1081e
commit
a885a2e824
|
@ -1,6 +1,9 @@
|
|||
import { apiFetchNotifications } from 'mastodon/api/notifications';
|
||||
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
|
||||
import type { NotificationGroupJSON } from 'mastodon/api_types/notifications';
|
||||
import type {
|
||||
NotificationGroupJSON,
|
||||
NotificationJSON,
|
||||
} from 'mastodon/api_types/notifications';
|
||||
import { allNotificationTypes } from 'mastodon/api_types/notifications';
|
||||
import type { ApiStatusJSON } from 'mastodon/api_types/statuses';
|
||||
import type { NotificationGap } from 'mastodon/reducers/notifications_groups';
|
||||
|
@ -24,7 +27,7 @@ function excludeAllTypesExcept(filter: string) {
|
|||
|
||||
function dispatchAssociatedRecords(
|
||||
dispatch: AppDispatch,
|
||||
notifications: NotificationGroupJSON[],
|
||||
notifications: NotificationGroupJSON[] | NotificationJSON[],
|
||||
) {
|
||||
const fetchedAccounts: ApiAccountJSON[] = [];
|
||||
const fetchedStatuses: ApiStatusJSON[] = [];
|
||||
|
@ -97,6 +100,15 @@ export const fetchNotificationsGap = createDataLoadingThunk(
|
|||
},
|
||||
);
|
||||
|
||||
export const processNewNotificationForGroups = createAppAsyncThunk(
|
||||
'notificationsGroups/processNew',
|
||||
(notification: NotificationJSON, { dispatch }) => {
|
||||
dispatchAssociatedRecords(dispatch, [notification]);
|
||||
|
||||
return notification;
|
||||
},
|
||||
);
|
||||
|
||||
export const setNotificationsFilter = createAppAsyncThunk(
|
||||
'notifications/filter/set',
|
||||
({ filterType }: { filterType: string }, { dispatch }) => {
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
deleteAnnouncement,
|
||||
} from './announcements';
|
||||
import { updateConversations } from './conversations';
|
||||
import { processNewNotificationForGroups } from './notification_groups';
|
||||
import { updateNotifications, expandNotifications } from './notifications';
|
||||
import { updateStatus } from './statuses';
|
||||
import {
|
||||
|
@ -98,10 +99,16 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
|
|||
case 'delete':
|
||||
dispatch(deleteFromTimelines(data.payload));
|
||||
break;
|
||||
case 'notification':
|
||||
case 'notification': {
|
||||
// @ts-expect-error
|
||||
dispatch(updateNotifications(JSON.parse(data.payload), messages, locale));
|
||||
const notificationJSON = JSON.parse(data.payload);
|
||||
dispatch(updateNotifications(notificationJSON, messages, locale));
|
||||
// TODO: remove this once the groups feature replaces the previous one
|
||||
if(getState().notificationsGroups.groups.length > 0) {
|
||||
dispatch(processNewNotificationForGroups(notificationJSON));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'conversation':
|
||||
// @ts-expect-error
|
||||
dispatch(updateConversations(JSON.parse(data.payload)));
|
||||
|
|
|
@ -39,6 +39,14 @@ export type NotificationType =
|
|||
| 'admin.sign_up'
|
||||
| 'admin.report';
|
||||
|
||||
export interface BaseNotificationJSON {
|
||||
id: string;
|
||||
type: NotificationType;
|
||||
created_at: string;
|
||||
group_key: string;
|
||||
account: ApiAccountJSON;
|
||||
}
|
||||
|
||||
export interface BaseNotificationGroupJSON {
|
||||
group_key: string;
|
||||
notifications_count: number;
|
||||
|
@ -55,13 +63,28 @@ interface NotificationGroupWithStatusJSON extends BaseNotificationGroupJSON {
|
|||
status: ApiStatusJSON;
|
||||
}
|
||||
|
||||
interface NotificationWithStatusJSON extends BaseNotificationJSON {
|
||||
type: NotificationWithStatusType;
|
||||
status: ApiStatusJSON;
|
||||
}
|
||||
|
||||
interface ReportNotificationGroupJSON extends BaseNotificationGroupJSON {
|
||||
type: 'admin.report';
|
||||
report: ApiReportJSON;
|
||||
}
|
||||
|
||||
interface ReportNotificationJSON extends BaseNotificationJSON {
|
||||
type: 'admin.report';
|
||||
report: ApiReportJSON;
|
||||
}
|
||||
|
||||
type SimpleNotificationTypes = 'follow' | 'follow_request' | 'admin.sign_up';
|
||||
interface SimpleNotificationGroupJSON extends BaseNotificationGroupJSON {
|
||||
type: 'follow' | 'follow_request' | 'admin.sign_up';
|
||||
type: SimpleNotificationTypes;
|
||||
}
|
||||
|
||||
interface SimpleNotificationJSON extends BaseNotificationJSON {
|
||||
type: SimpleNotificationTypes;
|
||||
}
|
||||
|
||||
export interface ApiAccountWarningJSON {
|
||||
|
@ -80,6 +103,11 @@ interface ModerationWarningNotificationGroupJSON
|
|||
moderation_warning: ApiAccountWarningJSON;
|
||||
}
|
||||
|
||||
interface ModerationWarningNotificationJSON extends BaseNotificationJSON {
|
||||
type: 'moderation_warning';
|
||||
moderation_warning: ApiAccountWarningJSON;
|
||||
}
|
||||
|
||||
export interface ApiAccountRelationshipSeveranceEventJSON {
|
||||
id: string;
|
||||
type: 'account_suspension' | 'domain_block' | 'user_domain_block';
|
||||
|
@ -96,6 +124,19 @@ interface AccountRelationshipSeveranceNotificationGroupJSON
|
|||
event: ApiAccountRelationshipSeveranceEventJSON;
|
||||
}
|
||||
|
||||
interface AccountRelationshipSeveranceNotificationJSON
|
||||
extends BaseNotificationJSON {
|
||||
type: 'severed_relationships';
|
||||
event: ApiAccountRelationshipSeveranceEventJSON;
|
||||
}
|
||||
|
||||
export type NotificationJSON =
|
||||
| SimpleNotificationJSON
|
||||
| ReportNotificationJSON
|
||||
| AccountRelationshipSeveranceNotificationJSON
|
||||
| NotificationWithStatusJSON
|
||||
| ModerationWarningNotificationJSON;
|
||||
|
||||
export type NotificationGroupJSON =
|
||||
| SimpleNotificationGroupJSON
|
||||
| ReportNotificationGroupJSON
|
||||
|
|
|
@ -52,8 +52,14 @@ export const NotificationWithStatus: React.FC<{
|
|||
{label}
|
||||
</div>
|
||||
|
||||
{/* @ts-expect-error -- <Status> is not yet typed */}
|
||||
<Status id={statusId} contextType='notifications' withDismiss skipPrepend avatarSize={40} />
|
||||
<Status
|
||||
// @ts-expect-error -- <Status> is not yet typed
|
||||
id={statusId}
|
||||
contextType='notifications'
|
||||
withDismiss
|
||||
skipPrepend
|
||||
avatarSize={40}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -3,6 +3,7 @@ import type {
|
|||
ApiAccountWarningJSON,
|
||||
BaseNotificationGroupJSON,
|
||||
NotificationGroupJSON,
|
||||
NotificationJSON,
|
||||
NotificationType,
|
||||
NotificationWithStatusType,
|
||||
} from 'mastodon/api_types/notifications';
|
||||
|
@ -157,3 +158,46 @@ export function createNotificationGroupFromJSON(
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function createNotificationGroupFromNotificationJSON(
|
||||
notification: NotificationJSON,
|
||||
) {
|
||||
const group = {
|
||||
sampleAccountsIds: [notification.account.id],
|
||||
group_key: notification.group_key,
|
||||
notifications_count: 1,
|
||||
type: notification.type,
|
||||
most_recent_notification_id: notification.id,
|
||||
page_min_id: notification.id,
|
||||
page_max_id: notification.id,
|
||||
latest_page_notification_at: notification.created_at,
|
||||
} as NotificationGroup;
|
||||
|
||||
switch (notification.type) {
|
||||
case 'favourite':
|
||||
case 'reblog':
|
||||
case 'status':
|
||||
case 'mention':
|
||||
case 'poll':
|
||||
case 'update':
|
||||
return { ...group, statusId: notification.status.id };
|
||||
case 'admin.report':
|
||||
return { ...group, report: createReportFromJSON(notification.report) };
|
||||
case 'severed_relationships':
|
||||
return {
|
||||
...group,
|
||||
event: createAccountRelationshipSeveranceEventFromJSON(
|
||||
notification.event,
|
||||
),
|
||||
};
|
||||
case 'moderation_warning':
|
||||
return {
|
||||
...group,
|
||||
moderationWarning: createAccountWarningFromJSON(
|
||||
notification.moderation_warning,
|
||||
),
|
||||
};
|
||||
default:
|
||||
return group;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +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 { notificationsGroupsReducer } from './notifications_groups';
|
||||
import { pictureInPictureReducer } from './picture_in_picture';
|
||||
import polls from './polls';
|
||||
import push_notifications from './push_notifications';
|
||||
|
@ -66,7 +66,7 @@ const reducers = {
|
|||
search,
|
||||
media_attachments,
|
||||
notifications,
|
||||
notificationsGroups: notificationGroupsReducer,
|
||||
notificationsGroups: notificationsGroupsReducer,
|
||||
height_cache,
|
||||
custom_emojis,
|
||||
lists,
|
||||
|
|
|
@ -3,8 +3,12 @@ import { createReducer, isAnyOf } from '@reduxjs/toolkit';
|
|||
import {
|
||||
fetchNotifications,
|
||||
fetchNotificationsGap,
|
||||
processNewNotificationForGroups,
|
||||
} from 'mastodon/actions/notification_groups';
|
||||
import { createNotificationGroupFromJSON } from 'mastodon/models/notification_group';
|
||||
import {
|
||||
createNotificationGroupFromJSON,
|
||||
createNotificationGroupFromNotificationJSON,
|
||||
} from 'mastodon/models/notification_group';
|
||||
import type { NotificationGroup } from 'mastodon/models/notification_group';
|
||||
|
||||
export interface NotificationGap {
|
||||
|
@ -28,9 +32,8 @@ const initialState: NotificationGroupsState = {
|
|||
readMarkerId: '0',
|
||||
};
|
||||
|
||||
export const notificationGroupsReducer = createReducer<NotificationGroupsState>(
|
||||
initialState,
|
||||
(builder) => {
|
||||
export const notificationsGroupsReducer =
|
||||
createReducer<NotificationGroupsState>(initialState, (builder) => {
|
||||
builder
|
||||
.addCase(fetchNotifications.fulfilled, (state, action) => {
|
||||
state.groups = action.payload.map((json) =>
|
||||
|
@ -69,6 +72,36 @@ export const notificationGroupsReducer = createReducer<NotificationGroupsState>(
|
|||
|
||||
state.isLoading = false;
|
||||
})
|
||||
.addCase(processNewNotificationForGroups.fulfilled, (state, action) => {
|
||||
const notification = action.payload;
|
||||
const existingGroupIndex = state.groups.findIndex(
|
||||
(group) =>
|
||||
group.type !== 'gap' && group.group_key === notification.group_key,
|
||||
);
|
||||
|
||||
if (existingGroupIndex > -1) {
|
||||
const existingGroup = state.groups[existingGroupIndex];
|
||||
|
||||
if (existingGroup && existingGroup.type !== 'gap') {
|
||||
// Update the existing group
|
||||
existingGroup.sampleAccountsIds.unshift(notification.account.id);
|
||||
existingGroup.sampleAccountsIds.pop();
|
||||
|
||||
existingGroup.most_recent_notification_id = notification.id;
|
||||
existingGroup.page_max_id = notification.id;
|
||||
existingGroup.latest_page_notification_at = notification.created_at;
|
||||
existingGroup.notifications_count += 1;
|
||||
|
||||
state.groups.splice(existingGroupIndex, 1);
|
||||
state.groups.unshift(existingGroup);
|
||||
}
|
||||
} else {
|
||||
// Create a new group
|
||||
state.groups.unshift(
|
||||
createNotificationGroupFromNotificationJSON(notification),
|
||||
);
|
||||
}
|
||||
})
|
||||
.addMatcher(
|
||||
isAnyOf(fetchNotifications.pending, fetchNotificationsGap.pending),
|
||||
(state) => {
|
||||
|
@ -81,5 +114,4 @@ export const notificationGroupsReducer = createReducer<NotificationGroupsState>(
|
|||
state.isLoading = false;
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
@ -30,6 +30,7 @@ class Notification < ApplicationRecord
|
|||
'Poll' => :poll,
|
||||
}.freeze
|
||||
|
||||
# Please update app/javascript/api_types/notification.ts if you change this
|
||||
PROPERTIES = {
|
||||
mention: {
|
||||
filterable: true,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class REST::NotificationGroupSerializer < ActiveModel::Serializer
|
||||
# Please update app/javascript/api_types/notification.ts when making changes to the attributes
|
||||
attributes :group_key, :notifications_count, :type, :most_recent_notification_id
|
||||
|
||||
attribute :page_min_id, if: :paginated?
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class REST::NotificationSerializer < ActiveModel::Serializer
|
||||
# Please update app/javascript/api_types/notification.ts when making changes to the attributes
|
||||
attributes :id, :type, :created_at, :group_key
|
||||
|
||||
belongs_to :from_account, key: :account, serializer: REST::AccountSerializer
|
||||
|
|
Loading…
Reference in New Issue