Refactor and redesign user menu (#32812)

* Initial quick settings menu

* Total refactor

* Quick design fixes.

* Refactor to use a view model.

* Remove unused strings

* Apply label

* Refactor naming

* Fixup most tests

* Remove specific theming for old user menu

* prettier

* Lots of cleanup

* Allow overriding the menu classes

* update snap

* Oops translations

* tidy

* Cleanup guest flows.

* Copyrights

* Remove unused classname

* Match guest view to designs

* Add guest screenshots

* Update guests

* snapshot

* Cleanup

* fix import

* Update tests

* More sceenshot fixes

* update collapsed

* move statements to prevent flake

* update snap

* Kick it along

* Click the room list

* Fiddle with the room video list.

* More screenshot adjustments

* fix imports

* fix another import

* Update snaps

* update snaps

* Fix snap flakes

* Refactor to move actions to view component, and callbacks to Actions

* Cleanup

* Cleanup

* Cleanup

* invert auth

* More bits

* fix

* Change md buttons to sm

* Try to assemble the snapshot component of the house of cards

* Consistent newlines between tests

* Update snapshot

Not sure why this was like this, this seems consistet for a logged in user

* Update snapshot

again these seem sensible for a guest

* Remove test

I don't really understand why the thing it asserts matters, so I'm removing
it for now.

* Update snapshot

* screenshot

* Don't show profile picture for guests

I'm not really sure what it meant for this interface to have a
property with a default value, so I've removed it and added the
property to the view model.

* Show avatar in story

* update snapshots for showAvatar

* Update screenshots

& hopefully make hover consistent in one

* Use outline home icon

---------

Co-authored-by: David Baker <dbkr@users.noreply.github.com>
This commit is contained in:
Will Hunt
2026-05-06 08:34:36 +00:00
committed by GitHub
co-authored by David Baker
parent bbd2d81a08
commit d4f419d1b5
58 changed files with 1231 additions and 874 deletions
@@ -0,0 +1,129 @@
/*
* Copyright 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import { BaseViewModel, type UserMenuSnapshot, type UserMenuViewActions } from "@element-hq/web-shared-components";
import { OwnProfileStore } from "../../stores/OwnProfileStore";
import { UPDATE_EVENT } from "../../stores/AsyncStore";
import type { MatrixDispatcher } from "../../dispatcher/dispatcher";
import Modal from "../../Modal";
import { Action } from "../../dispatcher/actions";
import { UserTab } from "../../components/views/dialogs/UserTab";
import FeedbackDialog from "../../components/views/dialogs/FeedbackDialog";
import { shouldShowFeedback } from "../../utils/Feedback";
import { getHomePageUrl } from "../../utils/pages";
import SdkConfig from "../../SdkConfig";
import type { MatrixClient } from "matrix-js-sdk/src/matrix";
// Matches maximum size of an avatar in the UserMenu
const AVATAR_PX = 88;
export class UserMenuViewModel extends BaseViewModel<UserMenuSnapshot, undefined> implements UserMenuViewActions {
private static computeSnapshot(
client: MatrixClient,
isPanelCollapsed: boolean,
accountManagementEndpoint?: string,
): UserMenuSnapshot {
const hasHomePage = !!getHomePageUrl(SdkConfig.get(), client);
const isAuthenticated = !client.isGuest();
const userId = client.getSafeUserId();
const displayName = OwnProfileStore.instance.displayName || userId;
const avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(AVATAR_PX) ?? undefined;
return {
open: false,
userId,
displayName,
avatarUrl,
expanded: !isPanelCollapsed,
manageAccountHref: accountManagementEndpoint,
showAvatar: isAuthenticated,
actions: {
createAccount: !isAuthenticated,
signIn: !isAuthenticated,
openHomePage: hasHomePage,
linkNewDevice: isAuthenticated,
openSecurity: isAuthenticated,
openFeedback: shouldShowFeedback(),
openSettings: true,
},
};
}
public constructor(
private readonly dispatcher: MatrixDispatcher,
client: MatrixClient,
isPanelCollapsed: boolean,
accountManagementEndpoint?: string,
) {
super(undefined, UserMenuViewModel.computeSnapshot(client, isPanelCollapsed, accountManagementEndpoint));
OwnProfileStore.instance.on(UPDATE_EVENT, this.recalculateProfile);
}
public dispose(): void {
OwnProfileStore.instance.off(UPDATE_EVENT, this.recalculateProfile);
super.dispose();
}
public readonly recalculateProfile = (): void => {
const displayName = OwnProfileStore.instance.displayName || this.snapshot.current.userId;
const avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(AVATAR_PX) ?? undefined;
this.snapshot.merge({ displayName, avatarUrl });
};
public readonly setOpen = (isOpen: boolean): void => {
this.snapshot.merge({ open: isOpen });
};
public readonly setExpanded = (expanded: boolean): void => {
this.snapshot.merge({ expanded });
};
public readonly createAccount = (): void => {
this.setOpen(false);
this.dispatcher.dispatch({ action: "start_registration" });
};
public readonly signIn = (): void => {
this.setOpen(false);
this.dispatcher.dispatch({ action: "start_login" });
};
public readonly openHomePage = (): void => {
this.setOpen(false);
this.dispatcher.dispatch({ action: Action.ViewHomePage });
};
public readonly openFeedback = (): void => {
this.setOpen(false);
Modal.createDialog(FeedbackDialog);
};
public readonly linkNewDevice = (): void => {
this.setOpen(false);
this.dispatcher.dispatch({
action: Action.ViewUserSettings,
initialTabId: UserTab.SessionManager,
props: { showMsc4108QrCode: true },
});
};
public readonly openSecurity = (): void => {
this.setOpen(false);
this.dispatcher.dispatch({
action: Action.ViewUserSettings,
initialTabId: UserTab.Security,
});
};
public readonly openSettings = (): void => {
this.setOpen(false);
this.dispatcher.dispatch({
action: Action.ViewUserSettings,
});
};
}