feat: introduce fluent emoji
This commit is contained in:
		
							parent
							
								
									be0d396106
								
							
						
					
					
						commit
						d106fb39ab
					
				
					 15 changed files with 70 additions and 29 deletions
				
			
		|  | @ -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
									
									
								
							
							
						
						
									
										3
									
								
								.gitmodules
									
										
									
									
										vendored
									
									
								
							|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -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
									
								
							
							
						
						
									
										1
									
								
								fluent-emojis
									
										
									
									
									
										Submodule
									
								
							|  | @ -0,0 +1 @@ | ||||||
|  | Subproject commit cae981eb4c5189ea9ea3230e83b876a5068df7d1 | ||||||
|  | @ -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 | ||||||
|  | @ -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; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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), | ||||||
| 			}); | 			}); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -56,6 +56,7 @@ function getReactionName(reaction: string): string { | ||||||
| 			display: block; | 			display: block; | ||||||
| 			width: 60px; | 			width: 60px; | ||||||
| 			font-size: 60px; // unicodeな絵文字についてはwidthが効かないため | 			font-size: 60px; // unicodeな絵文字についてはwidthが効かないため | ||||||
|  | 			object-fit: contain; | ||||||
| 			margin: 0 auto; | 			margin: 0 auto; | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -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')); | ||||||
|  |  | ||||||
|  | @ -63,7 +63,7 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [ | ||||||
| 	'imageNewTab', | 	'imageNewTab', | ||||||
| 	'disableShowingAnimatedImages', | 	'disableShowingAnimatedImages', | ||||||
| 	'disablePagesScript', | 	'disablePagesScript', | ||||||
| 	'useOsNativeEmojis', | 	'emojiStyle', | ||||||
| 	'disableDrawer', | 	'disableDrawer', | ||||||
| 	'useBlurEffectForModal', | 	'useBlurEffectForModal', | ||||||
| 	'useBlurEffect', | 	'useBlurEffect', | ||||||
|  |  | ||||||
							
								
								
									
										20
									
								
								packages/client/src/scripts/emoji-base.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								packages/client/src/scripts/emoji-base.ts
									
										
									
									
									
										Normal 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`; | ||||||
|  | } | ||||||
|  | @ -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`; |  | ||||||
| } |  | ||||||
|  | @ -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', | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue