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..b64121be74 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,8 @@ 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'; +import promiseLimit from "promise-limit"; type GridItem = { checked: boolean; @@ -326,28 +328,39 @@ async function onUpdateButtonClicked() { return; } - const action = () => { - return updatedItems.map(item => - misskeyApi( - 'admin/emoji/update', - { - // eslint-disable-next-line - 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), - fileId: item.fileId, - }) - .then(() => ({ item, success: true, err: undefined })) - .catch(err => ({ item, success: false, err })), - ); + const delay = (ms: number) => new Promise(resolve => window.setTimeout(resolve, ms)); + + type ApiResponse = { + item: any; + success: boolean; + err?: unknown; }; - const result = await os.promiseDialog(Promise.all(action())); + 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 }; + } + }; + + const action = async (): Promise => { + const limit = promiseLimit(2); + return await Promise.all(updatedItems.map(async it => limit(() => execute(it)))); + }; + + 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..e57f990a74 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.register.vue @@ -75,17 +75,16 @@ 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 promiseLimit from 'promise-limit'; import type { RequestLogItem } from '@/pages/admin/custom-emojis-manager.impl.js'; +import { emptyStrToEmptyArray, emptyStrToNull, roleIdsParser } 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'; +import { extractDroppedItems, flattenDroppedFiles } from '@/utility/file-drop.js'; import type { GridSetting } from '@/components/grid/grid.js'; import type { GridRow } from '@/components/grid/row.js'; import { misskeyApi } from '@/utility/misskey-api.js'; -import { - emptyStrToEmptyArray, - emptyStrToNull, - roleIdsParser, -} from '@/pages/admin/custom-emojis-manager.impl.js'; import MkGrid from '@/components/grid/MkGrid.vue'; import { i18n } from '@/i18n.js'; import MkSelect from '@/components/MkSelect.vue'; @@ -96,7 +95,6 @@ import * as os from '@/os.js'; import { validators } from '@/components/grid/cell-validators.js'; import { chooseFileFromDrive, chooseFileFromPc } from '@/utility/select-file.js'; import { uploadFile } from '@/utility/upload.js'; -import { extractDroppedItems, flattenDroppedFiles } from '@/utility/file-drop.js'; import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue'; import { copyGridDataToClipboard } from '@/components/grid/grid-utils.js'; @@ -247,7 +245,56 @@ const registerButtonDisabled = ref(false); const requestLogs = ref([]); const isDragOver = ref(false); -async function onRegistryClicked() { +type ApiResponse = { + item: any; + success: boolean; + err?: unknown; +}; + +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 }; + } +}; + +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; + } + + async function action(): Promise { + const limit = promiseLimit(3); + return await Promise.all(targets.map(item => limit(() => execute(item, 'admin/emoji/copy', { emojiId: item.id })))); + } + + const result = await os.promiseDialog(action()); + const failedItems = result.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 = result.map(it => ({ + failed: !it.success, + url: it.item.url, + name: it.item.name, + error: it.err ? JSON.stringify(it.err) : undefined, + })); +}; + +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 +304,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 items = gridItems.value.slice(0, MAXIMUM_EMOJI_REGISTER_COUNT); - const result = await os.promiseDialog(Promise.all(upload())); + async function action(): Promise { + const limit = promiseLimit(2); + return await Promise.all(items.map(item => limit(() => execute(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 result = await os.promiseDialog(action()); const failedItems = result.filter(it => !it.success); - if (failedItems.length > 0) { await os.alert({ type: 'error', @@ -295,10 +337,10 @@ async function onRegistryClicked() { error: it.err ? JSON.stringify(it.err) : undefined, })); - // 登録に成功したものは一覧から除く + // Remove successfully registered items from the list const successItems = result.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..b3fc6bed82 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