mirror of
https://github.com/paricafe/misskey.git
synced 2025-04-12 11:09:36 -05:00
enhance: チャットのリアクションを削除できるように
This commit is contained in:
parent
8e72c68205
commit
98554579ea
11 changed files with 226 additions and 16 deletions
packages
backend/src
frontend/src/pages/chat
misskey-js
|
@ -33,6 +33,20 @@ const MAX_ROOM_MEMBERS = 30;
|
|||
const MAX_REACTIONS_PER_MESSAGE = 100;
|
||||
const isCustomEmojiRegexp = /^:([\w+-]+)(?:@\.)?:$/;
|
||||
|
||||
// TODO: ReactionServiceのやつと共通化
|
||||
function normalizeEmojiString(x: string) {
|
||||
const match = emojiRegex.exec(x);
|
||||
if (match) {
|
||||
// 合字を含む1つの絵文字
|
||||
const unicode = match[0];
|
||||
|
||||
// 異体字セレクタ除去
|
||||
return unicode.match('\u200d') ? unicode : unicode.replace(/\ufe0f/g, '');
|
||||
} else {
|
||||
throw new Error('invalid emoji');
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ChatService {
|
||||
constructor(
|
||||
|
@ -751,24 +765,10 @@ export class ChatService {
|
|||
public async react(messageId: MiChatMessage['id'], userId: MiUser['id'], reaction_: string) {
|
||||
let reaction;
|
||||
|
||||
// TODO: ReactionServiceのやつと共通化
|
||||
function normalize(x: string) {
|
||||
const match = emojiRegex.exec(x);
|
||||
if (match) {
|
||||
// 合字を含む1つの絵文字
|
||||
const unicode = match[0];
|
||||
|
||||
// 異体字セレクタ除去
|
||||
return unicode.match('\u200d') ? unicode : unicode.replace(/\ufe0f/g, '');
|
||||
} else {
|
||||
throw new Error('invalid emoji');
|
||||
}
|
||||
}
|
||||
|
||||
const custom = reaction_.match(isCustomEmojiRegexp);
|
||||
|
||||
if (custom == null) {
|
||||
reaction = normalize(reaction_);
|
||||
reaction = normalizeEmojiString(reaction_);
|
||||
} else {
|
||||
const name = custom[1];
|
||||
const emoji = (await this.customEmojiService.localEmojisCache.fetch()).get(name);
|
||||
|
@ -827,6 +827,52 @@ export class ChatService {
|
|||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async unreact(messageId: MiChatMessage['id'], userId: MiUser['id'], reaction_: string) {
|
||||
let reaction;
|
||||
|
||||
const custom = reaction_.match(isCustomEmojiRegexp);
|
||||
|
||||
if (custom == null) {
|
||||
reaction = normalizeEmojiString(reaction_);
|
||||
} else { // 削除されたカスタム絵文字のリアクションを削除したいかもしれないので絵文字の存在チェックはする必要なし
|
||||
const name = custom[1];
|
||||
reaction = `:${name}:`;
|
||||
}
|
||||
|
||||
// NOTE: 自分のリアクションを(あれば)削除するだけなので諸々の権限チェックは必要なし
|
||||
|
||||
const message = await this.chatMessagesRepository.findOneByOrFail({ id: messageId });
|
||||
|
||||
const room = message.toRoomId ? await this.chatRoomsRepository.findOneByOrFail({ id: message.toRoomId }) : null;
|
||||
|
||||
await this.chatMessagesRepository.createQueryBuilder().update()
|
||||
.set({
|
||||
reactions: () => `array_remove("reactions", '${userId}/${reaction}')`,
|
||||
})
|
||||
.where('id = :id', { id: message.id })
|
||||
.execute();
|
||||
|
||||
// TODO: 実際に削除が行われたときのみイベントを発行する
|
||||
|
||||
if (room) {
|
||||
this.globalEventService.publishChatRoomStream(room.id, 'unreact', {
|
||||
messageId: message.id,
|
||||
user: await this.userEntityService.pack(userId),
|
||||
reaction,
|
||||
});
|
||||
} else {
|
||||
this.globalEventService.publishChatUserStream(message.fromUserId, message.toUserId!, 'unreact', {
|
||||
messageId: message.id,
|
||||
reaction,
|
||||
});
|
||||
this.globalEventService.publishChatUserStream(message.toUserId!, message.fromUserId, 'unreact', {
|
||||
messageId: message.id,
|
||||
reaction,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getMyMemberships(userId: MiUser['id'], limit: number, sinceId?: MiChatRoomMembership['id'] | null, untilId?: MiChatRoomMembership['id'] | null) {
|
||||
const query = this.queryService.makePaginationQuery(this.chatRoomMembershipsRepository.createQueryBuilder('membership'), sinceId, untilId)
|
||||
|
|
|
@ -167,6 +167,11 @@ export interface ChatEventTypes {
|
|||
user?: Packed<'UserLite'>;
|
||||
messageId: MiChatMessage['id'];
|
||||
};
|
||||
unreact: {
|
||||
reaction: string;
|
||||
user?: Packed<'UserLite'>;
|
||||
messageId: MiChatMessage['id'];
|
||||
};
|
||||
}
|
||||
|
||||
export interface ReversiEventTypes {
|
||||
|
|
|
@ -401,6 +401,7 @@ export * as 'chat/messages/create-to-room' from './endpoints/chat/messages/creat
|
|||
export * as 'chat/messages/delete' from './endpoints/chat/messages/delete.js';
|
||||
export * as 'chat/messages/show' from './endpoints/chat/messages/show.js';
|
||||
export * as 'chat/messages/react' from './endpoints/chat/messages/react.js';
|
||||
export * as 'chat/messages/unreact' from './endpoints/chat/messages/unreact.js';
|
||||
export * as 'chat/messages/user-timeline' from './endpoints/chat/messages/user-timeline.js';
|
||||
export * as 'chat/messages/room-timeline' from './endpoints/chat/messages/room-timeline.js';
|
||||
export * as 'chat/messages/search' from './endpoints/chat/messages/search.js';
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ChatService } from '@/core/ChatService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['chat'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:chat',
|
||||
|
||||
res: {
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchMessage: {
|
||||
message: 'No such message.',
|
||||
code: 'NO_SUCH_MESSAGE',
|
||||
id: 'c39ea42f-e3ca-428a-ad57-390e0a711595',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
messageId: { type: 'string', format: 'misskey:id' },
|
||||
reaction: { type: 'string' },
|
||||
},
|
||||
required: ['messageId', 'reaction'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private chatService: ChatService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.chatService.unreact(ps.messageId, me.id, ps.reaction);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:moveClass="prefer.s.animation ? $style.transition_reaction_move : ''"
|
||||
tag="div" :class="$style.reactions"
|
||||
>
|
||||
<div v-for="record in message.reactions" :key="record.reaction + record.user.id" :class="$style.reaction">
|
||||
<div v-for="record in message.reactions" :key="record.reaction + record.user.id" :class="$style.reaction" @click="onReactionClick(record)">
|
||||
<MkAvatar :user="record.user" :link="false" :class="$style.reactionAvatar"/>
|
||||
<MkReactionIcon
|
||||
:withTooltip="true"
|
||||
|
@ -87,6 +87,15 @@ function react(ev: MouseEvent) {
|
|||
});
|
||||
}
|
||||
|
||||
function onReactionClick(record: Misskey.entities.ChatMessage['reactions'][0]) {
|
||||
if (record.user.id === $i.id) {
|
||||
misskeyApi('chat/messages/unreact', {
|
||||
messageId: props.message.id,
|
||||
reaction: record.reaction,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function showMenu(ev: MouseEvent) {
|
||||
const menu: MenuItem[] = [];
|
||||
|
||||
|
|
|
@ -170,6 +170,7 @@ async function initialize() {
|
|||
connection.value.on('message', onMessage);
|
||||
connection.value.on('deleted', onDeleted);
|
||||
connection.value.on('react', onReact);
|
||||
connection.value.on('unreact', onUnreact);
|
||||
} else {
|
||||
const [r, m] = await Promise.all([
|
||||
misskeyApi('chat/rooms/show', { roomId: props.roomId }),
|
||||
|
@ -189,6 +190,7 @@ async function initialize() {
|
|||
connection.value.on('message', onMessage);
|
||||
connection.value.on('deleted', onDeleted);
|
||||
connection.value.on('react', onReact);
|
||||
connection.value.on('unreact', onUnreact);
|
||||
}
|
||||
|
||||
window.document.addEventListener('visibilitychange', onVisibilitychange);
|
||||
|
@ -268,6 +270,16 @@ function onReact(ctx) {
|
|||
}
|
||||
}
|
||||
|
||||
function onUnreact(ctx) {
|
||||
const message = messages.value.find(m => m.id === ctx.messageId);
|
||||
if (message) {
|
||||
const index = message.reactions.findIndex(r => r.reaction === ctx.reaction && r.user.id === ctx.user.id);
|
||||
if (index !== -1) {
|
||||
message.reactions.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onIndicatorClick() {
|
||||
showIndicator.value = false;
|
||||
}
|
||||
|
|
|
@ -1001,6 +1001,12 @@ type ChatMessagesShowRequest = operations['chat___messages___show']['requestBody
|
|||
// @public (undocumented)
|
||||
type ChatMessagesShowResponse = operations['chat___messages___show']['responses']['200']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type ChatMessagesUnreactRequest = operations['chat___messages___unreact']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type ChatMessagesUnreactResponse = operations['chat___messages___unreact']['responses']['200']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type ChatMessagesUserTimelineRequest = operations['chat___messages___user-timeline']['requestBody']['content']['application/json'];
|
||||
|
||||
|
@ -1613,6 +1619,8 @@ declare namespace entities {
|
|||
ChatMessagesSearchResponse,
|
||||
ChatMessagesShowRequest,
|
||||
ChatMessagesShowResponse,
|
||||
ChatMessagesUnreactRequest,
|
||||
ChatMessagesUnreactResponse,
|
||||
ChatMessagesUserTimelineRequest,
|
||||
ChatMessagesUserTimelineResponse,
|
||||
ChatRoomsCreateRequest,
|
||||
|
|
|
@ -1622,6 +1622,17 @@ declare module '../api.js' {
|
|||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:chat*
|
||||
*/
|
||||
request<E extends 'chat/messages/unreact', P extends Endpoints[E]['req']>(
|
||||
endpoint: E,
|
||||
params: P,
|
||||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
|
|
|
@ -223,6 +223,8 @@ import type {
|
|||
ChatMessagesSearchResponse,
|
||||
ChatMessagesShowRequest,
|
||||
ChatMessagesShowResponse,
|
||||
ChatMessagesUnreactRequest,
|
||||
ChatMessagesUnreactResponse,
|
||||
ChatMessagesUserTimelineRequest,
|
||||
ChatMessagesUserTimelineResponse,
|
||||
ChatRoomsCreateRequest,
|
||||
|
@ -780,6 +782,7 @@ export type Endpoints = {
|
|||
'chat/messages/room-timeline': { req: ChatMessagesRoomTimelineRequest; res: ChatMessagesRoomTimelineResponse };
|
||||
'chat/messages/search': { req: ChatMessagesSearchRequest; res: ChatMessagesSearchResponse };
|
||||
'chat/messages/show': { req: ChatMessagesShowRequest; res: ChatMessagesShowResponse };
|
||||
'chat/messages/unreact': { req: ChatMessagesUnreactRequest; res: ChatMessagesUnreactResponse };
|
||||
'chat/messages/user-timeline': { req: ChatMessagesUserTimelineRequest; res: ChatMessagesUserTimelineResponse };
|
||||
'chat/rooms/create': { req: ChatRoomsCreateRequest; res: ChatRoomsCreateResponse };
|
||||
'chat/rooms/delete': { req: ChatRoomsDeleteRequest; res: ChatRoomsDeleteResponse };
|
||||
|
|
|
@ -226,6 +226,8 @@ export type ChatMessagesSearchRequest = operations['chat___messages___search']['
|
|||
export type ChatMessagesSearchResponse = operations['chat___messages___search']['responses']['200']['content']['application/json'];
|
||||
export type ChatMessagesShowRequest = operations['chat___messages___show']['requestBody']['content']['application/json'];
|
||||
export type ChatMessagesShowResponse = operations['chat___messages___show']['responses']['200']['content']['application/json'];
|
||||
export type ChatMessagesUnreactRequest = operations['chat___messages___unreact']['requestBody']['content']['application/json'];
|
||||
export type ChatMessagesUnreactResponse = operations['chat___messages___unreact']['responses']['200']['content']['application/json'];
|
||||
export type ChatMessagesUserTimelineRequest = operations['chat___messages___user-timeline']['requestBody']['content']['application/json'];
|
||||
export type ChatMessagesUserTimelineResponse = operations['chat___messages___user-timeline']['responses']['200']['content']['application/json'];
|
||||
export type ChatRoomsCreateRequest = operations['chat___rooms___create']['requestBody']['content']['application/json'];
|
||||
|
|
|
@ -1430,6 +1430,15 @@ export type paths = {
|
|||
*/
|
||||
post: operations['chat___messages___show'];
|
||||
};
|
||||
'/chat/messages/unreact': {
|
||||
/**
|
||||
* chat/messages/unreact
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:chat*
|
||||
*/
|
||||
post: operations['chat___messages___unreact'];
|
||||
};
|
||||
'/chat/messages/user-timeline': {
|
||||
/**
|
||||
* chat/messages/user-timeline
|
||||
|
@ -14424,6 +14433,61 @@ export type operations = {
|
|||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* chat/messages/unreact
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:chat*
|
||||
*/
|
||||
chat___messages___unreact: {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
/** Format: misskey:id */
|
||||
messageId: string;
|
||||
reaction: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK (with results) */
|
||||
200: {
|
||||
content: {
|
||||
'application/json': unknown;
|
||||
};
|
||||
};
|
||||
/** @description Client error */
|
||||
400: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Authentication error */
|
||||
401: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Forbidden error */
|
||||
403: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description I'm Ai */
|
||||
418: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Internal server error */
|
||||
500: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* chat/messages/user-timeline
|
||||
* @description No description provided.
|
||||
|
|
Loading…
Add table
Reference in a new issue