parent
38d0b62167
commit
30d6992684
|
@ -34,6 +34,7 @@
|
||||||
- ノート作成時のパフォーマンスを向上
|
- ノート作成時のパフォーマンスを向上
|
||||||
- アンテナのタイムライン取得時のパフォーマンスを向上
|
- アンテナのタイムライン取得時のパフォーマンスを向上
|
||||||
- チャンネルのタイムライン取得時のパフォーマンスを向上
|
- チャンネルのタイムライン取得時のパフォーマンスを向上
|
||||||
|
- 通知に関する全体的なパフォーマンスを向上
|
||||||
|
|
||||||
## 13.10.3
|
## 13.10.3
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
export class cleanup1680582195041 {
|
||||||
|
name = 'cleanup1680582195041'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`DROP TABLE "notification" `);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -169,10 +169,6 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||||
this.globalEventService.publishMainStream(userId, 'readAllChannels');
|
this.globalEventService.publishMainStream(userId, 'readAllChannels');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.notificationService.readNotificationByQuery(userId, {
|
|
||||||
noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id)]),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { setTimeout } from 'node:timers/promises';
|
import { setTimeout } from 'node:timers/promises';
|
||||||
|
import Redis from 'ioredis';
|
||||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
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 { User } from '@/models/entities/User.js';
|
||||||
import type { Notification } from '@/models/entities/Notification.js';
|
import type { Notification } from '@/models/entities/Notification.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
|
@ -17,15 +18,15 @@ export class NotificationService implements OnApplicationShutdown {
|
||||||
#shutdownController = new AbortController();
|
#shutdownController = new AbortController();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.redis)
|
||||||
|
private redisClient: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
@Inject(DI.userProfilesRepository)
|
@Inject(DI.userProfilesRepository)
|
||||||
private userProfilesRepository: UserProfilesRepository,
|
private userProfilesRepository: UserProfilesRepository,
|
||||||
|
|
||||||
@Inject(DI.notificationsRepository)
|
|
||||||
private notificationsRepository: NotificationsRepository,
|
|
||||||
|
|
||||||
@Inject(DI.mutingsRepository)
|
@Inject(DI.mutingsRepository)
|
||||||
private mutingsRepository: MutingsRepository,
|
private mutingsRepository: MutingsRepository,
|
||||||
|
|
||||||
|
@ -38,50 +39,31 @@ export class NotificationService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async readNotification(
|
public async readAllNotification(
|
||||||
userId: User['id'],
|
userId: User['id'],
|
||||||
notificationIds: Notification['id'][],
|
|
||||||
) {
|
) {
|
||||||
if (notificationIds.length === 0) return;
|
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`);
|
||||||
|
|
||||||
// Update documents
|
const latestNotificationIdsRes = await this.redisClient.xrevrange(
|
||||||
const result = await this.notificationsRepository.update({
|
`notificationTimeline:${userId}`,
|
||||||
notifieeId: userId,
|
'+',
|
||||||
id: In(notificationIds),
|
'-',
|
||||||
isRead: false,
|
'COUNT', 1);
|
||||||
}, {
|
console.log('latestNotificationIdsRes', latestNotificationIdsRes);
|
||||||
isRead: true,
|
const latestNotificationId = latestNotificationIdsRes[0]?.[0];
|
||||||
});
|
|
||||||
|
|
||||||
if (result.affected === 0) return;
|
if (latestNotificationId == null) return;
|
||||||
|
|
||||||
if (!await this.userEntityService.getHasUnreadNotification(userId)) return this.postReadAllNotifications(userId);
|
this.redisClient.set(`latestReadNotification:${userId}`, latestNotificationId);
|
||||||
else return this.postReadNotifications(userId, notificationIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
if (latestReadNotificationId == null || (latestReadNotificationId < latestNotificationId)) {
|
||||||
public async readNotificationByQuery(
|
return this.postReadAllNotifications(userId);
|
||||||
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));
|
|
||||||
|
|
||||||
return this.readNotification(userId, notificationIds);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private postReadAllNotifications(userId: User['id']) {
|
private postReadAllNotifications(userId: User['id']) {
|
||||||
this.globalEventService.publishMainStream(userId, 'readAllNotifications');
|
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
|
@bindThis
|
||||||
|
@ -90,47 +72,48 @@ export class NotificationService implements OnApplicationShutdown {
|
||||||
type: Notification['type'],
|
type: Notification['type'],
|
||||||
data: Partial<Notification>,
|
data: Partial<Notification>,
|
||||||
): Promise<Notification | null> {
|
): Promise<Notification | null> {
|
||||||
if (data.notifierId && (notifieeId === data.notifierId)) {
|
// TODO: Cache
|
||||||
return null;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const profile = await this.userProfilesRepository.findOneBy({ userId: notifieeId });
|
const notification = {
|
||||||
|
|
||||||
// TODO: Cache
|
|
||||||
const isMuted = profile?.mutingNotificationTypes.includes(type);
|
|
||||||
|
|
||||||
// Create notification
|
|
||||||
const notification = await this.notificationsRepository.insert({
|
|
||||||
id: this.idService.genId(),
|
id: this.idService.genId(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
notifieeId: notifieeId,
|
|
||||||
type: type,
|
type: type,
|
||||||
// 相手がこの通知をミュートしているようなら、既読を予めつけておく
|
|
||||||
isRead: isMuted,
|
|
||||||
...data,
|
...data,
|
||||||
} as Partial<Notification>)
|
} as Notification;
|
||||||
.then(x => this.notificationsRepository.findOneByOrFail(x.identifiers[0]));
|
|
||||||
|
|
||||||
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
|
// Publish notification event
|
||||||
this.globalEventService.publishMainStream(notifieeId, 'notification', packed);
|
this.globalEventService.publishMainStream(notifieeId, 'notification', packed);
|
||||||
|
|
||||||
// 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
|
// 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
|
||||||
setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => {
|
setTimeout(2000, 'unread notification', { signal: this.#shutdownController.signal }).then(async () => {
|
||||||
const fresh = await this.notificationsRepository.findOneBy({ id: notification.id });
|
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${notifieeId}`);
|
||||||
if (fresh == null) return; // 既に削除されているかもしれない
|
if (latestReadNotificationId && (latestReadNotificationId >= notification.id)) 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
|
|
||||||
|
|
||||||
this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed);
|
this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed);
|
||||||
this.pushNotificationService.pushNotification(notifieeId, 'notification', packed);
|
this.pushNotificationService.pushNotification(notifieeId, 'notification', packed);
|
||||||
|
|
|
@ -15,10 +15,6 @@ type PushNotificationsTypes = {
|
||||||
antenna: { id: string, name: string };
|
antenna: { id: string, name: string };
|
||||||
note: Packed<'Note'>;
|
note: Packed<'Note'>;
|
||||||
};
|
};
|
||||||
'readNotifications': { notificationIds: string[] };
|
|
||||||
'readAllNotifications': undefined;
|
|
||||||
'readAntenna': { antennaId: string };
|
|
||||||
'readAllAntennas': undefined;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reduce length because push message servers have character limits
|
// Reduce length because push message servers have character limits
|
||||||
|
@ -72,14 +68,6 @@ export class PushNotificationService {
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const subscription of subscriptions) {
|
for (const subscription of subscriptions) {
|
||||||
// Continue if sendReadMessage is false
|
|
||||||
if ([
|
|
||||||
'readNotifications',
|
|
||||||
'readAllNotifications',
|
|
||||||
'readAntenna',
|
|
||||||
'readAllAntennas',
|
|
||||||
].includes(type) && !subscription.sendReadMessage) continue;
|
|
||||||
|
|
||||||
const pushSubscription = {
|
const pushSubscription = {
|
||||||
endpoint: subscription.endpoint,
|
endpoint: subscription.endpoint,
|
||||||
keys: {
|
keys: {
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { ModuleRef } from '@nestjs/core';
|
import { ModuleRef } from '@nestjs/core';
|
||||||
|
import { In } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
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 { awaitAll } from '@/misc/prelude/await-all.js';
|
||||||
import type { Notification } from '@/models/entities/Notification.js';
|
import type { Notification } from '@/models/entities/Notification.js';
|
||||||
import type { Note } from '@/models/entities/Note.js';
|
import type { Note } from '@/models/entities/Note.js';
|
||||||
|
@ -25,8 +26,11 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
constructor(
|
constructor(
|
||||||
private moduleRef: ModuleRef,
|
private moduleRef: ModuleRef,
|
||||||
|
|
||||||
@Inject(DI.notificationsRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notificationsRepository: NotificationsRepository,
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
|
@Inject(DI.usersRepository)
|
||||||
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
@Inject(DI.noteReactionsRepository)
|
@Inject(DI.noteReactionsRepository)
|
||||||
private noteReactionsRepository: NoteReactionsRepository,
|
private noteReactionsRepository: NoteReactionsRepository,
|
||||||
|
@ -48,30 +52,39 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async pack(
|
public async pack(
|
||||||
src: Notification['id'] | Notification,
|
src: Notification,
|
||||||
|
meId: User['id'],
|
||||||
options: {
|
options: {
|
||||||
_hint_?: {
|
|
||||||
packedNotes: Map<Note['id'], Packed<'Note'>>;
|
},
|
||||||
};
|
hint?: {
|
||||||
|
packedNotes: Map<Note['id'], Packed<'Note'>>;
|
||||||
|
packedUsers: Map<User['id'], Packed<'User'>>;
|
||||||
},
|
},
|
||||||
): Promise<Packed<'Notification'>> {
|
): 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 token = notification.appAccessTokenId ? await this.accessTokensRepository.findOneByOrFail({ id: notification.appAccessTokenId }) : null;
|
||||||
const noteIfNeed = NOTE_REQUIRED_NOTIFICATION_TYPES.has(notification.type) && notification.noteId != null ? (
|
const noteIfNeed = NOTE_REQUIRED_NOTIFICATION_TYPES.has(notification.type) && notification.noteId != null ? (
|
||||||
options._hint_?.packedNotes != null
|
hint?.packedNotes != null
|
||||||
? options._hint_.packedNotes.get(notification.noteId)
|
? hint.packedNotes.get(notification.noteId)
|
||||||
: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, {
|
: this.noteEntityService.pack(notification.noteId!, { id: meId }, {
|
||||||
detail: true,
|
detail: true,
|
||||||
})
|
})
|
||||||
) : undefined;
|
) : 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({
|
return await awaitAll({
|
||||||
id: notification.id,
|
id: notification.id,
|
||||||
createdAt: notification.createdAt.toISOString(),
|
createdAt: new Date(notification.createdAt).toISOString(),
|
||||||
type: notification.type,
|
type: notification.type,
|
||||||
isRead: notification.isRead,
|
|
||||||
userId: notification.notifierId,
|
userId: notification.notifierId,
|
||||||
user: notification.notifierId ? this.userEntityService.pack(notification.notifier ?? notification.notifierId) : null,
|
...(userIfNeed != null ? { user: userIfNeed } : {}),
|
||||||
...(noteIfNeed != null ? { note: noteIfNeed } : {}),
|
...(noteIfNeed != null ? { note: noteIfNeed } : {}),
|
||||||
...(notification.type === 'reaction' ? {
|
...(notification.type === 'reaction' ? {
|
||||||
reaction: notification.reaction,
|
reaction: notification.reaction,
|
||||||
|
@ -87,9 +100,6 @@ 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
|
@bindThis
|
||||||
public async packMany(
|
public async packMany(
|
||||||
notifications: Notification[],
|
notifications: Notification[],
|
||||||
|
@ -97,23 +107,29 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
) {
|
) {
|
||||||
if (notifications.length === 0) return [];
|
if (notifications.length === 0) return [];
|
||||||
|
|
||||||
for (const notification of notifications) {
|
const noteIds = notifications.map(x => x.noteId).filter(isNotNull);
|
||||||
if (meId !== notification.notifieeId) {
|
const notes = noteIds.length > 0 ? await this.notesRepository.find({
|
||||||
// because we call note packMany with meId, all notifieeId should be same as meId
|
where: { id: In(noteIds) },
|
||||||
throw new Error('TRY_TO_PACK_ANOTHER_USER_NOTIFICATION');
|
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 notes = notifications.map(x => x.note).filter(isNotNull);
|
|
||||||
const packedNotesArray = await this.noteEntityService.packMany(notes, { id: meId }, {
|
const packedNotesArray = await this.noteEntityService.packMany(notes, { id: meId }, {
|
||||||
detail: true,
|
detail: true,
|
||||||
});
|
});
|
||||||
const packedNotes = new Map(packedNotesArray.map(p => [p.id, p]));
|
const packedNotes = new Map(packedNotesArray.map(p => [p.id, p]));
|
||||||
|
|
||||||
return await Promise.all(notifications.map(x => this.pack(x, {
|
const userIds = notifications.map(x => x.notifierId).filter(isNotNull);
|
||||||
_hint_: {
|
const users = userIds.length > 0 ? await this.usersRepository.find({
|
||||||
packedNotes,
|
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,
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { In, Not } from 'typeorm';
|
import { In, Not } from 'typeorm';
|
||||||
|
import Redis from 'ioredis';
|
||||||
import Ajv from 'ajv';
|
import Ajv from 'ajv';
|
||||||
import { ModuleRef } from '@nestjs/core';
|
import { ModuleRef } from '@nestjs/core';
|
||||||
import { DI } from '@/di-symbols.js';
|
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 { Instance } from '@/models/entities/Instance.js';
|
||||||
import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
|
import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
|
||||||
import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } 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 { bindThis } from '@/decorators.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import type { OnModuleInit } from '@nestjs/common';
|
import type { OnModuleInit } from '@nestjs/common';
|
||||||
|
@ -60,6 +61,9 @@ export class UserEntityService implements OnModuleInit {
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
|
@Inject(DI.redis)
|
||||||
|
private redisClient: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@ -90,9 +94,6 @@ export class UserEntityService implements OnModuleInit {
|
||||||
@Inject(DI.channelFollowingsRepository)
|
@Inject(DI.channelFollowingsRepository)
|
||||||
private channelFollowingsRepository: ChannelFollowingsRepository,
|
private channelFollowingsRepository: ChannelFollowingsRepository,
|
||||||
|
|
||||||
@Inject(DI.notificationsRepository)
|
|
||||||
private notificationsRepository: NotificationsRepository,
|
|
||||||
|
|
||||||
@Inject(DI.userNotePiningsRepository)
|
@Inject(DI.userNotePiningsRepository)
|
||||||
private userNotePiningsRepository: UserNotePiningsRepository,
|
private userNotePiningsRepository: UserNotePiningsRepository,
|
||||||
|
|
||||||
|
@ -247,21 +248,17 @@ export class UserEntityService implements OnModuleInit {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getHasUnreadNotification(userId: User['id']): Promise<boolean> {
|
public async getHasUnreadNotification(userId: User['id']): Promise<boolean> {
|
||||||
const mute = await this.mutingsRepository.findBy({
|
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`);
|
||||||
muterId: userId,
|
|
||||||
});
|
|
||||||
const mutedUserIds = mute.map(m => m.muteeId);
|
|
||||||
|
|
||||||
const count = await this.notificationsRepository.count({
|
const latestNotificationIdsRes = await this.redisClient.xrevrange(
|
||||||
where: {
|
`notificationTimeline:${userId}`,
|
||||||
notifieeId: userId,
|
'+',
|
||||||
...(mutedUserIds.length > 0 ? { notifierId: Not(In(mutedUserIds)) } : {}),
|
'-',
|
||||||
isRead: false,
|
'COUNT', 1);
|
||||||
},
|
console.log('latestNotificationIdsRes', latestNotificationIdsRes);
|
||||||
take: 1,
|
const latestNotificationId = latestNotificationIdsRes[0]?.[0];
|
||||||
});
|
|
||||||
|
|
||||||
return count > 0;
|
return latestNotificationId != null && (latestReadNotificationId == null || latestReadNotificationId < latestNotificationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
|
|
@ -33,7 +33,6 @@ export const DI = {
|
||||||
emojisRepository: Symbol('emojisRepository'),
|
emojisRepository: Symbol('emojisRepository'),
|
||||||
driveFilesRepository: Symbol('driveFilesRepository'),
|
driveFilesRepository: Symbol('driveFilesRepository'),
|
||||||
driveFoldersRepository: Symbol('driveFoldersRepository'),
|
driveFoldersRepository: Symbol('driveFoldersRepository'),
|
||||||
notificationsRepository: Symbol('notificationsRepository'),
|
|
||||||
metasRepository: Symbol('metasRepository'),
|
metasRepository: Symbol('metasRepository'),
|
||||||
mutingsRepository: Symbol('mutingsRepository'),
|
mutingsRepository: Symbol('mutingsRepository'),
|
||||||
renoteMutingsRepository: Symbol('renoteMutingsRepository'),
|
renoteMutingsRepository: Symbol('renoteMutingsRepository'),
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { DI } from '@/di-symbols.js';
|
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 { DataSource } from 'typeorm';
|
||||||
import type { Provider } from '@nestjs/common';
|
import type { Provider } from '@nestjs/common';
|
||||||
|
|
||||||
|
@ -172,12 +172,6 @@ const $driveFoldersRepository: Provider = {
|
||||||
inject: [DI.db],
|
inject: [DI.db],
|
||||||
};
|
};
|
||||||
|
|
||||||
const $notificationsRepository: Provider = {
|
|
||||||
provide: DI.notificationsRepository,
|
|
||||||
useFactory: (db: DataSource) => db.getRepository(Notification),
|
|
||||||
inject: [DI.db],
|
|
||||||
};
|
|
||||||
|
|
||||||
const $metasRepository: Provider = {
|
const $metasRepository: Provider = {
|
||||||
provide: DI.metasRepository,
|
provide: DI.metasRepository,
|
||||||
useFactory: (db: DataSource) => db.getRepository(Meta),
|
useFactory: (db: DataSource) => db.getRepository(Meta),
|
||||||
|
@ -426,7 +420,6 @@ const $roleAssignmentsRepository: Provider = {
|
||||||
$emojisRepository,
|
$emojisRepository,
|
||||||
$driveFilesRepository,
|
$driveFilesRepository,
|
||||||
$driveFoldersRepository,
|
$driveFoldersRepository,
|
||||||
$notificationsRepository,
|
|
||||||
$metasRepository,
|
$metasRepository,
|
||||||
$mutingsRepository,
|
$mutingsRepository,
|
||||||
$renoteMutingsRepository,
|
$renoteMutingsRepository,
|
||||||
|
@ -493,7 +486,6 @@ const $roleAssignmentsRepository: Provider = {
|
||||||
$emojisRepository,
|
$emojisRepository,
|
||||||
$driveFilesRepository,
|
$driveFilesRepository,
|
||||||
$driveFoldersRepository,
|
$driveFoldersRepository,
|
||||||
$notificationsRepository,
|
|
||||||
$metasRepository,
|
$metasRepository,
|
||||||
$mutingsRepository,
|
$mutingsRepository,
|
||||||
$renoteMutingsRepository,
|
$renoteMutingsRepository,
|
||||||
|
|
|
@ -1,54 +1,19 @@
|
||||||
import { Entity, Index, JoinColumn, ManyToOne, Column, PrimaryColumn } from 'typeorm';
|
import { notificationTypes } from '@/types.js';
|
||||||
import { notificationTypes, obsoleteNotificationTypes } from '@/types.js';
|
|
||||||
import { id } from '../id.js';
|
|
||||||
import { User } from './User.js';
|
import { User } from './User.js';
|
||||||
import { Note } from './Note.js';
|
import { Note } from './Note.js';
|
||||||
import { FollowRequest } from './FollowRequest.js';
|
import { FollowRequest } from './FollowRequest.js';
|
||||||
import { AccessToken } from './AccessToken.js';
|
import { AccessToken } from './AccessToken.js';
|
||||||
|
|
||||||
@Entity()
|
export type Notification = {
|
||||||
export class Notification {
|
id: string;
|
||||||
@PrimaryColumn(id())
|
|
||||||
public id: string;
|
|
||||||
|
|
||||||
@Index()
|
// RedisのためDateではなくstring
|
||||||
@Column('timestamp with time zone', {
|
createdAt: string;
|
||||||
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;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通知の送信者(initiator)
|
* 通知の送信者(initiator)
|
||||||
*/
|
*/
|
||||||
@Index()
|
notifierId: User['id'] | null;
|
||||||
@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;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通知の種類。
|
* 通知の種類。
|
||||||
|
@ -64,104 +29,37 @@ export class Notification {
|
||||||
* achievementEarned - 実績を獲得
|
* achievementEarned - 実績を獲得
|
||||||
* app - アプリ通知
|
* app - アプリ通知
|
||||||
*/
|
*/
|
||||||
@Index()
|
type: typeof notificationTypes[number];
|
||||||
@Column('enum', {
|
|
||||||
enum: [
|
|
||||||
...notificationTypes,
|
|
||||||
...obsoleteNotificationTypes,
|
|
||||||
],
|
|
||||||
comment: 'The type of the Notification.',
|
|
||||||
})
|
|
||||||
public type: typeof notificationTypes[number];
|
|
||||||
|
|
||||||
/**
|
noteId: Note['id'] | null;
|
||||||
* 通知が読まれたかどうか
|
|
||||||
*/
|
|
||||||
@Index()
|
|
||||||
@Column('boolean', {
|
|
||||||
default: false,
|
|
||||||
comment: 'Whether the Notification is read.',
|
|
||||||
})
|
|
||||||
public isRead: boolean;
|
|
||||||
|
|
||||||
@Column({
|
followRequestId: FollowRequest['id'] | null;
|
||||||
...id(),
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
public noteId: Note['id'] | null;
|
|
||||||
|
|
||||||
@ManyToOne(type => Note, {
|
reaction: string | null;
|
||||||
onDelete: 'CASCADE',
|
|
||||||
})
|
|
||||||
@JoinColumn()
|
|
||||||
public note: Note | null;
|
|
||||||
|
|
||||||
@Column({
|
choice: number | null;
|
||||||
...id(),
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
public followRequestId: FollowRequest['id'] | null;
|
|
||||||
|
|
||||||
@ManyToOne(type => FollowRequest, {
|
achievement: string | null;
|
||||||
onDelete: 'CASCADE',
|
|
||||||
})
|
|
||||||
@JoinColumn()
|
|
||||||
public followRequest: FollowRequest | null;
|
|
||||||
|
|
||||||
@Column('varchar', {
|
|
||||||
length: 128, nullable: true,
|
|
||||||
})
|
|
||||||
public reaction: string | null;
|
|
||||||
|
|
||||||
@Column('integer', {
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
public choice: number | null;
|
|
||||||
|
|
||||||
@Column('varchar', {
|
|
||||||
length: 128, nullable: true,
|
|
||||||
})
|
|
||||||
public achievement: string | null;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* アプリ通知のbody
|
* アプリ通知のbody
|
||||||
*/
|
*/
|
||||||
@Column('varchar', {
|
customBody: string | null;
|
||||||
length: 2048, nullable: true,
|
|
||||||
})
|
|
||||||
public customBody: string | null;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* アプリ通知のheader
|
* アプリ通知のheader
|
||||||
* (省略時はアプリ名で表示されることを期待)
|
* (省略時はアプリ名で表示されることを期待)
|
||||||
*/
|
*/
|
||||||
@Column('varchar', {
|
customHeader: string | null;
|
||||||
length: 256, nullable: true,
|
|
||||||
})
|
|
||||||
public customHeader: string | null;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* アプリ通知のicon(URL)
|
* アプリ通知のicon(URL)
|
||||||
* (省略時はアプリアイコンで表示されることを期待)
|
* (省略時はアプリアイコンで表示されることを期待)
|
||||||
*/
|
*/
|
||||||
@Column('varchar', {
|
customIcon: string | null;
|
||||||
length: 1024, nullable: true,
|
|
||||||
})
|
|
||||||
public customIcon: string | null;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* アプリ通知のアプリ(のトークン)
|
* アプリ通知のアプリ(のトークン)
|
||||||
*/
|
*/
|
||||||
@Index()
|
appAccessTokenId: AccessToken['id'] | null;
|
||||||
@Column({
|
|
||||||
...id(),
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
public appAccessTokenId: AccessToken['id'] | null;
|
|
||||||
|
|
||||||
@ManyToOne(type => AccessToken, {
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
})
|
|
||||||
@JoinColumn()
|
|
||||||
public appAccessToken: AccessToken | null;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,6 @@ import { NoteFavorite } from '@/models/entities/NoteFavorite.js';
|
||||||
import { NoteReaction } from '@/models/entities/NoteReaction.js';
|
import { NoteReaction } from '@/models/entities/NoteReaction.js';
|
||||||
import { NoteThreadMuting } from '@/models/entities/NoteThreadMuting.js';
|
import { NoteThreadMuting } from '@/models/entities/NoteThreadMuting.js';
|
||||||
import { NoteUnread } from '@/models/entities/NoteUnread.js';
|
import { NoteUnread } from '@/models/entities/NoteUnread.js';
|
||||||
import { Notification } from '@/models/entities/Notification.js';
|
|
||||||
import { Page } from '@/models/entities/Page.js';
|
import { Page } from '@/models/entities/Page.js';
|
||||||
import { PageLike } from '@/models/entities/PageLike.js';
|
import { PageLike } from '@/models/entities/PageLike.js';
|
||||||
import { PasswordResetRequest } from '@/models/entities/PasswordResetRequest.js';
|
import { PasswordResetRequest } from '@/models/entities/PasswordResetRequest.js';
|
||||||
|
@ -100,7 +99,6 @@ export {
|
||||||
NoteReaction,
|
NoteReaction,
|
||||||
NoteThreadMuting,
|
NoteThreadMuting,
|
||||||
NoteUnread,
|
NoteUnread,
|
||||||
Notification,
|
|
||||||
Page,
|
Page,
|
||||||
PageLike,
|
PageLike,
|
||||||
PasswordResetRequest,
|
PasswordResetRequest,
|
||||||
|
@ -167,7 +165,6 @@ export type NoteFavoritesRepository = Repository<NoteFavorite>;
|
||||||
export type NoteReactionsRepository = Repository<NoteReaction>;
|
export type NoteReactionsRepository = Repository<NoteReaction>;
|
||||||
export type NoteThreadMutingsRepository = Repository<NoteThreadMuting>;
|
export type NoteThreadMutingsRepository = Repository<NoteThreadMuting>;
|
||||||
export type NoteUnreadsRepository = Repository<NoteUnread>;
|
export type NoteUnreadsRepository = Repository<NoteUnread>;
|
||||||
export type NotificationsRepository = Repository<Notification>;
|
|
||||||
export type PagesRepository = Repository<Page>;
|
export type PagesRepository = Repository<Page>;
|
||||||
export type PageLikesRepository = Repository<PageLike>;
|
export type PageLikesRepository = Repository<PageLike>;
|
||||||
export type PasswordResetRequestsRepository = Repository<PasswordResetRequest>;
|
export type PasswordResetRequestsRepository = Repository<PasswordResetRequest>;
|
||||||
|
|
|
@ -14,10 +14,6 @@ export const packedNotificationSchema = {
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
format: 'date-time',
|
format: 'date-time',
|
||||||
},
|
},
|
||||||
isRead: {
|
|
||||||
type: 'boolean',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
type: {
|
type: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
|
|
@ -40,7 +40,6 @@ import { NoteFavorite } from '@/models/entities/NoteFavorite.js';
|
||||||
import { NoteReaction } from '@/models/entities/NoteReaction.js';
|
import { NoteReaction } from '@/models/entities/NoteReaction.js';
|
||||||
import { NoteThreadMuting } from '@/models/entities/NoteThreadMuting.js';
|
import { NoteThreadMuting } from '@/models/entities/NoteThreadMuting.js';
|
||||||
import { NoteUnread } from '@/models/entities/NoteUnread.js';
|
import { NoteUnread } from '@/models/entities/NoteUnread.js';
|
||||||
import { Notification } from '@/models/entities/Notification.js';
|
|
||||||
import { Page } from '@/models/entities/Page.js';
|
import { Page } from '@/models/entities/Page.js';
|
||||||
import { PageLike } from '@/models/entities/PageLike.js';
|
import { PageLike } from '@/models/entities/PageLike.js';
|
||||||
import { PasswordResetRequest } from '@/models/entities/PasswordResetRequest.js';
|
import { PasswordResetRequest } from '@/models/entities/PasswordResetRequest.js';
|
||||||
|
@ -155,7 +154,6 @@ export const entities = [
|
||||||
DriveFolder,
|
DriveFolder,
|
||||||
Poll,
|
Poll,
|
||||||
PollVote,
|
PollVote,
|
||||||
Notification,
|
|
||||||
Emoji,
|
Emoji,
|
||||||
Hashtag,
|
Hashtag,
|
||||||
SwSubscription,
|
SwSubscription,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { In, LessThan } from 'typeorm';
|
import { In, LessThan } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
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 { Config } from '@/config.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
@ -20,9 +20,6 @@ export class CleanProcessorService {
|
||||||
@Inject(DI.userIpsRepository)
|
@Inject(DI.userIpsRepository)
|
||||||
private userIpsRepository: UserIpsRepository,
|
private userIpsRepository: UserIpsRepository,
|
||||||
|
|
||||||
@Inject(DI.notificationsRepository)
|
|
||||||
private notificationsRepository: NotificationsRepository,
|
|
||||||
|
|
||||||
@Inject(DI.mutedNotesRepository)
|
@Inject(DI.mutedNotesRepository)
|
||||||
private mutedNotesRepository: MutedNotesRepository,
|
private mutedNotesRepository: MutedNotesRepository,
|
||||||
|
|
||||||
|
@ -46,10 +43,6 @@ export class CleanProcessorService {
|
||||||
createdAt: LessThan(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90))),
|
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({
|
this.mutedNotesRepository.delete({
|
||||||
id: LessThan(this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90)))),
|
id: LessThan(this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90)))),
|
||||||
reason: 'word',
|
reason: 'word',
|
||||||
|
|
|
@ -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___notes_userListTimeline from './endpoints/notes/user-list-timeline.js';
|
||||||
import * as ep___notifications_create from './endpoints/notifications/create.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_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___pagePush from './endpoints/page-push.js';
|
||||||
import * as ep___pages_create from './endpoints/pages/create.js';
|
import * as ep___pages_create from './endpoints/pages/create.js';
|
||||||
import * as ep___pages_delete from './endpoints/pages/delete.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 $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_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_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 $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_create: Provider = { provide: 'ep:pages/create', useClass: ep___pages_create.default };
|
||||||
const $pages_delete: Provider = { provide: 'ep:pages/delete', useClass: ep___pages_delete.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,
|
$notes_userListTimeline,
|
||||||
$notifications_create,
|
$notifications_create,
|
||||||
$notifications_markAllAsRead,
|
$notifications_markAllAsRead,
|
||||||
$notifications_read,
|
|
||||||
$pagePush,
|
$pagePush,
|
||||||
$pages_create,
|
$pages_create,
|
||||||
$pages_delete,
|
$pages_delete,
|
||||||
|
@ -1266,7 +1263,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||||
$notes_userListTimeline,
|
$notes_userListTimeline,
|
||||||
$notifications_create,
|
$notifications_create,
|
||||||
$notifications_markAllAsRead,
|
$notifications_markAllAsRead,
|
||||||
$notifications_read,
|
|
||||||
$pagePush,
|
$pagePush,
|
||||||
$pages_create,
|
$pages_create,
|
||||||
$pages_delete,
|
$pages_delete,
|
||||||
|
|
|
@ -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___notes_userListTimeline from './endpoints/notes/user-list-timeline.js';
|
||||||
import * as ep___notifications_create from './endpoints/notifications/create.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_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___pagePush from './endpoints/page-push.js';
|
||||||
import * as ep___pages_create from './endpoints/pages/create.js';
|
import * as ep___pages_create from './endpoints/pages/create.js';
|
||||||
import * as ep___pages_delete from './endpoints/pages/delete.js';
|
import * as ep___pages_delete from './endpoints/pages/delete.js';
|
||||||
|
@ -598,7 +597,6 @@ const eps = [
|
||||||
['notes/user-list-timeline', ep___notes_userListTimeline],
|
['notes/user-list-timeline', ep___notes_userListTimeline],
|
||||||
['notifications/create', ep___notifications_create],
|
['notifications/create', ep___notifications_create],
|
||||||
['notifications/mark-all-as-read', ep___notifications_markAllAsRead],
|
['notifications/mark-all-as-read', ep___notifications_markAllAsRead],
|
||||||
['notifications/read', ep___notifications_read],
|
|
||||||
['page-push', ep___pagePush],
|
['page-push', ep___pagePush],
|
||||||
['pages/create', ep___pages_create],
|
['pages/create', ep___pages_create],
|
||||||
['pages/delete', ep___pages_delete],
|
['pages/delete', ep___pages_delete],
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
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 type { User } from '@/models/entities/User.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
|
@ -36,9 +36,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
@Inject(DI.followingsRepository)
|
@Inject(DI.followingsRepository)
|
||||||
private followingsRepository: FollowingsRepository,
|
private followingsRepository: FollowingsRepository,
|
||||||
|
|
||||||
@Inject(DI.notificationsRepository)
|
|
||||||
private notificationsRepository: NotificationsRepository,
|
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private userFollowingService: UserFollowingService,
|
private userFollowingService: UserFollowingService,
|
||||||
private userSuspendService: UserSuspendService,
|
private userSuspendService: UserSuspendService,
|
||||||
|
@ -73,7 +70,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
(async () => {
|
(async () => {
|
||||||
await this.userSuspendService.doPostSuspend(user).catch(e => {});
|
await this.userSuspendService.doPostSuspend(user).catch(e => {});
|
||||||
await this.unFollowAll(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);
|
await this.userFollowingService.unfollow(follower, followee, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
|
||||||
private async readAllNotify(notifier: User) {
|
|
||||||
await this.notificationsRepository.update({
|
|
||||||
notifierId: notifier.id,
|
|
||||||
isRead: false,
|
|
||||||
}, {
|
|
||||||
isRead: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { Brackets } from 'typeorm';
|
import { Brackets, In } from 'typeorm';
|
||||||
|
import Redis from 'ioredis';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
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 { obsoleteNotificationTypes, notificationTypes } from '@/types.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { QueryService } from '@/core/QueryService.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 { NotificationEntityService } from '@/core/entities/NotificationEntityService.js';
|
||||||
import { NotificationService } from '@/core/NotificationService.js';
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import { Notification } from '@/models/entities/Notification.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['account', 'notifications'],
|
tags: ['account', 'notifications'],
|
||||||
|
@ -38,8 +41,6 @@ export const paramDef = {
|
||||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||||
sinceId: { type: 'string', format: 'misskey:id' },
|
sinceId: { type: 'string', format: 'misskey:id' },
|
||||||
untilId: { 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 },
|
markAsRead: { type: 'boolean', default: true },
|
||||||
// 後方互換のため、廃止された通知タイプも受け付ける
|
// 後方互換のため、廃止された通知タイプも受け付ける
|
||||||
includeTypes: { type: 'array', items: {
|
includeTypes: { type: 'array', items: {
|
||||||
|
@ -56,21 +57,22 @@ export const paramDef = {
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.redis)
|
||||||
|
private redisClient: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
@Inject(DI.followingsRepository)
|
|
||||||
private followingsRepository: FollowingsRepository,
|
|
||||||
|
|
||||||
@Inject(DI.mutingsRepository)
|
@Inject(DI.mutingsRepository)
|
||||||
private mutingsRepository: MutingsRepository,
|
private mutingsRepository: MutingsRepository,
|
||||||
|
|
||||||
@Inject(DI.userProfilesRepository)
|
@Inject(DI.userProfilesRepository)
|
||||||
private userProfilesRepository: UserProfilesRepository,
|
private userProfilesRepository: UserProfilesRepository,
|
||||||
|
|
||||||
@Inject(DI.notificationsRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notificationsRepository: NotificationsRepository,
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
|
private idService: IdService,
|
||||||
private notificationEntityService: NotificationEntityService,
|
private notificationEntityService: NotificationEntityService,
|
||||||
private notificationService: NotificationService,
|
private notificationService: NotificationService,
|
||||||
private queryService: QueryService,
|
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 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 excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
|
||||||
|
|
||||||
const followingQuery = this.followingsRepository.createQueryBuilder('following')
|
const notificationsRes = await this.redisClient.xrevrange(
|
||||||
.select('following.followeeId')
|
`notificationTimeline:${me.id}`,
|
||||||
.where('following.followerId = :followerId', { followerId: me.id });
|
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+',
|
||||||
|
'-',
|
||||||
|
'COUNT', ps.limit + 1); // untilIdに指定したものも含まれるため+1
|
||||||
|
|
||||||
const mutingQuery = this.mutingsRepository.createQueryBuilder('muting')
|
if (notificationsRes.length === 0) {
|
||||||
.select('muting.muteeId')
|
return [];
|
||||||
.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());
|
|
||||||
|
|
||||||
// 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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let notifications = notificationsRes.map(x => JSON.parse(x[1][1])).filter(x => x.id !== ps.untilId) as Notification[];
|
||||||
|
|
||||||
if (includeTypes && includeTypes.length > 0) {
|
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) {
|
} 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) {
|
if (notifications.length === 0) {
|
||||||
query.andWhere('notification.isRead = false');
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const notifications = await query.take(ps.limit).getMany();
|
|
||||||
|
|
||||||
// Mark all as read
|
// Mark all as read
|
||||||
if (notifications.length > 0 && ps.markAsRead) {
|
if (ps.markAsRead) {
|
||||||
this.notificationService.readNotification(me.id, notifications.map(x => x.id));
|
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);
|
this.noteReadService.read(me.id, notes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { NotificationsRepository } from '@/models/index.js';
|
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.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 { DI } from '@/di-symbols.js';
|
||||||
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['notifications', 'account'],
|
tags: ['notifications', 'account'],
|
||||||
|
@ -23,24 +21,10 @@ export const paramDef = {
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.notificationsRepository)
|
private notificationService: NotificationService,
|
||||||
private notificationsRepository: NotificationsRepository,
|
|
||||||
|
|
||||||
private globalEventService: GlobalEventService,
|
|
||||||
private pushNotificationService: PushNotificationService,
|
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
// Update documents
|
this.notificationService.readAllNotification(me.id);
|
||||||
await this.notificationsRepository.update({
|
|
||||||
notifieeId: me.id,
|
|
||||||
isRead: false,
|
|
||||||
}, {
|
|
||||||
isRead: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 全ての通知を読みましたよというイベントを発行
|
|
||||||
this.globalEventService.publishMainStream(me.id, 'readAllNotifications');
|
|
||||||
this.pushNotificationService.pushNotification(me.id, 'readAllNotifications', undefined);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -195,8 +195,7 @@ export default class Connection {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private onReadNotification(payload: any) {
|
private onReadNotification(payload: any) {
|
||||||
if (!payload.id) return;
|
this.notificationService.readAllNotification(this.user!.id);
|
||||||
this.notificationService.readNotification(this.user!.id, [payload.id]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -83,7 +83,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, shallowRef, onMounted, onUnmounted, watch } from 'vue';
|
import { ref, shallowRef } from 'vue';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'misskey-js';
|
||||||
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||||
import MkFollowButton from '@/components/MkFollowButton.vue';
|
import MkFollowButton from '@/components/MkFollowButton.vue';
|
||||||
|
@ -94,7 +94,6 @@ import { notePage } from '@/filters/note';
|
||||||
import { userPage } from '@/filters/user';
|
import { userPage } from '@/filters/user';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { stream } from '@/stream';
|
|
||||||
import { useTooltip } from '@/scripts/use-tooltip';
|
import { useTooltip } from '@/scripts/use-tooltip';
|
||||||
import { $i } from '@/account';
|
import { $i } from '@/account';
|
||||||
|
|
||||||
|
@ -110,35 +109,6 @@ const props = withDefaults(defineProps<{
|
||||||
const elRef = shallowRef<HTMLElement>(null);
|
const elRef = shallowRef<HTMLElement>(null);
|
||||||
const reactionRef = ref(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 followRequestDone = ref(false);
|
||||||
|
|
||||||
const acceptFollowRequest = () => {
|
const acceptFollowRequest = () => {
|
||||||
|
|
|
@ -29,7 +29,6 @@ import { notificationTypes } from '@/const';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
includeTypes?: typeof notificationTypes[number][];
|
includeTypes?: typeof notificationTypes[number][];
|
||||||
unreadOnly?: boolean;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
|
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
|
||||||
|
@ -40,23 +39,17 @@ const pagination: Paging = {
|
||||||
params: computed(() => ({
|
params: computed(() => ({
|
||||||
includeTypes: props.includeTypes ?? undefined,
|
includeTypes: props.includeTypes ?? undefined,
|
||||||
excludeTypes: props.includeTypes ? undefined : $i.mutingNotificationTypes,
|
excludeTypes: props.includeTypes ? undefined : $i.mutingNotificationTypes,
|
||||||
unreadOnly: props.unreadOnly,
|
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
const onNotification = (notification) => {
|
const onNotification = (notification) => {
|
||||||
const isMuted = props.includeTypes ? !props.includeTypes.includes(notification.type) : $i.mutingNotificationTypes.includes(notification.type);
|
const isMuted = props.includeTypes ? !props.includeTypes.includes(notification.type) : $i.mutingNotificationTypes.includes(notification.type);
|
||||||
if (isMuted || document.visibilityState === 'visible') {
|
if (isMuted || document.visibilityState === 'visible') {
|
||||||
stream.send('readNotification', {
|
stream.send('readNotification');
|
||||||
id: notification.id,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isMuted) {
|
if (!isMuted) {
|
||||||
pagingComponent.value.prepend({
|
pagingComponent.value.prepend(notification);
|
||||||
...notification,
|
|
||||||
isRead: document.visibilityState === 'visible',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -65,30 +58,6 @@ let connection;
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
connection = stream.useChannel('main');
|
connection = stream.useChannel('main');
|
||||||
connection.on('notification', onNotification);
|
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(() => {
|
onUnmounted(() => {
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
<MkStickyContainer>
|
<MkStickyContainer>
|
||||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||||
<MkSpacer :content-max="800">
|
<MkSpacer :content-max="800">
|
||||||
<div v-if="tab === 'all' || tab === 'unread'">
|
<div v-if="tab === 'all'">
|
||||||
<XNotifications class="notifications" :include-types="includeTypes" :unread-only="unreadOnly"/>
|
<XNotifications class="notifications" :include-types="includeTypes"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="tab === 'mentions'">
|
<div v-else-if="tab === 'mentions'">
|
||||||
<MkNotes :pagination="mentionsPagination"/>
|
<MkNotes :pagination="mentionsPagination"/>
|
||||||
|
@ -26,7 +26,6 @@ import { notificationTypes } from '@/const';
|
||||||
|
|
||||||
let tab = $ref('all');
|
let tab = $ref('all');
|
||||||
let includeTypes = $ref<string[] | null>(null);
|
let includeTypes = $ref<string[] | null>(null);
|
||||||
let unreadOnly = $computed(() => tab === 'unread');
|
|
||||||
|
|
||||||
const mentionsPagination = {
|
const mentionsPagination = {
|
||||||
endpoint: 'notes/mentions' as const,
|
endpoint: 'notes/mentions' as const,
|
||||||
|
@ -76,10 +75,6 @@ const headerTabs = $computed(() => [{
|
||||||
key: 'all',
|
key: 'all',
|
||||||
title: i18n.ts.all,
|
title: i18n.ts.all,
|
||||||
icon: 'ti ti-point',
|
icon: 'ti ti-point',
|
||||||
}, {
|
|
||||||
key: 'unread',
|
|
||||||
title: i18n.ts.unread,
|
|
||||||
icon: 'ti ti-loader',
|
|
||||||
}, {
|
}, {
|
||||||
key: 'mentions',
|
key: 'mentions',
|
||||||
title: i18n.ts.mentions,
|
title: i18n.ts.mentions,
|
||||||
|
|
|
@ -53,9 +53,7 @@ function onNotification(notification) {
|
||||||
if ($i.mutingNotificationTypes.includes(notification.type)) return;
|
if ($i.mutingNotificationTypes.includes(notification.type)) return;
|
||||||
|
|
||||||
if (document.visibilityState === 'visible') {
|
if (document.visibilityState === 'visible') {
|
||||||
stream.send('readNotification', {
|
stream.send('readNotification');
|
||||||
id: notification.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
notifications.unshift(notification);
|
notifications.unshift(notification);
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
|
|
|
@ -515,7 +515,6 @@ export type Endpoints = {
|
||||||
// notifications
|
// notifications
|
||||||
'notifications/create': { req: { body: string; header?: string | null; icon?: string | null; }; res: null; };
|
'notifications/create': { req: { body: string; header?: string | null; icon?: string | null; }; res: null; };
|
||||||
'notifications/mark-all-as-read': { req: NoParams; res: null; };
|
'notifications/mark-all-as-read': { req: NoParams; res: null; };
|
||||||
'notifications/read': { req: { notificationId: Notification['id']; }; res: null; };
|
|
||||||
|
|
||||||
// page-push
|
// page-push
|
||||||
'page-push': { req: { pageId: Page['id']; event: string; var?: any; }; res: null; };
|
'page-push': { req: { pageId: Page['id']; event: string; var?: any; }; res: 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();
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { createEmptyNotification, createNotification } from '@/scripts/create-notification';
|
import { createEmptyNotification, createNotification } from '@/scripts/create-notification';
|
||||||
import { swLang } from '@/scripts/lang';
|
import { swLang } from '@/scripts/lang';
|
||||||
import { swNotificationRead } from '@/scripts/notification-read';
|
import { api } from '@/scripts/operations';
|
||||||
import { pushNotificationDataMap } from '@/types';
|
import { pushNotificationDataMap } from '@/types';
|
||||||
import * as swos from '@/scripts/operations';
|
import * as swos from '@/scripts/operations';
|
||||||
import { acct as getAcct } from '@/filters/user';
|
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;
|
if ((new Date()).getTime() - data.dateTime > 1000 * 60 * 60 * 24) break;
|
||||||
|
|
||||||
return createNotification(data);
|
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();
|
await createEmptyNotification();
|
||||||
|
@ -154,7 +130,7 @@ globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEv
|
||||||
client.focus();
|
client.focus();
|
||||||
}
|
}
|
||||||
if (data.type === 'notification') {
|
if (data.type === 'notification') {
|
||||||
swNotificationRead.then(that => that.read(data));
|
api('notifications/mark-all-as-read', data.userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
notification.close();
|
notification.close();
|
||||||
|
@ -165,7 +141,7 @@ globalThis.addEventListener('notificationclose', (ev: ServiceWorkerGlobalScopeEv
|
||||||
const data: pushNotificationDataMap[keyof pushNotificationDataMap] = ev.notification.data;
|
const data: pushNotificationDataMap[keyof pushNotificationDataMap] = ev.notification.data;
|
||||||
|
|
||||||
if (data.type === 'notification') {
|
if (data.type === 'notification') {
|
||||||
swNotificationRead.then(that => that.read(data));
|
api('notifications/mark-all-as-read', data.userId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -17,10 +17,6 @@ type pushNotificationDataSourceMap = {
|
||||||
antenna: { id: string, name: string };
|
antenna: { id: string, name: string };
|
||||||
note: Misskey.entities.Note;
|
note: Misskey.entities.Note;
|
||||||
};
|
};
|
||||||
readNotifications: { notificationIds: string[] };
|
|
||||||
readAllNotifications: undefined;
|
|
||||||
readAntenna: { antennaId: string };
|
|
||||||
readAllAntennas: undefined;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type pushNotificationData<K extends keyof pushNotificationDataSourceMap> = {
|
export type pushNotificationData<K extends keyof pushNotificationDataSourceMap> = {
|
||||||
|
|
Loading…
Reference in New Issue