enhance(backend): チャンネルの既読管理を削除

- 現状上手く機能していない
- パフォーマンス上の理由
- 実装するにしてももっと効率的な方法がある
This commit is contained in:
syuilo 2023-04-05 07:52:49 +09:00
parent ecaf152b4a
commit 625fed8838
9 changed files with 6 additions and 101 deletions

View File

@ -502,18 +502,6 @@ export class NoteCreateService implements OnApplicationShutdown {
}); });
} }
// Channel
if (note.channelId) {
this.channelFollowingsRepository.findBy({ followeeId: note.channelId }).then(followings => {
for (const following of followings) {
this.noteReadService.insertNoteUnread(following.followerId, note, {
isSpecified: false,
isMentioned: false,
});
}
});
}
if (data.reply) { if (data.reply) {
this.saveReply(data.reply, note); this.saveReply(data.reply, note);
} }

View File

@ -1,28 +1,20 @@
import { setTimeout } from 'node:timers/promises'; import { setTimeout } from 'node:timers/promises';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import { In, IsNull, Not } from 'typeorm'; import { In } from 'typeorm';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { User } from '@/models/entities/User.js'; import type { User } from '@/models/entities/User.js';
import type { Channel } from '@/models/entities/Channel.js';
import type { Packed } from '@/misc/json-schema.js'; import type { Packed } from '@/misc/json-schema.js';
import type { Note } from '@/models/entities/Note.js'; import type { Note } from '@/models/entities/Note.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { UsersRepository, NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository, FollowingsRepository, ChannelFollowingsRepository } from '@/models/index.js'; import type { NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository } from '@/models/index.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { NotificationService } from './NotificationService.js';
import { AntennaService } from './AntennaService.js';
import { PushNotificationService } from './PushNotificationService.js';
@Injectable() @Injectable()
export class NoteReadService implements OnApplicationShutdown { export class NoteReadService implements OnApplicationShutdown {
#shutdownController = new AbortController(); #shutdownController = new AbortController();
constructor( constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.noteUnreadsRepository) @Inject(DI.noteUnreadsRepository)
private noteUnreadsRepository: NoteUnreadsRepository, private noteUnreadsRepository: NoteUnreadsRepository,
@ -32,18 +24,8 @@ export class NoteReadService implements OnApplicationShutdown {
@Inject(DI.noteThreadMutingsRepository) @Inject(DI.noteThreadMutingsRepository)
private noteThreadMutingsRepository: NoteThreadMutingsRepository, private noteThreadMutingsRepository: NoteThreadMutingsRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,
private userEntityService: UserEntityService,
private idService: IdService, private idService: IdService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private notificationService: NotificationService,
private antennaService: AntennaService,
private pushNotificationService: PushNotificationService,
) { ) {
} }
@ -54,7 +36,6 @@ export class NoteReadService implements OnApplicationShutdown {
isMentioned: boolean; isMentioned: boolean;
}): Promise<void> { }): Promise<void> {
//#region ミュートしているなら無視 //#region ミュートしているなら無視
// TODO: 現在の仕様ではChannelにミュートは適用されないのでよしなにケアする
const mute = await this.mutingsRepository.findBy({ const mute = await this.mutingsRepository.findBy({
muterId: userId, muterId: userId,
}); });
@ -74,7 +55,6 @@ export class NoteReadService implements OnApplicationShutdown {
userId: userId, userId: userId,
isSpecified: params.isSpecified, isSpecified: params.isSpecified,
isMentioned: params.isMentioned, isMentioned: params.isMentioned,
noteChannelId: note.channelId,
noteUserId: note.userId, noteUserId: note.userId,
}; };
@ -92,9 +72,6 @@ export class NoteReadService implements OnApplicationShutdown {
if (params.isSpecified) { if (params.isSpecified) {
this.globalEventService.publishMainStream(userId, 'unreadSpecifiedNote', note.id); this.globalEventService.publishMainStream(userId, 'unreadSpecifiedNote', note.id);
} }
if (note.channelId) {
this.globalEventService.publishMainStream(userId, 'unreadChannel', note.id);
}
}, () => { /* aborted, ignore it */ }); }, () => { /* aborted, ignore it */ });
} }
@ -102,22 +79,9 @@ export class NoteReadService implements OnApplicationShutdown {
public async read( public async read(
userId: User['id'], userId: User['id'],
notes: (Note | Packed<'Note'>)[], notes: (Note | Packed<'Note'>)[],
info?: {
following: Set<User['id']>;
followingChannels: Set<Channel['id']>;
},
): Promise<void> { ): Promise<void> {
const followingChannels = info?.followingChannels ? info.followingChannels : new Set<string>((await this.channelFollowingsRepository.find({
where: {
followerId: userId,
},
select: ['followeeId'],
})).map(x => x.followeeId));
const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId);
const readMentions: (Note | Packed<'Note'>)[] = []; const readMentions: (Note | Packed<'Note'>)[] = [];
const readSpecifiedNotes: (Note | Packed<'Note'>)[] = []; const readSpecifiedNotes: (Note | Packed<'Note'>)[] = [];
const readChannelNotes: (Note | Packed<'Note'>)[] = [];
for (const note of notes) { for (const note of notes) {
if (note.mentions && note.mentions.includes(userId)) { if (note.mentions && note.mentions.includes(userId)) {
@ -125,17 +89,13 @@ export class NoteReadService implements OnApplicationShutdown {
} else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) { } else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) {
readSpecifiedNotes.push(note); readSpecifiedNotes.push(note);
} }
if (note.channelId && followingChannels.has(note.channelId)) {
readChannelNotes.push(note);
}
} }
if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0) || (readChannelNotes.length > 0)) { if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0)) {
// Remove the record // Remove the record
await this.noteUnreadsRepository.delete({ await this.noteUnreadsRepository.delete({
userId: userId, userId: userId,
noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id), ...readChannelNotes.map(n => n.id)]), noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id)]),
}); });
// TODO: ↓まとめてクエリしたい // TODO: ↓まとめてクエリしたい
@ -159,16 +119,6 @@ export class NoteReadService implements OnApplicationShutdown {
this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes'); this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
} }
}); });
this.noteUnreadsRepository.countBy({
userId: userId,
noteChannelId: Not(IsNull()),
}).then(channelNoteCount => {
if (channelNoteCount === 0) {
// 全て既読になったイベントを発行
this.globalEventService.publishMainStream(userId, 'readAllChannels');
}
});
} }
} }

View File

@ -234,18 +234,6 @@ export class UserEntityService implements OnModuleInit {
return false; // TODO return false; // TODO
} }
@bindThis
public async getHasUnreadChannel(userId: User['id']): Promise<boolean> {
const channels = await this.channelFollowingsRepository.findBy({ followerId: userId });
const unread = channels.length > 0 ? await this.noteUnreadsRepository.findOneBy({
userId: userId,
noteChannelId: In(channels.map(x => x.followeeId)),
}) : null;
return unread != null;
}
@bindThis @bindThis
public async getHasUnreadNotification(userId: User['id']): Promise<boolean> { public async getHasUnreadNotification(userId: User['id']): Promise<boolean> {
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`); const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`);
@ -463,7 +451,7 @@ export class UserEntityService implements OnModuleInit {
}).then(count => count > 0), }).then(count => count > 0),
hasUnreadAnnouncement: this.getHasUnreadAnnouncement(user.id), hasUnreadAnnouncement: this.getHasUnreadAnnouncement(user.id),
hasUnreadAntenna: this.getHasUnreadAntenna(user.id), hasUnreadAntenna: this.getHasUnreadAntenna(user.id),
hasUnreadChannel: this.getHasUnreadChannel(user.id), hasUnreadChannel: false, // 後方互換性のため
hasUnreadNotification: this.getHasUnreadNotification(user.id), hasUnreadNotification: this.getHasUnreadNotification(user.id),
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id), hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
mutedWords: profile!.mutedWords, mutedWords: profile!.mutedWords,

View File

@ -311,10 +311,6 @@ export const packedMeDetailedOnlySchema = {
type: 'boolean', type: 'boolean',
nullable: false, optional: false, nullable: false, optional: false,
}, },
hasUnreadChannel: {
type: 'boolean',
nullable: false, optional: false,
},
hasUnreadNotification: { hasUnreadNotification: {
type: 'boolean', type: 'boolean',
nullable: false, optional: false, nullable: false, optional: false,

View File

@ -186,10 +186,7 @@ export default class Connection {
if (note == null) return; if (note == null) return;
if (this.user && (note.userId !== this.user.id)) { if (this.user && (note.userId !== this.user.id)) {
this.noteReadService.read(this.user.id, [note], { this.noteReadService.read(this.user.id, [note]);
following: this.following,
followingChannels: this.followingChannels,
});
} }
} }

View File

@ -97,8 +97,6 @@ export interface MainStreamTypes {
readAllAntennas: undefined; readAllAntennas: undefined;
unreadAntenna: Antenna; unreadAntenna: Antenna;
readAllAnnouncements: undefined; readAllAnnouncements: undefined;
readAllChannels: undefined;
unreadChannel: Note['id'];
myTokenRegenerated: undefined; myTokenRegenerated: undefined;
signin: Signin; signin: Signin;
registryUpdated: { registryUpdated: {

View File

@ -513,15 +513,6 @@ if ($i) {
updateAccount({ hasUnreadAnnouncement: false }); updateAccount({ hasUnreadAnnouncement: false });
}); });
main.on('readAllChannels', () => {
updateAccount({ hasUnreadChannel: false });
});
main.on('unreadChannel', () => {
updateAccount({ hasUnreadChannel: true });
sound.play('channel');
});
// トークンが再生成されたとき // トークンが再生成されたとき
// このままではMisskeyが利用できないので強制的にサインアウトさせる // このままではMisskeyが利用できないので強制的にサインアウトさせる
main.on('myTokenRegenerated', () => { main.on('myTokenRegenerated', () => {

View File

@ -88,7 +88,6 @@ export type MeDetailed = UserDetailed & {
hasPendingReceivedFollowRequest: boolean; hasPendingReceivedFollowRequest: boolean;
hasUnreadAnnouncement: boolean; hasUnreadAnnouncement: boolean;
hasUnreadAntenna: boolean; hasUnreadAntenna: boolean;
hasUnreadChannel: boolean;
hasUnreadMentions: boolean; hasUnreadMentions: boolean;
hasUnreadMessagingMessage: boolean; hasUnreadMessagingMessage: boolean;
hasUnreadNotification: boolean; hasUnreadNotification: boolean;

View File

@ -28,8 +28,6 @@ export type Channels = {
readAllAntennas: () => void; readAllAntennas: () => void;
unreadAntenna: (payload: Antenna) => void; unreadAntenna: (payload: Antenna) => void;
readAllAnnouncements: () => void; readAllAnnouncements: () => void;
readAllChannels: () => void;
unreadChannel: (payload: Note['id']) => void;
myTokenRegenerated: () => void; myTokenRegenerated: () => void;
reversiNoInvites: () => void; reversiNoInvites: () => void;
reversiInvited: (payload: FIXME) => void; reversiInvited: (payload: FIXME) => void;