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>
This commit is contained in:
parent
c6a4caa8be
commit
5e71418d5c
5 changed files with 65 additions and 18 deletions
|
@ -21,6 +21,7 @@
|
||||||
### Client
|
### Client
|
||||||
- Feat: 新しいゲームを追加
|
- Feat: 新しいゲームを追加
|
||||||
- Enhance: ハッシュタグ入力時に、本文の末尾の行に何も書かれていない場合は新たにスペースを追加しないように
|
- Enhance: ハッシュタグ入力時に、本文の末尾の行に何も書かれていない場合は新たにスペースを追加しないように
|
||||||
|
- Fix: ネイティブモードの絵文字がモノクロにならないように
|
||||||
- Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正
|
- Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正
|
||||||
- Enhance: チャンネルノートのピン留めをノートのメニューからできるよ
|
- Enhance: チャンネルノートのピン留めをノートのメニューからできるよ
|
||||||
|
|
||||||
|
|
|
@ -5,15 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<img v-if="!useOsNativeEmojis" :class="$style.root" :src="url" :alt="props.emoji" decoding="async" @pointerenter="computeTitle" @click="onClick"/>
|
<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 :alt="props.emoji" @pointerenter="computeTitle" @click="onClick">{{ colorizedNativeEmoji }}</span>
|
||||||
<span v-else>{{ emoji }}</span>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, inject } from 'vue';
|
import { computed, inject } from 'vue';
|
||||||
import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base.js';
|
import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base.js';
|
||||||
import { defaultStore } from '@/store.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 * as os from '@/os.js';
|
||||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||||
import * as sound from '@/scripts/sound.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 char2path = defaultStore.state.emojiStyle === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath;
|
||||||
|
|
||||||
const useOsNativeEmojis = computed(() => defaultStore.state.emojiStyle === 'native');
|
const useOsNativeEmojis = computed(() => defaultStore.state.emojiStyle === 'native');
|
||||||
const url = computed(() => {
|
const url = computed(() => char2path(props.emoji));
|
||||||
return 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
|
// 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 {
|
function computeTitle(event: PointerEvent): void {
|
||||||
|
|
|
@ -36,7 +36,8 @@ for (let i = 0; i < emojilist.length; i++) {
|
||||||
export const emojiCharByCategory = _charGroupByCategory;
|
export const emojiCharByCategory = _charGroupByCategory;
|
||||||
|
|
||||||
export function getEmojiName(char: string): string | null {
|
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) {
|
if (idx == null) {
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} 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 {
|
export interface CustomEmojiFolderTree {
|
||||||
value: string;
|
value: string;
|
||||||
category: string;
|
category: string;
|
||||||
|
|
41
packages/frontend/test/emoji.test.ts
Normal file
41
packages/frontend/test/emoji.test.ts
Normal file
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -17,21 +17,23 @@ updateI18n(locales['en-US']);
|
||||||
// XXX: misskey-js panics if WebSocket is not defined
|
// XXX: misskey-js panics if WebSocket is not defined
|
||||||
vi.stubGlobal('WebSocket', class WebSocket extends EventTarget { static CLOSING = 2; });
|
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?
|
// XXX: defaultStore somehow becomes undefined in vitest?
|
||||||
vi.mock('@/store.js', () => {
|
vi.mock('@/store.js', () => {
|
||||||
return {
|
return {
|
||||||
defaultStore: {
|
defaultStore: {
|
||||||
state: {
|
state: defaultStoreState,
|
||||||
|
|
||||||
// なんかtestがうまいこと動かないのでここに書く
|
|
||||||
dataSaver: {
|
|
||||||
media: false,
|
|
||||||
avatar: false,
|
|
||||||
urlPreview: false,
|
|
||||||
code: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue