From 21e4c3dfe9b439a76782ab86be93298de2878ab9 Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Mon, 16 Jan 2023 09:39:58 +0000
Subject: [PATCH 01/25] wip

---
 .../src/server/api/endpoints/emojis.ts        |  4 +-
 .../src/components/MkAutocomplete.vue         | 90 +++++++++----------
 .../frontend/src/components/MkEmojiPicker.vue |  4 +-
 packages/frontend/src/custom-emojis.ts        | 18 ++--
 packages/frontend/src/pages/about.emojis.vue  |  7 +-
 .../src/pages/custom-emojis-manager.vue       |  8 ++
 .../frontend/src/pages/mfm-cheat-sheet.vue    |  2 +-
 packages/frontend/src/scripts/aiscript/api.ts |  2 +-
 8 files changed, 74 insertions(+), 61 deletions(-)

diff --git a/packages/backend/src/server/api/endpoints/emojis.ts b/packages/backend/src/server/api/endpoints/emojis.ts
index 97dcfde59..67538b0bd 100644
--- a/packages/backend/src/server/api/endpoints/emojis.ts
+++ b/packages/backend/src/server/api/endpoints/emojis.ts
@@ -10,6 +10,8 @@ export const meta = {
 	tags: ['meta'],
 
 	requireCredential: false,
+	allowGet: true,
+	cacheSec: 60,
 
 	res: {
 		type: 'object',
@@ -75,7 +77,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 				},
 				cache: {
 					id: 'meta_emojis',
-					milliseconds: 3600000,	// 1 hour
+					milliseconds: 60000,	// 1 minute
 				},
 			});
 
diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue
index 702fba979..ab4bf4f79 100644
--- a/packages/frontend/src/components/MkAutocomplete.vue
+++ b/packages/frontend/src/components/MkAutocomplete.vue
@@ -33,7 +33,7 @@
 </template>
 
 <script lang="ts">
-import { markRaw, ref, shallowRef, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
+import { markRaw, ref, shallowRef, computed, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
 import sanitizeHtml from 'sanitize-html';
 import contains from '@/scripts/contains';
 import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base';
@@ -61,59 +61,59 @@ type EmojiDef = {
 
 const lib = emojilist.filter(x => x.category !== 'flags');
 
-const char2path = defaultStore.state.emojiStyle === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath;
+const emojiDb = computed(() => {
+	const char2path = defaultStore.reactiveState.emojiStyle.value === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath;
 
-const emjdb: EmojiDef[] = lib.map(x => ({
-	emoji: x.char,
-	name: x.name,
-	url: char2path(x.char),
-}));
-
-for (const x of lib) {
-	if (x.keywords) {
-		for (const k of x.keywords) {
-			emjdb.push({
-				emoji: x.char,
-				name: k,
-				aliasOf: x.name,
-				url: char2path(x.char),
-			});
-		}
-	}
-}
-
-emjdb.sort((a, b) => a.name.length - b.name.length);
-
-//#region Construct Emoji DB
-const emojiDefinitions: EmojiDef[] = [];
-
-for (const x of customEmojis) {
-	emojiDefinitions.push({
+	const unicodeEmojiDB: EmojiDef[] = lib.map(x => ({
+		emoji: x.char,
 		name: x.name,
-		emoji: `:${x.name}:`,
-		isCustomEmoji: true,
-	});
+		url: char2path(x.char),
+	}));
 
-	if (x.aliases) {
-		for (const alias of x.aliases) {
-			emojiDefinitions.push({
-				name: alias,
-				aliasOf: x.name,
-				emoji: `:${x.name}:`,
-				isCustomEmoji: true,
-			});
+	for (const x of lib) {
+		if (x.keywords) {
+			for (const k of x.keywords) {
+				unicodeEmojiDB.push({
+					emoji: x.char,
+					name: k,
+					aliasOf: x.name,
+					url: char2path(x.char),
+				});
+			}
 		}
 	}
-}
 
-emojiDefinitions.sort((a, b) => a.name.length - b.name.length);
+	unicodeEmojiDB.sort((a, b) => a.name.length - b.name.length);
 
-const emojiDb = markRaw(emojiDefinitions.concat(emjdb));
-//#endregion
+	//#region Construct Emoji DB
+	const customEmojiDB: EmojiDef[] = [];
+
+	for (const x of customEmojis.value) {
+		customEmojiDB.push({
+			name: x.name,
+			emoji: `:${x.name}:`,
+			isCustomEmoji: true,
+		});
+
+		if (x.aliases) {
+			for (const alias of x.aliases) {
+				customEmojiDB.push({
+					name: alias,
+					aliasOf: x.name,
+					emoji: `:${x.name}:`,
+					isCustomEmoji: true,
+				});
+			}
+		}
+	}
+
+	customEmojiDB.sort((a, b) => a.name.length - b.name.length);
+
+	return markRaw([ ...customEmojiDB, ...unicodeEmojiDB ]);
+});
 
 export default {
 	emojiDb,
-	emojiDefinitions,
 	emojilist,
 };
 </script>
@@ -230,7 +230,7 @@ function exec() {
 	} else if (props.type === 'emoji') {
 		if (!props.q || props.q === '') {
 			// 最近使った絵文字をサジェスト
-			emojis.value = defaultStore.state.recentlyUsedEmojis.map(emoji => emojiDb.find(dbEmoji => dbEmoji.emoji === emoji)).filter(x => x) as EmojiDef[];
+			emojis.value = defaultStore.state.recentlyUsedEmojis.map(emoji => emojiDb.value.find(dbEmoji => dbEmoji.emoji === emoji)).filter(x => x) as EmojiDef[];
 			return;
 		}
 
diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue
index 9c6d62ce8..abb14564f 100644
--- a/packages/frontend/src/components/MkEmojiPicker.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.vue
@@ -138,7 +138,7 @@ watch(q, () => {
 
 	const searchCustom = () => {
 		const max = 8;
-		const emojis = customEmojis;
+		const emojis = customEmojis.value;
 		const matches = new Set<Misskey.entities.CustomEmoji>();
 
 		const exactMatch = emojis.find(emoji => emoji.name === newQ);
@@ -323,7 +323,7 @@ function done(query?: string): boolean | void {
 	if (query == null || typeof query !== 'string') return;
 
 	const q2 = query.replace(/:/g, '');
-	const exactMatchCustom = customEmojis.find(emoji => emoji.name === q2);
+	const exactMatchCustom = customEmojis.value.find(emoji => emoji.name === q2);
 	if (exactMatchCustom) {
 		chosen(exactMatchCustom);
 		return true;
diff --git a/packages/frontend/src/custom-emojis.ts b/packages/frontend/src/custom-emojis.ts
index 19469999b..52a114823 100644
--- a/packages/frontend/src/custom-emojis.ts
+++ b/packages/frontend/src/custom-emojis.ts
@@ -1,20 +1,22 @@
-import { api } from './os';
+import { apiGet } from './os';
 import { miLocalStorage } from './local-storage';
+import { shallowRef } from 'vue';
+import * as Misskey from 'misskey-js';
 
 const storageCache = miLocalStorage.getItem('emojis');
-export let customEmojis = storageCache ? JSON.parse(storageCache) : [];
+export const customEmojis = shallowRef<Misskey.entities.CustomEmoji[]>(storageCache ? JSON.parse(storageCache) : []);
 
 fetchCustomEmojis();
 
 export async function fetchCustomEmojis() {
 	const now = Date.now();
 	const lastFetchedAt = miLocalStorage.getItem('lastEmojisFetchedAt');
-	if (lastFetchedAt && (now - parseInt(lastFetchedAt)) < 1000 * 60 * 60) return;
+	if (lastFetchedAt && (now - parseInt(lastFetchedAt)) < 1000 * 60) return;
 
-	const res = await api('emojis', {});
+	const res = await apiGet('emojis', {});
 
-	customEmojis = res.emojis;
-	miLocalStorage.setItem('emojis', JSON.stringify(customEmojis));
+	customEmojis.value = res.emojis;
+	miLocalStorage.setItem('emojis', JSON.stringify(res.emojis));
 	miLocalStorage.setItem('lastEmojisFetchedAt', now.toString());
 }
 
@@ -23,7 +25,7 @@ export function getCustomEmojiCategories() {
 	if (cachedCategories) return cachedCategories;
 
 	const categories = new Set();
-	for (const emoji of customEmojis) {
+	for (const emoji of customEmojis.value) {
 		categories.add(emoji.category);
 	}
 	const res = Array.from(categories);
@@ -36,7 +38,7 @@ export function getCustomEmojiTags() {
 	if (cachedTags) return cachedTags;
 
 	const tags = new Set();
-	for (const emoji of customEmojis) {
+	for (const emoji of customEmojis.value) {
 		for (const tag of emoji.aliases) {
 			tags.add(tag);
 		}
diff --git a/packages/frontend/src/pages/about.emojis.vue b/packages/frontend/src/pages/about.emojis.vue
index c0145a503..7d146d1fd 100644
--- a/packages/frontend/src/pages/about.emojis.vue
+++ b/packages/frontend/src/pages/about.emojis.vue
@@ -41,11 +41,12 @@ import MkTab from '@/components/MkTab.vue';
 import * as os from '@/os';
 import { customEmojis, getCustomEmojiCategories, getCustomEmojiTags } from '@/custom-emojis';
 import { i18n } from '@/i18n';
+import * as Misskey from 'misskey-js';
 
 const customEmojiCategories = getCustomEmojiCategories();
 const customEmojiTags = getCustomEmojiTags();
 let q = $ref('');
-let searchEmojis = $ref(null);
+let searchEmojis = $ref<Misskey.entities.CustomEmoji[]>(null);
 let selectedTags = $ref(new Set());
 
 function search() {
@@ -55,9 +56,9 @@ function search() {
 	}
 
 	if (selectedTags.size === 0) {
-		searchEmojis = customEmojis.filter(emoji => emoji.name.includes(q) || emoji.aliases.includes(q));
+		searchEmojis = customEmojis.value.filter(emoji => emoji.name.includes(q) || emoji.aliases.includes(q));
 	} else {
-		searchEmojis = customEmojis.filter(emoji => (emoji.name.includes(q) || emoji.aliases.includes(q)) && [...selectedTags].every(t => emoji.aliases.includes(t)));
+		searchEmojis = customEmojis.value.filter(emoji => (emoji.name.includes(q) || emoji.aliases.includes(q)) && [...selectedTags].every(t => emoji.aliases.includes(t)));
 	}
 }
 
diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue
index 87d205ed7..e113c38a1 100644
--- a/packages/frontend/src/pages/custom-emojis-manager.vue
+++ b/packages/frontend/src/pages/custom-emojis-manager.vue
@@ -79,6 +79,7 @@ import { selectFile, selectFiles } from '@/scripts/select-file';
 import * as os from '@/os';
 import { i18n } from '@/i18n';
 import { definePageMetadata } from '@/scripts/page-metadata';
+import { fetchCustomEmojis } from '@/custom-emojis';
 
 const emojisPaginationComponent = shallowRef<InstanceType<typeof MkPagination>>();
 
@@ -130,6 +131,7 @@ const add = async (ev: MouseEvent) => {
 	})));
 	promise.then(() => {
 		emojisPaginationComponent.value.reload();
+		fetchCustomEmojis();
 	});
 	os.promiseDialog(promise);
 };
@@ -147,6 +149,7 @@ const edit = (emoji) => {
 			} else if (result.deleted) {
 				emojisPaginationComponent.value.removeItem((item) => item.id === emoji.id);
 			}
+			fetchCustomEmojis();
 		},
 	}, 'closed');
 };
@@ -220,6 +223,7 @@ const setCategoryBulk = async () => {
 		category: result,
 	});
 	emojisPaginationComponent.value.reload();
+	fetchCustomEmojis();
 };
 
 const addTagBulk = async () => {
@@ -232,6 +236,7 @@ const addTagBulk = async () => {
 		aliases: result.split(' '),
 	});
 	emojisPaginationComponent.value.reload();
+	fetchCustomEmojis();
 };
 
 const removeTagBulk = async () => {
@@ -244,6 +249,7 @@ const removeTagBulk = async () => {
 		aliases: result.split(' '),
 	});
 	emojisPaginationComponent.value.reload();
+	fetchCustomEmojis();
 };
 
 const setTagBulk = async () => {
@@ -256,6 +262,7 @@ const setTagBulk = async () => {
 		aliases: result.split(' '),
 	});
 	emojisPaginationComponent.value.reload();
+	fetchCustomEmojis();
 };
 
 const delBulk = async () => {
@@ -268,6 +275,7 @@ const delBulk = async () => {
 		ids: selectedEmojis.value,
 	});
 	emojisPaginationComponent.value.reload();
+	fetchCustomEmojis();
 };
 
 const headerActions = $computed(() => [{
diff --git a/packages/frontend/src/pages/mfm-cheat-sheet.vue b/packages/frontend/src/pages/mfm-cheat-sheet.vue
index b3932ff7c..73a571623 100644
--- a/packages/frontend/src/pages/mfm-cheat-sheet.vue
+++ b/packages/frontend/src/pages/mfm-cheat-sheet.vue
@@ -313,7 +313,7 @@ let preview_mention = $ref('@example');
 let preview_hashtag = $ref('#test');
 let preview_url = $ref('https://example.com');
 let preview_link = $ref(`[${i18n.ts._mfm.dummy}](https://example.com)`);
-let preview_emoji = $ref(customEmojis.length ? `:${customEmojis[0].name}:` : ':emojiname:');
+let preview_emoji = $ref(customEmojis.value.length ? `:${customEmojis.value[0].name}:` : ':emojiname:');
 let preview_bold = $ref(`**${i18n.ts._mfm.dummy}**`);
 let preview_small = $ref(`<small>${i18n.ts._mfm.dummy}</small>`);
 let preview_center = $ref(`<center>${i18n.ts._mfm.dummy}</center>`);
diff --git a/packages/frontend/src/scripts/aiscript/api.ts b/packages/frontend/src/scripts/aiscript/api.ts
index 29736ac60..12f00bd32 100644
--- a/packages/frontend/src/scripts/aiscript/api.ts
+++ b/packages/frontend/src/scripts/aiscript/api.ts
@@ -10,7 +10,7 @@ export function createAiScriptEnv(opts) {
 		USER_ID: $i ? values.STR($i.id) : values.NULL,
 		USER_NAME: $i ? values.STR($i.name) : values.NULL,
 		USER_USERNAME: $i ? values.STR($i.username) : values.NULL,
-		CUSTOM_EMOJIS: utils.jsToVal(customEmojis),
+		CUSTOM_EMOJIS: utils.jsToVal(customEmojis.value),
 		'Mk:dialog': values.FN_NATIVE(async ([title, text, type]) => {
 			await os.alert({
 				type: type ? type.value : 'info',

From 4cd70df7f465be53dd4fa48e2ab5724b36f4d167 Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Mon, 16 Jan 2023 09:52:45 +0000
Subject: [PATCH 02/25] setInterval

---
 packages/frontend/src/components/MkAutocomplete.vue | 6 +++---
 packages/frontend/src/custom-emojis.ts              | 2 +-
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue
index ab4bf4f79..f85c92cec 100644
--- a/packages/frontend/src/components/MkAutocomplete.vue
+++ b/packages/frontend/src/components/MkAutocomplete.vue
@@ -237,20 +237,20 @@ function exec() {
 		const matched: EmojiDef[] = [];
 		const max = 30;
 
-		emojiDb.some(x => {
+		emojiDb.value.some(x => {
 			if (x.name.startsWith(props.q ?? '') && !x.aliasOf && !matched.some(y => y.emoji === x.emoji)) matched.push(x);
 			return matched.length === max;
 		});
 
 		if (matched.length < max) {
-			emojiDb.some(x => {
+			emojiDb.value.some(x => {
 				if (x.name.startsWith(props.q ?? '') && !matched.some(y => y.emoji === x.emoji)) matched.push(x);
 				return matched.length === max;
 			});
 		}
 
 		if (matched.length < max) {
-			emojiDb.some(x => {
+			emojiDb.value.some(x => {
 				if (x.name.includes(props.q ?? '') && !matched.some(y => y.emoji === x.emoji)) matched.push(x);
 				return matched.length === max;
 			});
diff --git a/packages/frontend/src/custom-emojis.ts b/packages/frontend/src/custom-emojis.ts
index 52a114823..6d20187ab 100644
--- a/packages/frontend/src/custom-emojis.ts
+++ b/packages/frontend/src/custom-emojis.ts
@@ -6,7 +6,7 @@ import * as Misskey from 'misskey-js';
 const storageCache = miLocalStorage.getItem('emojis');
 export const customEmojis = shallowRef<Misskey.entities.CustomEmoji[]>(storageCache ? JSON.parse(storageCache) : []);
 
-fetchCustomEmojis();
+window.setInterval(fetchCustomEmojis, 1000 * 60 * 10);
 
 export async function fetchCustomEmojis() {
 	const now = Date.now();

From f2a9194c79524298a23f7fcba7abe8ef1f6077c7 Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Mon, 16 Jan 2023 10:13:19 +0000
Subject: [PATCH 03/25] :v:

---
 packages/frontend/src/custom-emojis.ts        |  1 +
 .../src/pages/custom-emojis-manager.vue       | 21 +++++++++++++++++--
 2 files changed, 20 insertions(+), 2 deletions(-)

diff --git a/packages/frontend/src/custom-emojis.ts b/packages/frontend/src/custom-emojis.ts
index 6d20187ab..8fd5078bd 100644
--- a/packages/frontend/src/custom-emojis.ts
+++ b/packages/frontend/src/custom-emojis.ts
@@ -6,6 +6,7 @@ import * as Misskey from 'misskey-js';
 const storageCache = miLocalStorage.getItem('emojis');
 export const customEmojis = shallowRef<Misskey.entities.CustomEmoji[]>(storageCache ? JSON.parse(storageCache) : []);
 
+fetchCustomEmojis();
 window.setInterval(fetchCustomEmojis, 1000 * 60 * 10);
 
 export async function fetchCustomEmojis() {
diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue
index e113c38a1..00c736a6a 100644
--- a/packages/frontend/src/pages/custom-emojis-manager.vue
+++ b/packages/frontend/src/pages/custom-emojis-manager.vue
@@ -79,7 +79,7 @@ import { selectFile, selectFiles } from '@/scripts/select-file';
 import * as os from '@/os';
 import { i18n } from '@/i18n';
 import { definePageMetadata } from '@/scripts/page-metadata';
-import { fetchCustomEmojis } from '@/custom-emojis';
+import { fetchCustomEmojis, customEmojis } from '@/custom-emojis';
 
 const emojisPaginationComponent = shallowRef<InstanceType<typeof MkPagination>>();
 
@@ -146,10 +146,27 @@ const edit = (emoji) => {
 					...oldEmoji,
 					...result.updated,
 				}));
+
+				if (customEmojis.value.some(e => e.name === emoji.name)) {
+					customEmojis.value = [
+						{
+							name: result.updated.name,
+							aliases: result.updated.aliases,
+							category: result.updated.category,
+						},
+						...customEmojis.value,
+					];
+				} else {
+					customEmojis.value = customEmojis.value.map(e => e.name !== emoji.name ? e : {
+						name: result.updated.name,
+						aliases: result.updated.aliases,
+						category: result.updated.category,
+					});
+				}
 			} else if (result.deleted) {
 				emojisPaginationComponent.value.removeItem((item) => item.id === emoji.id);
+				customEmojis.value = customEmojis.value.filter(e => e.name !== emoji.name);
 			}
-			fetchCustomEmojis();
 		},
 	}, 'closed');
 };

From 43956f3ffb616e6813f179b7a9040f158812fbac Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Mon, 16 Jan 2023 10:36:29 +0000
Subject: [PATCH 04/25] customEmojiCategories as computed

---
 .../frontend/src/components/MkEmojiPicker.vue |  3 +--
 packages/frontend/src/custom-emojis.ts        | 22 +++++++------------
 packages/frontend/src/pages/about.emojis.vue  |  3 +--
 .../frontend/src/pages/emoji-edit-dialog.vue  |  5 ++---
 4 files changed, 12 insertions(+), 21 deletions(-)

diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue
index abb14564f..3399124e8 100644
--- a/packages/frontend/src/components/MkEmojiPicker.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.vue
@@ -88,7 +88,7 @@ import { deviceKind } from '@/scripts/device-kind';
 import { instance } from '@/instance';
 import { i18n } from '@/i18n';
 import { defaultStore } from '@/store';
-import { getCustomEmojiCategories, customEmojis } from '@/custom-emojis';
+import { customEmojiCategories, customEmojis } from '@/custom-emojis';
 
 const props = withDefaults(defineProps<{
 	showPinned?: boolean;
@@ -104,7 +104,6 @@ const emit = defineEmits<{
 	(ev: 'chosen', v: string): void;
 }>();
 
-const customEmojiCategories = getCustomEmojiCategories();
 const searchEl = shallowRef<HTMLInputElement>();
 const emojisEl = shallowRef<HTMLDivElement>();
 
diff --git a/packages/frontend/src/custom-emojis.ts b/packages/frontend/src/custom-emojis.ts
index 8fd5078bd..57842459b 100644
--- a/packages/frontend/src/custom-emojis.ts
+++ b/packages/frontend/src/custom-emojis.ts
@@ -1,10 +1,17 @@
 import { apiGet } from './os';
 import { miLocalStorage } from './local-storage';
-import { shallowRef } from 'vue';
+import { shallowRef, computed, markRaw } from 'vue';
 import * as Misskey from 'misskey-js';
 
 const storageCache = miLocalStorage.getItem('emojis');
 export const customEmojis = shallowRef<Misskey.entities.CustomEmoji[]>(storageCache ? JSON.parse(storageCache) : []);
+export const customEmojiCategories = computed<string[]>(() => {
+	const categories = new Set<string>();
+	for (const emoji of customEmojis.value) {
+		categories.add(emoji.category);
+	}
+	return markRaw(Array.from(categories));
+});
 
 fetchCustomEmojis();
 window.setInterval(fetchCustomEmojis, 1000 * 60 * 10);
@@ -21,19 +28,6 @@ export async function fetchCustomEmojis() {
 	miLocalStorage.setItem('lastEmojisFetchedAt', now.toString());
 }
 
-let cachedCategories;
-export function getCustomEmojiCategories() {
-	if (cachedCategories) return cachedCategories;
-
-	const categories = new Set();
-	for (const emoji of customEmojis.value) {
-		categories.add(emoji.category);
-	}
-	const res = Array.from(categories);
-	cachedCategories = res;
-	return res;
-}
-
 let cachedTags;
 export function getCustomEmojiTags() {
 	if (cachedTags) return cachedTags;
diff --git a/packages/frontend/src/pages/about.emojis.vue b/packages/frontend/src/pages/about.emojis.vue
index 7d146d1fd..d964e48b3 100644
--- a/packages/frontend/src/pages/about.emojis.vue
+++ b/packages/frontend/src/pages/about.emojis.vue
@@ -39,11 +39,10 @@ import MkSelect from '@/components/MkSelect.vue';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
 import MkTab from '@/components/MkTab.vue';
 import * as os from '@/os';
-import { customEmojis, getCustomEmojiCategories, getCustomEmojiTags } from '@/custom-emojis';
+import { customEmojis, customEmojiCategories, getCustomEmojiTags } from '@/custom-emojis';
 import { i18n } from '@/i18n';
 import * as Misskey from 'misskey-js';
 
-const customEmojiCategories = getCustomEmojiCategories();
 const customEmojiTags = getCustomEmojiTags();
 let q = $ref('');
 let searchEmojis = $ref<Misskey.entities.CustomEmoji[]>(null);
diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue
index b2880b60b..4d84ed7f1 100644
--- a/packages/frontend/src/pages/emoji-edit-dialog.vue
+++ b/packages/frontend/src/pages/emoji-edit-dialog.vue
@@ -15,7 +15,7 @@
 			<MkInput v-model="name">
 				<template #label>{{ i18n.ts.name }}</template>
 			</MkInput>
-			<MkInput v-model="category" :datalist="categories">
+			<MkInput v-model="category" :datalist="customEmojiCategories">
 				<template #label>{{ i18n.ts.category }}</template>
 			</MkInput>
 			<MkInput v-model="aliases">
@@ -36,7 +36,7 @@ import MkInput from '@/components/MkInput.vue';
 import * as os from '@/os';
 import { unique } from '@/scripts/array';
 import { i18n } from '@/i18n';
-import { getCustomEmojiCategories } from '@/custom-emojis';
+import { customEmojiCategories } from '@/custom-emojis';
 
 const props = defineProps<{
 	emoji: any,
@@ -46,7 +46,6 @@ let dialog = $ref(null);
 let name: string = $ref(props.emoji.name);
 let category: string = $ref(props.emoji.category);
 let aliases: string = $ref(props.emoji.aliases.join(' '));
-const categories = getCustomEmojiCategories();
 
 const emit = defineEmits<{
 	(ev: 'done', v: { deleted?: boolean, updated?: any }): void,

From 002f98987d8b26b7a86c5bb6505cefa439701a94 Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Mon, 16 Jan 2023 10:51:51 +0000
Subject: [PATCH 05/25] fix

---
 packages/frontend/src/pages/custom-emojis-manager.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue
index 00c736a6a..20b6c32e8 100644
--- a/packages/frontend/src/pages/custom-emojis-manager.vue
+++ b/packages/frontend/src/pages/custom-emojis-manager.vue
@@ -147,7 +147,7 @@ const edit = (emoji) => {
 					...result.updated,
 				}));
 
-				if (customEmojis.value.some(e => e.name === emoji.name)) {
+				if (!customEmojis.value.some(e => e.name === emoji.name)) {
 					customEmojis.value = [
 						{
 							name: result.updated.name,

From 890564e1daeec3b4b08e933945640ecead48a12e Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Mon, 16 Jan 2023 10:56:43 +0000
Subject: [PATCH 06/25] refactor

---
 packages/frontend/src/components/MkAutocomplete.vue | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue
index f85c92cec..2cb3aeb3d 100644
--- a/packages/frontend/src/components/MkAutocomplete.vue
+++ b/packages/frontend/src/components/MkAutocomplete.vue
@@ -62,6 +62,7 @@ type EmojiDef = {
 const lib = emojilist.filter(x => x.category !== 'flags');
 
 const emojiDb = computed(() => {
+	//#region Unicode Emoji
 	const char2path = defaultStore.reactiveState.emojiStyle.value === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath;
 
 	const unicodeEmojiDB: EmojiDef[] = lib.map(x => ({
@@ -84,8 +85,9 @@ const emojiDb = computed(() => {
 	}
 
 	unicodeEmojiDB.sort((a, b) => a.name.length - b.name.length);
+	//#endregion
 
-	//#region Construct Emoji DB
+	//#region Custom Emoji
 	const customEmojiDB: EmojiDef[] = [];
 
 	for (const x of customEmojis.value) {
@@ -108,6 +110,7 @@ const emojiDb = computed(() => {
 	}
 
 	customEmojiDB.sort((a, b) => a.name.length - b.name.length);
+	//#endregion
 
 	return markRaw([ ...customEmojiDB, ...unicodeEmojiDB ]);
 });

From a5f54580a9a069d824ac0c52149682d34349ddfc Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Sun, 22 Jan 2023 12:57:51 +0000
Subject: [PATCH 07/25] fix

---
 packages/frontend/src/components/global/MkEmoji.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue
index 93f50da20..691392d73 100644
--- a/packages/frontend/src/components/global/MkEmoji.vue
+++ b/packages/frontend/src/components/global/MkEmoji.vue
@@ -32,7 +32,7 @@ const url = computed(() => {
 	if (char.value) {
 		return char2path(char.value);
 	} else if (props.host == null && !customEmojiName.includes('@')) {
-		const found = customEmojis.find(x => x.name === customEmojiName);
+		const found = customEmojis.value.find(x => x.name === customEmojiName);
 		return found ? found.url : null;
 	} else {
 		const rawUrl = props.host ? `/emoji/${customEmojiName}@${props.host}.webp` : `/emoji/${customEmojiName}.webp`;

From 2f6c45e11842cde64c2b724a45b2c316b09af57c Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Sun, 22 Jan 2023 14:53:24 +0000
Subject: [PATCH 08/25] wip

---
 .../backend/src/core/CustomEmojiService.ts    |  8 ++++
 .../endpoints/admin/emoji/add-aliases-bulk.ts |  9 +++++
 .../server/api/endpoints/admin/emoji/add.ts   | 39 +++++--------------
 .../api/endpoints/admin/emoji/delete-bulk.ts  | 10 ++++-
 .../api/endpoints/admin/emoji/delete.ts       |  8 ++++
 .../admin/emoji/remove-aliases-bulk.ts        |  9 +++++
 .../endpoints/admin/emoji/set-aliases-bulk.ts |  9 +++++
 .../admin/emoji/set-category-bulk.ts          |  9 +++++
 .../api/endpoints/admin/emoji/update.ts       | 21 ++++++++++
 .../backend/src/server/api/stream/types.ts    | 10 +++++
 packages/frontend/src/custom-emojis.ts        | 14 ++++++-
 packages/frontend/src/init.ts                 |  5 ---
 12 files changed, 114 insertions(+), 37 deletions(-)

diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts
index 18b4067f6..1f0b21415 100644
--- a/packages/backend/src/core/CustomEmojiService.ts
+++ b/packages/backend/src/core/CustomEmojiService.ts
@@ -2,6 +2,8 @@ import { Inject, Injectable } from '@nestjs/common';
 import { DataSource, In, IsNull } from 'typeorm';
 import { DI } from '@/di-symbols.js';
 import { IdService } from '@/core/IdService.js';
+import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
 import type { DriveFile } from '@/models/entities/DriveFile.js';
 import type { Emoji } from '@/models/entities/Emoji.js';
 import type { EmojisRepository } from '@/models/index.js';
@@ -17,6 +19,8 @@ export class CustomEmojiService {
 		private emojisRepository: EmojisRepository,
 
 		private idService: IdService,
+		private emojiEntityService: EmojiEntityService,
+		private globalEventService: GlobalEventService,
 	) {
 	}
 
@@ -42,6 +46,10 @@ export class CustomEmojiService {
 
 		await this.db.queryResultCache!.remove(['meta_emojis']);
 
+		this.globalEventService.publishBroadcastStream('emojiAdded', {
+			emoji: await this.emojiEntityService.pack(emoji.id),
+		});
+
 		return emoji;
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts
index 9b6c774f0..c683cd24c 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts
@@ -3,6 +3,8 @@ import { DataSource, In } from 'typeorm';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import type { EmojisRepository } from '@/models/index.js';
 import { DI } from '@/di-symbols.js';
+import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
 
 export const meta = {
 	tags: ['admin'],
@@ -35,6 +37,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 
 		@Inject(DI.emojisRepository)
 		private emojisRepository: EmojisRepository,
+
+		private emojiEntityService: EmojiEntityService,
+		private globalEventService: GlobalEventService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const emojis = await this.emojisRepository.findBy({
@@ -49,6 +54,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 			}
 
 			await this.db.queryResultCache!.remove(['meta_emojis']);
+
+			this.globalEventService.publishBroadcastStream('emojiUpdated', {
+				emojis: await this.emojiEntityService.packMany(ps.ids),
+			});
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
index abca1d169..1bb05c15c 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts
@@ -2,12 +2,10 @@ import { Inject, Injectable } from '@nestjs/common';
 import rndstr from 'rndstr';
 import { DataSource } from 'typeorm';
 import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { DriveFilesRepository, EmojisRepository } from '@/models/index.js';
-import { IdService } from '@/core/IdService.js';
+import type { DriveFilesRepository } from '@/models/index.js';
 import { DI } from '@/di-symbols.js';
-import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { CustomEmojiService } from '@/core/CustomEmojiService.js';
 import { ModerationLogService } from '@/core/ModerationLogService.js';
-import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
 import { ApiError } from '../../../error.js';
 
 export const meta = {
@@ -39,43 +37,26 @@ export const paramDef = {
 @Injectable()
 export default class extends Endpoint<typeof meta, typeof paramDef> {
 	constructor(
-		@Inject(DI.db)
-		private db: DataSource,
-
 		@Inject(DI.driveFilesRepository)
 		private driveFilesRepository: DriveFilesRepository,
 
-		@Inject(DI.emojisRepository)
-		private emojisRepository: EmojisRepository,
+		private customEmojiService: CustomEmojiService,
 
-		private emojiEntityService: EmojiEntityService,
-		private idService: IdService,
-		private globalEventService: GlobalEventService,
 		private moderationLogService: ModerationLogService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
-			const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
+			const driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
 
-			if (file == null) throw new ApiError(meta.errors.noSuchFile);
+			if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
 
-			const name = file.name.split('.')[0].match(/^[a-z0-9_]+$/) ? file.name.split('.')[0] : `_${rndstr('a-z0-9', 8)}_`;
+			const name = driveFile.name.split('.')[0].match(/^[a-z0-9_]+$/) ? driveFile.name.split('.')[0] : `_${rndstr('a-z0-9', 8)}_`;
 
-			const emoji = await this.emojisRepository.insert({
-				id: this.idService.genId(),
-				updatedAt: new Date(),
-				name: name,
+			const emoji = await this.customEmojiService.add({
+				driveFile,
+				name,
 				category: null,
-				host: null,
 				aliases: [],
-				originalUrl: file.url,
-				publicUrl: file.webpublicUrl ?? file.url,
-				type: file.webpublicType ?? file.type,
-			}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
-
-			await this.db.queryResultCache!.remove(['meta_emojis']);
-
-			this.globalEventService.publishBroadcastStream('emojiAdded', {
-				emoji: await this.emojiEntityService.pack(emoji.id),
+				host: null,
 			});
 
 			this.moderationLogService.insertModerationLog(me, 'addEmoji', {
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts
index ae45105b2..0c337237d 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts
@@ -4,6 +4,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
 import type { EmojisRepository } from '@/models/index.js';
 import { DI } from '@/di-symbols.js';
 import { ModerationLogService } from '@/core/ModerationLogService.js';
+import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
 
 export const meta = {
 	tags: ['admin'],
@@ -35,6 +37,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 		private emojisRepository: EmojisRepository,
 
 		private moderationLogService: ModerationLogService,
+		private emojiEntityService: EmojiEntityService,
+		private globalEventService: GlobalEventService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const emojis = await this.emojisRepository.findBy({
@@ -43,13 +47,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 
 			for (const emoji of emojis) {
 				await this.emojisRepository.delete(emoji.id);
-	
 				await this.db.queryResultCache!.remove(['meta_emojis']);
-	
 				this.moderationLogService.insertModerationLog(me, 'deleteEmoji', {
 					emoji: emoji,
 				});
 			}
+
+			this.globalEventService.publishBroadcastStream('emojiDeleted', {
+				emojis: await this.emojiEntityService.packMany(emojis),
+			});
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts
index e237d87d3..c1a60a277 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts
@@ -5,6 +5,8 @@ import type { EmojisRepository } from '@/models/index.js';
 import { DI } from '@/di-symbols.js';
 import { ModerationLogService } from '@/core/ModerationLogService.js';
 import { ApiError } from '../../../error.js';
+import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
 
 export const meta = {
 	tags: ['admin'],
@@ -42,6 +44,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 		private emojisRepository: EmojisRepository,
 
 		private moderationLogService: ModerationLogService,
+		private emojiEntityService: EmojiEntityService,
+		private globalEventService: GlobalEventService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const emoji = await this.emojisRepository.findOneBy({ id: ps.id });
@@ -52,6 +56,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 
 			await this.db.queryResultCache!.remove(['meta_emojis']);
 
+			this.globalEventService.publishBroadcastStream('emojiDeleted', {
+				emojis: [ await this.emojiEntityService.pack(emoji) ],
+			});
+
 			this.moderationLogService.insertModerationLog(me, 'deleteEmoji', {
 				emoji: emoji,
 			});
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts
index 5fc9e024b..065965f64 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts
@@ -3,6 +3,8 @@ import { DataSource, In } from 'typeorm';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import type { EmojisRepository } from '@/models/index.js';
 import { DI } from '@/di-symbols.js';
+import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
 
 export const meta = {
 	tags: ['admin'],
@@ -35,6 +37,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 
 		@Inject(DI.emojisRepository)
 		private emojisRepository: EmojisRepository,
+
+		private emojiEntityService: EmojiEntityService,
+		private globalEventService: GlobalEventService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const emojis = await this.emojisRepository.findBy({
@@ -49,6 +54,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 			}
 
 			await this.db.queryResultCache!.remove(['meta_emojis']);
+		
+			this.globalEventService.publishBroadcastStream('emojiUpdated', {
+				emojis: await this.emojiEntityService.packMany(ps.ids),
+			});
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts
index 8b5ba8fbf..51c0f329a 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts
@@ -3,6 +3,8 @@ import { DataSource, In } from 'typeorm';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import type { EmojisRepository } from '@/models/index.js';
 import { DI } from '@/di-symbols.js';
+import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
 
 export const meta = {
 	tags: ['admin'],
@@ -35,6 +37,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 
 		@Inject(DI.emojisRepository)
 		private emojisRepository: EmojisRepository,
+
+		private emojiEntityService: EmojiEntityService,
+		private globalEventService: GlobalEventService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			await this.emojisRepository.update({
@@ -45,6 +50,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 			});
 
 			await this.db.queryResultCache!.remove(['meta_emojis']);
+
+			this.globalEventService.publishBroadcastStream('emojiUpdated', {
+				emojis: await this.emojiEntityService.packMany(ps.ids),
+			});
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts
index 827b5ace7..3329cab7b 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts
@@ -3,6 +3,8 @@ import { DataSource, In } from 'typeorm';
 import { Endpoint } from '@/server/api/endpoint-base.js';
 import type { EmojisRepository } from '@/models/index.js';
 import { DI } from '@/di-symbols.js';
+import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
 
 export const meta = {
 	tags: ['admin'],
@@ -37,6 +39,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 
 		@Inject(DI.emojisRepository)
 		private emojisRepository: EmojisRepository,
+
+		private emojiEntityService: EmojiEntityService,
+		private globalEventService: GlobalEventService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			await this.emojisRepository.update({
@@ -47,6 +52,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 			});
 
 			await this.db.queryResultCache!.remove(['meta_emojis']);
+
+			this.globalEventService.publishBroadcastStream('emojiUpdated', {
+				emojis: await this.emojiEntityService.packMany(ps.ids),
+			});
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
index fb0ef1287..6c4c8d0e6 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
@@ -4,6 +4,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
 import type { EmojisRepository } from '@/models/index.js';
 import { DI } from '@/di-symbols.js';
 import { ApiError } from '../../../error.js';
+import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
 
 export const meta = {
 	tags: ['admin'],
@@ -48,6 +50,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 
 		@Inject(DI.emojisRepository)
 		private emojisRepository: EmojisRepository,
+
+		private emojiEntityService: EmojiEntityService,
+		private globalEventService: GlobalEventService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
 			const emoji = await this.emojisRepository.findOneBy({ id: ps.id });
@@ -62,6 +67,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 			});
 
 			await this.db.queryResultCache!.remove(['meta_emojis']);
+
+			const updated = await this.emojiEntityService.pack(emoji.id);
+
+			if (emoji.name === ps.name) {
+				this.globalEventService.publishBroadcastStream('emojiUpdated', {
+					emojis: [ updated ],
+				});
+			} else {
+				this.globalEventService.publishBroadcastStream('emojiDeleted', {
+					emojis: [ emoji ],
+				});
+
+				this.globalEventService.publishBroadcastStream('emojiAdded', {
+					emoji: updated,
+				});	
+			}
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/stream/types.ts b/packages/backend/src/server/api/stream/types.ts
index a442529bb..9daaa4acf 100644
--- a/packages/backend/src/server/api/stream/types.ts
+++ b/packages/backend/src/server/api/stream/types.ts
@@ -49,6 +49,16 @@ export interface BroadcastTypes {
 	emojiAdded: {
 		emoji: Packed<'Emoji'>;
 	};
+	emojiUpdated: {
+		emojis: Packed<'Emoji'>[];
+	};
+	emojiDeleted: {
+		emojis: {
+			id?: string;
+			name: string;
+			[other: string]: any;
+		}[];
+	};
 }
 
 export interface UserStreamTypes {
diff --git a/packages/frontend/src/custom-emojis.ts b/packages/frontend/src/custom-emojis.ts
index 57842459b..5b13176cd 100644
--- a/packages/frontend/src/custom-emojis.ts
+++ b/packages/frontend/src/custom-emojis.ts
@@ -2,6 +2,7 @@ import { apiGet } from './os';
 import { miLocalStorage } from './local-storage';
 import { shallowRef, computed, markRaw } from 'vue';
 import * as Misskey from 'misskey-js';
+import { stream } from '@/stream';
 
 const storageCache = miLocalStorage.getItem('emojis');
 export const customEmojis = shallowRef<Misskey.entities.CustomEmoji[]>(storageCache ? JSON.parse(storageCache) : []);
@@ -14,7 +15,18 @@ export const customEmojiCategories = computed<string[]>(() => {
 });
 
 fetchCustomEmojis();
-window.setInterval(fetchCustomEmojis, 1000 * 60 * 10);
+
+stream.on('emojiAdded', emojiData => {
+	customEmojis.value = [ emojiData.emoji, ...customEmojis.value ]
+});
+
+stream.on('emojiUpdated', emojiData => {
+	customEmojis.value = customEmojis.value.map(item => emojiData.emojis.find(search => search.name === item.name) as Misskey.entities.CustomEmoji ?? item);
+});
+
+stream.on('emojiDeleted', emojiData => {
+	customEmojis.value = customEmojis.value.filter(item => !emojiData.emojis.some(search => search.name === item.name))
+});
 
 export async function fetchCustomEmojis() {
 	const now = Date.now();
diff --git a/packages/frontend/src/init.ts b/packages/frontend/src/init.ts
index 36897545e..2432b5f6f 100644
--- a/packages/frontend/src/init.ts
+++ b/packages/frontend/src/init.ts
@@ -338,11 +338,6 @@ import { fetchCustomEmojis } from './custom-emojis';
 		}
 	});
 
-	stream.on('emojiAdded', emojiData => {
-		// TODO
-		//store.commit('instance/set', );
-	});
-
 	for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) {
 		import('./plugin').then(({ install }) => {
 			install(plugin);

From 0b2f945bb61fc5c3a33ab93d0134f4bfe3a05c9d Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Sun, 22 Jan 2023 15:13:03 +0000
Subject: [PATCH 09/25] wip

---
 .../backend/src/server/api/endpoints/admin/emoji/update.ts  | 2 +-
 packages/frontend/src/custom-emojis.ts                      | 6 ++++--
 2 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
index 6c4c8d0e6..22bedc710 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
@@ -76,7 +76,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 				});
 			} else {
 				this.globalEventService.publishBroadcastStream('emojiDeleted', {
-					emojis: [ emoji ],
+					emojis: [ await this.emojiEntityService.pack(emoji) ],
 				});
 
 				this.globalEventService.publishBroadcastStream('emojiAdded', {
diff --git a/packages/frontend/src/custom-emojis.ts b/packages/frontend/src/custom-emojis.ts
index 5b13176cd..b080eca40 100644
--- a/packages/frontend/src/custom-emojis.ts
+++ b/packages/frontend/src/custom-emojis.ts
@@ -17,7 +17,9 @@ export const customEmojiCategories = computed<string[]>(() => {
 fetchCustomEmojis();
 
 stream.on('emojiAdded', emojiData => {
-	customEmojis.value = [ emojiData.emoji, ...customEmojis.value ]
+	setTimeout(() => {
+		customEmojis.value = [ emojiData.emoji, ...customEmojis.value ]
+	}, 100);
 });
 
 stream.on('emojiUpdated', emojiData => {
@@ -25,7 +27,7 @@ stream.on('emojiUpdated', emojiData => {
 });
 
 stream.on('emojiDeleted', emojiData => {
-	customEmojis.value = customEmojis.value.filter(item => !emojiData.emojis.some(search => search.name === item.name))
+	customEmojis.value = customEmojis.value.filter(item => !emojiData.emojis.some(search => search.name === item.name));
 });
 
 export async function fetchCustomEmojis() {

From 226e0c4714c2d02598141e3e408c4fb29d90354b Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Sun, 22 Jan 2023 15:17:20 +0000
Subject: [PATCH 10/25] :v:

---
 .../src/pages/custom-emojis-manager.vue       | 25 -------------------
 1 file changed, 25 deletions(-)

diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue
index 20b6c32e8..87d205ed7 100644
--- a/packages/frontend/src/pages/custom-emojis-manager.vue
+++ b/packages/frontend/src/pages/custom-emojis-manager.vue
@@ -79,7 +79,6 @@ import { selectFile, selectFiles } from '@/scripts/select-file';
 import * as os from '@/os';
 import { i18n } from '@/i18n';
 import { definePageMetadata } from '@/scripts/page-metadata';
-import { fetchCustomEmojis, customEmojis } from '@/custom-emojis';
 
 const emojisPaginationComponent = shallowRef<InstanceType<typeof MkPagination>>();
 
@@ -131,7 +130,6 @@ const add = async (ev: MouseEvent) => {
 	})));
 	promise.then(() => {
 		emojisPaginationComponent.value.reload();
-		fetchCustomEmojis();
 	});
 	os.promiseDialog(promise);
 };
@@ -146,26 +144,8 @@ const edit = (emoji) => {
 					...oldEmoji,
 					...result.updated,
 				}));
-
-				if (!customEmojis.value.some(e => e.name === emoji.name)) {
-					customEmojis.value = [
-						{
-							name: result.updated.name,
-							aliases: result.updated.aliases,
-							category: result.updated.category,
-						},
-						...customEmojis.value,
-					];
-				} else {
-					customEmojis.value = customEmojis.value.map(e => e.name !== emoji.name ? e : {
-						name: result.updated.name,
-						aliases: result.updated.aliases,
-						category: result.updated.category,
-					});
-				}
 			} else if (result.deleted) {
 				emojisPaginationComponent.value.removeItem((item) => item.id === emoji.id);
-				customEmojis.value = customEmojis.value.filter(e => e.name !== emoji.name);
 			}
 		},
 	}, 'closed');
@@ -240,7 +220,6 @@ const setCategoryBulk = async () => {
 		category: result,
 	});
 	emojisPaginationComponent.value.reload();
-	fetchCustomEmojis();
 };
 
 const addTagBulk = async () => {
@@ -253,7 +232,6 @@ const addTagBulk = async () => {
 		aliases: result.split(' '),
 	});
 	emojisPaginationComponent.value.reload();
-	fetchCustomEmojis();
 };
 
 const removeTagBulk = async () => {
@@ -266,7 +244,6 @@ const removeTagBulk = async () => {
 		aliases: result.split(' '),
 	});
 	emojisPaginationComponent.value.reload();
-	fetchCustomEmojis();
 };
 
 const setTagBulk = async () => {
@@ -279,7 +256,6 @@ const setTagBulk = async () => {
 		aliases: result.split(' '),
 	});
 	emojisPaginationComponent.value.reload();
-	fetchCustomEmojis();
 };
 
 const delBulk = async () => {
@@ -292,7 +268,6 @@ const delBulk = async () => {
 		ids: selectedEmojis.value,
 	});
 	emojisPaginationComponent.value.reload();
-	fetchCustomEmojis();
 };
 
 const headerActions = $computed(() => [{

From d347f0a087747d4ae19255372ced2d69f62fc00d Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Sun, 22 Jan 2023 16:07:17 +0000
Subject: [PATCH 11/25] wip

---
 .../src/core/entities/EmojiEntityService.ts       |  4 +++-
 .../api/endpoints/admin/emoji/list-remote.ts      |  2 +-
 .../src/server/api/endpoints/admin/emoji/list.ts  |  2 +-
 .../frontend/src/components/global/MkEmoji.vue    | 15 ++++++++-------
 packages/frontend/src/custom-emojis.ts            | 10 +++++-----
 5 files changed, 18 insertions(+), 15 deletions(-)

diff --git a/packages/backend/src/core/entities/EmojiEntityService.ts b/packages/backend/src/core/entities/EmojiEntityService.ts
index 611552d89..7d248e634 100644
--- a/packages/backend/src/core/entities/EmojiEntityService.ts
+++ b/packages/backend/src/core/entities/EmojiEntityService.ts
@@ -22,8 +22,10 @@ export class EmojiEntityService {
 	@bindThis
 	public async pack(
 		src: Emoji['id'] | Emoji,
-		opts: { omitHost?: boolean; omitId?: boolean; withUrl?: boolean; } = {},
+		opts: { omitHost?: boolean; omitId?: boolean; withUrl?: boolean; } = { omitHost: true, omitId: true, withUrl: true },
 	): Promise<Packed<'Emoji'>> {
+		opts = { omitHost: true, omitId: true, withUrl: true, ...opts }
+
 		const emoji = typeof src === 'object' ? src : await this.emojisRepository.findOneByOrFail({ id: src });
 
 		return {
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts
index d9ce97194..8e0ea2e11 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts
@@ -101,7 +101,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 				.take(ps.limit)
 				.getMany();
 
-			return this.emojiEntityService.packMany(emojis);
+			return this.emojiEntityService.packMany(emojis, { omitHost: false, omitId: false, withUrl: false });
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
index 1a6096f36..1b1931f8e 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
@@ -98,7 +98,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 				emojis = await q.take(ps.limit).getMany();
 			}
 
-			return this.emojiEntityService.packMany(emojis);
+			return this.emojiEntityService.packMany(emojis, { omitHost: false, omitId: false, withUrl: false });
 		});
 	}
 }
diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue
index 691392d73..5eba8764b 100644
--- a/packages/frontend/src/components/global/MkEmoji.vue
+++ b/packages/frontend/src/components/global/MkEmoji.vue
@@ -1,6 +1,6 @@
 <template>
 <span v-if="isCustom && errored">:{{ customEmojiName }}:</span>
-<img v-else-if="isCustom" :class="[$style.root, $style.custom, { [$style.normal]: normal, [$style.noStyle]: noStyle }]" :src="url" :alt="alt" :title="alt" decoding="async" @error="errored = true"/>
+<img v-else-if="isCustom" :class="[$style.root, $style.custom, { [$style.normal]: normal, [$style.noStyle]: noStyle }]" :src="url" :alt="alt" :title="alt" decoding="async" @error="errored = true" @load="errored = false"/>
 <img v-else-if="char && !useOsNativeEmojis" :class="$style.root" :src="url" :alt="alt" decoding="async" @pointerenter="computeTitle"/>
 <span v-else-if="char && useOsNativeEmojis" :alt="alt" @pointerenter="computeTitle">{{ char }}</span>
 <span v-else>{{ emoji }}</span>
@@ -25,29 +25,30 @@ const props = defineProps<{
 const char2path = defaultStore.state.emojiStyle === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath;
 
 const isCustom = computed(() => props.emoji.startsWith(':'));
-const customEmojiName = props.emoji.substr(1, props.emoji.length - 2).replace('@.', '');
+const customEmojiName = computed(() => props.emoji.substr(1, props.emoji.length - 2).replace('@.', ''));
 const char = computed(() => isCustom.value ? undefined : props.emoji);
 const useOsNativeEmojis = computed(() => defaultStore.state.emojiStyle === 'native' && !props.isReaction);
 const url = computed(() => {
 	if (char.value) {
 		return char2path(char.value);
-	} else if (props.host == null && !customEmojiName.includes('@')) {
-		const found = customEmojis.value.find(x => x.name === customEmojiName);
+	} else if (props.host == null && !customEmojiName.value.includes('@')) {
+		const found = customEmojis.value.find(x => x.name === customEmojiName.value);
+		console.log(found)
 		return found ? found.url : null;
 	} else {
-		const rawUrl = props.host ? `/emoji/${customEmojiName}@${props.host}.webp` : `/emoji/${customEmojiName}.webp`;
+		const rawUrl = props.host ? `/emoji/${customEmojiName.value}@${props.host}.webp` : `/emoji/${customEmojiName.value}.webp`;
 		return defaultStore.state.disableShowingAnimatedImages
 			? getStaticImageUrl(rawUrl)
 			: rawUrl;
 	}
 });
-const alt = computed(() => isCustom.value ? `:${customEmojiName}:` : char.value);
+const alt = computed(() => isCustom.value ? `:${customEmojiName.value}:` : char.value);
 let errored = $ref(isCustom.value && url.value == null);
 
 // 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 {
 	const title = isCustom.value
-		? `:${customEmojiName}:`
+		? `:${customEmojiName.value}:`
 		: (getEmojiName(char.value as string) ?? char.value as string);
 	(event.target as HTMLElement).title = title;
 }
diff --git a/packages/frontend/src/custom-emojis.ts b/packages/frontend/src/custom-emojis.ts
index b080eca40..33f006b2a 100644
--- a/packages/frontend/src/custom-emojis.ts
+++ b/packages/frontend/src/custom-emojis.ts
@@ -1,6 +1,6 @@
 import { apiGet } from './os';
 import { miLocalStorage } from './local-storage';
-import { shallowRef, computed, markRaw } from 'vue';
+import { shallowRef, computed, markRaw, watch } from 'vue';
 import * as Misskey from 'misskey-js';
 import { stream } from '@/stream';
 
@@ -14,12 +14,12 @@ export const customEmojiCategories = computed<string[]>(() => {
 	return markRaw(Array.from(categories));
 });
 
-fetchCustomEmojis();
+watch(customEmojis, (newVal) => {
+	console.log('new', newVal)
+});
 
 stream.on('emojiAdded', emojiData => {
-	setTimeout(() => {
-		customEmojis.value = [ emojiData.emoji, ...customEmojis.value ]
-	}, 100);
+	customEmojis.value = [ emojiData.emoji, ...customEmojis.value ];
 });
 
 stream.on('emojiUpdated', emojiData => {

From 8a6750278e58807a181aa6ad6ac7a85de919caf9 Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Sun, 22 Jan 2023 17:11:28 +0000
Subject: [PATCH 12/25] :v:

---
 packages/frontend/src/components/MkEmojiPicker.section.vue | 6 ++++--
 packages/frontend/src/components/MkEmojiPicker.vue         | 2 +-
 2 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/packages/frontend/src/components/MkEmojiPicker.section.vue b/packages/frontend/src/components/MkEmojiPicker.section.vue
index 8b0b7cf29..acced4479 100644
--- a/packages/frontend/src/components/MkEmojiPicker.section.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.section.vue
@@ -18,10 +18,10 @@
 </template>
 
 <script lang="ts" setup>
-import { ref } from 'vue';
+import { ref, computed, Ref } from 'vue';
 
 const props = defineProps<{
-	emojis: string[];
+	emojis: string[] | Ref<string[]>;
 	initialShown?: boolean;
 }>();
 
@@ -29,5 +29,7 @@ const emit = defineEmits<{
 	(ev: 'chosen', v: string, event: MouseEvent): void;
 }>();
 
+const emojis = computed(() => Array.isArray(props.emojis) ? props.emojis : props.emojis.value);
+
 const shown = ref(!!props.initialShown);
 </script>
diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue
index 3399124e8..b288cb22f 100644
--- a/packages/frontend/src/components/MkEmojiPicker.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.vue
@@ -60,7 +60,7 @@
 		</div>
 		<div v-once class="group">
 			<header class="_acrylic">{{ i18n.ts.customEmojis }}</header>
-			<XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':')" @chosen="chosen">{{ category || i18n.ts.other }}</XSection>
+			<XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="computed(() => customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':'))" @chosen="chosen">{{ category || i18n.ts.other }}</XSection>
 		</div>
 		<div v-once class="group">
 			<header class="_acrylic">{{ i18n.ts.emoji }}</header>

From 0cffe60abc9a9e14f5815af472c100e630166c29 Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Sun, 22 Jan 2023 17:14:05 +0000
Subject: [PATCH 13/25] =?UTF-8?q?1=E6=99=82=E9=96=93=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/backend/src/server/api/endpoints/emojis.ts | 4 ++--
 packages/frontend/src/custom-emojis.ts              | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/packages/backend/src/server/api/endpoints/emojis.ts b/packages/backend/src/server/api/endpoints/emojis.ts
index 9dfb12b76..77854afb3 100644
--- a/packages/backend/src/server/api/endpoints/emojis.ts
+++ b/packages/backend/src/server/api/endpoints/emojis.ts
@@ -11,7 +11,7 @@ export const meta = {
 
 	requireCredential: false,
 	allowGet: true,
-	cacheSec: 60,
+	cacheSec: 3600,
 
 	res: {
 		type: 'object',
@@ -77,7 +77,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 				},
 				cache: {
 					id: 'meta_emojis',
-					milliseconds: 60000,	// 1 minute
+					milliseconds: 3600000,	// 1 hour
 				},
 			});
 
diff --git a/packages/frontend/src/custom-emojis.ts b/packages/frontend/src/custom-emojis.ts
index 33f006b2a..89b8887b2 100644
--- a/packages/frontend/src/custom-emojis.ts
+++ b/packages/frontend/src/custom-emojis.ts
@@ -33,7 +33,7 @@ stream.on('emojiDeleted', emojiData => {
 export async function fetchCustomEmojis() {
 	const now = Date.now();
 	const lastFetchedAt = miLocalStorage.getItem('lastEmojisFetchedAt');
-	if (lastFetchedAt && (now - parseInt(lastFetchedAt)) < 1000 * 60) return;
+	if (lastFetchedAt && (now - parseInt(lastFetchedAt)) < 1000 * 60 * 60) return;
 
 	const res = await apiGet('emojis', {});
 

From 0d44129ae34526503d391cbade18f26e9bba9c77 Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Sun, 22 Jan 2023 17:20:53 +0000
Subject: [PATCH 14/25] remove console.log

---
 packages/frontend/src/components/global/MkEmoji.vue | 1 -
 packages/frontend/src/custom-emojis.ts              | 4 ----
 2 files changed, 5 deletions(-)

diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue
index 5eba8764b..c489ab5fa 100644
--- a/packages/frontend/src/components/global/MkEmoji.vue
+++ b/packages/frontend/src/components/global/MkEmoji.vue
@@ -33,7 +33,6 @@ const url = computed(() => {
 		return char2path(char.value);
 	} else if (props.host == null && !customEmojiName.value.includes('@')) {
 		const found = customEmojis.value.find(x => x.name === customEmojiName.value);
-		console.log(found)
 		return found ? found.url : null;
 	} else {
 		const rawUrl = props.host ? `/emoji/${customEmojiName.value}@${props.host}.webp` : `/emoji/${customEmojiName.value}.webp`;
diff --git a/packages/frontend/src/custom-emojis.ts b/packages/frontend/src/custom-emojis.ts
index 89b8887b2..2cd088993 100644
--- a/packages/frontend/src/custom-emojis.ts
+++ b/packages/frontend/src/custom-emojis.ts
@@ -14,10 +14,6 @@ export const customEmojiCategories = computed<string[]>(() => {
 	return markRaw(Array.from(categories));
 });
 
-watch(customEmojis, (newVal) => {
-	console.log('new', newVal)
-});
-
 stream.on('emojiAdded', emojiData => {
 	customEmojis.value = [ emojiData.emoji, ...customEmojis.value ];
 });

From 93dd0638ade0ba896365f5f9cc1acba103db386e Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Sun, 22 Jan 2023 17:33:20 +0000
Subject: [PATCH 15/25] better category null handling

---
 packages/frontend/src/components/MkEmojiPicker.vue | 10 +++++++++-
 packages/frontend/src/custom-emojis.ts             | 10 ++++++----
 2 files changed, 15 insertions(+), 5 deletions(-)

diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue
index b288cb22f..479d8eb7f 100644
--- a/packages/frontend/src/components/MkEmojiPicker.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.vue
@@ -60,7 +60,15 @@
 		</div>
 		<div v-once class="group">
 			<header class="_acrylic">{{ i18n.ts.customEmojis }}</header>
-			<XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="computed(() => customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':'))" @chosen="chosen">{{ category || i18n.ts.other }}</XSection>
+			<XSection
+				v-for="category in customEmojiCategories"
+				:key="`custom:${category}`"
+				:initial-shown="false"
+				:emojis="computed(() => customEmojis.filter(e => category === null ? e.category == null || e.category === 'null' : e.category === category).map(e => `:${e.name}:`))"
+				@chosen="chosen"
+			>
+				{{ category || i18n.ts.other }}
+			</XSection>
 		</div>
 		<div v-once class="group">
 			<header class="_acrylic">{{ i18n.ts.emoji }}</header>
diff --git a/packages/frontend/src/custom-emojis.ts b/packages/frontend/src/custom-emojis.ts
index 2cd088993..4dd2e0909 100644
--- a/packages/frontend/src/custom-emojis.ts
+++ b/packages/frontend/src/custom-emojis.ts
@@ -1,17 +1,19 @@
 import { apiGet } from './os';
 import { miLocalStorage } from './local-storage';
-import { shallowRef, computed, markRaw, watch } from 'vue';
+import { shallowRef, computed, markRaw } from 'vue';
 import * as Misskey from 'misskey-js';
 import { stream } from '@/stream';
 
 const storageCache = miLocalStorage.getItem('emojis');
 export const customEmojis = shallowRef<Misskey.entities.CustomEmoji[]>(storageCache ? JSON.parse(storageCache) : []);
-export const customEmojiCategories = computed<string[]>(() => {
+export const customEmojiCategories = computed<[ ...string[], null ]>(() => {
 	const categories = new Set<string>();
 	for (const emoji of customEmojis.value) {
-		categories.add(emoji.category);
+		if (emoji.category && emoji.category !== 'null') {
+			categories.add(emoji.category);
+		}
 	}
-	return markRaw(Array.from(categories));
+	return markRaw([ ...Array.from(categories), null ]);
 });
 
 stream.on('emojiAdded', emojiData => {

From 26fbb3a5603096615ec304c1ca4b3da22ad6b456 Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Sun, 22 Jan 2023 17:39:11 +0000
Subject: [PATCH 16/25] fix

---
 packages/frontend/src/components/MkEmojiPicker.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue
index 479d8eb7f..f64cc6e9a 100644
--- a/packages/frontend/src/components/MkEmojiPicker.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.vue
@@ -64,7 +64,7 @@
 				v-for="category in customEmojiCategories"
 				:key="`custom:${category}`"
 				:initial-shown="false"
-				:emojis="computed(() => customEmojis.filter(e => category === null ? e.category == null || e.category === 'null' : e.category === category).map(e => `:${e.name}:`))"
+				:emojis="computed(() => customEmojis.filter(e => category === null ? (e.category === 'null' || !e.category) : e.category === category).map(e => `:${e.name}:`))"
 				@chosen="chosen"
 			>
 				{{ category || i18n.ts.other }}

From 5f49ac1b11ab7927ab17df2bd8a30eaf803e94f4 Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Wed, 25 Jan 2023 06:21:08 +0000
Subject: [PATCH 17/25] =?UTF-8?q?fix(client):=20=E3=82=A2=E3=83=8B?=
 =?UTF-8?q?=E3=83=A1=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3=E3=82=92=E3=82=AA?=
 =?UTF-8?q?=E3=83=95=E3=81=AB=E8=A8=AD=E5=AE=9A=E3=81=97=E3=81=A6=E3=82=82?=
 =?UTF-8?q?=E7=B5=B5=E6=96=87=E5=AD=97=E3=81=AE=E3=82=A2=E3=83=8B=E3=83=A1?=
 =?UTF-8?q?=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3=E3=81=8C=E6=AD=A2=E3=81=BE?=
 =?UTF-8?q?=E3=82=89=E3=81=AA=E3=81=84=20Fix=20#9720?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/frontend/src/components/global/MkEmoji.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue
index 93f50da20..991c6fb7c 100644
--- a/packages/frontend/src/components/global/MkEmoji.vue
+++ b/packages/frontend/src/components/global/MkEmoji.vue
@@ -33,7 +33,7 @@ const url = computed(() => {
 		return char2path(char.value);
 	} else if (props.host == null && !customEmojiName.includes('@')) {
 		const found = customEmojis.find(x => x.name === customEmojiName);
-		return found ? found.url : null;
+		return found ? defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(found.url) : found.url : null;
 	} else {
 		const rawUrl = props.host ? `/emoji/${customEmojiName}@${props.host}.webp` : `/emoji/${customEmojiName}.webp`;
 		return defaultStore.state.disableShowingAnimatedImages

From 5ffa106cc1ada8c0680a57cef027114232258870 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Wed, 25 Jan 2023 19:34:10 +0900
Subject: [PATCH 18/25] =?UTF-8?q?=E3=82=B5=E3=83=BC=E3=83=89=E3=83=91?=
 =?UTF-8?q?=E3=83=BC=E3=83=86=E3=82=A3=E3=81=8B=E3=82=89=E3=82=82=E8=87=AA?=
 =?UTF-8?q?=E8=BA=AB=E3=81=AE=E3=83=AD=E3=83=BC=E3=83=AB=E3=82=92=E7=A2=BA?=
 =?UTF-8?q?=E8=AA=8D=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Close #9700
---
 packages/backend/src/core/entities/UserEntityService.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index 34b523e14..f532b5bf6 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -496,10 +496,10 @@ export class UserEntityService implements OnModuleInit {
 				showTimelineReplies: user.showTimelineReplies ?? falsy,
 				achievements: profile!.achievements,
 				loggedInDays: profile!.loggedInDates.length,
+				policies: this.roleService.getUserPolicies(user.id),
 			} : {}),
 
 			...(opts.includeSecrets ? {
-				policies: this.roleService.getUserPolicies(user.id),
 				email: profile!.email,
 				emailVerified: profile!.emailVerified,
 				securityKeysList: profile!.twoFactorEnabled

From 1e3447bccbbabf09bafabf6df79d9ce3da04a506 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Wed, 25 Jan 2023 19:45:25 +0900
Subject: [PATCH 19/25] :art:

---
 packages/frontend/src/components/MkSuperMenu.vue | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue
index bb2a789b3..5d33ad0ad 100644
--- a/packages/frontend/src/components/MkSuperMenu.vue
+++ b/packages/frontend/src/components/MkSuperMenu.vue
@@ -6,15 +6,15 @@
 		<div class="items">
 			<template v-for="(item, i) in group.items">
 				<a v-if="item.type === 'a'" :href="item.href" :target="item.target" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }">
-					<i v-if="item.icon" class="icon ti-fw" :class="item.icon"></i>
+					<span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
 					<span class="text">{{ item.text }}</span>
 				</a>
 				<button v-else-if="item.type === 'button'" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="ev => item.action(ev)">
-					<i v-if="item.icon" class="icon ti-fw" :class="item.icon"></i>
+					<span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
 					<span class="text">{{ item.text }}</span>
 				</button>
 				<MkA v-else :to="item.to" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }">
-					<i v-if="item.icon" class="icon ti-fw" :class="item.icon"></i>
+					<span v-if="item.icon" class="icon"><i :class="item.icon" class="ti-fw"></i></span>
 					<span class="text">{{ item.text }}</span>
 				</MkA>
 			</template>

From 80d2e157f6a08d593367b2b933beb2a30b666b83 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Wed, 25 Jan 2023 19:49:17 +0900
Subject: [PATCH 20/25] :art:

---
 packages/frontend/src/components/MkMenu.vue | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue
index 94dabcac9..eee77a947 100644
--- a/packages/frontend/src/components/MkMenu.vue
+++ b/packages/frontend/src/components/MkMenu.vue
@@ -335,8 +335,7 @@ onBeforeUnmount(() => {
 }
 
 .icon {
-	margin-right: 5px;
-	width: 20px;
+	margin-right: 8px;
 }
 
 .caret {

From 7131eb1827311186698ebd5aa75d30c5ceafafe4 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 26 Jan 2023 11:31:43 +0900
Subject: [PATCH 21/25] fix(server): turnstile-failed: missing-input-secret

Fix #9726
---
 packages/backend/src/core/CaptchaService.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/backend/src/core/CaptchaService.ts b/packages/backend/src/core/CaptchaService.ts
index c8428a26b..7aaa1b833 100644
--- a/packages/backend/src/core/CaptchaService.ts
+++ b/packages/backend/src/core/CaptchaService.ts
@@ -23,9 +23,9 @@ export class CaptchaService {
 	
 		const res = await this.httpRequestService.send(url, {
 			method: 'POST',
-			body: JSON.stringify(params),
+			body: params.toString(),
 			headers: {
-				'Content-Type': 'application/json',
+				'Content-Type': 'application/x-www-form-urlencoded',
 			},
 		}, { throwErrorWhenResponseNotOk: false });
 	

From ec8074cd494da59f35e20cb8bc4e896060343877 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 26 Jan 2023 11:32:43 +0900
Subject: [PATCH 22/25] New Crowdin updates (#9724)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)
---
 locales/uk-UA.yml | 23 +++++++++++++++++++++++
 1 file changed, 23 insertions(+)

diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml
index a77bfa4f8..a85233996 100644
--- a/locales/uk-UA.yml
+++ b/locales/uk-UA.yml
@@ -956,9 +956,11 @@ _achievements:
     _login3:
       title: "Новачок I"
       description: "3 дні користування загально"
+      flavor: "Відсьогодні називайте мене \"Місскіст\""
     _login7:
       title: "Новачок II"
       description: "7 днів користування загально"
+      flavor: "Ви звикли до цього?"
     _login15:
       title: "Новачок III"
       description: "15 днів користування загально"
@@ -971,6 +973,7 @@ _achievements:
     _login100:
       title: "Міскієць III"
       description: "100 днів користування загально"
+      flavor: "Цей юзер лютий місскіст"
     _login200:
       title: "Завсідник I"
       description: "200 днів користування загально"
@@ -983,6 +986,7 @@ _achievements:
     _login500:
       title: "Ветеран I"
       description: "500 днів користування загально"
+      flavor: "Meine Kameraden, ich liebe sie, die Notizen."
     _login600:
       title: "Ветеран II"
       description: "600 днів користування загально"
@@ -990,13 +994,25 @@ _achievements:
       title: "Ветеран III"
       description: "700 днів користування загально"
     _login800:
+      title: "Майстер нотаток I"
       description: "800 днів користування загально"
     _login900:
+      title: "Майстер нотаток II"
       description: "900 днів користування загально"
     _login1000:
+      title: "Майстер нотаток III"
       description: "1000 днів користування загально"
       flavor: "Дякуємо, що користуєтеся Misskey!"
+    _myNoteFavorited1:
+      title: "У пошуках зірок"
+    _markedAsCat:
+      flavor: "Я дам тобі ім'я пізніше"
+    _following1:
+      title: "Перша підписка"
+    _following10:
+      title: "Продовжуй, продовжуй"
     _following50:
+      title: "Багато друзів"
       description: "Кількість підписок сягнула 50"
     _following100:
       title: "100 друзів"
@@ -1013,6 +1029,7 @@ _achievements:
     _followers50:
       description: "Кількість підписників досягла 50"
     _followers100:
+      title: "Популярна особа"
       description: "Кількість підписників досягла 100"
     _followers300:
       description: "Кількість підписників досягла 300"
@@ -1021,11 +1038,17 @@ _achievements:
     _followers1000:
       title: "Інфлюенсер"
       description: "Кількість підписників досягла 1000"
+    _passedSinceAccountCreated1:
+      title: "Перша річниця"
+    _passedSinceAccountCreated2:
+      title: "Друга річниця"
     _passedSinceAccountCreated3:
+      title: "Третя річниця"
       description: "Минуло 3 роки з моменту створення акаунта"
     _loggedInOnBirthday:
       title: "З Днем народження!"
     _brainDiver:
+      title: "Brain Diver"
       flavor: "Misskey-Misskey La-Tu-Ma"
 _role:
   priority: "Пріоритет"

From 41a6ed0de01f5ddb711e874e6d9cf0f97ab8cb2a Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 26 Jan 2023 11:33:31 +0900
Subject: [PATCH 23/25] lint

---
 packages/frontend/src/custom-emojis.ts | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/packages/frontend/src/custom-emojis.ts b/packages/frontend/src/custom-emojis.ts
index 4dd2e0909..0ba7cab5e 100644
--- a/packages/frontend/src/custom-emojis.ts
+++ b/packages/frontend/src/custom-emojis.ts
@@ -1,7 +1,7 @@
-import { apiGet } from './os';
-import { miLocalStorage } from './local-storage';
 import { shallowRef, computed, markRaw } from 'vue';
 import * as Misskey from 'misskey-js';
+import { apiGet } from './os';
+import { miLocalStorage } from './local-storage';
 import { stream } from '@/stream';
 
 const storageCache = miLocalStorage.getItem('emojis');
@@ -13,11 +13,11 @@ export const customEmojiCategories = computed<[ ...string[], null ]>(() => {
 			categories.add(emoji.category);
 		}
 	}
-	return markRaw([ ...Array.from(categories), null ]);
+	return markRaw([...Array.from(categories), null]);
 });
 
 stream.on('emojiAdded', emojiData => {
-	customEmojis.value = [ emojiData.emoji, ...customEmojis.value ];
+	customEmojis.value = [emojiData.emoji, ...customEmojis.value];
 });
 
 stream.on('emojiUpdated', emojiData => {

From d87bb807c30597dd050607b5d37030cc9f27f922 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 26 Jan 2023 11:39:21 +0900
Subject: [PATCH 24/25] tweak error screen

---
 packages/backend/src/server/web/boot.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js
index e635959fc..c6cb25e43 100644
--- a/packages/backend/src/server/web/boot.js
+++ b/packages/backend/src/server/web/boot.js
@@ -154,7 +154,7 @@
 				<path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path>
 			</svg>
 			<h1>An error has occurred!</h1>
-			<button class="button-big" onclick="location.reload(true);">
+			<button class="button-big" onclick="location.reload();">
 				<span class="button-label-big">Refresh</span>
 			</button>
 			<p class="dont-worry">Don't worry, it's (probably) not your fault.</p>

From aa8adc07aa225838a090bde499343514200bf55c Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Thu, 26 Jan 2023 11:40:36 +0900
Subject: [PATCH 25/25] 13.2.3

---
 CHANGELOG.md | 7 +++++++
 package.json | 2 +-
 2 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8dd21ce6d..b0dbb6e24 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,13 @@
 You should also include the user name that made the change.
 -->
 
+## 13.2.3 (2023/01/26)
+### Improvements
+- カスタム絵文字の更新をリアルタイムで反映するように
+
+### Bugfixes
+- turnstile-failed: missing-input-secret
+
 ## 13.2.2 (2023/01/25)
 ### Improvements
 - サーバーのパフォーマンスを改善
diff --git a/package.json b/package.json
index 800ca39ef..c5a556aea 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "13.2.2",
+	"version": "13.2.3",
 	"codename": "nasubi",
 	"repository": {
 		"type": "git",