summaryrefslogtreecommitdiff
path: root/packages/frontend/src
diff options
context:
space:
mode:
authorNamekuji <11836635+nmkj-io@users.noreply.github.com>2023-04-29 11:09:29 -0400
committerGitHub <noreply@github.com>2023-04-30 00:09:29 +0900
commitd28866f71a2fd1809f595bf599cc3914834f74e2 (patch)
tree70c0319864ef00b45484820bd149f7d4d4d943fb /packages/frontend/src
parentfix(frontend): ロールのタイトルのバグを解決、Reactivity Transf... (diff)
downloadsharkey-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.vue16
-rw-r--r--packages/frontend/src/components/MkInfo.vue1
-rw-r--r--packages/frontend/src/components/MkNote.vue4
-rw-r--r--packages/frontend/src/components/MkNoteDetailed.vue5
-rw-r--r--packages/frontend/src/os.ts3
-rw-r--r--packages/frontend/src/pages/settings/import-export.vue9
-rw-r--r--packages/frontend/src/pages/settings/index.vue6
-rw-r--r--packages/frontend/src/pages/settings/migration.vue118
-rw-r--r--packages/frontend/src/pages/settings/profile.vue8
-rw-r--r--packages/frontend/src/pages/user/home.vue2
-rw-r--r--packages/frontend/src/scripts/achievements.ts1
-rw-r--r--packages/frontend/src/scripts/please-login.ts2
-rw-r--r--packages/frontend/src/scripts/show-moved-dialog.ts16
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');
+}