diff options
| author | Namekuji <11836635+nmkj-io@users.noreply.github.com> | 2023-04-29 11:09:29 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-04-30 00:09:29 +0900 |
| commit | d28866f71a2fd1809f595bf599cc3914834f74e2 (patch) | |
| tree | 70c0319864ef00b45484820bd149f7d4d4d943fb /packages/frontend/src | |
| parent | fix(frontend): ロールのタイトルのバグを解決、Reactivity Transf... (diff) | |
| download | sharkey-d28866f71a2fd1809f595bf599cc3914834f74e2.tar.gz sharkey-d28866f71a2fd1809f595bf599cc3914834f74e2.tar.bz2 sharkey-d28866f71a2fd1809f595bf599cc3914834f74e2.zip | |
enhance: account migration (#10592)
* copy block and mute then create follow and unfollow jobs
* copy block and mute and update lists when detecting an account has moved
* no need to care promise orders
* refactor updating actor and target
* automatically accept if a locked account had accepted an old account
* fix exception format
* prevent the old account from calling some endpoints
* do not unfollow when moving
* adjust following and follower counts
* check movedToUri when receiving a follow request
* skip if no need to adjust
* Revert "disable account migration"
This reverts commit 2321214c98591bcfe1385c1ab5bf0ff7b471ae1d.
* fix translation specifier
* fix checking alsoKnownAs and uri
* fix updating account
* fix refollowing locked account
* decrease followersCount if followed by the old account
* adjust following and followers counts when unfollowing
* fix copying mutings
* prohibit moved account from moving again
* fix move service
* allow app creation after moving
* fix lint
* remove unnecessary field
* fix cache update
* add e2e test
* add e2e test of accepting the new account automatically
* force follow if any error happens
* remove unnecessary joins
* use Array.map instead of for const of
* ユーザーリストの移行は追加のみを行う
* nanka iroiro
* fix misskey-js?
* :v:
* 移行を行ったアカウントからのフォローリクエストの自動許可を調整
* newUriを外に出す
* newUriを外に出す2
* clean up
* fix newUri
* prevent moving if the destination account has already moved
* set alsoKnownAs via /i/update
* fix database initialization
* add return type
* prohibit updating alsoKnownAs after moving
* skip to add to alsoKnownAs if toUrl is known
* skip adding to the list if it already has
* use Acct.parse instead
* rename error code
* :art:
* 制限を5から10に緩和
* movedTo(Uri), alsoKnownAsはユーザーidを返すように
* test api res
* fix
* 元アカウントはミュートし続ける
* :art:
* unfollow
* fix
* getUserUriをUserEntityServiceに
* ?
* job!
* :art:
* instance => server
* accountMovedShort, forbiddenBecauseYouAreMigrated
* accountMovedShort
* fix test
* import, pin禁止
* 実績を凍結する
* clean up
* :v:
* change message
* ブロック, フォロー, ミュート, リストのインポートファイルの制限を32MiBに
* Revert "ブロック, フォロー, ミュート, リストのインポートファイルの制限を32MiBに"
This reverts commit 3bd7be35d8aa455cb01ae58f8172a71a50485db1.
* validateAlsoKnownAs
* 移行後2時間以内はインポート可能なファイルサイズを拡大
* clean up
* どうせactorをupdatePersonで更新するならupdatePersonしか移行処理を発行しないことにする
* handle error?
* リモートからの移行処理の条件を是正
* log, port
* fix
* fix
* enhance(dev): non-production環境でhttpサーバー間でもユーザー、ノートの連合が可能なように
* refactor (use checkHttps)
* MISSKEY_WEBFINGER_USE_HTTP
* Environment Variable readme
* NEVER USE IN PRODUCTION
* fix punyHost
* fix indent
* fix
* experimental
---------
Co-authored-by: tamaina <tamaina@hotmail.co.jp>
Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
Diffstat (limited to 'packages/frontend/src')
| -rw-r--r-- | packages/frontend/src/components/MkAccountMoved.vue | 16 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkInfo.vue | 1 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkNote.vue | 4 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkNoteDetailed.vue | 5 | ||||
| -rw-r--r-- | packages/frontend/src/os.ts | 3 | ||||
| -rw-r--r-- | packages/frontend/src/pages/settings/import-export.vue | 9 | ||||
| -rw-r--r-- | packages/frontend/src/pages/settings/index.vue | 6 | ||||
| -rw-r--r-- | packages/frontend/src/pages/settings/migration.vue | 118 | ||||
| -rw-r--r-- | packages/frontend/src/pages/settings/profile.vue | 8 | ||||
| -rw-r--r-- | packages/frontend/src/pages/user/home.vue | 2 | ||||
| -rw-r--r-- | packages/frontend/src/scripts/achievements.ts | 1 | ||||
| -rw-r--r-- | packages/frontend/src/scripts/please-login.ts | 2 | ||||
| -rw-r--r-- | packages/frontend/src/scripts/show-moved-dialog.ts | 16 |
13 files changed, 143 insertions, 48 deletions
diff --git a/packages/frontend/src/components/MkAccountMoved.vue b/packages/frontend/src/components/MkAccountMoved.vue index 98979de236..b02bfdc2b8 100644 --- a/packages/frontend/src/components/MkAccountMoved.vue +++ b/packages/frontend/src/components/MkAccountMoved.vue @@ -1,8 +1,8 @@ <template> -<div :class="$style.root"> +<div v-if="user" :class="$style.root"> <i class="ti ti-plane-departure" style="margin-right: 8px;"></i> {{ i18n.ts.accountMoved }} - <MkMention :class="$style.link" :username="username" :host="host ?? localHost"/> + <MkMention :class="$style.link" :username="user.username" :host="user.host ?? localHost"/> </div> </template> @@ -10,11 +10,17 @@ import MkMention from './MkMention.vue'; import { i18n } from '@/i18n'; import { host as localHost } from '@/config'; +import { ref } from 'vue'; +import { UserLite } from 'misskey-js/built/entities'; +import { api } from '@/os'; -defineProps<{ - username: string; - host: string; +const user = ref<UserLite>(); + +const props = defineProps<{ + movedTo: string; // user id }>(); + +api('users/show', { userId: props.movedTo }).then(u => user.value = u); </script> <style lang="scss" module> diff --git a/packages/frontend/src/components/MkInfo.vue b/packages/frontend/src/components/MkInfo.vue index dc7344d707..cda428a77c 100644 --- a/packages/frontend/src/components/MkInfo.vue +++ b/packages/frontend/src/components/MkInfo.vue @@ -21,6 +21,7 @@ const props = defineProps<{ background: var(--infoBg); color: var(--infoFg); border-radius: var(--radius); + white-space: pre-wrap; &.warn { background: var(--infoWarnBg); diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index cb4bb8f23e..eba3c211a6 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -162,6 +162,7 @@ import { claimAchievement } from '@/scripts/achievements'; import { getNoteSummary } from '@/scripts/get-note-summary'; import { MenuItem } from '@/types/menu'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; +import { showMovedDialog } from '@/scripts/show-moved-dialog'; const props = defineProps<{ note: misskey.entities.Note; @@ -255,6 +256,7 @@ useTooltip(renoteButton, async (showing) => { function renote(viaKeyboard = false) { pleaseLogin(); + showMovedDialog(); let items = [] as MenuItem[]; @@ -335,6 +337,7 @@ function reply(viaKeyboard = false): void { function react(viaKeyboard = false): void { pleaseLogin(); + showMovedDialog(); if (appearNote.reactionAcceptance === 'likeOnly') { os.api('notes/reactions/create', { noteId: appearNote.id, @@ -401,6 +404,7 @@ async function clip() { function showRenoteMenu(viaKeyboard = false): void { if (!isMyRenote) return; + pleaseLogin(); os.popupMenu([{ text: i18n.ts.unrenote, icon: 'ti ti-trash', diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index b9ab366850..0d6d329d98 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -166,6 +166,7 @@ import { useTooltip } from '@/scripts/use-tooltip'; import { claimAchievement } from '@/scripts/achievements'; import { MenuItem } from '@/types/menu'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; +import { showMovedDialog } from '@/scripts/show-moved-dialog'; const props = defineProps<{ note: misskey.entities.Note; @@ -248,6 +249,7 @@ useTooltip(renoteButton, async (showing) => { function renote(viaKeyboard = false) { pleaseLogin(); + showMovedDialog(); let items = [] as MenuItem[]; @@ -318,6 +320,7 @@ function renote(viaKeyboard = false) { function reply(viaKeyboard = false): void { pleaseLogin(); + showMovedDialog(); os.post({ reply: appearNote, animation: !viaKeyboard, @@ -328,6 +331,7 @@ function reply(viaKeyboard = false): void { function react(viaKeyboard = false): void { pleaseLogin(); + showMovedDialog(); if (appearNote.reactionAcceptance === 'likeOnly') { os.api('notes/reactions/create', { noteId: appearNote.id, @@ -394,6 +398,7 @@ async function clip() { function showRenoteMenu(viaKeyboard = false): void { if (!isMyRenote) return; + pleaseLogin(); os.popupMenu([{ text: i18n.ts.unrenote, icon: 'ti ti-trash', diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index 157aa79064..67acee5aca 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -18,6 +18,7 @@ import MkPopupMenu from '@/components/MkPopupMenu.vue'; import MkContextMenu from '@/components/MkContextMenu.vue'; import { MenuItem } from '@/types/menu'; import copyToClipboard from './scripts/copy-to-clipboard'; +import { showMovedDialog } from './scripts/show-moved-dialog'; export const openingWindowsCount = ref(0); @@ -578,6 +579,8 @@ export function contextMenu(items: MenuItem[] | Ref<MenuItem[]>, ev: MouseEvent) } export function post(props: Record<string, any> = {}): Promise<void> { + showMovedDialog(); + return new Promise((resolve, reject) => { // NOTE: MkPostFormDialogをdynamic importするとiOSでテキストエリアに自動フォーカスできない // NOTE: ただ、dynamic importしない場合、MkPostFormDialogインスタンスが使いまわされ、 diff --git a/packages/frontend/src/pages/settings/import-export.vue b/packages/frontend/src/pages/settings/import-export.vue index a8274f5601..c883efe691 100644 --- a/packages/frontend/src/pages/settings/import-export.vue +++ b/packages/frontend/src/pages/settings/import-export.vue @@ -32,7 +32,7 @@ <MkButton primary :class="$style.button" inline @click="exportFollowing()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton> </div> </MkFolder> - <MkFolder> + <MkFolder v-if="$i && !$i.movedTo"> <template #label>{{ i18n.ts.import }}</template> <template #icon><i class="ti ti-upload"></i></template> <MkButton primary :class="$style.button" inline @click="importFollowing($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton> @@ -47,7 +47,7 @@ <template #icon><i class="ti ti-download"></i></template> <MkButton primary :class="$style.button" inline @click="exportUserLists()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton> </MkFolder> - <MkFolder> + <MkFolder v-if="$i && !$i.movedTo"> <template #label>{{ i18n.ts.import }}</template> <template #icon><i class="ti ti-upload"></i></template> <MkButton primary :class="$style.button" inline @click="importUserLists($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton> @@ -62,7 +62,7 @@ <template #icon><i class="ti ti-download"></i></template> <MkButton primary :class="$style.button" inline @click="exportMuting()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton> </MkFolder> - <MkFolder> + <MkFolder v-if="$i && !$i.movedTo"> <template #label>{{ i18n.ts.import }}</template> <template #icon><i class="ti ti-upload"></i></template> <MkButton primary :class="$style.button" inline @click="importMuting($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton> @@ -77,7 +77,7 @@ <template #icon><i class="ti ti-download"></i></template> <MkButton primary :class="$style.button" inline @click="exportBlocking()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton> </MkFolder> - <MkFolder> + <MkFolder v-if="$i && !$i.movedTo"> <template #label>{{ i18n.ts.import }}</template> <template #icon><i class="ti ti-upload"></i></template> <MkButton primary :class="$style.button" inline @click="importBlocking($event)"><i class="ti ti-upload"></i> {{ i18n.ts.import }}</MkButton> @@ -97,6 +97,7 @@ import * as os from '@/os'; import { selectFile } from '@/scripts/select-file'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; +import { $i } from '@/account'; const excludeMutingUsers = ref(false); const excludeInactiveUsers = ref(false); diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue index 17af7417fd..34a962ef4c 100644 --- a/packages/frontend/src/pages/settings/index.vue +++ b/packages/frontend/src/pages/settings/index.vue @@ -164,12 +164,12 @@ const menuDef = computed(() => [{ text: i18n.ts.importAndExport, to: '/settings/import-export', active: currentPage?.route.name === 'import-export', - }, /*{ + }, { icon: 'ti ti-plane', - text: i18n.ts.accountMigration, + text: `${i18n.ts.accountMigration} (${i18n.ts.experimental})`, to: '/settings/migration', active: currentPage?.route.name === 'migration', - },*/ { + }, { icon: 'ti ti-dots', text: i18n.ts.other, to: '/settings/other', diff --git a/packages/frontend/src/pages/settings/migration.vue b/packages/frontend/src/pages/settings/migration.vue index 2ef8af7481..fa4da0dae3 100644 --- a/packages/frontend/src/pages/settings/migration.vue +++ b/packages/frontend/src/pages/settings/migration.vue @@ -1,63 +1,121 @@ <template> <div class="_gaps_m"> - <FormSection first> + <FormInfo warn> + {{ i18n.ts.ThisIsExperimentalFeature }} + </FormInfo> + <MkFolder :default-open="true"> + <template #icon><i class="ti ti-plane-arrival"></i></template> + <template #label>{{ i18n.ts._accountMigration.moveFrom }}</template> + <template #caption>{{ i18n.ts._accountMigration.moveFromSub }}</template> + + <div class="_gaps_m"> + <FormInfo warn> + {{ i18n.ts._accountMigration.moveFromDescription }} + </FormInfo> + <div> + <MkButton :disabled="accountAliases.length >= 10" inline style="margin-right: 8px;" @click="add"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> + <MkButton inline primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> + </div> + <div class="_gaps"> + <MkInput v-for="(_, i) in accountAliases" v-model="accountAliases[i]"> + <template #prefix><i class="ti ti-plane-arrival"></i></template> + <template #label>{{ i18n.t('_accountMigration.moveFromLabel', { n: i + 1 }) }}</template> + </MkInput> + </div> + </div> + </MkFolder> + + <MkFolder :default-open="!!$i?.movedTo"> + <template #icon><i class="ti ti-plane-departure"></i></template> <template #label>{{ i18n.ts._accountMigration.moveTo }}</template> - <MkInput v-model="moveToAccount" manual-save> - <template #prefix><i class="ti ti-plane-departure"></i></template> - <template #label>{{ i18n.ts._accountMigration.moveToLabel }}</template> - </MkInput> - </FormSection> - <FormInfo warn>{{ i18n.ts._accountMigration.moveAccountDescription }}</FormInfo> - <FormSection> - <template #label>{{ i18n.ts._accountMigration.moveFrom }}</template> - <MkInput v-model="accountAlias" manual-save> - <template #prefix><i class="ti ti-plane-arrival"></i></template> - <template #label>{{ i18n.ts._accountMigration.moveFromLabel }}</template> - </MkInput> - </FormSection> - <FormInfo warn>{{ i18n.ts._accountMigration.moveFromDescription }}</FormInfo> + <div class="_gaps_m"> + <FormInfo>{{ i18n.ts._accountMigration.moveAccountDescription }}</FormInfo> + + <template v-if="$i && !$i.movedTo"> + <FormInfo>{{ i18n.ts._accountMigration.moveAccountHowTo }}</FormInfo> + <FormInfo warn>{{ i18n.ts._accountMigration.moveCannotBeUndone }}</FormInfo> + + <MkInput v-model="moveToAccount"> + <template #prefix><i class="ti ti-plane-departure"></i></template> + <template #label>{{ i18n.ts._accountMigration.moveToLabel }}</template> + </MkInput> + <MkButton inline danger :disabled="!moveToAccount" @click="move"> + <i class="ti ti-check"></i> {{ i18n.ts._accountMigration.startMigration }} + </MkButton> + </template> + <template v-else-if="$i"> + <FormInfo>{{ i18n.ts._accountMigration.postMigrationNote }}</FormInfo> + <FormInfo warn>{{ i18n.ts._accountMigration.movedAndCannotBeUndone }}</FormInfo> + <div>{{ i18n.ts._accountMigration.movedTo }}</div> + <MkUserInfo v-if="movedTo" :user="movedTo" class="_panel _shadow" /> + </template> + </div> + </MkFolder> </div> </template> <script lang="ts" setup> -import { ref, watch } from 'vue'; -import FormSection from '@/components/form/section.vue'; +import { ref } from 'vue'; import FormInfo from '@/components/MkInfo.vue'; import MkInput from '@/components/MkInput.vue'; +import MkButton from '@/components/MkButton.vue'; +import MkFolder from '@/components/MkFolder.vue'; +import MkUserInfo from '@/components/MkUserInfo.vue'; import * as os from '@/os'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; +import { $i } from '@/account'; +import { toString } from 'misskey-js/built/acct'; +import { UserDetailed } from 'misskey-js/built/entities'; +import { unisonReload } from '@/scripts/unison-reload'; const moveToAccount = ref(''); -const accountAlias = ref(''); +const movedTo = ref<UserDetailed>(); +const accountAliases = ref(['']); + +async function init() { + if ($i?.movedTo) { + movedTo.value = await os.api('users/show', { userId: $i.movedTo }); + } else { + moveToAccount.value = ''; + } + + if ($i?.alsoKnownAs && $i.alsoKnownAs.length > 0) { + const alsoKnownAs = await os.api('users/show', { userIds: $i.alsoKnownAs }); + accountAliases.value = (alsoKnownAs && alsoKnownAs.length > 0) ? alsoKnownAs.map(user => `@${toString(user)}`) : ['']; + } else { + accountAliases.value = ['']; + } +} async function move(): Promise<void> { const account = moveToAccount.value; const confirm = await os.confirm({ type: 'warning', - text: i18n.t('migrationConfirm', { account: account.toString() }), + text: i18n.t('_accountMigration.migrationConfirm', { account }), }); if (confirm.canceled) return; - os.apiWithDialog('i/move', { + await os.apiWithDialog('i/move', { moveToAccount: account, }); + unisonReload(); +} + +function add(): void { + accountAliases.value.push(''); } async function save(): Promise<void> { - const account = accountAlias.value; - os.apiWithDialog('i/known-as', { - alsoKnownAs: account, + const alsoKnownAs = accountAliases.value.map(alias => alias.trim()).filter(alias => alias !== ''); + const i = await os.apiWithDialog('i/update', { + alsoKnownAs, }); + $i.alsoKnownAs = i.alsoKnownAs; + init(); } -watch(accountAlias, async () => { - await save(); -}); - -watch(moveToAccount, async () => { - await move(); -}); +init(); definePageMetadata({ title: i18n.ts.accountMigration, diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue index a5f6c11f89..db21cf49da 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -38,6 +38,10 @@ <template #label>{{ i18n.ts._profile.metadataEdit }}</template> <div class="_gaps_m"> + <div> + <MkButton :disabled="fields.length >= 16" inline style="margin-right: 8px;" @click="addField"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> + <MkButton inline primary @click="saveFields"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> + </div> <FormSplit v-for="(record, i) in fields" :min-width="250"> <MkInput v-model="record.name" small> <template #label>{{ i18n.ts._profile.metadataLabel }} #{{ i + 1 }}</template> @@ -46,10 +50,6 @@ <template #label>{{ i18n.ts._profile.metadataContent }} #{{ i + 1 }}</template> </MkInput> </FormSplit> - <div> - <MkButton :disabled="fields.length >= 16" inline style="margin-right: 8px;" @click="addField"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> - <MkButton inline primary @click="saveFields"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> - </div> </div> </MkFolder> <template #caption>{{ i18n.ts._profile.metadataDescription }}</template> diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index 0f9145e974..57063c92de 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -7,7 +7,7 @@ <!-- <div class="punished" v-if="user.isSilenced"><i class="ti ti-alert-triangle" style="margin-right: 8px;"></i> {{ i18n.ts.userSilenced }}</div> --> <div class="profile _gaps"> - <MkAccountMoved v-if="user.movedToUri" :host="user.movedToUri.host" :username="user.movedToUri.username"/> + <MkAccountMoved v-if="user.movedTo" :movedTo="user.movedTo" /> <MkRemoteCaution v-if="user.host != null" :href="user.url ?? user.uri!" class="warn"/> <div :key="user.id" class="main _panel"> diff --git a/packages/frontend/src/scripts/achievements.ts b/packages/frontend/src/scripts/achievements.ts index f1da8a76da..fbca005769 100644 --- a/packages/frontend/src/scripts/achievements.ts +++ b/packages/frontend/src/scripts/achievements.ts @@ -464,6 +464,7 @@ const claimingQueue = new Set<string>(); export async function claimAchievement(type: typeof ACHIEVEMENT_TYPES[number]) { if ($i == null) return; + if ($i.movedTo) return; if (claimedAchievements.includes(type)) return; claimingQueue.add(type); claimedAchievements.push(type); diff --git a/packages/frontend/src/scripts/please-login.ts b/packages/frontend/src/scripts/please-login.ts index b8fb853cc1..c101a127f3 100644 --- a/packages/frontend/src/scripts/please-login.ts +++ b/packages/frontend/src/scripts/please-login.ts @@ -17,5 +17,5 @@ export function pleaseLogin(path?: string) { }, }, 'closed'); - if (!path) throw new Error('signin required'); + throw new Error('signin required'); } diff --git a/packages/frontend/src/scripts/show-moved-dialog.ts b/packages/frontend/src/scripts/show-moved-dialog.ts new file mode 100644 index 0000000000..acb26c36e2 --- /dev/null +++ b/packages/frontend/src/scripts/show-moved-dialog.ts @@ -0,0 +1,16 @@ +import * as os from '@/os'; +import { $i } from '@/account'; +import { i18n } from '@/i18n'; + +export function showMovedDialog() { + if (!$i) return; + if (!$i.movedTo) return; + + os.alert({ + type: 'error', + title: i18n.ts.accountMovedShort, + text: i18n.ts.operationForbidden, + }); + + throw new Error('account moved'); +} |