From d42869a4df84aa92aac47ad84481aff74c0c8259 Mon Sep 17 00:00:00 2001 From: "lunya.pet" Date: Sat, 28 Jun 2025 19:09:52 +0200 Subject: [PATCH 1/7] feat(beta emoji panel): Sequential updates, retries --- .../custom-emojis-manager.local.list.vue | 48 +++++--- .../admin/custom-emojis-manager.register.vue | 109 +++++++++++++----- .../admin/custom-emojis-manager.remote.vue | 52 ++++++--- 3 files changed, 152 insertions(+), 57 deletions(-) diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue index 59b780bff6..03d4817e35 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue @@ -326,28 +326,50 @@ async function onUpdateButtonClicked() { return; } - const action = () => { - return updatedItems.map(item => - misskeyApi( - 'admin/emoji/update', - { - // eslint-disable-next-line - id: item.id!, + const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + + type ApiResponse = { + item: any; + success: boolean; + err?: unknown; + }; + + const executeWithRetries = async (item: any, retries: number = 3): Promise => { + for (let attempt = 0; attempt <= retries; attempt++) { + try { + await misskeyApi('admin/emoji/update', { + id: item.id, name: item.name, category: emptyStrToNull(item.category), aliases: emptyStrToEmptyArray(item.aliases), license: emptyStrToNull(item.license), isSensitive: item.isSensitive, localOnly: item.localOnly, - roleIdsThatCanBeUsedThisEmojiAsReaction: item.roleIdsThatCanBeUsedThisEmojiAsReaction.map(it => it.id), + roleIdsThatCanBeUsedThisEmojiAsReaction: item.roleIdsThatCanBeUsedThisEmojiAsReaction.map((it: any) => it.id), fileId: item.fileId, - }) - .then(() => ({ item, success: true, err: undefined })) - .catch(err => ({ item, success: false, err })), - ); + }); + return { item, success: true }; + } catch (err) { + if (attempt < retries) { + console.warn(`Retrying ${item.name}, attempt ${attempt + 1}`); + await delay(1000 * (attempt + 1)); + } else { + return { item, success: false, err }; + } + } + } + return { item, success: false, err: new Error('Unknown error') }; }; - const result = await os.promiseDialog(Promise.all(action())); + const action = async (): Promise => { + const results: ApiResponse[] = []; + for (const item of updatedItems) { + results.push(await executeWithRetries(item)); + } + return results; + }; + + const result = await os.promiseDialog(action()); const failedItems = result.filter(it => !it.success); if (failedItems.length > 0) { diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue index e8e944df32..8cf8bfb0f5 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue @@ -247,7 +247,67 @@ const registerButtonDisabled = ref(false); const requestLogs = ref([]); const isDragOver = ref(false); -async function onRegistryClicked() { +const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + +type ApiResponse = { + item: any; + success: boolean; + err?: unknown; +}; + +const executeWithRetries = async (item: any, apiEndpoint: string, payload: any, retries: number = 3): Promise => { + for (let attempt = 0; attempt <= retries; attempt++) { + try { + await misskeyApi(apiEndpoint, payload); + return { item, success: true }; + } catch (err) { + if (attempt < retries) { + console.warn(`Retrying ${item.id || item.name}, attempt ${attempt + 1}`); + await delay(1000 * (attempt + 1)); // Exponential backoff + } else { + return { item, success: false, err }; + } + } + } + return { item, success: false, err: new Error('Unknown error') }; // Ensures all code paths return a value +}; + +const importEmojis = async (targets: any[]): Promise => { + const confirm = await os.confirm({ + type: 'info', + title: i18n.ts._customEmojisManager._remote.confirmImportEmojisTitle, + text: i18n.tsx._customEmojisManager._remote.confirmImportEmojisDescription({ count: targets.length }), + }); + + if (confirm.canceled) { + return; + } + + const results: ApiResponse[] = []; + for (const item of targets) { + results.push(await executeWithRetries(item, 'admin/emoji/copy', { emojiId: item.id })); + } + + const failedItems = results.filter(it => !it.success); + if (failedItems.length > 0) { + await os.alert({ + type: 'error', + title: i18n.ts.somethingHappened, + text: i18n.ts._customEmojisManager._gridCommon.alertEmojisRegisterFailedDescription, + }); + } + + requestLogs.value = results.map(it => ({ + failed: !it.success, + url: it.item.url, + name: it.item.name, + error: it.err ? JSON.stringify(it.err) : undefined, + })); + + await refreshCustomEmojis(); +}; + +const onRegistryClicked = async (): Promise => { const dialogSelection = await os.confirm({ type: 'info', text: i18n.tsx._customEmojisManager._local._register.confirmRegisterEmojisDescription({ count: MAXIMUM_EMOJI_REGISTER_COUNT }), @@ -257,29 +317,24 @@ async function onRegistryClicked() { return; } - const items = gridItems.value; - const upload = () => { - return items.slice(0, MAXIMUM_EMOJI_REGISTER_COUNT) - .map(item => - misskeyApi( - 'admin/emoji/add', { - name: item.name, - category: emptyStrToNull(item.category), - aliases: emptyStrToEmptyArray(item.aliases), - license: emptyStrToNull(item.license), - isSensitive: item.isSensitive, - localOnly: item.localOnly, - roleIdsThatCanBeUsedThisEmojiAsReaction: item.roleIdsThatCanBeUsedThisEmojiAsReaction.map(it => it.id), - fileId: item.fileId!, - }) - .then(() => ({ item, success: true, err: undefined })) - .catch(err => ({ item, success: false, err })), - ); - }; - - const result = await os.promiseDialog(Promise.all(upload())); - const failedItems = result.filter(it => !it.success); + const items = gridItems.value.slice(0, MAXIMUM_EMOJI_REGISTER_COUNT); + const results: ApiResponse[] = []; + for (const item of items) { + results.push( + await executeWithRetries(item, 'admin/emoji/add', { + name: item.name, + category: emptyStrToNull(item.category), + aliases: emptyStrToEmptyArray(item.aliases), + license: emptyStrToNull(item.license), + isSensitive: item.isSensitive, + localOnly: item.localOnly, + roleIdsThatCanBeUsedThisEmojiAsReaction: item.roleIdsThatCanBeUsedThisEmojiAsReaction.map((it: any) => it.id), + fileId: item.fileId!, + }) + ); + } + const failedItems = results.filter(it => !it.success); if (failedItems.length > 0) { await os.alert({ type: 'error', @@ -288,17 +343,17 @@ async function onRegistryClicked() { }); } - requestLogs.value = result.map(it => ({ + requestLogs.value = results.map(it => ({ failed: !it.success, url: it.item.url, name: it.item.name, error: it.err ? JSON.stringify(it.err) : undefined, })); - // 登録に成功したものは一覧から除く - const successItems = result.filter(it => it.success).map(it => it.item); + // Remove successfully registered items from the list + const successItems = results.filter(it => it.success).map(it => it.item); gridItems.value = gridItems.value.filter(it => !successItems.includes(it)); -} +}; async function onClearClicked() { const result = await os.confirm({ diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue index 2fd7e331a2..b32350b04f 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue @@ -310,7 +310,34 @@ function onGridCellValueChange(event: GridCellValueChangeEvent) { } } -async function importEmojis(targets: GridItem[]) { +const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + +type ApiResponse = { + item: any; + success: boolean; + err?: unknown; +}; + +const executeWithRetries = async (item: any, retries: number = 3): Promise => { + for (let attempt = 0; attempt <= retries; attempt++) { + try { + await misskeyApi('admin/emoji/copy', { + emojiId: item.id, + }); + return { item, success: true }; + } catch (err) { + if (attempt < retries) { + console.warn(`Retrying ${item.id}, attempt ${attempt + 1}`); + await delay(1000 * (attempt + 1)); // Exponential backoff + } else { + return { item, success: false, err }; + } + } + } + return { item, success: false, err: new Error('Unknown error') }; // Ensures all code paths return a value +}; + +const importEmojis = async (targets: any[]): Promise => { const confirm = await os.confirm({ type: 'info', title: i18n.ts._customEmojisManager._remote.confirmImportEmojisTitle, @@ -321,21 +348,12 @@ async function importEmojis(targets: GridItem[]) { return; } - const result = await os.promiseDialog( - Promise.all( - targets.map(item => - misskeyApi( - 'admin/emoji/copy', - { - emojiId: item.id!, - }) - .then(() => ({ item, success: true, err: undefined })) - .catch(err => ({ item, success: false, err })), - ), - ), - ); - const failedItems = result.filter(it => !it.success); + const results: ApiResponse[] = []; + for (const item of targets) { + results.push(await executeWithRetries(item)); + } + const failedItems = results.filter(it => !it.success); if (failedItems.length > 0) { await os.alert({ type: 'error', @@ -344,7 +362,7 @@ async function importEmojis(targets: GridItem[]) { }); } - requestLogs.value = result.map(it => ({ + requestLogs.value = results.map(it => ({ failed: !it.success, url: it.item.url, name: it.item.name, @@ -352,7 +370,7 @@ async function importEmojis(targets: GridItem[]) { })); await refreshCustomEmojis(); -} +}; async function refreshCustomEmojis() { const query: Misskey.entities.V2AdminEmojiListRequest['query'] = { From 772686dac2342d5ed676757d42603ac35991ac9a Mon Sep 17 00:00:00 2001 From: "lunya.pet" Date: Sat, 28 Jun 2025 19:24:16 +0200 Subject: [PATCH 2/7] fix: Fix eslint screaming, fix missing loading when registering emojis --- .../custom-emojis-manager.local.list.vue | 2 +- .../admin/custom-emojis-manager.register.vue | 21 ++++++++++++------- .../admin/custom-emojis-manager.remote.vue | 2 +- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue index 03d4817e35..ff4ba0d557 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue @@ -326,7 +326,7 @@ async function onUpdateButtonClicked() { return; } - const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + const delay = (ms: number) => new Promise(resolve => window.setTimeout(resolve, ms)); type ApiResponse = { item: any; diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue index 8cf8bfb0f5..914968a473 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue @@ -247,7 +247,7 @@ const registerButtonDisabled = ref(false); const requestLogs = ref([]); const isDragOver = ref(false); -const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); +const delay = (ms: number) => new Promise(resolve => window.setTimeout(resolve, ms)); type ApiResponse = { item: any; @@ -255,7 +255,7 @@ type ApiResponse = { err?: unknown; }; -const executeWithRetries = async (item: any, apiEndpoint: string, payload: any, retries: number = 3): Promise => { +const executeWithRetries = async (item: any, apiEndpoint: string, payload: any, retries = 3): Promise => { for (let attempt = 0; attempt <= retries; attempt++) { try { await misskeyApi(apiEndpoint, payload); @@ -283,12 +283,17 @@ const importEmojis = async (targets: any[]): Promise => { return; } - const results: ApiResponse[] = []; - for (const item of targets) { - results.push(await executeWithRetries(item, 'admin/emoji/copy', { emojiId: item.id })); + async function action(): Promise { + const results: ApiResponse[] = []; + for (const item of targets) { + results.push(await executeWithRetries(item, 'admin/emoji/copy', { emojiId: item.id })); + } + + return results; } - const failedItems = results.filter(it => !it.success); + const result = await os.promiseDialog(action()); + const failedItems = result.filter(it => !it.success); if (failedItems.length > 0) { await os.alert({ type: 'error', @@ -297,7 +302,7 @@ const importEmojis = async (targets: any[]): Promise => { }); } - requestLogs.value = results.map(it => ({ + requestLogs.value = result.map(it => ({ failed: !it.success, url: it.item.url, name: it.item.name, @@ -330,7 +335,7 @@ const onRegistryClicked = async (): Promise => { localOnly: item.localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: item.roleIdsThatCanBeUsedThisEmojiAsReaction.map((it: any) => it.id), fileId: item.fileId!, - }) + }), ); } diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue index b32350b04f..0d54a1faba 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue @@ -310,7 +310,7 @@ function onGridCellValueChange(event: GridCellValueChangeEvent) { } } -const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); +const delay = (ms: number) => new Promise(resolve => window.setTimeout(resolve, ms)); type ApiResponse = { item: any; From a5b6a456cbb8187607960de83798abf164981936 Mon Sep 17 00:00:00 2001 From: "lunya.pet" Date: Sun, 29 Jun 2025 09:12:57 +0200 Subject: [PATCH 3/7] fix: Fix more missing loading --- .../admin/custom-emojis-manager.register.vue | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue index 914968a473..f35f9dc009 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue @@ -288,7 +288,7 @@ const importEmojis = async (targets: any[]): Promise => { for (const item of targets) { results.push(await executeWithRetries(item, 'admin/emoji/copy', { emojiId: item.id })); } - + return results; } @@ -308,8 +308,6 @@ const importEmojis = async (targets: any[]): Promise => { name: it.item.name, error: it.err ? JSON.stringify(it.err) : undefined, })); - - await refreshCustomEmojis(); }; const onRegistryClicked = async (): Promise => { @@ -323,23 +321,29 @@ const onRegistryClicked = async (): Promise => { } const items = gridItems.value.slice(0, MAXIMUM_EMOJI_REGISTER_COUNT); - const results: ApiResponse[] = []; - for (const item of items) { - results.push( - await executeWithRetries(item, 'admin/emoji/add', { - name: item.name, - category: emptyStrToNull(item.category), - aliases: emptyStrToEmptyArray(item.aliases), - license: emptyStrToNull(item.license), - isSensitive: item.isSensitive, - localOnly: item.localOnly, - roleIdsThatCanBeUsedThisEmojiAsReaction: item.roleIdsThatCanBeUsedThisEmojiAsReaction.map((it: any) => it.id), - fileId: item.fileId!, - }), - ); + + async function action(): Promise { + const results: ApiResponse[] = []; + for (const item of items) { + results.push( + await executeWithRetries(item, 'admin/emoji/add', { + name: item.name, + category: emptyStrToNull(item.category), + aliases: emptyStrToEmptyArray(item.aliases), + license: emptyStrToNull(item.license), + isSensitive: item.isSensitive, + localOnly: item.localOnly, + roleIdsThatCanBeUsedThisEmojiAsReaction: item.roleIdsThatCanBeUsedThisEmojiAsReaction.map((it: any) => it.id), + fileId: item.fileId!, + }), + ); + } + + return results; } - const failedItems = results.filter(it => !it.success); + const result = await os.promiseDialog(action()); + const failedItems = result.filter(it => !it.success); if (failedItems.length > 0) { await os.alert({ type: 'error', @@ -348,7 +352,7 @@ const onRegistryClicked = async (): Promise => { }); } - requestLogs.value = results.map(it => ({ + requestLogs.value = result.map(it => ({ failed: !it.success, url: it.item.url, name: it.item.name, From 7fcce1c7efaba821e8f42135ecb89506d6380b3e Mon Sep 17 00:00:00 2001 From: "lunya.pet" Date: Sun, 29 Jun 2025 09:20:04 +0200 Subject: [PATCH 4/7] fix: IDE not renaming things moment --- .../frontend/src/pages/admin/custom-emojis-manager.register.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue index f35f9dc009..c5083263dd 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue @@ -360,7 +360,7 @@ const onRegistryClicked = async (): Promise => { })); // Remove successfully registered items from the list - const successItems = results.filter(it => it.success).map(it => it.item); + const successItems = result.filter(it => it.success).map(it => it.item); gridItems.value = gridItems.value.filter(it => !successItems.includes(it)); }; From 04605283f974baeb79f1d84d14fbf0a24dbdfcf0 Mon Sep 17 00:00:00 2001 From: "lunya.pet" Date: Sun, 29 Jun 2025 12:42:07 +0200 Subject: [PATCH 5/7] fix: Use retryOnThrottled --- .../custom-emojis-manager.local.list.vue | 43 ++++++++----------- .../admin/custom-emojis-manager.register.vue | 27 ++++-------- .../admin/custom-emojis-manager.remote.vue | 29 +++++-------- 3 files changed, 37 insertions(+), 62 deletions(-) diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue index ff4ba0d557..3b37f26a39 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue @@ -90,6 +90,7 @@ import MkPagingButtons from '@/components/MkPagingButtons.vue'; import { selectFile } from '@/utility/select-file.js'; import { copyGridDataToClipboard, removeDataFromGrid } from '@/components/grid/grid-utils.js'; import { useLoading } from '@/components/hook/useLoading.js'; +import { retryOnThrottled } from '@@/js/retry-on-throttled'; type GridItem = { checked: boolean; @@ -334,37 +335,29 @@ async function onUpdateButtonClicked() { err?: unknown; }; - const executeWithRetries = async (item: any, retries: number = 3): Promise => { - for (let attempt = 0; attempt <= retries; attempt++) { - try { - await misskeyApi('admin/emoji/update', { - id: item.id, - name: item.name, - category: emptyStrToNull(item.category), - aliases: emptyStrToEmptyArray(item.aliases), - license: emptyStrToNull(item.license), - isSensitive: item.isSensitive, - localOnly: item.localOnly, - roleIdsThatCanBeUsedThisEmojiAsReaction: item.roleIdsThatCanBeUsedThisEmojiAsReaction.map((it: any) => it.id), - fileId: item.fileId, - }); - return { item, success: true }; - } catch (err) { - if (attempt < retries) { - console.warn(`Retrying ${item.name}, attempt ${attempt + 1}`); - await delay(1000 * (attempt + 1)); - } else { - return { item, success: false, err }; - } - } + const execute = async (item: any): Promise => { + try { + await retryOnThrottled(() => misskeyApi('admin/emoji/update', { + id: item.id, + name: item.name, + category: emptyStrToNull(item.category), + aliases: emptyStrToEmptyArray(item.aliases), + license: emptyStrToNull(item.license), + isSensitive: item.isSensitive, + localOnly: item.localOnly, + roleIdsThatCanBeUsedThisEmojiAsReaction: item.roleIdsThatCanBeUsedThisEmojiAsReaction.map((it: any) => it.id), + fileId: item.fileId, + })); + return { item, success: true }; + } catch (error) { + return { item, success: false, err: error }; } - return { item, success: false, err: new Error('Unknown error') }; }; const action = async (): Promise => { const results: ApiResponse[] = []; for (const item of updatedItems) { - results.push(await executeWithRetries(item)); + results.push(await execute(item)); } return results; }; diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue index c5083263dd..78678342ea 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue @@ -75,6 +75,7 @@ SPDX-License-Identifier: AGPL-3.0-only /* eslint-disable @typescript-eslint/no-non-null-assertion */ import * as Misskey from 'misskey-js'; import { onMounted, ref, useCssModule } from 'vue'; +import { retryOnThrottled } from '@@/js/retry-on-throttled'; import type { RequestLogItem } from '@/pages/admin/custom-emojis-manager.impl.js'; import type { GridCellValidationEvent, GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js'; import type { DroppedFile } from '@/utility/file-drop.js'; @@ -247,29 +248,19 @@ const registerButtonDisabled = ref(false); const requestLogs = ref([]); const isDragOver = ref(false); -const delay = (ms: number) => new Promise(resolve => window.setTimeout(resolve, ms)); - type ApiResponse = { item: any; success: boolean; err?: unknown; }; -const executeWithRetries = async (item: any, apiEndpoint: string, payload: any, retries = 3): Promise => { - for (let attempt = 0; attempt <= retries; attempt++) { - try { - await misskeyApi(apiEndpoint, payload); - return { item, success: true }; - } catch (err) { - if (attempt < retries) { - console.warn(`Retrying ${item.id || item.name}, attempt ${attempt + 1}`); - await delay(1000 * (attempt + 1)); // Exponential backoff - } else { - return { item, success: false, err }; - } - } +const execute = async (item: any, apiEndpoint: string, payload: any): Promise => { + try { + await retryOnThrottled(() => misskeyApi(apiEndpoint, payload)); + return { item, success: true }; + } catch (err) { + return { item, success: false, err }; } - return { item, success: false, err: new Error('Unknown error') }; // Ensures all code paths return a value }; const importEmojis = async (targets: any[]): Promise => { @@ -286,7 +277,7 @@ const importEmojis = async (targets: any[]): Promise => { async function action(): Promise { const results: ApiResponse[] = []; for (const item of targets) { - results.push(await executeWithRetries(item, 'admin/emoji/copy', { emojiId: item.id })); + results.push(await execute(item, 'admin/emoji/copy', { emojiId: item.id })); } return results; @@ -326,7 +317,7 @@ const onRegistryClicked = async (): Promise => { const results: ApiResponse[] = []; for (const item of items) { results.push( - await executeWithRetries(item, 'admin/emoji/add', { + await execute(item, 'admin/emoji/add', { name: item.name, category: emptyStrToNull(item.category), aliases: emptyStrToEmptyArray(item.aliases), diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue index 0d54a1faba..fb32ff4865 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue @@ -142,6 +142,7 @@ SPDX-License-Identifier: AGPL-3.0-only