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 { 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 type {
|
||||||
|
NotificationGroupJSON,
|
||||||
|
NotificationJSON,
|
||||||
|
} from 'mastodon/api_types/notifications';
|
||||||
import { allNotificationTypes } 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 type { NotificationGap } from 'mastodon/reducers/notifications_groups';
|
import type { NotificationGap } from 'mastodon/reducers/notifications_groups';
|
||||||
|
@ -24,7 +27,7 @@ function excludeAllTypesExcept(filter: string) {
|
||||||
|
|
||||||
function dispatchAssociatedRecords(
|
function dispatchAssociatedRecords(
|
||||||
dispatch: AppDispatch,
|
dispatch: AppDispatch,
|
||||||
notifications: NotificationGroupJSON[],
|
notifications: NotificationGroupJSON[] | NotificationJSON[],
|
||||||
) {
|
) {
|
||||||
const fetchedAccounts: ApiAccountJSON[] = [];
|
const fetchedAccounts: ApiAccountJSON[] = [];
|
||||||
const fetchedStatuses: ApiStatusJSON[] = [];
|
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(
|
export const setNotificationsFilter = createAppAsyncThunk(
|
||||||
'notifications/filter/set',
|
'notifications/filter/set',
|
||||||
({ filterType }: { filterType: string }, { dispatch }) => {
|
({ filterType }: { filterType: string }, { dispatch }) => {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
deleteAnnouncement,
|
deleteAnnouncement,
|
||||||
} from './announcements';
|
} from './announcements';
|
||||||
import { updateConversations } from './conversations';
|
import { updateConversations } from './conversations';
|
||||||
|
import { processNewNotificationForGroups } from './notification_groups';
|
||||||
import { updateNotifications, expandNotifications } from './notifications';
|
import { updateNotifications, expandNotifications } from './notifications';
|
||||||
import { updateStatus } from './statuses';
|
import { updateStatus } from './statuses';
|
||||||
import {
|
import {
|
||||||
|
@ -98,10 +99,16 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
|
||||||
case 'delete':
|
case 'delete':
|
||||||
dispatch(deleteFromTimelines(data.payload));
|
dispatch(deleteFromTimelines(data.payload));
|
||||||
break;
|
break;
|
||||||
case 'notification':
|
case 'notification': {
|
||||||
// @ts-expect-error
|
// @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;
|
break;
|
||||||
|
}
|
||||||
case 'conversation':
|
case 'conversation':
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
dispatch(updateConversations(JSON.parse(data.payload)));
|
dispatch(updateConversations(JSON.parse(data.payload)));
|
||||||
|
|
|
@ -39,6 +39,14 @@ export type NotificationType =
|
||||||
| 'admin.sign_up'
|
| 'admin.sign_up'
|
||||||
| 'admin.report';
|
| 'admin.report';
|
||||||
|
|
||||||
|
export interface BaseNotificationJSON {
|
||||||
|
id: string;
|
||||||
|
type: NotificationType;
|
||||||
|
created_at: string;
|
||||||
|
group_key: string;
|
||||||
|
account: ApiAccountJSON;
|
||||||
|
}
|
||||||
|
|
||||||
export interface BaseNotificationGroupJSON {
|
export interface BaseNotificationGroupJSON {
|
||||||
group_key: string;
|
group_key: string;
|
||||||
notifications_count: number;
|
notifications_count: number;
|
||||||
|
@ -55,13 +63,28 @@ interface NotificationGroupWithStatusJSON extends BaseNotificationGroupJSON {
|
||||||
status: ApiStatusJSON;
|
status: ApiStatusJSON;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface NotificationWithStatusJSON extends BaseNotificationJSON {
|
||||||
|
type: NotificationWithStatusType;
|
||||||
|
status: ApiStatusJSON;
|
||||||
|
}
|
||||||
|
|
||||||
interface ReportNotificationGroupJSON extends BaseNotificationGroupJSON {
|
interface ReportNotificationGroupJSON extends BaseNotificationGroupJSON {
|
||||||
type: 'admin.report';
|
type: 'admin.report';
|
||||||
report: ApiReportJSON;
|
report: ApiReportJSON;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ReportNotificationJSON extends BaseNotificationJSON {
|
||||||
|
type: 'admin.report';
|
||||||
|
report: ApiReportJSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
type SimpleNotificationTypes = 'follow' | 'follow_request' | 'admin.sign_up';
|
||||||
interface SimpleNotificationGroupJSON extends BaseNotificationGroupJSON {
|
interface SimpleNotificationGroupJSON extends BaseNotificationGroupJSON {
|
||||||
type: 'follow' | 'follow_request' | 'admin.sign_up';
|
type: SimpleNotificationTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SimpleNotificationJSON extends BaseNotificationJSON {
|
||||||
|
type: SimpleNotificationTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiAccountWarningJSON {
|
export interface ApiAccountWarningJSON {
|
||||||
|
@ -80,6 +103,11 @@ interface ModerationWarningNotificationGroupJSON
|
||||||
moderation_warning: ApiAccountWarningJSON;
|
moderation_warning: ApiAccountWarningJSON;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ModerationWarningNotificationJSON extends BaseNotificationJSON {
|
||||||
|
type: 'moderation_warning';
|
||||||
|
moderation_warning: ApiAccountWarningJSON;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ApiAccountRelationshipSeveranceEventJSON {
|
export interface ApiAccountRelationshipSeveranceEventJSON {
|
||||||
id: string;
|
id: string;
|
||||||
type: 'account_suspension' | 'domain_block' | 'user_domain_block';
|
type: 'account_suspension' | 'domain_block' | 'user_domain_block';
|
||||||
|
@ -96,6 +124,19 @@ interface AccountRelationshipSeveranceNotificationGroupJSON
|
||||||
event: ApiAccountRelationshipSeveranceEventJSON;
|
event: ApiAccountRelationshipSeveranceEventJSON;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AccountRelationshipSeveranceNotificationJSON
|
||||||
|
extends BaseNotificationJSON {
|
||||||
|
type: 'severed_relationships';
|
||||||
|
event: ApiAccountRelationshipSeveranceEventJSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NotificationJSON =
|
||||||
|
| SimpleNotificationJSON
|
||||||
|
| ReportNotificationJSON
|
||||||
|
| AccountRelationshipSeveranceNotificationJSON
|
||||||
|
| NotificationWithStatusJSON
|
||||||
|
| ModerationWarningNotificationJSON;
|
||||||
|
|
||||||
export type NotificationGroupJSON =
|
export type NotificationGroupJSON =
|
||||||
| SimpleNotificationGroupJSON
|
| SimpleNotificationGroupJSON
|
||||||
| ReportNotificationGroupJSON
|
| ReportNotificationGroupJSON
|
||||||
|
|
|
@ -52,8 +52,14 @@ export const NotificationWithStatus: React.FC<{
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* @ts-expect-error -- <Status> is not yet typed */}
|
<Status
|
||||||
<Status id={statusId} contextType='notifications' withDismiss skipPrepend avatarSize={40} />
|
// @ts-expect-error -- <Status> is not yet typed
|
||||||
|
id={statusId}
|
||||||
|
contextType='notifications'
|
||||||
|
withDismiss
|
||||||
|
skipPrepend
|
||||||
|
avatarSize={40}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,6 +3,7 @@ import type {
|
||||||
ApiAccountWarningJSON,
|
ApiAccountWarningJSON,
|
||||||
BaseNotificationGroupJSON,
|
BaseNotificationGroupJSON,
|
||||||
NotificationGroupJSON,
|
NotificationGroupJSON,
|
||||||
|
NotificationJSON,
|
||||||
NotificationType,
|
NotificationType,
|
||||||
NotificationWithStatusType,
|
NotificationWithStatusType,
|
||||||
} from 'mastodon/api_types/notifications';
|
} 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 { notificationPolicyReducer } from './notification_policy';
|
||||||
import { notificationRequestsReducer } from './notification_requests';
|
import { notificationRequestsReducer } from './notification_requests';
|
||||||
import notifications from './notifications';
|
import notifications from './notifications';
|
||||||
import { notificationGroupsReducer } from './notifications_groups';
|
import { notificationsGroupsReducer } from './notifications_groups';
|
||||||
import { pictureInPictureReducer } from './picture_in_picture';
|
import { pictureInPictureReducer } from './picture_in_picture';
|
||||||
import polls from './polls';
|
import polls from './polls';
|
||||||
import push_notifications from './push_notifications';
|
import push_notifications from './push_notifications';
|
||||||
|
@ -66,7 +66,7 @@ const reducers = {
|
||||||
search,
|
search,
|
||||||
media_attachments,
|
media_attachments,
|
||||||
notifications,
|
notifications,
|
||||||
notificationsGroups: notificationGroupsReducer,
|
notificationsGroups: notificationsGroupsReducer,
|
||||||
height_cache,
|
height_cache,
|
||||||
custom_emojis,
|
custom_emojis,
|
||||||
lists,
|
lists,
|
||||||
|
|
|
@ -3,8 +3,12 @@ import { createReducer, isAnyOf } from '@reduxjs/toolkit';
|
||||||
import {
|
import {
|
||||||
fetchNotifications,
|
fetchNotifications,
|
||||||
fetchNotificationsGap,
|
fetchNotificationsGap,
|
||||||
|
processNewNotificationForGroups,
|
||||||
} from 'mastodon/actions/notification_groups';
|
} 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';
|
import type { NotificationGroup } from 'mastodon/models/notification_group';
|
||||||
|
|
||||||
export interface NotificationGap {
|
export interface NotificationGap {
|
||||||
|
@ -28,9 +32,8 @@ const initialState: NotificationGroupsState = {
|
||||||
readMarkerId: '0',
|
readMarkerId: '0',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const notificationGroupsReducer = createReducer<NotificationGroupsState>(
|
export const notificationsGroupsReducer =
|
||||||
initialState,
|
createReducer<NotificationGroupsState>(initialState, (builder) => {
|
||||||
(builder) => {
|
|
||||||
builder
|
builder
|
||||||
.addCase(fetchNotifications.fulfilled, (state, action) => {
|
.addCase(fetchNotifications.fulfilled, (state, action) => {
|
||||||
state.groups = action.payload.map((json) =>
|
state.groups = action.payload.map((json) =>
|
||||||
|
@ -69,6 +72,36 @@ export const notificationGroupsReducer = createReducer<NotificationGroupsState>(
|
||||||
|
|
||||||
state.isLoading = false;
|
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(
|
.addMatcher(
|
||||||
isAnyOf(fetchNotifications.pending, fetchNotificationsGap.pending),
|
isAnyOf(fetchNotifications.pending, fetchNotificationsGap.pending),
|
||||||
(state) => {
|
(state) => {
|
||||||
|
@ -81,5 +114,4 @@ export const notificationGroupsReducer = createReducer<NotificationGroupsState>(
|
||||||
state.isLoading = false;
|
state.isLoading = false;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ class Notification < ApplicationRecord
|
||||||
'Poll' => :poll,
|
'Poll' => :poll,
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
|
# Please update app/javascript/api_types/notification.ts if you change this
|
||||||
PROPERTIES = {
|
PROPERTIES = {
|
||||||
mention: {
|
mention: {
|
||||||
filterable: true,
|
filterable: true,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class REST::NotificationGroupSerializer < ActiveModel::Serializer
|
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
|
attributes :group_key, :notifications_count, :type, :most_recent_notification_id
|
||||||
|
|
||||||
attribute :page_min_id, if: :paginated?
|
attribute :page_min_id, if: :paginated?
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class REST::NotificationSerializer < ActiveModel::Serializer
|
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
|
attributes :id, :type, :created_at, :group_key
|
||||||
|
|
||||||
belongs_to :from_account, key: :account, serializer: REST::AccountSerializer
|
belongs_to :from_account, key: :account, serializer: REST::AccountSerializer
|
||||||
|
|
Loading…
Reference in New Issue