Skip to content

Commit 1aa35ef

Browse files
2025.4.1-io.9d (#1317)
2025.4.1-io.9d
2 parents 52d4aba + 5885ad8 commit 1aa35ef

9 files changed

Lines changed: 366 additions & 302 deletions

File tree

‎package.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
{
22
"name": "misskey",
3-
"version": "2025.4.1-io.9c",
3+
"version": "2025.4.1-io.9d",
44
"codename": "nasubi",
55
"repository": {
66
"type": "git",
77
"url": "https://github.com/MisskeyIO/misskey.git"
88
},
9-
"packageManager": "pnpm@10.26.1+sha512.664074abc367d2c9324fdc18037097ce0a8f126034160f709928e9e9f95d98714347044e5c3164d65bd5da6c59c6be362b107546292a8eecb7999196e5ce58fa",
9+
"packageManager": "pnpm@10.26.2+sha512.0e308ff2005fc7410366f154f625f6631ab2b16b1d2e70238444dd6ae9d630a8482d92a451144debc492416896ed16f7b114a86ec68b8404b2443869e68ffda6",
1010
"workspaces": [
1111
"packages/frontend-shared",
1212
"packages/frontend",

‎packages/frontend/src/accounts.ts‎

Lines changed: 93 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,16 @@
66
import { defineAsyncComponent, ref } from 'vue';
77
import * as Misskey from 'misskey-js';
88
import { apiUrl, host } from '@@/js/config.js';
9-
import { generateClientTransactionId } from '@/utility/misskey-api.js';
10-
import type { MenuItem } from '@/types/menu.js';
9+
import { generateClientTransactionId, misskeyApi } from '@/utility/misskey-api.js';
10+
import type { MenuButton, MenuItem, MenuUser } from '@/types/menu.js';
1111
import { showSuspendedDialog } from '@/utility/show-suspended-dialog.js';
1212
import { i18n } from '@/i18n.js';
1313
import { miLocalStorage } from '@/local-storage.js';
14-
import { waiting, popup, popupMenu, success, alert } from '@/os.js';
14+
import { popup, popupMenu, success, alert } from '@/os.js';
1515
import { unisonReload, reloadChannel } from '@/utility/unison-reload.js';
1616
import { prefer } from '@/preferences.js';
1717
import { store } from '@/store.js';
1818
import { $i } from '@/i.js';
19-
import { signout } from '@/signout.js';
2019

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

@@ -40,9 +39,11 @@ export async function getAccounts(): Promise<{
4039
}
4140

4241
async function addAccount(host: string, user: Misskey.entities.User, token: AccountWithToken['token']) {
42+
const key = host + '/' + user.id;
43+
store.set('accountTokens', { ...store.s.accountTokens, [key]: token });
44+
store.set('accountInfos', { ...store.s.accountInfos, [key]: user });
45+
4346
if (!prefer.s.accounts.some(x => x[0] === host && x[1].id === user.id)) {
44-
store.set('accountTokens', { ...store.s.accountTokens, [host + '/' + user.id]: token });
45-
store.set('accountInfos', { ...store.s.accountInfos, [host + '/' + user.id]: user });
4647
prefer.commit('accounts', [...prefer.s.accounts, [host, { id: user.id, username: user.username }]]);
4748
}
4849
}
@@ -60,6 +61,17 @@ export async function removeAccount(host: string, id: AccountWithToken['id']) {
6061

6162
const isAccountDeleted = Symbol('isAccountDeleted');
6263

64+
async function removeAccountByToken(token: string) {
65+
const entry = Object.entries(store.s.accountTokens).find(([, value]) => value === token);
66+
if (!entry) return;
67+
const [key] = entry;
68+
const separatorIndex = key.lastIndexOf('/');
69+
if (separatorIndex === -1) return;
70+
const accountHost = key.slice(0, separatorIndex);
71+
const accountId = key.slice(separatorIndex + 1);
72+
await removeAccount(accountHost, accountId);
73+
}
74+
6375
function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Promise<Misskey.entities.MeDetailed> {
6476
return new Promise((done, fail) => {
6577
window.fetch(`${apiUrl}/i`, {
@@ -152,16 +164,14 @@ export function updateCurrentAccountPartial(accountData: Partial<Misskey.entitie
152164

153165
export async function refreshCurrentAccount() {
154166
if (!$i) return;
155-
return fetchAccount($i.token, $i.id).then(updateCurrentAccount).catch(reason => {
156-
if (reason === isAccountDeleted) {
157-
removeAccount(host, $i!.id);
158-
if (Object.keys(store.s.accountTokens).length > 0) {
159-
login(Object.values(store.s.accountTokens)[0]);
160-
} else {
161-
signout();
167+
return fetchAccount($i.token, $i.id)
168+
.then(updateCurrentAccount)
169+
.catch(async reason => {
170+
if (reason === isAccountDeleted) {
171+
const { signout } = await import('@/signout.js');
172+
await signout();
162173
}
163-
}
164-
});
174+
});
165175
}
166176

167177
export async function login(token: AccountWithToken['token'], redirect?: string) {
@@ -171,7 +181,10 @@ export async function login(token: AccountWithToken['token'], redirect?: string)
171181
showing: showing,
172182
}, {}, 'closed');
173183

174-
const me = await fetchAccount(token, undefined, true).catch(reason => {
184+
const me = await fetchAccount(token, undefined, true).catch(async reason => {
185+
if (reason === isAccountDeleted) {
186+
await removeAccountByToken(token);
187+
}
175188
showing.value = false;
176189
throw reason;
177190
});
@@ -203,7 +216,6 @@ export async function switchAccount(host: string, id: string) {
203216
} else {
204217
await popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
205218
done: async (res: Misskey.entities.SigninFlowResponse & { finished: true }) => {
206-
store.set('accountTokens', { ...store.s.accountTokens, [host + '/' + res.id]: res.i });
207219
login(res.i);
208220
},
209221
}, 'closed');
@@ -218,61 +230,72 @@ export async function openAccountMenu(opts: {
218230
}, ev: MouseEvent) {
219231
if (!$i) return;
220232

221-
function createItem(host: string, id: Misskey.entities.User['id'], username: Misskey.entities.User['username'], account: Misskey.entities.User | null | undefined, token: string): MenuItem {
222-
if (account) {
223-
return {
224-
type: 'user' as const,
225-
user: account,
226-
active: opts.active != null ? opts.active === id : false,
227-
action: async () => {
228-
if (opts.onChoose) {
229-
opts.onChoose(account);
230-
} else {
231-
switchAccount(host, id);
232-
}
233-
},
234-
};
235-
} else {
236-
return {
237-
type: 'button' as const,
238-
text: username,
239-
active: opts.active != null ? opts.active === id : false,
240-
action: async () => {
241-
if (opts.onChoose) {
242-
fetchAccount(token, id).then(account => {
243-
opts.onChoose(account);
244-
});
245-
} else {
246-
switchAccount(host, id);
247-
}
248-
},
249-
};
233+
async function switchAccountByUser(account: Misskey.entities.User) {
234+
const storedAccounts = await getAccounts();
235+
const found = storedAccounts.find(x => x.id === account.id);
236+
if (found?.token) {
237+
login(found.token);
238+
return;
239+
}
240+
if (found) {
241+
await switchAccount(found.host, found.id);
250242
}
251243
}
252244

253-
const menuItems: MenuItem[] = [];
245+
function switchAccountWithToken(token: string) {
246+
login(token);
247+
}
248+
249+
const storedAccounts = await getAccounts().then(accounts => accounts.filter(x => x.id !== $i.id));
250+
const accountsPromise = storedAccounts.length > 0
251+
? misskeyApi('users/show', { userIds: storedAccounts.map(x => x.id) })
252+
: Promise.resolve([] as Misskey.entities.User[]);
253+
254+
function createItem(account: Misskey.entities.User) {
255+
return {
256+
type: 'user' as const,
257+
user: account,
258+
active: opts.active != null ? opts.active === account.id : false,
259+
action: () => {
260+
if (opts.onChoose) {
261+
opts.onChoose(account);
262+
} else {
263+
switchAccountByUser(account);
264+
}
265+
},
266+
};
267+
}
268+
269+
const accountItemPromises: MenuItem[] = storedAccounts.map(a => new Promise<MenuButton | MenuUser>(res => {
270+
accountsPromise.then(accounts => {
271+
const account = accounts.find(x => x.id === a.id);
272+
if (account == null) {
273+
return res({
274+
type: 'button' as const,
275+
text: a.username ?? a.id,
276+
active: opts.active != null ? opts.active === a.id : false,
277+
action: () => {
278+
if (a.token) {
279+
switchAccountWithToken(a.token);
280+
} else {
281+
switchAccount(a.host, a.id);
282+
}
283+
},
284+
});
285+
}
254286

255-
// TODO: $iのホストも比較したいけど通常null
256-
const accountItems = (await getAccounts().then(accounts => accounts.filter(x => x.id !== $i.id))).map(a => createItem(a.host, a.id, a.username, a.user, a.token));
287+
res(createItem(account));
288+
});
289+
}));
257290

258291
if (opts.withExtraOperation) {
259-
menuItems.push({
260-
type: 'link',
292+
popupMenu([...[{
293+
type: 'link' as const,
261294
text: i18n.ts.profile,
262295
to: `/@${$i.username}`,
263296
avatar: $i,
264-
}, {
265-
type: 'divider',
266-
});
267-
268-
if (opts.includeCurrentAccount) {
269-
menuItems.push(createItem(host, $i.id, $i.username, $i, $i.token));
270-
}
271-
272-
menuItems.push(...accountItems);
273-
274-
menuItems.push({
275-
type: 'parent',
297+
}, { type: 'divider' as const }, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, {
298+
type: 'parent' as const,
276299
icon: 'ti ti-plus',
277300
text: i18n.ts.addAccount,
278301
children: [{
@@ -289,28 +312,24 @@ export async function openAccountMenu(opts: {
289312
action: () => {
290313
getAccountWithSignupDialog().then(res => {
291314
if (res != null) {
292-
switchAccount(host, res.id);
315+
switchAccountWithToken(res.token);
293316
}
294317
});
295318
},
296319
}],
297320
}, {
298-
type: 'link',
321+
type: 'link' as const,
299322
icon: 'ti ti-users',
300323
text: i18n.ts.manageAccounts,
301324
to: '/settings/accounts',
325+
}]], ev.currentTarget ?? ev.target, {
326+
align: 'left',
302327
});
303328
} else {
304-
if (opts.includeCurrentAccount) {
305-
menuItems.push(createItem(host, $i.id, $i.username, $i, $i.token));
306-
}
307-
308-
menuItems.push(...accountItems);
329+
popupMenu([...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises], ev.currentTarget ?? ev.target, {
330+
align: 'left',
331+
});
309332
}
310-
311-
popupMenu(menuItems, ev.currentTarget ?? ev.target, {
312-
align: 'left',
313-
});
314333
}
315334

316335
export function getAccountWithSigninDialog(): Promise<{ id: string, token: string } | null> {

‎packages/frontend/src/pages/settings/accounts.vue‎

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,42 +11,66 @@ SPDX-License-Identifier: AGPL-3.0-only
1111
<!--<MkButton @click="refreshAllAccounts"><i class="ti ti-refresh"></i></MkButton>-->
1212
</div>
1313

14-
<MkUserCardMini v-for="x in accounts" :key="x[0] + x[1].id" :user="x[1]" :class="$style.user" @click.prevent="menu(x[0], x[1], $event)"/>
14+
<template v-for="x in accounts" :key="x.host + x.id">
15+
<MkUserCardMini v-if="x.user" :user="x.user" :class="$style.user" @click.prevent="menu(x.host, x.id, $event)"/>
16+
<div v-else :class="$style.unknownUser" @click.prevent="menu(x.host, x.id, $event)">
17+
<span :class="$style.unknownUserAvatarMock"><i class="ti ti-user"></i></span>
18+
<div>
19+
<span :class="$style.unknownUserTitle">{{ x.username }}</span>
20+
<span :class="$style.unknownUserSub">@{{ x.username }}</span>
21+
</div>
22+
</div>
23+
</template>
1524
</div>
1625
</SearchMarker>
1726
</template>
1827

1928
<script lang="ts" setup>
20-
import { ref, computed } from 'vue';
29+
import { ref, computed, onMounted, watch } from 'vue';
2130
import * as Misskey from 'misskey-js';
2231
import type { MenuItem } from '@/types/menu.js';
2332
import MkButton from '@/components/MkButton.vue';
2433
import * as os from '@/os.js';
2534
import { misskeyApi } from '@/utility/misskey-api.js';
26-
import { $i } from '@/i.js';
27-
import { switchAccount, removeAccount, login, getAccountWithSigninDialog, getAccountWithSignupDialog } from '@/accounts.js';
35+
import { getAccounts, switchAccount, removeAccount, login, getAccountWithSigninDialog, getAccountWithSignupDialog } from '@/accounts.js';
2836
import { i18n } from '@/i18n.js';
2937
import { definePage } from '@/page.js';
3038
import MkUserCardMini from '@/components/MkUserCardMini.vue';
3139
import { prefer } from '@/preferences.js';
3240
33-
const accounts = prefer.r.accounts;
41+
const accounts = ref<Awaited<ReturnType<typeof getAccounts>>[number][]>([]);
3442
35-
function refreshAllAccounts() {
36-
// TODO
43+
async function loadAccounts() {
44+
const entries = await getAccounts();
45+
const missingIds = entries.filter(account => !account.user).map(account => account.id);
46+
let usersMap = new Map<string, Misskey.entities.User>();
47+
48+
if (missingIds.length > 0) {
49+
try {
50+
const users = await misskeyApi('users/show', { userIds: missingIds });
51+
usersMap = new Map(users.map(user => [user.id, user]));
52+
} catch {
53+
// ignore
54+
}
55+
}
56+
57+
accounts.value = entries.map(account => ({
58+
...account,
59+
user: account.user ?? usersMap.get(account.id) ?? null,
60+
}));
3761
}
3862
39-
function menu(host: string, account: Misskey.entities.UserDetailed, ev: MouseEvent) {
63+
function menu(host: string, accountId: string, ev: MouseEvent) {
4064
let menu: MenuItem[];
4165
4266
menu = [{
4367
text: i18n.ts.switch,
4468
icon: 'ti ti-switch-horizontal',
45-
action: () => switchAccount(host, account.id),
69+
action: () => switchAccount(host, accountId),
4670
}, {
4771
text: i18n.ts.remove,
4872
icon: 'ti ti-trash',
49-
action: () => removeAccount(host, account.id),
73+
action: () => removeAccount(host, accountId),
5074
}];
5175
5276
os.popupMenu(menu, ev.currentTarget ?? ev.target);
@@ -78,6 +102,14 @@ function createAccount() {
78102
});
79103
}
80104
105+
onMounted(() => {
106+
void loadAccounts();
107+
});
108+
109+
watch(prefer.r.accounts, () => {
110+
void loadAccounts();
111+
}, { deep: true });
112+
81113
const headerActions = computed(() => []);
82114
83115
const headerTabs = computed(() => []);

‎packages/frontend/src/pages/settings/index.vue‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ const menuDef = computed<SuperMenuDef[]>(() => [{
8080
text: i18n.ts.profile,
8181
to: '/settings/profile',
8282
active: currentPage.value?.route.name === 'profile',
83+
}, {
84+
icon: 'ti ti-users',
85+
text: i18n.ts.accounts,
86+
to: '/settings/accounts',
87+
active: currentPage.value?.route.name === 'accounts',
8388
}, {
8489
icon: 'ti ti-lock-open',
8590
text: i18n.ts.privacy,
@@ -178,7 +183,6 @@ const menuDef = computed<SuperMenuDef[]>(() => [{
178183
const { canceled } = await os.confirm({
179184
type: 'warning',
180185
title: i18n.ts.logoutConfirm,
181-
text: i18n.ts.logoutWillClearClientData,
182186
});
183187
if (canceled) return;
184188
signout();

‎packages/frontend/src/router.definition.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ export const ROUTE_DEF: RouteDef[] = [{
176176
component: page(() => import('@/pages/settings/custom-css.vue')),
177177
}, {
178178
path: '/accounts',
179-
name: 'profile',
179+
name: 'accounts',
180180
component: page(() => import('@/pages/settings/accounts.vue')),
181181
}, {
182182
path: '/account-stats',

0 commit comments

Comments
 (0)