diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2022-09-18 03:27:08 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-09-18 03:27:08 +0900 |
| commit | b75184ec8e3436200bacdcd832e3324702553d20 (patch) | |
| tree | 8b7e316f29e95df921db57289c8b8da476d18f07 /packages/backend/src/core/NoteReadService.ts | |
| parent | Update ROADMAP.md (diff) | |
| download | sharkey-b75184ec8e3436200bacdcd832e3324702553d20.tar.gz sharkey-b75184ec8e3436200bacdcd832e3324702553d20.tar.bz2 sharkey-b75184ec8e3436200bacdcd832e3324702553d20.zip | |
なんかもうめっちゃ変えた
Diffstat (limited to 'packages/backend/src/core/NoteReadService.ts')
| -rw-r--r-- | packages/backend/src/core/NoteReadService.ts | 214 |
1 files changed, 214 insertions, 0 deletions
diff --git a/packages/backend/src/core/NoteReadService.ts b/packages/backend/src/core/NoteReadService.ts new file mode 100644 index 0000000000..b1572c631a --- /dev/null +++ b/packages/backend/src/core/NoteReadService.ts @@ -0,0 +1,214 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { In, IsNull, Not } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import type { User } from '@/models/entities/User.js'; +import type { Channel } from '@/models/entities/Channel.js'; +import type { Packed } from '@/misc/schema.js'; +import type { Note } from '@/models/entities/Note.js'; +import { IdService } from '@/core/IdService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { UserEntityService } from './entities/UserEntityService.js'; +import { NotificationService } from './NotificationService.js'; +import { AntennaService } from './AntennaService.js'; + +@Injectable() +export class NoteReadService { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.noteUnreadsRepository) + private noteUnreadsRepository: NoteUnreadsRepository, + + @Inject(DI.mutingsRepository) + private mutingsRepository: MutingsRepository, + + @Inject(DI.noteThreadMutingsRepository) + private noteThreadMutingsRepository: NoteThreadMutingsRepository, + + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + @Inject(DI.channelFollowingsRepository) + private channelFollowingsRepository: ChannelFollowingsRepository, + + @Inject(DI.antennaNotesRepository) + private antennaNotesRepository: AntennaNotesRepository, + + private userEntityService: UserEntityService, + private idService: IdService, + private globalEventServie: GlobalEventService, + private notificationService: NotificationService, + private antennaService: AntennaService, + ) { + } + + public async insertNoteUnread(userId: User['id'], note: Note, params: { + // NOTE: isSpecifiedがtrueならisMentionedは必ずfalse + isSpecified: boolean; + isMentioned: boolean; + }): Promise<void> { + //#region ミュートしているなら無視 + // TODO: 現在の仕様ではChannelにミュートは適用されないのでよしなにケアする + const mute = await this.mutingsRepository.findBy({ + muterId: userId, + }); + if (mute.map(m => m.muteeId).includes(note.userId)) return; + //#endregion + + // スレッドミュート + const threadMute = await this.noteThreadMutingsRepository.findOneBy({ + userId: userId, + threadId: note.threadId ?? note.id, + }); + if (threadMute) return; + + const unread = { + id: this.idService.genId(), + noteId: note.id, + userId: userId, + isSpecified: params.isSpecified, + isMentioned: params.isMentioned, + noteChannelId: note.channelId, + noteUserId: note.userId, + }; + + await this.noteUnreadsRepository.insert(unread); + + // 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する + setTimeout(async () => { + const exist = await this.noteUnreadsRepository.findOneBy({ id: unread.id }); + + if (exist == null) return; + + if (params.isMentioned) { + this.globalEventServie.publishMainStream(userId, 'unreadMention', note.id); + } + if (params.isSpecified) { + this.globalEventServie.publishMainStream(userId, 'unreadSpecifiedNote', note.id); + } + if (note.channelId) { + this.globalEventServie.publishMainStream(userId, 'unreadChannel', note.id); + } + }, 2000); + } + + public async read( + userId: User['id'], + notes: (Note | Packed<'Note'>)[], + info?: { + following: Set<User['id']>; + followingChannels: Set<Channel['id']>; + }, + ): Promise<void> { + const following = info?.following ? info.following : new Set<string>((await this.followingsRepository.find({ + where: { + followerId: userId, + }, + select: ['followeeId'], + })).map(x => x.followeeId)); + 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 readSpecifiedNotes: (Note | Packed<'Note'>)[] = []; + const readChannelNotes: (Note | Packed<'Note'>)[] = []; + const readAntennaNotes: (Note | Packed<'Note'>)[] = []; + + for (const note of notes) { + if (note.mentions && note.mentions.includes(userId)) { + readMentions.push(note); + } else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) { + readSpecifiedNotes.push(note); + } + + if (note.channelId && followingChannels.has(note.channelId)) { + readChannelNotes.push(note); + } + + if (note.user != null) { // たぶんnullになることは無いはずだけど一応 + for (const antenna of myAntennas) { + if (await this.antennaService.checkHitAntenna(antenna, note, note.user, undefined, Array.from(following))) { + readAntennaNotes.push(note); + } + } + } + } + + if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0) || (readChannelNotes.length > 0)) { + // Remove the record + await this.noteUnreadsRepository.delete({ + userId: userId, + noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id), ...readChannelNotes.map(n => n.id)]), + }); + + // TODO: ↓まとめてクエリしたい + + this.noteUnreadsRepository.countBy({ + userId: userId, + isMentioned: true, + }).then(mentionsCount => { + if (mentionsCount === 0) { + // 全て既読になったイベントを発行 + this.globalEventServie.publishMainStream(userId, 'readAllUnreadMentions'); + } + }); + + this.noteUnreadsRepository.countBy({ + userId: userId, + isSpecified: true, + }).then(specifiedCount => { + if (specifiedCount === 0) { + // 全て既読になったイベントを発行 + this.globalEventServie.publishMainStream(userId, 'readAllUnreadSpecifiedNotes'); + } + }); + + this.noteUnreadsRepository.countBy({ + userId: userId, + noteChannelId: Not(IsNull()), + }).then(channelNoteCount => { + if (channelNoteCount === 0) { + // 全て既読になったイベントを発行 + this.globalEventServie.publishMainStream(userId, 'readAllChannels'); + } + }); + + this.notificationService.readNotificationByQuery(userId, { + noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id)]), + }); + } + + if (readAntennaNotes.length > 0) { + await this.antennaNotesRepository.update({ + antennaId: In(myAntennas.map(a => a.id)), + noteId: In(readAntennaNotes.map(n => n.id)), + }, { + read: true, + }); + + // TODO: まとめてクエリしたい + for (const antenna of myAntennas) { + const count = await this.antennaNotesRepository.countBy({ + antennaId: antenna.id, + read: false, + }); + + if (count === 0) { + this.globalEventServie.publishMainStream(userId, 'readAntenna', antenna); + } + } + + this.userEntityService.getHasUnreadAntenna(userId).then(unread => { + if (!unread) { + this.globalEventServie.publishMainStream(userId, 'readAllAntennas'); + } + }); + } + } +} |