feat: introduce fluent emoji

This commit is contained in:
syuilo 2022-12-26 16:04:56 +09:00
parent be0d396106
commit d106fb39ab
15 changed files with 70 additions and 29 deletions

View file

@ -14,6 +14,7 @@ packages/*/node_modules
redis/ redis/
files/ files/
misskey-assets/ misskey-assets/
fluent-emojis/
.pnp.* .pnp.*
.yarn/* .yarn/*
!.yarn/patches !.yarn/patches

3
.gitmodules vendored
View file

@ -1,3 +1,6 @@
[submodule "misskey-assets"] [submodule "misskey-assets"]
path = misskey-assets path = misskey-assets
url = https://github.com/misskey-dev/assets.git url = https://github.com/misskey-dev/assets.git
[submodule "fluent-emojis"]
path = fluent-emojis
url = https://github.com/misskey-dev/emojis.git

View file

@ -43,6 +43,7 @@ You should also include the user name that made the change.
- Client: Implement the toggle to or not to close push notifications when notifications or messages are read @tamaina - Client: Implement the toggle to or not to close push notifications when notifications or messages are read @tamaina
- Client: show Unicode emoji tooltip with its name in MkReactionsViewer.reaction @saschanaz - Client: show Unicode emoji tooltip with its name in MkReactionsViewer.reaction @saschanaz
- Client: add user list widget @syuilo - Client: add user list widget @syuilo
- Client: introduce fluent emoji @syuilo
- Client: improve overall performance of client @syuilo - Client: improve overall performance of client @syuilo
### Bugfixes ### Bugfixes

1
fluent-emojis Submodule

@ -0,0 +1 @@
Subproject commit cae981eb4c5189ea9ea3230e83b876a5068df7d1

View file

@ -456,7 +456,8 @@ language: "言語"
uiLanguage: "UIの表示言語" uiLanguage: "UIの表示言語"
groupInvited: "グループに招待されました" groupInvited: "グループに招待されました"
aboutX: "{x}について" aboutX: "{x}について"
useOsNativeEmojis: "OSネイティブの絵文字を使用" emojiStyle: "絵文字のスタイル"
native: "ネイティブ"
disableDrawer: "メニューをドロワーで表示しない" disableDrawer: "メニューをドロワーで表示しない"
youHaveNoGroups: "グループがありません" youHaveNoGroups: "グループがありません"
joinOrCreateGroup: "既存のグループに招待してもらうか、新しくグループを作成してください。" joinOrCreateGroup: "既存のグループに招待してもらうか、新しくグループを作成してください。"

@ -1 +1 @@
Subproject commit 0179793ec891856d6f37a3be16ba4c22f67a81b5 Subproject commit cf3ce27b2eb8417233072e3d6d2fb7c5356c2364

View file

@ -217,6 +217,21 @@ export class ClientServerService {
return reply.sendFile('/apple-touch-icon.png', staticAssets); return reply.sendFile('/apple-touch-icon.png', staticAssets);
}); });
fastify.get<{ Params: { path: string } }>('/fluent-emoji/:path(.*)', async (request, reply) => {
const path = request.params.path;
if (!path.match(/^[0-9a-f-]+\.png$/)) {
reply.code(404);
return;
}
reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\'');
return await reply.sendFile(path, `${_dirname}/../../../../../fluent-emojis/dist/`, {
maxAge: ms('30 days'),
});
});
fastify.get<{ Params: { path: string } }>('/twemoji/:path(.*)', async (request, reply) => { fastify.get<{ Params: { path: string } }>('/twemoji/:path(.*)', async (request, reply) => {
const path = request.params.path; const path = request.params.path;

View file

@ -18,7 +18,7 @@
<ol v-else-if="emojis.length > 0" ref="suggests" class="emojis"> <ol v-else-if="emojis.length > 0" ref="suggests" class="emojis">
<li v-for="emoji in emojis" tabindex="-1" @click="complete(type, emoji.emoji)" @keydown="onKeydown"> <li v-for="emoji in emojis" tabindex="-1" @click="complete(type, emoji.emoji)" @keydown="onKeydown">
<span v-if="emoji.isCustomEmoji" class="emoji"><img :src="defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url" :alt="emoji.emoji"/></span> <span v-if="emoji.isCustomEmoji" class="emoji"><img :src="defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url" :alt="emoji.emoji"/></span>
<span v-else-if="!defaultStore.state.useOsNativeEmojis" class="emoji"><img :src="emoji.url" :alt="emoji.emoji"/></span> <span v-else-if="defaultStore.state.emojiStyle != 'native'" class="emoji"><img :src="emoji.url" :alt="emoji.emoji"/></span>
<span v-else class="emoji">{{ emoji.emoji }}</span> <span v-else class="emoji">{{ emoji.emoji }}</span>
<!-- eslint-disable-next-line vue/no-v-html --> <!-- eslint-disable-next-line vue/no-v-html -->
<span class="name" v-html="emoji.name.replace(q, `<b>${q}</b>`)"></span> <span class="name" v-html="emoji.name.replace(q, `<b>${q}</b>`)"></span>
@ -36,7 +36,7 @@
<script lang="ts"> <script lang="ts">
import { markRaw, ref, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'; import { markRaw, ref, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
import contains from '@/scripts/contains'; import contains from '@/scripts/contains';
import { char2filePath } from '@/scripts/twemoji-base'; import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base';
import { getStaticImageUrl } from '@/scripts/get-static-image-url'; import { getStaticImageUrl } from '@/scripts/get-static-image-url';
import { acct } from '@/filters/user'; import { acct } from '@/filters/user';
import * as os from '@/os'; import * as os from '@/os';
@ -59,9 +59,11 @@ const lib = emojilist.filter(x => x.category !== 'flags');
const emjdb: EmojiDef[] = lib.map(x => ({ const emjdb: EmojiDef[] = lib.map(x => ({
emoji: x.char, emoji: x.char,
name: x.name, name: x.name,
url: char2filePath(x.char), url: char2path(x.char),
})); }));
const char2path = defaultStore.state.emojiStyle === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath;
for (const x of lib) { for (const x of lib) {
if (x.keywords) { if (x.keywords) {
for (const k of x.keywords) { for (const k of x.keywords) {
@ -69,7 +71,7 @@ for (const x of lib) {
emoji: x.char, emoji: x.char,
name: k, name: k,
aliasOf: x.name, aliasOf: x.name,
url: char2filePath(x.char), url: char2path(x.char),
}); });
} }
} }

View file

@ -56,6 +56,7 @@ function getReactionName(reaction: string): string {
display: block; display: block;
width: 60px; width: 60px;
font-size: 60px; // unicodewidth font-size: 60px; // unicodewidth
object-fit: contain;
margin: 0 auto; margin: 0 auto;
} }

View file

@ -9,7 +9,7 @@
import { computed } from 'vue'; import { computed } from 'vue';
import { CustomEmoji } from 'misskey-js/built/entities'; import { CustomEmoji } from 'misskey-js/built/entities';
import { getStaticImageUrl } from '@/scripts/get-static-image-url'; import { getStaticImageUrl } from '@/scripts/get-static-image-url';
import { char2filePath } from '@/scripts/twemoji-base'; import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
import { instance } from '@/instance'; import { instance } from '@/instance';
import { getEmojiName } from '@/scripts/emojilist'; import { getEmojiName } from '@/scripts/emojilist';
@ -22,14 +22,16 @@ const props = defineProps<{
isReaction?: boolean; isReaction?: boolean;
}>(); }>();
const char2path = defaultStore.state.emojiStyle === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath;
const isCustom = computed(() => props.emoji.startsWith(':')); const isCustom = computed(() => props.emoji.startsWith(':'));
const char = computed(() => isCustom.value ? undefined : props.emoji); const char = computed(() => isCustom.value ? undefined : props.emoji);
const useOsNativeEmojis = computed(() => defaultStore.state.useOsNativeEmojis && !props.isReaction); const useOsNativeEmojis = computed(() => defaultStore.state.emojiStyle === 'native' && !props.isReaction);
const ce = computed(() => props.customEmojis ?? instance.emojis ?? []); const ce = computed(() => props.customEmojis ?? instance.emojis ?? []);
const customEmoji = computed(() => isCustom.value ? ce.value.find(x => x.name === props.emoji.substr(1, props.emoji.length - 2)) : undefined); const customEmoji = computed(() => isCustom.value ? ce.value.find(x => x.name === props.emoji.substr(1, props.emoji.length - 2)) : undefined);
const url = computed(() => { const url = computed(() => {
if (char.value) { if (char.value) {
return char2filePath(char.value); return char2path(char.value);
} else { } else {
const rawUrl = (customEmoji.value as CustomEmoji).url; const rawUrl = (customEmoji.value as CustomEmoji).url;
return defaultStore.state.disableShowingAnimatedImages return defaultStore.state.disableShowingAnimatedImages

View file

@ -48,10 +48,16 @@
<FormSwitch v-model="disableShowingAnimatedImages" class="_formBlock">{{ i18n.ts.disableShowingAnimatedImages }}</FormSwitch> <FormSwitch v-model="disableShowingAnimatedImages" class="_formBlock">{{ i18n.ts.disableShowingAnimatedImages }}</FormSwitch>
<FormSwitch v-model="squareAvatars" class="_formBlock">{{ i18n.ts.squareAvatars }}</FormSwitch> <FormSwitch v-model="squareAvatars" class="_formBlock">{{ i18n.ts.squareAvatars }}</FormSwitch>
<FormSwitch v-model="useSystemFont" class="_formBlock">{{ i18n.ts.useSystemFont }}</FormSwitch> <FormSwitch v-model="useSystemFont" class="_formBlock">{{ i18n.ts.useSystemFont }}</FormSwitch>
<FormSwitch v-model="useOsNativeEmojis" class="_formBlock"> <div class="_formBlock">
{{ i18n.ts.useOsNativeEmojis }} <FormRadios v-model="emojiStyle">
<div><Mfm :key="useOsNativeEmojis" text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></div> <template #label>{{ i18n.ts.emojiStyle }}</template>
</FormSwitch> <option value="native">{{ i18n.ts.native }}</option>
<option value="fluentEmoji">Fluent Emoji</option>
<option value="twemoji">Twemoji</option>
</FormRadios>
<div style="margin: 8px 0 0 0; font-size: 1.5em;"><Mfm :key="emojiStyle" text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></div>
</div>
<FormSwitch v-model="disableDrawer" class="_formBlock">{{ i18n.ts.disableDrawer }}</FormSwitch> <FormSwitch v-model="disableDrawer" class="_formBlock">{{ i18n.ts.disableDrawer }}</FormSwitch>
<FormRadios v-model="fontSize" class="_formBlock"> <FormRadios v-model="fontSize" class="_formBlock">
@ -129,7 +135,7 @@ const useBlurEffectForModal = computed(defaultStore.makeGetterSetter('useBlurEff
const useBlurEffect = computed(defaultStore.makeGetterSetter('useBlurEffect')); const useBlurEffect = computed(defaultStore.makeGetterSetter('useBlurEffect'));
const showGapBetweenNotesInTimeline = computed(defaultStore.makeGetterSetter('showGapBetweenNotesInTimeline')); const showGapBetweenNotesInTimeline = computed(defaultStore.makeGetterSetter('showGapBetweenNotesInTimeline'));
const disableAnimatedMfm = computed(defaultStore.makeGetterSetter('animatedMfm', v => !v, v => !v)); const disableAnimatedMfm = computed(defaultStore.makeGetterSetter('animatedMfm', v => !v, v => !v));
const useOsNativeEmojis = computed(defaultStore.makeGetterSetter('useOsNativeEmojis')); const emojiStyle = computed(defaultStore.makeGetterSetter('emojiStyle'));
const disableDrawer = computed(defaultStore.makeGetterSetter('disableDrawer')); const disableDrawer = computed(defaultStore.makeGetterSetter('disableDrawer'));
const disableShowingAnimatedImages = computed(defaultStore.makeGetterSetter('disableShowingAnimatedImages')); const disableShowingAnimatedImages = computed(defaultStore.makeGetterSetter('disableShowingAnimatedImages'));
const loadRawImages = computed(defaultStore.makeGetterSetter('loadRawImages')); const loadRawImages = computed(defaultStore.makeGetterSetter('loadRawImages'));

View file

@ -63,7 +63,7 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
'imageNewTab', 'imageNewTab',
'disableShowingAnimatedImages', 'disableShowingAnimatedImages',
'disablePagesScript', 'disablePagesScript',
'useOsNativeEmojis', 'emojiStyle',
'disableDrawer', 'disableDrawer',
'useBlurEffectForModal', 'useBlurEffectForModal',
'useBlurEffect', 'useBlurEffect',

View file

@ -0,0 +1,20 @@
const twemojiSvgBase = '/twemoji';
const fluentEmojiPngBase = '/fluent-emoji';
export function char2twemojiFilePath(char: string): string {
let codes = Array.from(char).map(x => x.codePointAt(0)?.toString(16));
if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f');
codes = codes.filter(x => x && x.length);
const fileName = codes.join('-');
return `${twemojiSvgBase}/${fileName}.svg`;
}
export function char2fluentEmojiFilePath(char: string): string {
let codes = Array.from(char).map(x => x.codePointAt(0)?.toString(16));
// Fluent Emojiは国旗非対応 https://github.com/microsoft/fluentui-emoji/issues/25
if (codes[0]?.startsWith('1f1')) return char2twemojiFilePath(char);
if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f');
codes = codes.filter(x => x && x.length);
const fileName = codes.map(x => x!.padStart(4, '0')).join('-');
return `${fluentEmojiPngBase}/${fileName}.png`;
}

View file

@ -1,12 +0,0 @@
export const twemojiSvgBase = '/twemoji';
export function char2fileName(char: string): string {
let codes = Array.from(char).map(x => x.codePointAt(0)?.toString(16));
if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f');
codes = codes.filter(x => x && x.length);
return codes.join('-');
}
export function char2filePath(char: string): string {
return `${twemojiSvgBase}/${char2fileName(char)}.svg`;
}

View file

@ -174,9 +174,9 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device', where: 'device',
default: false, default: false,
}, },
useOsNativeEmojis: { emojiStyle: {
where: 'device', where: 'device',
default: false, default: 'twemoji', // twemoji / fluentEmoji / native
}, },
disableDrawer: { disableDrawer: {
where: 'device', where: 'device',