feat: emoji skin tone

Closes #9959
This commit is contained in:
ThatOneCalculator 2023-06-22 20:58:44 -07:00
parent a63608d9ce
commit 8f419319a7
5 changed files with 60 additions and 87 deletions

View File

@ -1109,6 +1109,7 @@ isLocked: "This account has follow approvals"
isModerator: "Moderator" isModerator: "Moderator"
isAdmin: "Administrator" isAdmin: "Administrator"
isPatron: "Calckey Patron" isPatron: "Calckey Patron"
reactionPickerSkinTone: "Preferred emoji skin tone"
_sensitiveMediaDetection: _sensitiveMediaDetection:
description: "Reduces the effort of server moderation through automatically recognizing description: "Reduces the effort of server moderation through automatically recognizing

View File

@ -111,7 +111,7 @@
<div v-once class="group"> <div v-once class="group">
<header>{{ i18n.ts.emoji }}</header> <header>{{ i18n.ts.emoji }}</header>
<XSection <XSection
v-for="category in categories" v-for="category in unicodeEmojiCategories"
:key="category" :key="category"
:emojis=" :emojis="
emojilist emojilist
@ -164,9 +164,9 @@ import { ref, computed, watch, onMounted } from "vue";
import * as Misskey from "calckey-js"; import * as Misskey from "calckey-js";
import XSection from "@/components/MkEmojiPicker.section.vue"; import XSection from "@/components/MkEmojiPicker.section.vue";
import { import {
getEmojiData, emojilist,
unicodeEmojiCategories,
UnicodeEmojiDef, UnicodeEmojiDef,
unicodeEmojiCategories as categories,
getNicelyLabeledCategory, getNicelyLabeledCategory,
} from "@/scripts/emojilist"; } from "@/scripts/emojilist";
import { getStaticImageUrl } from "@/scripts/get-static-image-url"; import { getStaticImageUrl } from "@/scripts/get-static-image-url";
@ -219,7 +219,6 @@ const height = computed(() =>
const customEmojiCategories = emojiCategories; const customEmojiCategories = emojiCategories;
const customEmojis = instance.emojis; const customEmojis = instance.emojis;
const q = ref<string | null>(null); const q = ref<string | null>(null);
const emojilist = await getEmojiData();
const searchResultCustom = ref<Misskey.entities.CustomEmoji[]>([]); const searchResultCustom = ref<Misskey.entities.CustomEmoji[]>([]);
const searchResultUnicode = ref<UnicodeEmojiDef[]>([]); const searchResultUnicode = ref<UnicodeEmojiDef[]>([]);
const tab = ref<"index" | "custom" | "unicode" | "tags">("index"); const tab = ref<"index" | "custom" | "unicode" | "tags">("index");
@ -321,7 +320,7 @@ watch(q, () => {
// //
for (const emoji of emojis) { for (const emoji of emojis) {
if (keywords.every((keyword) => emoji.name.includes(keyword))) { if (keywords.every((keyword) => emoji.slug.includes(keyword))) {
matches.add(emoji); matches.add(emoji);
if (matches.size >= max) break; if (matches.size >= max) break;
} }
@ -401,7 +400,7 @@ function reset() {
function getKey( function getKey(
emoji: string | Misskey.entities.CustomEmoji | UnicodeEmojiDef emoji: string | Misskey.entities.CustomEmoji | UnicodeEmojiDef
): string { ): string {
return typeof emoji === "string" ? emoji : emoji.char || `:${emoji.name}:`; return typeof emoji === "string" ? emoji : emoji.emoji || `:${emoji.name}:`;
} }
function chosen(emoji: any, ev?: MouseEvent) { function chosen(emoji: any, ev?: MouseEvent) {

View File

@ -41,6 +41,27 @@
> >
</FromSlot> </FromSlot>
<FormRadios v-model="reactionPickerSkinTone" class="_formBlock">
<template #label>{{ i18n.ts.reactionPickerSkinTone }}</template>
<option :value="1">
<MkEmoji :normal="true" emoji="✌️" />
</option>
<option :value="6">
<MkEmoji :normal="true" emoji="✌🏿" />
</option>
<option :value="5">
<MkEmoji :normal="true" emoji="✌🏾" />
</option>
<option :value="4">
<MkEmoji :normal="true" emoji="✌🏽" />
</option>
<option :value="3">
<MkEmoji :normal="true" emoji="✌🏼" />
</option>
<option :value="2">
<MkEmoji :normal="true" emoji="✌🏻" />
</option>
</FormRadios>
<FormRadios v-model="reactionPickerSize" class="_formBlock"> <FormRadios v-model="reactionPickerSize" class="_formBlock">
<template #label>{{ i18n.ts.size }}</template> <template #label>{{ i18n.ts.size }}</template>
<option :value="1">{{ i18n.ts.small }}</option> <option :value="1">{{ i18n.ts.small }}</option>
@ -125,6 +146,9 @@ async function reloadAsk() {
let reactions = $ref(deepClone(defaultStore.state.reactions)); let reactions = $ref(deepClone(defaultStore.state.reactions));
const reactionPickerSkinTone = $computed(
defaultStore.makeGetterSetter("reactionPickerSkinTone")
);
const reactionPickerSize = $computed( const reactionPickerSize = $computed(
defaultStore.makeGetterSetter("reactionPickerSize") defaultStore.makeGetterSetter("reactionPickerSize")
); );

View File

@ -1,6 +1,7 @@
import data from "unicode-emoji-json/data-by-group.json"; import data from "unicode-emoji-json/data-by-group.json";
import components from "unicode-emoji-json/data-emoji-components.json"; import emojiComponents from "unicode-emoji-json/data-emoji-components.json";
import keywordSet from "emojilib"; import keywordSet from "emojilib";
import { defaultStore } from "@/store";
export const unicodeEmojiCategories = [ export const unicodeEmojiCategories = [
"emotion", "emotion",
@ -26,13 +27,16 @@ export const categoryMapping = {
"Flags": "flags", "Flags": "flags",
} as const; } as const;
const skinToneModifiers = [ function addSkinTone(emoji: string) {
"light_skin_tone", const skinTone = defaultStore.state.reactionPickerSkinTone;
"medium_light_skin_tone", if (skinTone === 1) return emoji;
"medium_skin_tone", if (skinTone === 2) return emoji + emojiComponents.light_skin_tone;
"medium_dark_skin_tone", if (skinTone === 3) return emoji + emojiComponents.medium_light_skin_tone;
"dark_skin_tone", if (skinTone === 4) return emoji + emojiComponents.medium_skin_tone;
]; if (skinTone === 5) return emoji + emojiComponents.medium_dark_skin_tone;
if (skinTone === 6) return emoji + emojiComponents.dark_skin_tone;
return emoji;
}
const newData = {}; const newData = {};
@ -42,17 +46,12 @@ Object.keys(data).forEach((originalCategory) => {
newData[newCategory] = newData[newCategory] || []; newData[newCategory] = newData[newCategory] || [];
Object.keys(data[originalCategory]).forEach((emojiIndex) => { Object.keys(data[originalCategory]).forEach((emojiIndex) => {
const emojiObj = { ...data[originalCategory][emojiIndex] }; const emojiObj = { ...data[originalCategory][emojiIndex] };
if (emojiObj.skin_tone_support) {
emojiObj.emoji = addSkinTone(emojiObj.emoji);
}
emojiObj.category = newCategory;
emojiObj.keywords = keywordSet[emojiObj.emoji]; emojiObj.keywords = keywordSet[emojiObj.emoji];
newData[newCategory].push(emojiObj); newData[newCategory].push(emojiObj);
if (emojiObj.skin_tone_support) {
skinToneModifiers.forEach((modifier) => {
const modifiedEmojiObj = { ...emojiObj };
modifiedEmojiObj.emoji += components[modifier];
modifiedEmojiObj.skin_tone = modifier;
newData[newCategory].push(modifiedEmojiObj);
});
}
}); });
} }
}); });
@ -60,76 +59,22 @@ Object.keys(data).forEach((originalCategory) => {
export type UnicodeEmojiDef = { export type UnicodeEmojiDef = {
emoji: string; emoji: string;
category: typeof unicodeEmojiCategories[number]; category: typeof unicodeEmojiCategories[number];
skin_tone_support: boolean;
name: string;
slug: string; slug: string;
emoji_version: string;
skin_tone?: string;
keywords?: string[]; keywords?: string[];
}; };
export const emojilist = newData as UnicodeEmojiDef[]; export const emojilist: UnicodeEmojiDef[] = Object.keys(newData).reduce((acc, category) => {
const categoryItems = newData[category].map((item) => {
const storeName = "emojiList"; return {
emoji: item.emoji,
function openDatabase() { slug: item.slug,
return new Promise<IDBDatabase>((resolve, reject) => { category: item.category,
const openRequest = indexedDB.open("emojiDatabase", 1); keywords: item.keywords || [],
};
openRequest.onupgradeneeded = () => {
const db = openRequest.result;
if (!db.objectStoreNames.contains(storeName)) {
db.createObjectStore(storeName);
}
};
openRequest.onsuccess = () => {
resolve(openRequest.result);
};
openRequest.onerror = () => {
reject(openRequest.error);
};
}); });
} return acc.concat(categoryItems);
}, []);
function storeData(db: IDBDatabase, data) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, "readwrite");
const store = transaction.objectStore(storeName);
store.put(data, "emojiListKey");
transaction.oncomplete = resolve;
transaction.onerror = reject;
});
}
function getData(db: IDBDatabase): Promise<UnicodeEmojiDef[]> {
return new Promise((resolve, reject) => {
const transaction = db.transaction(storeName, "readonly");
const store = transaction.objectStore(storeName);
const getRequest = store.get("emojiListKey");
getRequest.onsuccess = () => resolve(getRequest.result);
getRequest.onerror = reject;
});
}
export async function getEmojiData(): Promise<UnicodeEmojiDef[]> {
try {
const db = await openDatabase();
const cachedData = await getData(db);
if (cachedData) {
return cachedData;
} else {
await storeData(db, emojilist);
console.log("Emoji data stored in IndexedDB");
return emojilist;
}
} catch (err) {
console.error("Error accessing IndexedDB:", err);
return emojilist;
}
}
export function getNicelyLabeledCategory(internalName) { export function getNicelyLabeledCategory(internalName) {
return Object.keys(categoryMapping).find( return Object.keys(categoryMapping).find(

View File

@ -242,6 +242,10 @@ export const defaultStore = markRaw(
where: "device", where: "device",
default: "remote" as "none" | "remote" | "always", default: "remote" as "none" | "remote" | "always",
}, },
reactionPickerSkinTone: {
where: "account",
default: 1,
},
reactionPickerSize: { reactionPickerSize: {
where: "device", where: "device",
default: 3, default: 3,