From 22f99b42f6e34154baff568b831099f84cb9901f Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Tue, 25 Mar 2025 18:30:28 +0900
Subject: [PATCH] enhance(frontend): refactor and improve ux

---
 packages/frontend/src/components/MkNote.vue   |  3 +-
 .../src/components/MkNoteDetailed.vue         |  4 ++-
 .../src/components/global/MkCustomEmoji.vue   |  5 ++-
 .../src/components/global/MkEmoji.vue         |  5 ++-
 packages/frontend/src/di.ts                   |  1 +
 packages/frontend/src/pages/chat/XMessage.vue | 31 +++++++++++++++++--
 6 files changed, 38 insertions(+), 11 deletions(-)

diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index 73ff85b150..07da1bd4d9 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -379,7 +379,8 @@ const keymap = {
 	},
 } as const satisfies Keymap;
 
-provide('react', (reaction: string) => {
+provide(DI.mfmEmojiReactCallback, (reaction) => {
+	sound.playMisskeySfx('reaction');
 	misskeyApi('notes/reactions/create', {
 		noteId: appearNote.value.id,
 		reaction: reaction,
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index 4f74432041..a26eb808e4 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -256,6 +256,7 @@ import { isEnabledUrlPreview } from '@/instance.js';
 import { getAppearNote } from '@/utility/get-appear-note.js';
 import { prefer } from '@/preferences.js';
 import { getPluginHandlers } from '@/plugin.js';
+import { DI } from '@/di.js';
 
 const props = withDefaults(defineProps<{
 	note: Misskey.entities.Note;
@@ -337,7 +338,8 @@ const keymap = {
 	},
 } as const satisfies Keymap;
 
-provide('react', (reaction: string) => {
+provide(DI.mfmEmojiReactCallback, (reaction) => {
+	sound.playMisskeySfx('reaction');
 	misskeyApi('notes/reactions/create', {
 		noteId: appearNote.value.id,
 		reaction: reaction,
diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue
index af8f1d035e..dda45ceaa2 100644
--- a/packages/frontend/src/components/global/MkCustomEmoji.vue
+++ b/packages/frontend/src/components/global/MkCustomEmoji.vue
@@ -35,11 +35,11 @@ import { customEmojisMap } from '@/custom-emojis.js';
 import * as os from '@/os.js';
 import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
 import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
-import * as sound from '@/utility/sound.js';
 import { i18n } from '@/i18n.js';
 import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';
 import { $i } from '@/i.js';
 import { prefer } from '@/preferences.js';
+import { DI } from '@/di.js';
 
 const props = defineProps<{
 	name: string;
@@ -53,7 +53,7 @@ const props = defineProps<{
 	fallbackToImage?: boolean;
 }>();
 
-const react = inject<((name: string) => void) | null>('react', null);
+const react = inject(DI.mfmEmojiReactCallback);
 
 const customEmojiName = computed(() => (props.name[0] === ':' ? props.name.substring(1, props.name.length - 1) : props.name).replace('@.', ''));
 const isLocal = computed(() => !props.host && (customEmojiName.value.endsWith('@.') || !customEmojiName.value.includes('@')));
@@ -109,7 +109,6 @@ function onClick(ev: MouseEvent) {
 				icon: 'ti ti-plus',
 				action: () => {
 					react(`:${props.name}:`);
-					sound.playMisskeySfx('reaction');
 				},
 			});
 		}
diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue
index ca67a28b70..198c0d8ace 100644
--- a/packages/frontend/src/components/global/MkEmoji.vue
+++ b/packages/frontend/src/components/global/MkEmoji.vue
@@ -15,9 +15,9 @@ import { char2fluentEmojiFilePath, char2twemojiFilePath } from '@@/js/emoji-base
 import type { MenuItem } from '@/types/menu.js';
 import * as os from '@/os.js';
 import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
-import * as sound from '@/utility/sound.js';
 import { i18n } from '@/i18n.js';
 import { prefer } from '@/preferences.js';
+import { DI } from '@/di.js';
 
 const props = defineProps<{
 	emoji: string;
@@ -25,7 +25,7 @@ const props = defineProps<{
 	menuReaction?: boolean;
 }>();
 
-const react = inject<((name: string) => void) | null>('react', null);
+const react = inject(DI.mfmEmojiReactCallback);
 
 const char2path = prefer.s.emojiStyle === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath;
 
@@ -59,7 +59,6 @@ function onClick(ev: MouseEvent) {
 				icon: 'ti ti-plus',
 				action: () => {
 					react(props.emoji);
-					sound.playMisskeySfx('reaction');
 				},
 			});
 		}
diff --git a/packages/frontend/src/di.ts b/packages/frontend/src/di.ts
index f9fc282315..b58c8c9659 100644
--- a/packages/frontend/src/di.ts
+++ b/packages/frontend/src/di.ts
@@ -14,4 +14,5 @@ export const DI = {
 	viewId: Symbol() as InjectionKey<string>,
 	currentStickyTop: Symbol() as InjectionKey<Ref<number>>,
 	currentStickyBottom: Symbol() as InjectionKey<Ref<number>>,
+	mfmEmojiReactCallback: Symbol() as InjectionKey<(emoji: string) => void>,
 };
diff --git a/packages/frontend/src/pages/chat/XMessage.vue b/packages/frontend/src/pages/chat/XMessage.vue
index 7ee30e7e8d..cbb817de05 100644
--- a/packages/frontend/src/pages/chat/XMessage.vue
+++ b/packages/frontend/src/pages/chat/XMessage.vue
@@ -10,7 +10,16 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div v-if="!isMe && prefer.s['chat.showSenderName']" :class="$style.header"><MkUserName :user="message.fromUser"/></div>
 		<MkFukidashi :class="$style.fukidashi" :tail="isMe ? 'right' : 'left'" :accented="isMe">
 			<div v-if="!message.isDeleted" :class="$style.content">
-				<Mfm v-if="message.text" ref="text" class="_selectable" :text="message.text" :i="$i"/>
+				<Mfm
+					v-if="message.text"
+					ref="text"
+					class="_selectable"
+					:text="message.text"
+					:i="$i"
+					:nyaize="'respect'"
+					:enableEmojiMenu="true"
+					:enableEmojiMenuReaction="true"
+				/>
 				<MkMediaList v-if="message.file" :mediaList="[message.file]" :class="$style.file"/>
 			</div>
 			<div v-else :class="$style.content">
@@ -47,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, defineAsyncComponent } from 'vue';
+import { computed, defineAsyncComponent, provide } from 'vue';
 import * as mfm from 'mfm-js';
 import * as Misskey from 'misskey-js';
 import { url } from '@@/js/config.js';
@@ -65,6 +74,7 @@ import { reactionPicker } from '@/utility/reaction-picker.js';
 import * as sound from '@/utility/sound.js';
 import MkReactionIcon from '@/components/MkReactionIcon.vue';
 import { prefer } from '@/preferences.js';
+import { DI } from '@/di.js';
 
 const $i = ensureSignin();
 
@@ -76,10 +86,17 @@ const props = defineProps<{
 const isMe = computed(() => props.message.fromUserId === $i.id);
 const urls = computed(() => props.message.text ? extractUrlFromMfm(mfm.parse(props.message.text)) : []);
 
+provide(DI.mfmEmojiReactCallback, (reaction) => {
+	sound.playMisskeySfx('reaction');
+	misskeyApi('chat/messages/react', {
+		messageId: props.message.id,
+		reaction: reaction,
+	});
+});
+
 function react(ev: MouseEvent) {
 	reactionPicker.show(ev.currentTarget ?? ev.target, null, async (reaction) => {
 		sound.playMisskeySfx('reaction');
-
 		misskeyApi('chat/messages/react', {
 			messageId: props.message.id,
 			reaction: reaction,
@@ -93,6 +110,14 @@ function onReactionClick(record: Misskey.entities.ChatMessage['reactions'][0]) {
 			messageId: props.message.id,
 			reaction: record.reaction,
 		});
+	} else {
+		if (!props.message.reactions.some(r => r.user.id === $i.id && r.reaction === record.reaction)) {
+			sound.playMisskeySfx('reaction');
+			misskeyApi('chat/messages/react', {
+				messageId: props.message.id,
+				reaction: record.reaction,
+			});
+		}
 	}
 }