Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enhance: 通知の個別削除 #13399

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/backend/src/core/GlobalEventService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { MiChannel } from '@/models/Channel.js';
import type { MiUser } from '@/models/User.js';
import type { MiUserProfile } from '@/models/UserProfile.js';
import type { MiNote } from '@/models/Note.js';
import type { MiNotification } from '@/models/Notification.js';
import type { MiAntenna } from '@/models/Antenna.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiDriveFolder } from '@/models/DriveFolder.js';
Expand Down Expand Up @@ -69,6 +70,7 @@ export interface MainEventTypes {
file: Packed<'DriveFile'>;
};
readAllNotifications: undefined;
notificationDeleted: MiNotification['id'];
unreadNotification: Packed<'Notification'>;
unreadMention: MiNote['id'];
readAllUnreadMentions: undefined;
Expand Down
20 changes: 20 additions & 0 deletions packages/backend/src/core/NotificationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,26 @@ export class NotificationService implements OnApplicationShutdown {
*/
}

async #getNotification(userId: MiUser['id'], notificationId: MiNotification['id']) {
// 通知のidから読み取れる時間とタイムラグが有るため、ラグを考慮する必要がある
const notificationRes = await this.redisClient.xrange(
`notificationTimeline:${userId}`,
`${this.idService.parse(notificationId).date.getTime() - 1000 }-0`,
`${this.idService.parse(notificationId).date.getTime() + 1000 }-9999`,
'COUNT', 50);
return notificationRes.find(x => JSON.parse(x[1][1]).id === notificationId);
}

@bindThis
public async deleteNotification(userId: MiUser['id'], notificationId: MiNotification['id']) : Promise<MiNotification['id']|void> {
const targetResId = (await this.#getNotification(userId, notificationId))?.[0];
if (!targetResId) return;

await this.redisClient.xdel(`notificationTimeline:${userId}`, targetResId);
this.globalEventService.publishMainStream(userId, 'notificationDeleted', notificationId);
return notificationId;
}

@bindThis
public dispose(): void {
this.#shutdownController.abort();
Expand Down
3 changes: 3 additions & 0 deletions packages/backend/src/server/api/EndpointsModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ import * as ep___notes_translate from './endpoints/notes/translate.js';
import * as ep___notes_unrenote from './endpoints/notes/unrenote.js';
import * as ep___notes_userListTimeline from './endpoints/notes/user-list-timeline.js';
import * as ep___notifications_create from './endpoints/notifications/create.js';
import * as ep___notifications_delete from './endpoints/notifications/delete.js';
import * as ep___notifications_markAllAsRead from './endpoints/notifications/mark-all-as-read.js';
import * as ep___notifications_testNotification from './endpoints/notifications/test-notification.js';
import * as ep___pagePush from './endpoints/page-push.js';
Expand Down Expand Up @@ -664,6 +665,7 @@ const $notes_translate: Provider = { provide: 'ep:notes/translate', useClass: ep
const $notes_unrenote: Provider = { provide: 'ep:notes/unrenote', useClass: ep___notes_unrenote.default };
const $notes_userListTimeline: Provider = { provide: 'ep:notes/user-list-timeline', useClass: ep___notes_userListTimeline.default };
const $notifications_create: Provider = { provide: 'ep:notifications/create', useClass: ep___notifications_create.default };
const $notifications_delete: Provider = { provide: 'ep:notifications/delete', useClass: ep___notifications_delete.default };
const $notifications_markAllAsRead: Provider = { provide: 'ep:notifications/mark-all-as-read', useClass: ep___notifications_markAllAsRead.default };
const $notifications_testNotification: Provider = { provide: 'ep:notifications/test-notification', useClass: ep___notifications_testNotification.default };
const $pagePush: Provider = { provide: 'ep:page-push', useClass: ep___pagePush.default };
Expand Down Expand Up @@ -1039,6 +1041,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$notes_unrenote,
$notes_userListTimeline,
$notifications_create,
$notifications_delete,
$notifications_markAllAsRead,
$notifications_testNotification,
$pagePush,
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/server/api/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ import * as ep___notes_translate from './endpoints/notes/translate.js';
import * as ep___notes_unrenote from './endpoints/notes/unrenote.js';
import * as ep___notes_userListTimeline from './endpoints/notes/user-list-timeline.js';
import * as ep___notifications_create from './endpoints/notifications/create.js';
import * as ep___notifications_delete from './endpoints/notifications/delete.js';
import * as ep___notifications_markAllAsRead from './endpoints/notifications/mark-all-as-read.js';
import * as ep___notifications_testNotification from './endpoints/notifications/test-notification.js';
import * as ep___pagePush from './endpoints/page-push.js';
Expand Down Expand Up @@ -662,6 +663,7 @@ const eps = [
['notes/unrenote', ep___notes_unrenote],
['notes/user-list-timeline', ep___notes_userListTimeline],
['notifications/create', ep___notifications_create],
['notifications/delete', ep___notifications_delete],
['notifications/mark-all-as-read', ep___notifications_markAllAsRead],
['notifications/test-notification', ep___notifications_testNotification],
['page-push', ep___pagePush],
Expand Down
47 changes: 47 additions & 0 deletions packages/backend/src/server/api/endpoints/notifications/delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/

import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { NotificationService } from '@/core/NotificationService.js';
import { ApiError } from '../../error.js';

export const meta = {
tags: ['notifications', 'account'],

requireCredential: true,

kind: 'write:notifications',

errors: {
'noSuchNotification': {
message: 'No such notification.',
code: 'NO_SUCH_NOTIFICATION',
id: '2bcf7352-eff1-4470-b5fb-97f1b0097832',
},
},
} as const;

export const paramDef = {
type: 'object',
properties: {
notificationId: { type: 'string', format: 'misskey:id' },
},
required: ['notificationId'],
} as const;

@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private notificationService: NotificationService,
) {
super(meta, paramDef, async (ps, me) => {
const res = await this.notificationService.deleteNotification(me.id, ps.notificationId);
if (!res) {
throw new ApiError(meta.errors.noSuchNotification);
}
});
}
}
6 changes: 6 additions & 0 deletions packages/frontend/src/components/MkNotifications.vue
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ function onNotification(notification) {
}
}

function onNotificationDeleted(notificationId) {
pagingComponent.value?.removeItem(notificationId);
}

function reload() {
return new Promise<void>((res) => {
pagingComponent.value?.reload().then(() => {
Expand All @@ -80,12 +84,14 @@ let connection;
onMounted(() => {
connection = useStream().useChannel('main');
connection.on('notification', onNotification);
connection.on('notificationDeleted', onNotificationDeleted);
});

onActivated(() => {
pagingComponent.value?.reload();
connection = useStream().useChannel('main');
connection.on('notification', onNotification);
connection.on('notificationDeleted', onNotificationDeleted);
});

onUnmounted(() => {
Expand Down
4 changes: 4 additions & 0 deletions packages/misskey-js/etc/misskey-js.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1548,6 +1548,7 @@ declare namespace entities {
NotesUserListTimelineRequest,
NotesUserListTimelineResponse,
NotificationsCreateRequest,
NotificationsDeleteRequest,
PagePushRequest,
PagesCreateRequest,
PagesCreateResponse,
Expand Down Expand Up @@ -2552,6 +2553,9 @@ type Notification_2 = components['schemas']['Notification'];
// @public (undocumented)
type NotificationsCreateRequest = operations['notifications/create']['requestBody']['content']['application/json'];

// @public (undocumented)
type NotificationsDeleteRequest = operations['notifications/delete']['requestBody']['content']['application/json'];

// @public (undocumented)
export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "roleAssigned", "achievementEarned"];

Expand Down
11 changes: 11 additions & 0 deletions packages/misskey-js/src/autogen/apiClientJSDoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3195,6 +3195,17 @@ declare module '../api.js' {
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;

/**
* No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:notifications*
*/
request<E extends 'notifications/delete', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;

/**
* No description provided.
*
Expand Down
2 changes: 2 additions & 0 deletions packages/misskey-js/src/autogen/endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,7 @@ import type {
NotesUserListTimelineRequest,
NotesUserListTimelineResponse,
NotificationsCreateRequest,
NotificationsDeleteRequest,
PagePushRequest,
PagesCreateRequest,
PagesCreateResponse,
Expand Down Expand Up @@ -840,6 +841,7 @@ export type Endpoints = {
'notes/unrenote': { req: NotesUnrenoteRequest; res: EmptyResponse };
'notes/user-list-timeline': { req: NotesUserListTimelineRequest; res: NotesUserListTimelineResponse };
'notifications/create': { req: NotificationsCreateRequest; res: EmptyResponse };
'notifications/delete': { req: NotificationsDeleteRequest; res: EmptyResponse };
'notifications/mark-all-as-read': { req: EmptyRequest; res: EmptyResponse };
'notifications/test-notification': { req: EmptyRequest; res: EmptyResponse };
'page-push': { req: PagePushRequest; res: EmptyResponse };
Expand Down
1 change: 1 addition & 0 deletions packages/misskey-js/src/autogen/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,7 @@ export type NotesUnrenoteRequest = operations['notes/unrenote']['requestBody']['
export type NotesUserListTimelineRequest = operations['notes/user-list-timeline']['requestBody']['content']['application/json'];
export type NotesUserListTimelineResponse = operations['notes/user-list-timeline']['responses']['200']['content']['application/json'];
export type NotificationsCreateRequest = operations['notifications/create']['requestBody']['content']['application/json'];
export type NotificationsDeleteRequest = operations['notifications/delete']['requestBody']['content']['application/json'];
export type PagePushRequest = operations['page-push']['requestBody']['content']['application/json'];
export type PagesCreateRequest = operations['pages/create']['requestBody']['content']['application/json'];
export type PagesCreateResponse = operations['pages/create']['responses']['200']['content']['application/json'];
Expand Down
61 changes: 61 additions & 0 deletions packages/misskey-js/src/autogen/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2770,6 +2770,15 @@ export type paths = {
*/
post: operations['notifications/create'];
};
'/notifications/delete': {
/**
* notifications/delete
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:notifications*
*/
post: operations['notifications/delete'];
};
'/notifications/mark-all-as-read': {
/**
* notifications/mark-all-as-read
Expand Down Expand Up @@ -22035,6 +22044,58 @@ export type operations = {
};
};
};
/**
* notifications/delete
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:notifications*
*/
'notifications/delete': {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
notificationId: string;
};
};
};
responses: {
/** @description OK (without any results) */
204: {
content: never;
};
/** @description Client error */
400: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Authentication error */
401: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Forbidden error */
403: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description I'm Ai */
418: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Internal server error */
500: {
content: {
'application/json': components['schemas']['Error'];
};
};
};
};
/**
* notifications/mark-all-as-read
* @description No description provided.
Expand Down
Loading