diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index cb0b079df0..96e0b80987 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -162,26 +162,30 @@ export class ReactionService { }; // Create reaction - try { - await this.noteReactionsRepository.insert(record); - } catch (e) { - if (isDuplicateKeyValueError(e)) { - const exists = await this.noteReactionsRepository.findOneByOrFail({ - noteId: note.id, - userId: user.id, - }); + const exists = await this.noteReactionsRepository.findOneBy({ + noteId: note.id, + userId: user.id, + reaction: record.reaction, + }); - if (exists.reaction !== reaction) { - // 別のリアクションがすでにされていたら置き換える - await this.delete(user, note); - await this.noteReactionsRepository.insert(record); - } else { - // 同じリアクションがすでにされていたらエラー - throw new IdentifiableError('51c42bb4-931a-456b-bff7-e5a8a70dd298'); - } + const count = await this.noteReactionsRepository.countBy({ + noteId: note.id, + userId: user.id, + }); + + if (count > 3) { + throw new IdentifiableError('51c42bb4-931a-456b-bff7-e5a8a70dd298'); + } + + if (exists == null) { + if (user.host == null) { + await this.noteReactionsRepository.insert(record); } else { - throw e; + throw new IdentifiableError('51c42bb4-931a-456b-bff7-e5a8a70dd298'); } + } else { + // 同じリアクションがすでにされていたらエラー + throw new IdentifiableError('51c42bb4-931a-456b-bff7-e5a8a70dd298'); } // Increment reactions count @@ -275,17 +279,24 @@ export class ReactionService { } @bindThis - public async delete(user: { id: MiUser['id']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote) { + public async delete(user: { id: MiUser['id']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote, reaction?: string) { // if already unreacted - const exist = await this.noteReactionsRepository.findOneBy({ - noteId: note.id, - userId: user.id, - }); - + let exist; + if (reaction == null) { + exist = await this.noteReactionsRepository.findOneBy({ + noteId: note.id, + userId: user.id, + }); + } else { + exist = await this.noteReactionsRepository.findOneBy({ + noteId: note.id, + userId: user.id, + reaction: reaction.replace(/@./, ''), + }); + } if (exist == null) { throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted'); } - // Delete reaction const result = await this.noteReactionsRepository.delete(exist.id); diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 49f643a864..675772a608 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -209,6 +209,30 @@ export class NoteEntityService implements OnModuleInit { return undefined; } + @bindThis + public async populateMyReactions(note: { id: MiNote['id']; reactions: MiNote['reactions']; reactionAndUserPairCache?: MiNote['reactionAndUserPairCache']; }, meId: MiUser['id'], _hint_?: { + myReactions: Map; + }) { + const reactionsCount = Object.values(note.reactions).reduce((a, b) => a + b, 0); + + if (reactionsCount === 0) return undefined; + + // パフォーマンスのためノートが作成されてから2秒以上経っていない場合はリアクションを取得しない + if (this.idService.parse(note.id).date.getTime() + 2000 > Date.now()) { + return undefined; + } + + const reactions = await this.noteReactionsRepository.findBy({ + userId: meId, + noteId: note.id, + }); + + if (reactions.length > 0) { + return reactions.map(reaction => this.reactionService.convertLegacyReaction(reaction.reaction)); + } + + return undefined; + } @bindThis public async isVisibleForMe(note: MiNote, meId: MiUser['id'] | null): Promise { @@ -382,6 +406,7 @@ export class NoteEntityService implements OnModuleInit { ...(meId && Object.keys(note.reactions).length > 0 ? { myReaction: this.populateMyReaction(note, meId, options?._hint_), + myReactions: this.populateMyReactions(note, meId, options?._hint_), } : {}), } : {}), }); diff --git a/packages/backend/src/models/NoteReaction.ts b/packages/backend/src/models/NoteReaction.ts index 42dfcaa9ad..26792f10b9 100644 --- a/packages/backend/src/models/NoteReaction.ts +++ b/packages/backend/src/models/NoteReaction.ts @@ -9,7 +9,8 @@ import { MiUser } from './User.js'; import { MiNote } from './Note.js'; @Entity('note_reaction') -@Index(['userId', 'noteId'], { unique: true }) +@Index(['userId', 'noteId', 'reaction'], { unique: true }) +@Index(['userId', 'noteId']) export class MiNoteReaction { @PrimaryColumn(id()) public id: string; diff --git a/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts b/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts index e6c3bbbcf5..cdc8b503e7 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions/delete.ts @@ -42,6 +42,7 @@ export const paramDef = { type: 'object', properties: { noteId: { type: 'string', format: 'misskey:id' }, + reaction: { type: 'string' }, }, required: ['noteId'], } as const; @@ -57,7 +58,7 @@ export default class extends Endpoint { // eslint- if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); throw err; }); - await this.reactionService.delete(me, note).catch(err => { + await this.reactionService.delete(me, note, ps.reaction ?? undefined).catch(err => { if (err.id === '60527ec9-b4cb-4a88-a6bd-32d3ad26817d') throw new ApiError(meta.errors.notReacted); throw err; }); diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 6eeaa15732..976ebd7a8f 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -4,43 +4,45 @@ SPDX-License-Identifier: AGPL-3.0-only -->