Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d2ce24d
fix(frontend): ログアウトするとすべてのアカウントからログアウトされる問題を修正
kakkokari-gtyih Dec 27, 2025
a2e05c6
Update Changelog
kakkokari-gtyih Dec 27, 2025
4afcb58
enhance: implement refresh accounts
kakkokari-gtyih Dec 27, 2025
e2920db
fix: アカウント削除の際にもそのアカウント関連の設定値を削除するように
kakkokari-gtyih Dec 27, 2025
8d90e9a
rename
kakkokari-gtyih Dec 27, 2025
527ecdb
Update Changelog
kakkokari-gtyih Dec 27, 2025
a2250a2
fix: simplify state removal logic
kakkokari-gtyih Dec 27, 2025
821d238
fix: replace individual delete calls with delMany for improved effici…
kakkokari-gtyih Dec 27, 2025
727e84b
fix
kakkokari-gtyih Dec 27, 2025
55c296e
Merge branch 'develop' into fix-signout
kakkokari-gtyih Dec 28, 2025
1fa0c17
Merge branch 'develop' into fix-signout
kakkokari-gtyih Dec 28, 2025
2bf40ac
Merge branch 'develop' into fix-signout
kakkokari-gtyih Dec 30, 2025
89cf12d
Merge branch 'develop' into fix-signout
kakkokari-gtyih Dec 31, 2025
4475cf4
Merge branch 'develop' into fix-signout
kakkokari-gtyih Jan 1, 2026
40ea7ec
Merge branch 'develop' into fix-signout
kakkokari-gtyih Jan 2, 2026
208681d
Merge branch 'develop' into fix-signout
kakkokari-gtyih Jan 7, 2026
cf76064
Merge branch 'develop' into fix-signout
kakkokari-gtyih Jan 9, 2026
3e62dbc
Merge branch 'develop' into fix-signout
kakkokari-gtyih Jan 9, 2026
cea634b
Merge remote-tracking branch 'msky/develop' into fix-signout
kakkokari-gtyih Jan 11, 2026
659bde8
fix types
kakkokari-gtyih Jan 11, 2026
b3a1833
Merge branch 'develop' into fix-signout
kakkokari-gtyih Jan 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@
- Enhance: ウィジェットの表示設定をプレビューを見ながら行えるように
- Enhance: ウィジェットの設定項目のラベルの多言語対応
- Enhance: 画面幅が広いときにメディアを横並びで表示できるようにするオプションを追加
- Enhance: アカウント管理ページで、全てのアカウントから一括でログアウトできるように
- Enhance: パフォーマンスの向上
- Fix: ドライブクリーナーでファイルを削除しても画面に反映されない問題を修正 #16061
- Fix: 非ログイン時にログインを求めるダイアログが表示された後にダイアログのぼかしが解除されず操作不能になることがある問題を修正
- Fix: ドライブのソートが「登録日(昇順)」の場合に正しく動作しない問題を修正
- Fix: ログアウトボタンを押下するとすべてのアカウントからログアウトする問題を修正
- Fix: アカウント管理ページで、アカウントの追加・削除を行ってもリストに反映されない問題を修正
- Fix: 高度なMFMのピッカーを使用する際の挙動を改善
- Fix: 管理画面でアーカイブ済のお知らせを表示した際にアクティブなお知らせが多い旨の警告が出る問題を修正
- Fix: ファイルタブのセンシティブメディアを開く際に確認ダイアログを出す設定が適用されない問題を修正
Expand Down
2 changes: 2 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ noAccountDescription: "自己紹介はありません"
login: "ログイン"
loggingIn: "ログイン中"
logout: "ログアウト"
logoutFromAll: "すべてのアカウントからログアウト"
signup: "新規登録"
uploading: "アップロード中"
save: "保存"
Expand Down Expand Up @@ -992,6 +993,7 @@ numberOfPageCache: "ページキャッシュ数"
numberOfPageCacheDescription: "多くすると利便性が向上しますが、負荷とメモリ使用量が増えます。"
logoutConfirm: "ログアウトしますか?"
logoutWillClearClientData: "ログアウトするとクライアントの設定情報がブラウザから消去されます。再ログイン時に設定情報を復元できるようにするためには、設定の自動バックアップを有効にしてください。"
logoutFromOtherAccountConfirm: "{username}からログアウトしますか?"
lastActiveDate: "最終利用日時"
statusbar: "ステータスバー"
pleaseSelect: "選択してください"
Expand Down
55 changes: 42 additions & 13 deletions packages/frontend/src/accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,24 @@ import { signout } from '@/signout.js';

type AccountWithToken = Misskey.entities.MeDetailed & { token: string };

export async function getAccounts(): Promise<{
export type AccountData = {
host: string;
id: Misskey.entities.User['id'];
username: Misskey.entities.User['username'];
user?: Misskey.entities.MeDetailed | null;
token: string | null;
}[]> {
};

export async function getAccounts(): Promise<AccountData[]> {
const tokens = store.s.accountTokens;
const accountInfos = store.s.accountInfos;
const accounts = prefer.s.accounts;
return accounts.map(([host, user]) => ({
host,
id: user.id,
username: user.username,
user: accountInfos[host + '/' + user.id],
token: tokens[host + '/' + user.id] ?? null,
user: accountInfos[`${host}/${user.id}`],
token: tokens[`${host}/${user.id}`] ?? null,
}));
}

Expand All @@ -53,10 +55,15 @@ export async function removeAccount(host: string, id: AccountWithToken['id']) {
const accountInfos = JSON.parse(JSON.stringify(store.s.accountInfos));
delete accountInfos[host + '/' + id];
store.set('accountInfos', accountInfos);

prefer.commit('accounts', prefer.s.accounts.filter(x => x[0] !== host || x[1].id !== id));
}

export async function removeAccountAssociatedData(host: string, id: AccountWithToken['id']) {
// 設定・状態を削除
prefer.clearAccountSettingsFromDevice(host, id);
await store.clearAccountDataFromDevice(id);
}

const isAccountDeleted = Symbol('isAccountDeleted');

function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Promise<Misskey.entities.MeDetailed> {
Expand Down Expand Up @@ -162,14 +169,36 @@ export async function refreshCurrentAccount() {
});
}

export async function login(token: AccountWithToken['token'], redirect?: string) {
export async function refreshAccounts() {
const accounts = await getAccounts();
for (const account of accounts) {
if (account.host === host && account.id === $i?.id) {
await refreshCurrentAccount();
} else if (account.token) {
try {
const user = await fetchAccount(account.token, account.id);
store.set('accountInfos', { ...store.s.accountInfos, [account.host + '/' + account.id]: user });
} catch (e) {
if (e === isAccountDeleted) {
await removeAccount(account.host, account.id);
await removeAccountAssociatedData(account.host, account.id);
}
}
}
}
}

export async function login(token: AccountWithToken['token'], redirect?: string, showWaiting = true) {
const showing = ref(true);
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkWaitingDialog.vue')), {
success: false,
showing: showing,
}, {
closed: () => dispose(),
});

if (showWaiting) {
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkWaitingDialog.vue')), {
success: false,
showing: showing,
}, {
closed: () => dispose(),
});
}

const me = await fetchAccount(token, undefined, true).catch(reason => {
showing.value = false;
Expand All @@ -195,7 +224,7 @@ export async function login(token: AccountWithToken['token'], redirect?: string)
}

export async function switchAccount(host: string, id: string) {
const token = store.s.accountTokens[host + '/' + id];
const token = store.s.accountTokens[`${host}/${id}`];
if (token) {
login(token);
} else {
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/src/components/MkUserCardMini.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-adaptive-bg :class="[$style.root]">
<MkAvatar :class="$style.avatar" :user="user" indicator/>
<div :class="$style.body">
<span :class="$style.name"><MkUserName :user="user"/></span>
<span :class="$style.name"><MkUserName :user="user"/><slot name="nameSuffix"></slot></span>
<span :class="$style.sub"><slot name="sub"><span class="_monospace">@{{ acct(user) }}</span></slot></span>
</div>
<MkMiniChart v-if="chartValues" :class="$style.chart" :src="chartValues"/>
Expand Down
14 changes: 13 additions & 1 deletion packages/frontend/src/lib/pizzax.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { BroadcastChannel } from 'broadcast-channel';
import type { Ref } from 'vue';
import { $i } from '@/i.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { get, set } from '@/utility/idb-proxy.js';
import { get, set, delMany } from '@/utility/idb-proxy.js';
import { store } from '@/store.js';
import { deepClone } from '@/utility/clone.js';
import { deepMerge } from '@/utility/merge.js';
Expand Down Expand Up @@ -222,6 +222,18 @@ export class Pizzax<T extends StateDef> {
return this.def[key].default;
}

/** 現在のアカウントに紐づくデータをデバイスから削除します */
public async clearAccountDataFromDevice(id = $i?.id) {
if (id == null) return;

const deviceAccountStateKey = `pizzax::${this.key}::${id}` satisfies typeof this.deviceAccountStateKeyName;
const registryCacheKey = `pizzax::${this.key}::cache::${id}` satisfies typeof this.registryCacheKeyName;

await this.addIdbSetJob(async () => {
await delMany([deviceAccountStateKey, registryCacheKey]);
});
}

/**
* 特定のキーの、簡易的なcomputed refを作ります
* 主にvue上で設定コントロールのmodelとして使う用
Expand Down
131 changes: 100 additions & 31 deletions packages/frontend/src/pages/settings/accounts.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,48 +8,97 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps">
<div class="_buttons">
<MkButton primary @click="addAccount"><i class="ti ti-plus"></i> {{ i18n.ts.addAccount }}</MkButton>
<!--<MkButton @click="refreshAllAccounts"><i class="ti ti-refresh"></i></MkButton>-->
<MkButton @click="refreshAllAccounts"><i class="ti ti-refresh"></i> {{ i18n.ts.reloadAccountsList }}</MkButton>
<MkButton danger @click="logoutFromAll"><i class="ti ti-power"></i> {{ i18n.ts.logoutFromAll }}</MkButton>
</div>

<template v-for="x in accounts" :key="x.host + x.id">
<MkUserCardMini v-if="x.user" :user="x.user" :class="$style.user" @click.prevent="showMenu(x.host, x.id, $event)"/>
<MkUserCardMini v-if="x.user" :user="x.user" :class="$style.user" @click.prevent="showMenu(x, $event)">
<template #nameSuffix>
<span v-if="x.id === $i?.id" :class="$style.currentAccountTag">{{ i18n.ts.loggingIn }}</span>
</template>
</MkUserCardMini>
<button v-else v-panel class="_button" :class="$style.unknownUser" @click="showMenu(x, $event)">
<div :class="$style.unknownUserAvatarMock"><i class="ti ti-user-question"></i></div>
<div>
<div :class="$style.unknownUserTitle">{{ i18n.ts.unknown }}<span v-if="x.id === $i?.id" :class="$style.currentAccountTag">{{ i18n.ts.loggingIn }}</span></div>
<div :class="$style.unknownUserSub">ID: <span class="_monospace">{{ x.id }}</span></div>
</div>
</button>
</template>
</div>
</SearchMarker>
</template>

<script lang="ts" setup>
import { ref, computed } from 'vue';
import * as Misskey from 'misskey-js';
import { host as local } from '@@/js/config.js';
import type { MenuItem } from '@/types/menu.js';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { $i } from '@/i.js';
import { switchAccount, removeAccount, login, getAccountWithSigninDialog, getAccountWithSignupDialog, getAccounts } from '@/accounts.js';
import { switchAccount, removeAccount, removeAccountAssociatedData, getAccountWithSigninDialog, getAccountWithSignupDialog, getAccounts, refreshAccounts } from '@/accounts.js';
import type { AccountData } from '@/accounts.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
import { prefer } from '@/preferences.js';
import { signout } from '@/signout.js';

const accounts = await getAccounts();
const accounts = ref<AccountData[]>([]);

getAccounts().then((res) => {
accounts.value = res;
});

function refreshAllAccounts() {
// TODO
os.promiseDialog((async () => {
await refreshAccounts();
accounts.value = await getAccounts();
})());
}

function showMenu(host: string, id: string, ev: PointerEvent) {
let menu: MenuItem[];

menu = [{
text: i18n.ts.switch,
icon: 'ti ti-switch-horizontal',
action: () => switchAccount(host, id),
}, {
text: i18n.ts.remove,
icon: 'ti ti-trash',
action: () => removeAccount(host, id),
}];
function showMenu(a: AccountData, ev: PointerEvent) {
const menu: MenuItem[] = [];

if ($i != null && $i.id === a.id && ($i.host ?? local) === a.host) {
menu.push({
text: i18n.ts.logout,
icon: 'ti ti-power',
danger: true,
action: async () => {
const { canceled } = await os.confirm({
type: 'warning',
title: i18n.ts.logoutConfirm,
text: i18n.ts.logoutWillClearClientData,
});
if (canceled) return;
signout();
},
});
} else {
menu.push({
text: i18n.ts.switch,
icon: 'ti ti-switch-horizontal',
action: () => switchAccount(a.host, a.id),
}, {
text: i18n.ts.logout,
icon: 'ti ti-power',
danger: true,
action: async () => {
const { canceled } = await os.confirm({
type: 'warning',
title: i18n.tsx.logoutFromOtherAccountConfirm({ username: `<plain>@${a.username}</plain>` }),
text: i18n.ts.logoutWillClearClientData,
});
if (canceled) return;
await os.promiseDialog((async () => {
await removeAccount(a.host, a.id);
await removeAccountAssociatedData(a.host, a.id);
accounts.value = await getAccounts();
})());
},
});
}

os.popupMenu(menu, ev.currentTarget ?? ev.target);
}
Expand All @@ -64,20 +113,30 @@ function addAccount(ev: PointerEvent) {
}], ev.currentTarget ?? ev.target);
}

function addExistingAccount() {
getAccountWithSigninDialog().then((res) => {
if (res != null) {
os.success();
}
});
async function addExistingAccount() {
const res = await getAccountWithSigninDialog();
if (res != null) {
os.success();
}
accounts.value = await getAccounts();
}

function createAccount() {
getAccountWithSignupDialog().then((res) => {
if (res != null) {
login(res.token);
}
async function createAccount() {
const res = await getAccountWithSignupDialog();
if (res != null) {
os.success();
}
accounts.value = await getAccounts();
}

async function logoutFromAll() {
const { canceled } = await os.confirm({
type: 'warning',
title: i18n.ts.logoutConfirm,
text: i18n.ts.logoutWillClearClientData,
});
if (canceled) return;
signout(true);
}

const headerActions = computed(() => []);
Expand All @@ -95,6 +154,16 @@ definePage(() => ({
cursor: pointer;
}

.currentAccountTag {
display: inline-block;
margin-left: 8px;
padding: 0 6px;
font-size: 0.8em;
background: var(--MI_THEME-accentedBg);
color: var(--MI_THEME-accent);
border-radius: calc(var(--MI-radius) / 2);
}

.unknownUser {
display: flex;
align-items: center;
Expand Down
29 changes: 29 additions & 0 deletions packages/frontend/src/preferences/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@ export type StorageProvider = {

type PreferencesDefinitionRecord<Default, T = Default extends (...args: any) => infer R ? R : Default> = {
default: Default;
/** アカウントごとに異なる設定値をもたせるかどうか */
accountDependent?: boolean;
/** サーバーごとに異なる設定値をもたせるかどうか(他のサーバーを同一クライアントから操作できるようになった際に使用) */
serverDependent?: boolean;
mergeStrategy?: (a: T, b: T) => T;
};
Expand Down Expand Up @@ -451,6 +453,33 @@ export class PreferencesManager extends EventEmitter<PreferencesManagerEvents> {
this.save();
}

/** 現在の操作アカウントに紐づく設定値をデバイスから削除します(ログアウト時などに使用) */
public clearAccountSettingsFromDevice(targetHost = host, id = this.currentAccount?.id) {
if (id == null) return;

let changed = false;

for (const _key in PREF_DEF) {
const key = _key as keyof PREF;
const records = this.profile.preferences[key];

const index = records.findIndex((record: PrefRecord<typeof key>) => {
const scope = parseScope(record[0]);
return scope.server === targetHost && scope.account === id;
});
if (index === -1) continue;

records.splice(index, 1);
changed = true;

this.rewriteRawState(key, this.getMatchedRecordOf(key)[1]);
}

if (changed) {
this.save();
}
}

public isSyncEnabled<K extends keyof PREF>(key: K): boolean {
return this.getMatchedRecordOf(key)[2].sync ?? false;
}
Expand Down
Loading
Loading