feat: multi-account switcher in the avatar menu

SessionManager already stores one session per uid; this adds the UI and
the two accessors it was missing. The avatar menu now lists every stored
session — active entries switch in place (page reload re-derives all
state), signed-out entries keep their entry and route to the login page
with the email prefilled — plus an always-visible Add account action.

- session: listSessions() and switchTo(uid) accessors
- UserAction: account section under a divider with per-session avatar,
  nickname/email, signed-out badge, and Add account
- locales: navbar.addAccount / navbar.signedOut (en-US, zh-CN)

Authored By: TDvorak <info@tdvorak.dev>

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
pull/3587/head
Tomas Dvorak 2 weeks ago
parent c5daf0e675
commit 8d896e0255

@ -163,7 +163,9 @@
"toDarkMode": "Dark",
"toLightMode": "Light",
"myProfile": "My profile",
"dashboard": "Dashboard"
"dashboard": "Dashboard",
"addAccount": "Add account",
"signedOut": "Signed out"
},
"fileManager": {
"continueInDesktop": "Authorization successful, please continue in the desktop client.",

@ -163,7 +163,9 @@
"toDarkMode": "黑暗",
"toLightMode": "浅色",
"myProfile": "个人主页",
"dashboard": "管理面板"
"dashboard": "管理面板",
"addAccount": "添加账号",
"signedOut": "已退出登录"
},
"fileManager": {
"continueInDesktop": "授权成功,请在桌面客户端中继续操作。",

@ -24,6 +24,7 @@ import SessionManager, { Session } from "../../../session";
import { GroupBS } from "../../../session/utils.ts";
import UserAvatar from "../../Common/User/UserAvatar.tsx";
import { SquareMenuItem } from "../../FileManager/ContextMenu/ContextMenu.tsx";
import Add from "../../Icons/Add.tsx";
import HomeOutlined from "../../Icons/HomeOutlined.tsx";
import Person from "../../Icons/Person.tsx";
import SettingsOutlined from "../../Icons/SettingsOutlined.tsx";
@ -59,6 +60,27 @@ const UserPopover = ({ open, onClose, ...rest }: PopoverProps) => {
onClose && onClose({}, "backdropClick");
}, []);
// Other stored sessions for the account switcher. Signed-out entries
// route back to the login page with their email prefilled instead of
// switching directly.
const otherSessions = SessionManager.listSessions().filter((s) => s.user.id !== user.id);
const switchAccount = (s: Session) => {
onClose?.({}, "backdropClick");
if (s.signedOut) {
signWithHint(s.user.email ?? "");
return;
}
if (SessionManager.switchTo(s.user.id)) {
window.location.reload();
}
};
const addAccount = () => {
navigate("/session");
onClose?.({}, "backdropClick");
};
const openMyProfile = useCallback(() => {
navigate(`/profile/${user?.id}`);
onClose && onClose({}, "backdropClick");
@ -143,6 +165,26 @@ const UserPopover = ({ open, onClose, ...rest }: PopoverProps) => {
<ListItemText>{t("login.logout")}</ListItemText>
</SquareMenuItem>
</MenuList>
<Divider />
<MenuList dense sx={{ mx: 0.5 }}>
{otherSessions.map((s) => (
<SquareMenuItem key={s.user.id} onClick={() => switchAccount(s)}>
<ListItemIcon>
<UserAvatar sx={{ width: 24, height: 24 }} user={s.user} />
</ListItemIcon>
<ListItemText
primary={s.user.nickname}
secondary={s.signedOut ? t("navbar.signedOut") : s.user.email}
/>
</SquareMenuItem>
))}
<SquareMenuItem onClick={addAccount}>
<ListItemIcon>
<Add fontSize={"small"} />
</ListItemIcon>
<ListItemText>{t("navbar.addAccount")}</ListItemText>
</SquareMenuItem>
</MenuList>
</Popover>
);
};

@ -271,6 +271,24 @@ export class Manager {
this.syncToStore();
}
// listSessions returns every stored session for the account switcher.
// Signed-out entries are kept so the UI can offer one-click re-login.
public listSessions = (): Session[] => {
return Object.values(this.state.sessions);
};
// switchTo makes another stored session current; callers reload the app
// so all state re-derives from the new session.
public switchTo = (uid: string) => {
const session = this.state.sessions[uid];
if (!session || session.signedOut) {
return false;
}
this.state.current = uid;
this.syncToStore();
return true;
};
private syncToStore = () => {
localStorage.setItem(SESSION_KEY, JSON.stringify(this.state));
};

Loading…
Cancel
Save