From 5e71418d5caca1cea333ee1b8629987cc69c4fbc Mon Sep 17 00:00:00 2001
From: Kagami Sascha Rosylight <saschanaz@outlook.com>
Date: Sun, 7 Jan 2024 08:02:53 +0100
Subject: [PATCH] fix(frontend/emoji) restore U+FE0F for simple emojis (#12866)

* fix(frontend/emoji) restore U+FE0F for simple emojis

* Update CHANGELOG.md

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
---
 CHANGELOG.md                                  |  1 +
 .../src/components/global/MkEmoji.vue         | 10 ++---
 packages/frontend/src/scripts/emojilist.ts    |  7 +++-
 packages/frontend/test/emoji.test.ts          | 41 +++++++++++++++++++
 packages/frontend/test/init.ts                | 24 ++++++-----
 5 files changed, 65 insertions(+), 18 deletions(-)
 create mode 100644 packages/frontend/test/emoji.test.ts

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0d2fb4ccd5..474fcad674 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,6 +21,7 @@
 ### Client
 - Feat: 新しいゲームを追加
 - Enhance: ハッシュタグ入力時に、本文の末尾の行に何も書かれていない場合は新たにスペースを追加しないように
+- Fix: ネイティブモードの絵文字がモノクロにならないように
 - Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正
 - Enhance: チャンネルノートのピン留めをノートのメニューからできるよ
 
diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue
index 76ca8688d1..f6b21343b6 100644
--- a/packages/frontend/src/components/global/MkEmoji.vue
+++ b/packages/frontend/src/components/global/MkEmoji.vue
@@ -5,15 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <img v-if="!useOsNativeEmojis" :class="$style.root" :src="url" :alt="props.emoji" decoding="async" @pointerenter="computeTitle" @click="onClick"/>
-<span v-else-if="useOsNativeEmojis" :alt="props.emoji" @pointerenter="computeTitle" @click="onClick">{{ props.emoji }}</span>
-<span v-else>{{ emoji }}</span>
+<span v-else :alt="props.emoji" @pointerenter="computeTitle" @click="onClick">{{ colorizedNativeEmoji }}</span>
 </template>
 
 <script lang="ts" setup>
 import { computed, inject } from 'vue';
 import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base.js';
 import { defaultStore } from '@/store.js';
-import { getEmojiName } from '@/scripts/emojilist.js';
+import { colorizeEmoji, getEmojiName } from '@/scripts/emojilist.js';
 import * as os from '@/os.js';
 import copyToClipboard from '@/scripts/copy-to-clipboard.js';
 import * as sound from '@/scripts/sound.js';
@@ -30,9 +29,8 @@ const react = inject<((name: string) => void) | null>('react', null);
 const char2path = defaultStore.state.emojiStyle === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath;
 
 const useOsNativeEmojis = computed(() => defaultStore.state.emojiStyle === 'native');
-const url = computed(() => {
-	return char2path(props.emoji);
-});
+const url = computed(() => char2path(props.emoji));
+const colorizedNativeEmoji = computed(() => colorizeEmoji(props.emoji));
 
 // Searching from an array with 2000 items for every emoji felt like too energy-consuming, so I decided to do it lazily on pointerenter
 function computeTitle(event: PointerEvent): void {
diff --git a/packages/frontend/src/scripts/emojilist.ts b/packages/frontend/src/scripts/emojilist.ts
index 8885bf4b7f..4bd8bf94be 100644
--- a/packages/frontend/src/scripts/emojilist.ts
+++ b/packages/frontend/src/scripts/emojilist.ts
@@ -36,7 +36,8 @@ for (let i = 0; i < emojilist.length; i++) {
 export const emojiCharByCategory = _charGroupByCategory;
 
 export function getEmojiName(char: string): string | null {
-	const idx = _indexByChar.get(char);
+	// Colorize it because emojilist.json assumes that
+	const idx = _indexByChar.get(colorizeEmoji(char));
 	if (idx == null) {
 		return null;
 	} else {
@@ -44,6 +45,10 @@ export function getEmojiName(char: string): string | null {
 	}
 }
 
+export function colorizeEmoji(char: string) {
+	return char.length === 1 ? `${char}\uFE0F` : char;
+}
+
 export interface CustomEmojiFolderTree {
 	value: string;
 	category: string;
diff --git a/packages/frontend/test/emoji.test.ts b/packages/frontend/test/emoji.test.ts
new file mode 100644
index 0000000000..a1782a4913
--- /dev/null
+++ b/packages/frontend/test/emoji.test.ts
@@ -0,0 +1,41 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { describe, test, assert, afterEach } from 'vitest';
+import { render, cleanup, type RenderResult } from '@testing-library/vue';
+import { defaultStoreState } from './init.js';
+import { getEmojiName } from '@/scripts/emojilist.js';
+import { components } from '@/components/index.js';
+import { directives } from '@/directives/index.js';
+import MkEmoji from '@/components/global/MkEmoji.vue';
+
+describe('Emoji', () => {
+	const renderEmoji = (emoji: string): RenderResult => {
+		return render(MkEmoji, {
+			props: { emoji },
+			global: { directives, components },
+		});
+	};
+
+	afterEach(() => {
+		cleanup();
+		defaultStoreState.emojiStyle = '';
+	});
+
+	describe('MkEmoji', () => {
+		test('Should render selector-less heart with color in native mode', async () => {
+			defaultStoreState.emojiStyle = 'native';
+			const mkEmoji = await renderEmoji('\u2764'); // monochrome heart
+			assert.ok(mkEmoji.queryByText('\u2764\uFE0F')); // colored heart
+			assert.ok(!mkEmoji.queryByText('\u2764'));
+		});
+	});
+
+	describe('Emoji list', () => {
+		test('Should get the name of the heart', () => {
+			assert.strictEqual(getEmojiName('\u2764'), 'heart');
+		});
+	});
+});
diff --git a/packages/frontend/test/init.ts b/packages/frontend/test/init.ts
index 6d93ff8cb0..f21248cfee 100644
--- a/packages/frontend/test/init.ts
+++ b/packages/frontend/test/init.ts
@@ -17,21 +17,23 @@ updateI18n(locales['en-US']);
 // XXX: misskey-js panics if WebSocket is not defined
 vi.stubGlobal('WebSocket', class WebSocket extends EventTarget { static CLOSING = 2; });
 
+export const defaultStoreState: Record<string, unknown> = {
+
+	// なんかtestがうまいこと動かないのでここに書く
+	dataSaver: {
+		media: false,
+		avatar: false,
+		urlPreview: false,
+		code: false,
+	},
+
+};
+
 // XXX: defaultStore somehow becomes undefined in vitest?
 vi.mock('@/store.js', () => {
 	return {
 		defaultStore: {
-			state: {
-
-				// なんかtestがうまいこと動かないのでここに書く
-				dataSaver: {
-					media: false,
-					avatar: false,
-					urlPreview: false,
-					code: false,
-				},
-
-			},
+			state: defaultStoreState,
 		},
 	};
 });