summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2023-04-04 14:06:57 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2023-04-04 14:06:57 +0900
commit30d699268450af375dabc2226ec4f3196a53f7f7 (patch)
treee6e62a4b19f190d2a51e4a5281c051b6911f97a1
parentbuild(#10336): Storybook & Chromatic & msw (#10365) (diff)
downloadsharkey-30d699268450af375dabc2226ec4f3196a53f7f7.tar.gz
sharkey-30d699268450af375dabc2226ec4f3196a53f7f7.tar.bz2
sharkey-30d699268450af375dabc2226ec4f3196a53f7f7.zip
perf(backend): 通知をRedisに保存するように
Resolve #10168
-rw-r--r--CHANGELOG.md1
-rw-r--r--packages/backend/migration/1680582195041-cleanup.js11
-rw-r--r--packages/backend/src/core/NoteReadService.ts4
-rw-r--r--packages/backend/src/core/NotificationService.ts115
-rw-r--r--packages/backend/src/core/PushNotificationService.ts12
-rw-r--r--packages/backend/src/core/entities/NotificationEntityService.ts74
-rw-r--r--packages/backend/src/core/entities/UserEntityService.ts33
-rw-r--r--packages/backend/src/di-symbols.ts1
-rw-r--r--packages/backend/src/models/RepositoryModule.ts10
-rw-r--r--packages/backend/src/models/entities/Notification.ts134
-rw-r--r--packages/backend/src/models/index.ts3
-rw-r--r--packages/backend/src/models/json-schema/notification.ts4
-rw-r--r--packages/backend/src/postgres.ts2
-rw-r--r--packages/backend/src/queue/processors/CleanProcessorService.ts9
-rw-r--r--packages/backend/src/server/api/EndpointsModule.ts4
-rw-r--r--packages/backend/src/server/api/endpoints.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/admin/suspend-user.ts16
-rw-r--r--packages/backend/src/server/api/endpoints/i/notifications.ts106
-rw-r--r--packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts22
-rw-r--r--packages/backend/src/server/api/endpoints/notifications/read.ts57
-rw-r--r--packages/backend/src/server/api/stream/index.ts3
-rw-r--r--packages/frontend/src/components/MkNotification.vue32
-rw-r--r--packages/frontend/src/components/MkNotifications.vue35
-rw-r--r--packages/frontend/src/pages/notifications.vue9
-rw-r--r--packages/frontend/src/ui/_common_/common.vue4
-rw-r--r--packages/misskey-js/src/api.types.ts1
-rw-r--r--packages/sw/src/scripts/notification-read.ts58
-rw-r--r--packages/sw/src/sw.ts30
-rw-r--r--packages/sw/src/types.ts4
29 files changed, 184 insertions, 612 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5cee783ee0..00c90987d4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -34,6 +34,7 @@
- ノート作成時のパフォーマンスを向上
- アンテナのタイムライン取得時のパフォーマンスを向上
- チャンネルのタイムライン取得時のパフォーマンスを向上
+- 通知に関する全体的なパフォーマンスを向上
## 13.10.3
diff --git a/packages/backend/migration/1680582195041-cleanup.js b/packages/backend/migration/1680582195041-cleanup.js
new file mode 100644
index 0000000000..c587e456a5
--- /dev/null
+++ b/packages/backend/migration/1680582195041-cleanup.js
@@ -0,0 +1,11 @@
+export class cleanup1680582195041 {
+ name = 'cleanup1680582195041'
+
+ async up(queryRunner) {
+ await queryRunner.query(`DROP TABLE "notification" `);
+ }
+
+ async down(queryRunner) {
+
+ }
+}
diff --git a/packages/backend/src/core/NoteReadService.ts b/packages/backend/src/core/NoteReadService.ts
index 1bf0eb918f..7c6808fbd0 100644
--- a/packages/backend/src/core/NoteReadService.ts
+++ b/packages/backend/src/core/NoteReadService.ts
@@ -169,10 +169,6 @@ export class NoteReadService implements OnApplicationShutdown {
this.globalEventService.publishMainStream(userId, 'readAllChannels');
}
});
-
- this.notificationService.readNotificationByQuery(userId, {
- noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id)]),
- });
}
}
diff --git a/packages/backend/src/core/NotificationService.ts b/packages/backend/src/core/NotificationService.ts
index b984f3c77b..2a4dbba6a4 100644
--- a/packages/backend/src/core/NotificationService.ts
+++ b/packages/backend/src/core/NotificationService.ts
@@ -1,8 +1,9 @@
import { setTimeout } from 'node:timers/promises';
+import Redis from 'ioredis';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import { In } from 'typeorm';
import { DI } from '@/di-symbols.js';
-import type { MutingsRepository, NotificationsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
+import type { MutingsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
import type { User } from '@/models/entities/User.js';
import type { Notification } from '@/models/entities/Notification.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
@@ -17,15 +18,15 @@ export class NotificationService implements OnApplicationShutdown {
#shutdownController = new AbortController();
constructor(
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
- @Inject(DI.notificationsRepository)
- private notificationsRepository: NotificationsRepository,
-
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
@@ -38,50 +39,31 @@ export class NotificationService implements OnApplicationShutdown {
}
@bindThis
- public async readNotification(
+ public async readAllNotification(
userId: User['id'],
- notificationIds: Notification['id'][],
) {
- if (notificationIds.length === 0) return;
-
- // Update documents
- const result = await this.notificationsRepository.update({
- notifieeId: userId,
- id: In(notificationIds),
- isRead: false,
- }, {
- isRead: true,
- });
-
- if (result.affected === 0) return;
+ const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`);
+
+ const latestNotificationIdsRes = await this.redisClient.xrevrange(
+ `notificationTimeline:${userId}`,
+ '+',
+ '-',
+ 'COUNT', 1);
+ console.log('latestNotificationIdsRes', latestNotificationIdsRes);
+ const latestNotificationId = latestNotificationIdsRes[0]?.[0];
- if (!await this.userEntityService.getHasUnreadNotification(userId)) return this.postReadAllNotifications(userId);
- else return this.postReadNotifications(userId, notificationIds);
- }
+ if (latestNotificationId == null) return;
- @bindThis
- public async readNotificationByQuery(
- userId: User['id'],
- query: Record<string, any>,
- ) {
- const notificationIds = await this.notificationsRepository.findBy({
- ...query,
- notifieeId: userId,
- isRead: false,
- }).then(notifications => notifications.map(notification => notification.id));
+ this.redisClient.set(`latestReadNotification:${userId}`, latestNotificationId);
- return this.readNotification(userId, notificationIds);
+ if (latestReadNotificationId == null || (latestReadNotificationId < latestNotificationId)) {
+ return this.postReadAllNotifications(userId);
+ }
}
@bindThis
private postReadAllNotifications(userId: User['id']) {
this.globalEventService.publishMainStream(userId, 'readAllNotifications');
- return this.pushNotificationService.pushNotification(userId, 'readAllNotifications', undefined);
- }
-
- @bindThis
- private postReadNotifications(userId: User['id'], notificationIds: Notification['id'][]) {
- return this.pushNotificationService.pushNotification(userId, 'readNotifications', { notificationIds });
}
@bindThis
@@ -90,47 +72,48 @@ export class NotificationService implements OnApplicationShutdown {
type: Notification['type'],
data: Partial<Notification>,
): Promise<Notification | null> {
- if (data.notifierId && (notifieeId === data.notifierId)) {
- return null;
- }
-
- const profile = await this.userProfilesRepository.findOneBy({ userId: notifieeId });
-
// TODO: Cache
+ const profile = await this.userProfilesRepository.findOneBy({ userId: notifieeId });
const isMuted = profile?.mutingNotificationTypes.includes(type);
+ if (isMuted) return null;
+
+ if (data.notifierId) {
+ if (notifieeId === data.notifierId) {
+ return null;
+ }
+
+ // TODO: cache
+ const mutings = await this.mutingsRepository.findOneBy({
+ muterId: notifieeId,
+ muteeId: data.notifierId,
+ });
+ if (mutings) {
+ return null;
+ }
+ }
- // Create notification
- const notification = await this.notificationsRepository.insert({
+ const notification = {
id: this.idService.genId(),
createdAt: new Date(),
- notifieeId: notifieeId,
type: type,
- // 相手がこの通知をミュートしているようなら、既読を予めつけておく
- isRead: isMuted,
...data,
- } as Partial<Notification>)
- .then(x => this.notificationsRepository.findOneByOrFail(x.identifiers[0]));
+ } as Notification;
- const packed = await this.notificationEntityService.pack(notification, {});
+ this.redisClient.xadd(
+ `notificationTimeline:${notifieeId}`,
+ 'MAXLEN', '~', '300',
+ `${this.idService.parse(notification.id).date.getTime()}-*`,
+ 'data', JSON.stringify(notification));
+
+ const packed = await this.notificationEntityService.pack(notification, notifieeId, {});
// Publish notification event
this.globalEventService.publishMainStream(notifieeId, 'notification', packed);
// 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
- setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => {
- const fresh = await this.notificationsRepository.findOneBy({ id: notification.id });
- if (fresh == null) return; // 既に削除されているかもしれない
- if (fresh.isRead) return;
-
- //#region ただしミュートしているユーザーからの通知なら無視
- // TODO: Cache
- const mutings = await this.mutingsRepository.findBy({
- muterId: notifieeId,
- });
- if (data.notifierId && mutings.map(m => m.muteeId).includes(data.notifierId)) {
- return;
- }
- //#endregion
+ setTimeout(2000, 'unread notification', { signal: this.#shutdownController.signal }).then(async () => {
+ const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${notifieeId}`);
+ if (latestReadNotificationId && (latestReadNotificationId >= notification.id)) return;
this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed);
this.pushNotificationService.pushNotification(notifieeId, 'notification', packed);
diff --git a/packages/backend/src/core/PushNotificationService.ts b/packages/backend/src/core/PushNotificationService.ts
index 32c38ad480..69020f7e84 100644
--- a/packages/backend/src/core/PushNotificationService.ts
+++ b/packages/backend/src/core/PushNotificationService.ts
@@ -15,10 +15,6 @@ type PushNotificationsTypes = {
antenna: { id: string, name: string };
note: Packed<'Note'>;
};
- 'readNotifications': { notificationIds: string[] };
- 'readAllNotifications': undefined;
- 'readAntenna': { antennaId: string };
- 'readAllAntennas': undefined;
};
// Reduce length because push message servers have character limits
@@ -72,14 +68,6 @@ export class PushNotificationService {
});
for (const subscription of subscriptions) {
- // Continue if sendReadMessage is false
- if ([
- 'readNotifications',
- 'readAllNotifications',
- 'readAntenna',
- 'readAllAntennas',
- ].includes(type) && !subscription.sendReadMessage) continue;
-
const pushSubscription = {
endpoint: subscription.endpoint,
keys: {
diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts
index 70e56cb3d7..7cffb8d568 100644
--- a/packages/backend/src/core/entities/NotificationEntityService.ts
+++ b/packages/backend/src/core/entities/NotificationEntityService.ts
@@ -1,7 +1,8 @@
import { Inject, Injectable } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
+import { In } from 'typeorm';
import { DI } from '@/di-symbols.js';
-import type { AccessTokensRepository, NoteReactionsRepository, NotificationsRepository, User } from '@/models/index.js';
+import type { AccessTokensRepository, NoteReactionsRepository, NotesRepository, User, UsersRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Notification } from '@/models/entities/Notification.js';
import type { Note } from '@/models/entities/Note.js';
@@ -25,8 +26,11 @@ export class NotificationEntityService implements OnModuleInit {
constructor(
private moduleRef: ModuleRef,
- @Inject(DI.notificationsRepository)
- private notificationsRepository: NotificationsRepository,
+ @Inject(DI.notesRepository)
+ private notesRepository: NotesRepository,
+
+ @Inject(DI.usersRepository)
+ private usersRepository: UsersRepository,
@Inject(DI.noteReactionsRepository)
private noteReactionsRepository: NoteReactionsRepository,
@@ -48,30 +52,39 @@ export class NotificationEntityService implements OnModuleInit {
@bindThis
public async pack(
- src: Notification['id'] | Notification,
+ src: Notification,
+ meId: User['id'],
options: {
- _hint_?: {
- packedNotes: Map<Note['id'], Packed<'Note'>>;
- };
+
+ },
+ hint?: {
+ packedNotes: Map<Note['id'], Packed<'Note'>>;
+ packedUsers: Map<User['id'], Packed<'User'>>;
},
): Promise<Packed<'Notification'>> {
- const notification = typeof src === 'object' ? src : await this.notificationsRepository.findOneByOrFail({ id: src });
+ const notification = src;
const token = notification.appAccessTokenId ? await this.accessTokensRepository.findOneByOrFail({ id: notification.appAccessTokenId }) : null;
const noteIfNeed = NOTE_REQUIRED_NOTIFICATION_TYPES.has(notification.type) && notification.noteId != null ? (
- options._hint_?.packedNotes != null
- ? options._hint_.packedNotes.get(notification.noteId)
- : this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, {
+ hint?.packedNotes != null
+ ? hint.packedNotes.get(notification.noteId)
+ : this.noteEntityService.pack(notification.noteId!, { id: meId }, {
detail: true,
})
) : undefined;
+ const userIfNeed = notification.notifierId != null ? (
+ hint?.packedUsers != null
+ ? hint.packedUsers.get(notification.notifierId)
+ : this.userEntityService.pack(notification.notifierId!, { id: meId }, {
+ detail: false,
+ })
+ ) : undefined;
return await awaitAll({
id: notification.id,
- createdAt: notification.createdAt.toISOString(),
+ createdAt: new Date(notification.createdAt).toISOString(),
type: notification.type,
- isRead: notification.isRead,
userId: notification.notifierId,
- user: notification.notifierId ? this.userEntityService.pack(notification.notifier ?? notification.notifierId) : null,
+ ...(userIfNeed != null ? { user: userIfNeed } : {}),
...(noteIfNeed != null ? { note: noteIfNeed } : {}),
...(notification.type === 'reaction' ? {
reaction: notification.reaction,
@@ -87,33 +100,36 @@ export class NotificationEntityService implements OnModuleInit {
});
}
- /**
- * @param notifications you should join "note" property when fetch from DB, and all notifieeId should be same as meId
- */
@bindThis
public async packMany(
notifications: Notification[],
meId: User['id'],
) {
if (notifications.length === 0) return [];
-
- for (const notification of notifications) {
- if (meId !== notification.notifieeId) {
- // because we call note packMany with meId, all notifieeId should be same as meId
- throw new Error('TRY_TO_PACK_ANOTHER_USER_NOTIFICATION');
- }
- }
- const notes = notifications.map(x => x.note).filter(isNotNull);
+ const noteIds = notifications.map(x => x.noteId).filter(isNotNull);
+ const notes = noteIds.length > 0 ? await this.notesRepository.find({
+ where: { id: In(noteIds) },
+ relations: ['user', 'user.avatar', 'user.banner', 'reply', 'reply.user', 'reply.user.avatar', 'reply.user.banner', 'renote', 'renote.user', 'renote.user.avatar', 'renote.user.banner'],
+ }) : [];
const packedNotesArray = await this.noteEntityService.packMany(notes, { id: meId }, {
detail: true,
});
const packedNotes = new Map(packedNotesArray.map(p => [p.id, p]));
- return await Promise.all(notifications.map(x => this.pack(x, {
- _hint_: {
- packedNotes,
- },
+ const userIds = notifications.map(x => x.notifierId).filter(isNotNull);
+ const users = userIds.length > 0 ? await this.usersRepository.find({
+ where: { id: In(userIds) },
+ relations: ['avatar', 'banner'],
+ }) : [];
+ const packedUsersArray = await this.userEntityService.packMany(users, { id: meId }, {
+ detail: false,
+ });
+ const packedUsers = new Map(packedUsersArray.map(p => [p.id, p]));
+
+ return await Promise.all(notifications.map(x => this.pack(x, meId, {}, {
+ packedNotes,
+ packedUsers,
})));
}
}
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index 61fd6f2f66..ae7c47a990 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -1,5 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { In, Not } from 'typeorm';
+import Redis from 'ioredis';
import Ajv from 'ajv';
import { ModuleRef } from '@nestjs/core';
import { DI } from '@/di-symbols.js';
@@ -12,7 +13,7 @@ import { KVCache } from '@/misc/cache.js';
import type { Instance } from '@/models/entities/Instance.js';
import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/entities/User.js';
-import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, NotificationsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, AnnouncementsRepository, PagesRepository, UserProfile, RenoteMutingsRepository } from '@/models/index.js';
+import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, AnnouncementsRepository, PagesRepository, UserProfile, RenoteMutingsRepository } from '@/models/index.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import type { OnModuleInit } from '@nestjs/common';
@@ -60,6 +61,9 @@ export class UserEntityService implements OnModuleInit {
@Inject(DI.config)
private config: Config,
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -90,9 +94,6 @@ export class UserEntityService implements OnModuleInit {
@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,
- @Inject(DI.notificationsRepository)
- private notificationsRepository: NotificationsRepository,
-
@Inject(DI.userNotePiningsRepository)
private userNotePiningsRepository: UserNotePiningsRepository,
@@ -247,21 +248,17 @@ export class UserEntityService implements OnModuleInit {
@bindThis
public async getHasUnreadNotification(userId: User['id']): Promise<boolean> {
- const mute = await this.mutingsRepository.findBy({
- muterId: userId,
- });
- const mutedUserIds = mute.map(m => m.muteeId);
+ const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`);
+
+ const latestNotificationIdsRes = await this.redisClient.xrevrange(
+ `notificationTimeline:${userId}`,
+ '+',
+ '-',
+ 'COUNT', 1);
+ console.log('latestNotificationIdsRes', latestNotificationIdsRes);
+ const latestNotificationId = latestNotificationIdsRes[0]?.[0];
- const count = await this.notificationsRepository.count({
- where: {
- notifieeId: userId,
- ...(mutedUserIds.length > 0 ? { notifierId: Not(In(mutedUserIds)) } : {}),
- isRead: false,
- },
- take: 1,
- });
-
- return count > 0;
+ return latestNotificationId != null && (latestReadNotificationId == null || latestReadNotificationId < latestNotificationId);
}
@bindThis
diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts
index f2ab6cb864..56ce755a1a 100644
--- a/packages/backend/src/di-symbols.ts
+++ b/packages/backend/src/di-symbols.ts
@@ -33,7 +33,6 @@ export const DI = {
emojisRepository: Symbol('emojisRepository'),
driveFilesRepository: Symbol('driveFilesRepository'),
driveFoldersRepository: Symbol('driveFoldersRepository'),
- notificationsRepository: Symbol('notificationsRepository'),
metasRepository: Symbol('metasRepository'),
mutingsRepository: Symbol('mutingsRepository'),
renoteMutingsRepository: Symbol('renoteMutingsRepository'),
diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts
index b74ee3689c..7be7b81904 100644
--- a/packages/backend/src/models/RepositoryModule.ts
+++ b/packages/backend/src/models/RepositoryModule.ts
@@ -1,6 +1,6 @@
import { Module } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
-import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Notification, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, RenoteMuting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelFavorite, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment, ClipFavorite } from './index.js';
+import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, RenoteMuting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelFavorite, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment, ClipFavorite } from './index.js';
import type { DataSource } from 'typeorm';
import type { Provider } from '@nestjs/common';
@@ -172,12 +172,6 @@ const $driveFoldersRepository: Provider = {
inject: [DI.db],
};
-const $notificationsRepository: Provider = {
- provide: DI.notificationsRepository,
- useFactory: (db: DataSource) => db.getRepository(Notification),
- inject: [DI.db],
-};
-
const $metasRepository: Provider = {
provide: DI.metasRepository,
useFactory: (db: DataSource) => db.getRepository(Meta),
@@ -426,7 +420,6 @@ const $roleAssignmentsRepository: Provider = {
$emojisRepository,
$driveFilesRepository,
$driveFoldersRepository,
- $notificationsRepository,
$metasRepository,
$mutingsRepository,
$renoteMutingsRepository,
@@ -493,7 +486,6 @@ const $roleAssignmentsRepository: Provider = {
$emojisRepository,
$driveFilesRepository,
$driveFoldersRepository,
- $notificationsRepository,
$metasRepository,
$mutingsRepository,
$renoteMutingsRepository,
diff --git a/packages/backend/src/models/entities/Notification.ts b/packages/backend/src/models/entities/Notification.ts
index 51117efba5..aa6f997124 100644
--- a/packages/backend/src/models/entities/Notification.ts
+++ b/packages/backend/src/models/entities/Notification.ts
@@ -1,54 +1,19 @@
-import { Entity, Index, JoinColumn, ManyToOne, Column, PrimaryColumn } from 'typeorm';
-import { notificationTypes, obsoleteNotificationTypes } from '@/types.js';
-import { id } from '../id.js';
+import { notificationTypes } from '@/types.js';
import { User } from './User.js';
import { Note } from './Note.js';
import { FollowRequest } from './FollowRequest.js';
import { AccessToken } from './AccessToken.js';
-@Entity()
-export class Notification {
- @PrimaryColumn(id())
- public id: string;
+export type Notification = {
+ id: string;
- @Index()
- @Column('timestamp with time zone', {
- comment: 'The created date of the Notification.',
- })
- public createdAt: Date;
-
- /**
- * 通知の受信者
- */
- @Index()
- @Column({
- ...id(),
- comment: 'The ID of recipient user of the Notification.',
- })
- public notifieeId: User['id'];
-
- @ManyToOne(type => User, {
- onDelete: 'CASCADE',
- })
- @JoinColumn()
- public notifiee: User | null;
+ // RedisのためDateではなくstring
+ createdAt: string;
/**
* 通知の送信者(initiator)
*/
- @Index()
- @Column({
- ...id(),
- nullable: true,
- comment: 'The ID of sender user of the Notification.',
- })
- public notifierId: User['id'] | null;
-
- @ManyToOne(type => User, {
- onDelete: 'CASCADE',
- })
- @JoinColumn()
- public notifier: User | null;
+ notifierId: User['id'] | null;
/**
* 通知の種類。
@@ -64,104 +29,37 @@ export class Notification {
* achievementEarned - 実績を獲得
* app - アプリ通知
*/
- @Index()
- @Column('enum', {
- enum: [
- ...notificationTypes,
- ...obsoleteNotificationTypes,
- ],
- comment: 'The type of the Notification.',
- })
- public type: typeof notificationTypes[number];
+ type: typeof notificationTypes[number];
- /**
- * 通知が読まれたかどうか
- */
- @Index()
- @Column('boolean', {
- default: false,
- comment: 'Whether the Notification is read.',
- })
- public isRead: boolean;
-
- @Column({
- ...id(),
- nullable: true,
- })
- public noteId: Note['id'] | null;
-
- @ManyToOne(type => Note, {
- onDelete: 'CASCADE',
- })
- @JoinColumn()
- public note: Note | null;
+ noteId: Note['id'] | null;
- @Column({
- ...id(),
- nullable: true,
- })
- public followRequestId: FollowRequest['id'] | null;
+ followRequestId: FollowRequest['id'] | null;
- @ManyToOne(type => FollowRequest, {
- onDelete: 'CASCADE',
- })
- @JoinColumn()
- public followRequest: FollowRequest | null;
+ reaction: string | null;
- @Column('varchar', {
- length: 128, nullable: true,
- })
- public reaction: string | null;
+ choice: number | null;
- @Column('integer', {
- nullable: true,
- })
- public choice: number | null;
-
- @Column('varchar', {
- length: 128, nullable: true,
- })
- public achievement: string | null;
+ achievement: string | null;
/**
* アプリ通知のbody
*/
- @Column('varchar', {
- length: 2048, nullable: true,
- })
- public customBody: string | null;
+ customBody: string | null;
/**
* アプリ通知のheader
* (省略時はアプリ名で表示されることを期待)
*/
- @Column('varchar', {
- length: 256, nullable: true,
- })
- public customHeader: string | null;
+ customHeader: string | null;
/**
* アプリ通知のicon(URL)
* (省略時はアプリアイコンで表示されることを期待)
*/
- @Column('varchar', {
- length: 1024, nullable: true,
- })
- public customIcon: string | null;
+ customIcon: string | null;
/**
* アプリ通知のアプリ(のトークン)
*/
- @Index()
- @Column({
- ...id(),
- nullable: true,
- })
- public appAccessTokenId: AccessToken['id'] | null;
-
- @ManyToOne(type => AccessToken, {
- onDelete: 'CASCADE',
- })
- @JoinColumn()
- public appAccessToken: AccessToken | null;
+ appAccessTokenId: AccessToken['id'] | null;
}
diff --git a/packages/backend/src/models/index.ts b/packages/backend/src/models/index.ts
index c4c9717ed5..48d6e15f2a 100644
--- a/packages/backend/src/models/index.ts
+++ b/packages/backend/src/models/index.ts
@@ -32,7 +32,6 @@ import { NoteFavorite } from '@/models/entities/NoteFavorite.js';
import { NoteReaction } from '@/models/entities/NoteReaction.js';
import { NoteThreadMuting } from '@/models/entities/NoteThreadMuting.js';
import { NoteUnread } from '@/models/entities/NoteUnread.js';
-import { Notification } from '@/models/entities/Notification.js';
import { Page } from '@/models/entities/Page.js';
import { PageLike } from '@/models/entities/PageLike.js';
import { PasswordResetRequest } from '@/models/entities/PasswordResetRequest.js';
@@ -100,7 +99,6 @@ export {
NoteReaction,
NoteThreadMuting,
NoteUnread,
- Notification,
Page,
PageLike,
PasswordResetRequest,
@@ -167,7 +165,6 @@ export type NoteFavoritesRepository = Repository<NoteFavorite>;
export type NoteReactionsRepository = Repository<NoteReaction>;
export type NoteThreadMutingsRepository = Repository<NoteThreadMuting>;
export type NoteUnreadsRepository = Repository<NoteUnread>;
-export type NotificationsRepository = Repository<Notification>;
export type PagesRepository = Repository<Page>;
export type PageLikesRepository = Repository<PageLike>;
export type PasswordResetRequestsRepository = Repository<PasswordResetRequest>;
diff --git a/packages/backend/src/models/json-schema/notification.ts b/packages/backend/src/models/json-schema/notification.ts
index d3f2405cdd..e88ca61ba0 100644
--- a/packages/backend/src/models/json-schema/notification.ts
+++ b/packages/backend/src/models/json-schema/notification.ts
@@ -14,10 +14,6 @@ export const packedNotificationSchema = {
optional: false, nullable: false,
format: 'date-time',
},
- isRead: {
- type: 'boolean',
- optional: false, nullable: false,
- },
type: {
type: 'string',
optional: false, nullable: false,
diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts
index 024aa114fc..efeca46b49 100644
--- a/packages/backend/src/postgres.ts
+++ b/packages/backend/src/postgres.ts
@@ -40,7 +40,6 @@ import { NoteFavorite } from '@/models/entities/NoteFavorite.js';
import { NoteReaction } from '@/models/entities/NoteReaction.js';
import { NoteThreadMuting } from '@/models/entities/NoteThreadMuting.js';
import { NoteUnread } from '@/models/entities/NoteUnread.js';
-import { Notification } from '@/models/entities/Notification.js';
import { Page } from '@/models/entities/Page.js';
import { PageLike } from '@/models/entities/PageLike.js';
import { PasswordResetRequest } from '@/models/entities/PasswordResetRequest.js';
@@ -155,7 +154,6 @@ export const entities = [
DriveFolder,
Poll,
PollVote,
- Notification,
Emoji,
Hashtag,
SwSubscription,
diff --git a/packages/backend/src/queue/processors/CleanProcessorService.ts b/packages/backend/src/queue/processors/CleanProcessorService.ts
index 3feb86f86f..1936e8df23 100644
--- a/packages/backend/src/queue/processors/CleanProcessorService.ts
+++ b/packages/backend/src/queue/processors/CleanProcessorService.ts
@@ -1,7 +1,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { In, LessThan } from 'typeorm';
import { DI } from '@/di-symbols.js';
-import type { AntennasRepository, MutedNotesRepository, NotificationsRepository, RoleAssignmentsRepository, UserIpsRepository } from '@/models/index.js';
+import type { AntennasRepository, MutedNotesRepository, RoleAssignmentsRepository, UserIpsRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
@@ -20,9 +20,6 @@ export class CleanProcessorService {
@Inject(DI.userIpsRepository)
private userIpsRepository: UserIpsRepository,
- @Inject(DI.notificationsRepository)
- private notificationsRepository: NotificationsRepository,
-
@Inject(DI.mutedNotesRepository)
private mutedNotesRepository: MutedNotesRepository,
@@ -46,10 +43,6 @@ export class CleanProcessorService {
createdAt: LessThan(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90))),
});
- this.notificationsRepository.delete({
- createdAt: LessThan(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90))),
- });
-
this.mutedNotesRepository.delete({
id: LessThan(this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90)))),
reason: 'word',
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index f39643abeb..cab2477414 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -268,7 +268,6 @@ 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_markAllAsRead from './endpoints/notifications/mark-all-as-read.js';
-import * as ep___notifications_read from './endpoints/notifications/read.js';
import * as ep___pagePush from './endpoints/page-push.js';
import * as ep___pages_create from './endpoints/pages/create.js';
import * as ep___pages_delete from './endpoints/pages/delete.js';
@@ -600,7 +599,6 @@ const $notes_unrenote: Provider = { provide: 'ep:notes/unrenote', useClass: ep__
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_markAllAsRead: Provider = { provide: 'ep:notifications/mark-all-as-read', useClass: ep___notifications_markAllAsRead.default };
-const $notifications_read: Provider = { provide: 'ep:notifications/read', useClass: ep___notifications_read.default };
const $pagePush: Provider = { provide: 'ep:page-push', useClass: ep___pagePush.default };
const $pages_create: Provider = { provide: 'ep:pages/create', useClass: ep___pages_create.default };
const $pages_delete: Provider = { provide: 'ep:pages/delete', useClass: ep___pages_delete.default };
@@ -936,7 +934,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$notes_userListTimeline,
$notifications_create,
$notifications_markAllAsRead,
- $notifications_read,
$pagePush,
$pages_create,
$pages_delete,
@@ -1266,7 +1263,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$notes_userListTimeline,
$notifications_create,
$notifications_markAllAsRead,
- $notifications_read,
$pagePush,
$pages_create,
$pages_delete,
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index 16b20c1a4d..e33c2349cd 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -268,7 +268,6 @@ 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_markAllAsRead from './endpoints/notifications/mark-all-as-read.js';
-import * as ep___notifications_read from './endpoints/notifications/read.js';
import * as ep___pagePush from './endpoints/page-push.js';
import * as ep___pages_create from './endpoints/pages/create.js';
import * as ep___pages_delete from './endpoints/pages/delete.js';
@@ -598,7 +597,6 @@ const eps = [
['notes/user-list-timeline', ep___notes_userListTimeline],
['notifications/create', ep___notifications_create],
['notifications/mark-all-as-read', ep___notifications_markAllAsRead],
- ['notifications/read', ep___notifications_read],
['page-push', ep___pagePush],
['pages/create', ep___pages_create],
['pages/delete', ep___pages_delete],
diff --git a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts
index 3ad6c7c484..770b61850a 100644
--- a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts
@@ -1,6 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { UsersRepository, FollowingsRepository, NotificationsRepository } from '@/models/index.js';
+import type { UsersRepository, FollowingsRepository } from '@/models/index.js';
import type { User } from '@/models/entities/User.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
@@ -36,9 +36,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
- @Inject(DI.notificationsRepository)
- private notificationsRepository: NotificationsRepository,
-
private userEntityService: UserEntityService,
private userFollowingService: UserFollowingService,
private userSuspendService: UserSuspendService,
@@ -73,7 +70,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
(async () => {
await this.userSuspendService.doPostSuspend(user).catch(e => {});
await this.unFollowAll(user).catch(e => {});
- await this.readAllNotify(user).catch(e => {});
})();
});
}
@@ -96,14 +92,4 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
await this.userFollowingService.unfollow(follower, followee, true);
}
}
-
- @bindThis
- private async readAllNotify(notifier: User) {
- await this.notificationsRepository.update({
- notifierId: notifier.id,
- isRead: false,
- }, {
- isRead: true,
- });
- }
}
diff --git a/packages/backend/src/server/api/endpoints/i/notifications.ts b/packages/backend/src/server/api/endpoints/i/notifications.ts
index e3897d38bd..f27b4e86d4 100644
--- a/packages/backend/src/server/api/endpoints/i/notifications.ts
+++ b/packages/backend/src/server/api/endpoints/i/notifications.ts
@@ -1,6 +1,7 @@
-import { Brackets } from 'typeorm';
+import { Brackets, In } from 'typeorm';
+import Redis from 'ioredis';
import { Inject, Injectable } from '@nestjs/common';
-import type { UsersRepository, FollowingsRepository, MutingsRepository, UserProfilesRepository, NotificationsRepository } from '@/models/index.js';
+import type { UsersRepository, FollowingsRepository, MutingsRepository, UserProfilesRepository, NotesRepository } from '@/models/index.js';
import { obsoleteNotificationTypes, notificationTypes } from '@/types.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
@@ -8,6 +9,8 @@ import { NoteReadService } from '@/core/NoteReadService.js';
import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js';
import { NotificationService } from '@/core/NotificationService.js';
import { DI } from '@/di-symbols.js';
+import { IdService } from '@/core/IdService.js';
+import { Notification } from '@/models/entities/Notification.js';
export const meta = {
tags: ['account', 'notifications'],
@@ -38,8 +41,6 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
- following: { type: 'boolean', default: false },
- unreadOnly: { type: 'boolean', default: false },
markAsRead: { type: 'boolean', default: true },
// 後方互換のため、廃止された通知タイプも受け付ける
includeTypes: { type: 'array', items: {
@@ -56,21 +57,22 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
+ @Inject(DI.redis)
+ private redisClient: Redis.Redis,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
- @Inject(DI.followingsRepository)
- private followingsRepository: FollowingsRepository,
-
@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
- @Inject(DI.notificationsRepository)
- private notificationsRepository: NotificationsRepository,
+ @Inject(DI.notesRepository)
+ private notesRepository: NotesRepository,
+ private idService: IdService,
private notificationEntityService: NotificationEntityService,
private notificationService: NotificationService,
private queryService: QueryService,
@@ -89,85 +91,39 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
- const followingQuery = this.followingsRepository.createQueryBuilder('following')
- .select('following.followeeId')
- .where('following.followerId = :followerId', { followerId: me.id });
-
- const mutingQuery = this.mutingsRepository.createQueryBuilder('muting')
- .select('muting.muteeId')
- .where('muting.muterId = :muterId', { muterId: me.id });
-
- const mutingInstanceQuery = this.userProfilesRepository.createQueryBuilder('user_profile')
- .select('user_profile.mutedInstances')
- .where('user_profile.userId = :muterId', { muterId: me.id });
-
- const suspendedQuery = this.usersRepository.createQueryBuilder('users')
- .select('users.id')
- .where('users.isSuspended = TRUE');
-
- const query = this.queryService.makePaginationQuery(this.notificationsRepository.createQueryBuilder('notification'), ps.sinceId, ps.untilId)
- .andWhere('notification.notifieeId = :meId', { meId: me.id })
- .leftJoinAndSelect('notification.notifier', 'notifier')
- .leftJoinAndSelect('notification.note', 'note')
- .leftJoinAndSelect('notifier.avatar', 'notifierAvatar')
- .leftJoinAndSelect('notifier.banner', 'notifierBanner')
- .leftJoinAndSelect('note.user', 'user')
- .leftJoinAndSelect('user.avatar', 'avatar')
- .leftJoinAndSelect('user.banner', 'banner')
- .leftJoinAndSelect('note.reply', 'reply')
- .leftJoinAndSelect('note.renote', 'renote')
- .leftJoinAndSelect('reply.user', 'replyUser')
- .leftJoinAndSelect('replyUser.avatar', 'replyUserAvatar')
- .leftJoinAndSelect('replyUser.banner', 'replyUserBanner')
- .leftJoinAndSelect('renote.user', 'renoteUser')
- .leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
- .leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
-
- // muted users
- query.andWhere(new Brackets(qb => { qb
- .where(`notification.notifierId NOT IN (${ mutingQuery.getQuery() })`)
- .orWhere('notification.notifierId IS NULL');
- }));
- query.setParameters(mutingQuery.getParameters());
+ const notificationsRes = await this.redisClient.xrevrange(
+ `notificationTimeline:${me.id}`,
+ ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+',
+ '-',
+ 'COUNT', ps.limit + 1); // untilIdに指定したものも含まれるため+1
- // muted instances
- query.andWhere(new Brackets(qb => { qb
- .andWhere('notifier.host IS NULL')
- .orWhere(`NOT (( ${mutingInstanceQuery.getQuery()} )::jsonb ? notifier.host)`);
- }));
- query.setParameters(mutingInstanceQuery.getParameters());
-
- // suspended users
- query.andWhere(new Brackets(qb => { qb
- .where(`notification.notifierId NOT IN (${ suspendedQuery.getQuery() })`)
- .orWhere('notification.notifierId IS NULL');
- }));
-
- if (ps.following) {
- query.andWhere(`((notification.notifierId IN (${ followingQuery.getQuery() })) OR (notification.notifierId = :meId))`, { meId: me.id });
- query.setParameters(followingQuery.getParameters());
+ if (notificationsRes.length === 0) {
+ return [];
}
+ let notifications = notificationsRes.map(x => JSON.parse(x[1][1])).filter(x => x.id !== ps.untilId) as Notification[];
+
if (includeTypes && includeTypes.length > 0) {
- query.andWhere('notification.type IN (:...includeTypes)', { includeTypes });
+ notifications = notifications.filter(notification => includeTypes.includes(notification.type));
} else if (excludeTypes && excludeTypes.length > 0) {
- query.andWhere('notification.type NOT IN (:...excludeTypes)', { excludeTypes });
+ notifications = notifications.filter(notification => !excludeTypes.includes(notification.type));
}
- if (ps.unreadOnly) {
- query.andWhere('notification.isRead = false');
+ if (notifications.length === 0) {
+ return [];
}
- const notifications = await query.take(ps.limit).getMany();
-
// Mark all as read
- if (notifications.length > 0 && ps.markAsRead) {
- this.notificationService.readNotification(me.id, notifications.map(x => x.id));
+ if (ps.markAsRead) {
+ this.notificationService.readAllNotification(me.id);
}
- const notes = notifications.filter(notification => ['mention', 'reply', 'quote'].includes(notification.type)).map(notification => notification.note!);
+ const noteIds = notifications
+ .filter(notification => ['mention', 'reply', 'quote'].includes(notification.type))
+ .map(notification => notification.noteId!);
- if (notes.length > 0) {
+ if (noteIds.length > 0) {
+ const notes = await this.notesRepository.findBy({ id: In(noteIds) });
this.noteReadService.read(me.id, notes);
}
diff --git a/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts b/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts
index 09134cf48f..9ba6079189 100644
--- a/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts
+++ b/packages/backend/src/server/api/endpoints/notifications/mark-all-as-read.ts
@@ -1,9 +1,7 @@
import { Inject, Injectable } from '@nestjs/common';
-import type { NotificationsRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import { GlobalEventService } from '@/core/GlobalEventService.js';
-import { PushNotificationService } from '@/core/PushNotificationService.js';
import { DI } from '@/di-symbols.js';
+import { NotificationService } from '@/core/NotificationService.js';
export const meta = {
tags: ['notifications', 'account'],
@@ -23,24 +21,10 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
- @Inject(DI.notificationsRepository)
- private notificationsRepository: NotificationsRepository,
-
- private globalEventService: GlobalEventService,
- private pushNotificationService: PushNotificationService,
+ private notificationService: NotificationService,
) {
super(meta, paramDef, async (ps, me) => {
- // Update documents
- await this.notificationsRepository.update({
- notifieeId: me.id,
- isRead: false,
- }, {
- isRead: true,
- });
-
- // 全ての通知を読みましたよというイベントを発行
- this.globalEventService.publishMainStream(me.id, 'readAllNotifications');
- this.pushNotificationService.pushNotification(me.id, 'readAllNotifications', undefined);
+ this.notificationService.readAllNotification(me.id);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/notifications/read.ts b/packages/backend/src/server/api/endpoints/notifications/read.ts
deleted file mode 100644
index 6262c47fd0..0000000000
--- a/packages/backend/src/server/api/endpoints/notifications/read.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import { Injectable } from '@nestjs/common';
-import { Endpoint } from '@/server/api/endpoint-base.js';
-import { NotificationService } from '@/core/NotificationService.js';
-
-export const meta = {
- tags: ['notifications', 'account'],
-
- requireCredential: true,
-
- kind: 'write:notifications',
-
- description: 'Mark a notification as read.',
-
- errors: {
- noSuchNotification: {
- message: 'No such notification.',
- code: 'NO_SUCH_NOTIFICATION',
- id: 'efa929d5-05b5-47d1-beec-e6a4dbed011e',
- },
- },
-} as const;
-
-export const paramDef = {
- oneOf: [
- {
- type: 'object',
- properties: {
- notificationId: { type: 'string', format: 'misskey:id' },
- },
- required: ['notificationId'],
- },
- {
- type: 'object',
- properties: {
- notificationIds: {
- type: 'array',
- items: { type: 'string', format: 'misskey:id' },
- maxItems: 100,
- },
- },
- required: ['notificationIds'],
- },
- ],
-} as const;
-
-// eslint-disable-next-line import/no-default-export
-@Injectable()
-export default class extends Endpoint<typeof meta, typeof paramDef> {
- constructor(
- private notificationService: NotificationService,
- ) {
- super(meta, paramDef, async (ps, me) => {
- if ('notificationId' in ps) return this.notificationService.readNotification(me.id, [ps.notificationId]);
- return this.notificationService.readNotification(me.id, ps.notificationIds);
- });
- }
-}
diff --git a/packages/backend/src/server/api/stream/index.ts b/packages/backend/src/server/api/stream/index.ts
index 7c6eb9a20a..f1f8bfd3a2 100644
--- a/packages/backend/src/server/api/stream/index.ts
+++ b/packages/backend/src/server/api/stream/index.ts
@@ -195,8 +195,7 @@ export default class Connection {
@bindThis
private onReadNotification(payload: any) {
- if (!payload.id) return;
- this.notificationService.readNotification(this.user!.id, [payload.id]);
+ this.notificationService.readAllNotification(this.user!.id);
}
/**
diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue
index b60967de02..efae687e66 100644
--- a/packages/frontend/src/components/MkNotification.vue
+++ b/packages/frontend/src/components/MkNotification.vue
@@ -83,7 +83,7 @@
</template>
<script lang="ts" setup>
-import { ref, shallowRef, onMounted, onUnmounted, watch } from 'vue';
+import { ref, shallowRef } from 'vue';
import * as misskey from 'misskey-js';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
import MkFollowButton from '@/components/MkFollowButton.vue';
@@ -94,7 +94,6 @@ import { notePage } from '@/filters/note';
import { userPage } from '@/filters/user';
import { i18n } from '@/i18n';
import * as os from '@/os';
-import { stream } from '@/stream';
import { useTooltip } from '@/scripts/use-tooltip';
import { $i } from '@/account';
@@ -110,35 +109,6 @@ const props = withDefaults(defineProps<{
const elRef = shallowRef<HTMLElement>(null);
const reactionRef = ref(null);
-let readObserver: IntersectionObserver | undefined;
-let connection;
-
-onMounted(() => {
- if (!props.notification.isRead) {
- readObserver = new IntersectionObserver((entries, observer) => {
- if (!entries.some(entry => entry.isIntersecting)) return;
- stream.send('readNotification', {
- id: props.notification.id,
- });
- observer.disconnect();
- });
-
- readObserver.observe(elRef.value);
-
- connection = stream.useChannel('main');
- connection.on('readAllNotifications', () => readObserver.disconnect());
-
- watch(props.notification.isRead, () => {
- readObserver.disconnect();
- });
- }
-});
-
-onUnmounted(() => {
- if (readObserver) readObserver.disconnect();
- if (connection) connection.dispose();
-});
-
const followRequestDone = ref(false);
const acceptFollowRequest = () => {
diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index 874f1f90ea..1aea95fe0e 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -29,7 +29,6 @@ import { notificationTypes } from '@/const';
const props = defineProps<{
includeTypes?: typeof notificationTypes[number][];
- unreadOnly?: boolean;
}>();
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
@@ -40,23 +39,17 @@ const pagination: Paging = {
params: computed(() => ({
includeTypes: props.includeTypes ?? undefined,
excludeTypes: props.includeTypes ? undefined : $i.mutingNotificationTypes,
- unreadOnly: props.unreadOnly,
})),
};
const onNotification = (notification) => {
const isMuted = props.includeTypes ? !props.includeTypes.includes(notification.type) : $i.mutingNotificationTypes.includes(notification.type);
if (isMuted || document.visibilityState === 'visible') {
- stream.send('readNotification', {
- id: notification.id,
- });
+ stream.send('readNotification');
}
if (!isMuted) {
- pagingComponent.value.prepend({
- ...notification,
- isRead: document.visibilityState === 'visible',
- });
+ pagingComponent.value.prepend(notification);
}
};
@@ -65,30 +58,6 @@ let connection;
onMounted(() => {
connection = stream.useChannel('main');
connection.on('notification', onNotification);
- connection.on('readAllNotifications', () => {
- if (pagingComponent.value) {
- for (const item of pagingComponent.value.queue) {
- item.isRead = true;
- }
- for (const item of pagingComponent.value.items) {
- item.isRead = true;
- }
- }
- });
- connection.on('readNotifications', notificationIds => {
- if (pagingComponent.value) {
- for (let i = 0; i < pagingComponent.value.queue.length; i++) {
- if (notificationIds.includes(pagingComponent.value.queue[i].id)) {
- pagingComponent.value.queue[i].isRead = true;
- }
- }
- for (let i = 0; i < (pagingComponent.value.items || []).length; i++) {
- if (notificationIds.includes(pagingComponent.value.items[i].id)) {
- pagingComponent.value.items[i].isRead = true;
- }
- }
- }
- });
});
onUnmounted(() => {
diff --git a/packages/frontend/src/pages/notifications.vue b/packages/frontend/src/pages/notifications.vue
index a5c7cdaa71..1789606cd8 100644
--- a/packages/frontend/src/pages/notifications.vue
+++ b/packages/frontend/src/pages/notifications.vue
@@ -2,8 +2,8 @@
<MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="800">
- <div v-if="tab === 'all' || tab === 'unread'">
- <XNotifications class="notifications" :include-types="includeTypes" :unread-only="unreadOnly"/>
+ <div v-if="tab === 'all'">
+ <XNotifications class="notifications" :include-types="includeTypes"/>
</div>
<div v-else-if="tab === 'mentions'">
<MkNotes :pagination="mentionsPagination"/>
@@ -26,7 +26,6 @@ import { notificationTypes } from '@/const';
let tab = $ref('all');
let includeTypes = $ref<string[] | null>(null);
-let unreadOnly = $computed(() => tab === 'unread');
const mentionsPagination = {
endpoint: 'notes/mentions' as const,
@@ -77,10 +76,6 @@ const headerTabs = $computed(() => [{
title: i18n.ts.all,
icon: 'ti ti-point',
}, {
- key: 'unread',
- title: i18n.ts.unread,
- icon: 'ti ti-loader',
-}, {
key: 'mentions',
title: i18n.ts.mentions,
icon: 'ti ti-at',
diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue
index e1561cb396..5a32c076a4 100644
--- a/packages/frontend/src/ui/_common_/common.vue
+++ b/packages/frontend/src/ui/_common_/common.vue
@@ -53,9 +53,7 @@ function onNotification(notification) {
if ($i.mutingNotificationTypes.includes(notification.type)) return;
if (document.visibilityState === 'visible') {
- stream.send('readNotification', {
- id: notification.id,
- });
+ stream.send('readNotification');
notifications.unshift(notification);
window.setTimeout(() => {
diff --git a/packages/misskey-js/src/api.types.ts b/packages/misskey-js/src/api.types.ts
index 8659261949..d72e163cd4 100644
--- a/packages/misskey-js/src/api.types.ts
+++ b/packages/misskey-js/src/api.types.ts
@@ -515,7 +515,6 @@ export type Endpoints = {
// notifications
'notifications/create': { req: { body: string; header?: string | null; icon?: string | null; }; res: null; };
'notifications/mark-all-as-read': { req: NoParams; res: null; };
- 'notifications/read': { req: { notificationId: Notification['id']; }; res: null; };
// page-push
'page-push': { req: { pageId: Page['id']; event: string; var?: any; }; res: null; };
diff --git a/packages/sw/src/scripts/notification-read.ts b/packages/sw/src/scripts/notification-read.ts
deleted file mode 100644
index 3b1dde0cd5..0000000000
--- a/packages/sw/src/scripts/notification-read.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import { get } from 'idb-keyval';
-import { pushNotificationDataMap } from '@/types';
-import { api } from '@/scripts/operations';
-
-type Accounts = {
- [x: string]: {
- queue: string[],
- timeout: number | null
- }
-};
-
-class SwNotificationReadManager {
- private accounts: Accounts = {};
-
- public async construct() {
- const accounts = await get('accounts');
- if (!accounts) Error('Accounts are not recorded');
-
- this.accounts = accounts.reduce((acc, e) => {
- acc[e.id] = {
- queue: [],
- timeout: null
- };
- return acc;
- }, {} as Accounts);
-
- return this;
- }
-
- // プッシュ通知の既読をサーバーに送信
- public async read(data: pushNotificationDataMap[keyof pushNotificationDataMap]) {
- if (data.type !== 'notification' || !(data.userId in this.accounts)) return;
-
- const account = this.accounts[data.userId];
-
- account.queue.push(data.body.id as string);
-
- if (account.queue.length >= 20) {
- if (account.timeout) clearTimeout(account.timeout);
- const notificationIds = account.queue;
- account.queue = [];
- await api('notifications/read', data.userId, { notificationIds });
- return;
- }
-
- // 最後の呼び出しから200ms待ってまとめて処理する
- if (account.timeout) clearTimeout(account.timeout);
- account.timeout = setTimeout(() => {
- account.timeout = null;
-
- const notificationIds = account.queue;
- account.queue = [];
- api('notifications/read', data.userId, { notificationIds });
- }, 200);
- }
-}
-
-export const swNotificationRead = (new SwNotificationReadManager()).construct();
diff --git a/packages/sw/src/sw.ts b/packages/sw/src/sw.ts
index 6f4c487354..9ceef890dd 100644
--- a/packages/sw/src/sw.ts
+++ b/packages/sw/src/sw.ts
@@ -1,6 +1,6 @@
import { createEmptyNotification, createNotification } from '@/scripts/create-notification';
import { swLang } from '@/scripts/lang';
-import { swNotificationRead } from '@/scripts/notification-read';
+import { api } from '@/scripts/operations';
import { pushNotificationDataMap } from '@/types';
import * as swos from '@/scripts/operations';
import { acct as getAcct } from '@/filters/user';
@@ -54,30 +54,6 @@ globalThis.addEventListener('push', ev => {
if ((new Date()).getTime() - data.dateTime > 1000 * 60 * 60 * 24) break;
return createNotification(data);
- case 'readAllNotifications':
- for (const n of await globalThis.registration.getNotifications()) {
- if (n?.data?.type === 'notification') n.close();
- }
- break;
- case 'readAllAntennas':
- for (const n of await globalThis.registration.getNotifications()) {
- if (n?.data?.type === 'unreadAntennaNote') n.close();
- }
- break;
- case 'readNotifications':
- for (const n of await globalThis.registration.getNotifications()) {
- if (data.body.notificationIds.includes(n.data.body.id)) {
- n.close();
- }
- }
- break;
- case 'readAntenna':
- for (const n of await globalThis.registration.getNotifications()) {
- if (n?.data?.type === 'unreadAntennaNote' && data.body.antennaId === n.data.body.antenna.id) {
- n.close();
- }
- }
- break;
}
await createEmptyNotification();
@@ -154,7 +130,7 @@ globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEv
client.focus();
}
if (data.type === 'notification') {
- swNotificationRead.then(that => that.read(data));
+ api('notifications/mark-all-as-read', data.userId);
}
notification.close();
@@ -165,7 +141,7 @@ globalThis.addEventListener('notificationclose', (ev: ServiceWorkerGlobalScopeEv
const data: pushNotificationDataMap[keyof pushNotificationDataMap] = ev.notification.data;
if (data.type === 'notification') {
- swNotificationRead.then(that => that.read(data));
+ api('notifications/mark-all-as-read', data.userId);
}
});
diff --git a/packages/sw/src/types.ts b/packages/sw/src/types.ts
index 5b53ddecac..176b181be0 100644
--- a/packages/sw/src/types.ts
+++ b/packages/sw/src/types.ts
@@ -17,10 +17,6 @@ type pushNotificationDataSourceMap = {
antenna: { id: string, name: string };
note: Misskey.entities.Note;
};
- readNotifications: { notificationIds: string[] };
- readAllNotifications: undefined;
- readAntenna: { antennaId: string };
- readAllAntennas: undefined;
};
export type pushNotificationData<K extends keyof pushNotificationDataSourceMap> = {