Add user status on user profile icon (#33653)
* Add user status on user profile icon Currently unstyled & no tests * Style the user status icon * Update snapshot for avatar wrapper * More snapshot updates * add if braces * Split out user status functions to avoid circular dep which has the weird effect of just breaking jest's mocking. * type imports * Update imports * Update snapshot * Tests * baseline image * Just snapshot the component itself --------- Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
@@ -12,21 +12,10 @@ import { logger as rootLogger } from "matrix-js-sdk/src/logger";
|
||||
import { useMatrixClientContext } from "../contexts/MatrixClientContext";
|
||||
import { useTypedEventEmitter } from "./useEventEmitter";
|
||||
import { useFeatureEnabled } from "./useSettings";
|
||||
import { type UserStatus, validateUserStatus } from "../utils/userStatus";
|
||||
|
||||
const logger = rootLogger.getChild("useUserStatus");
|
||||
|
||||
export interface UserStatus {
|
||||
emoji: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
const MAX_STATUS_TEXT_BYTES = 256;
|
||||
|
||||
export function userStatusTextWithinMaxLength(text: string): boolean {
|
||||
const textEncoder = new TextEncoder();
|
||||
return textEncoder.encode(text).length <= MAX_STATUS_TEXT_BYTES;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to get the MSC4426 user status for a given user ID. Returns undefined if the feature is disabled,
|
||||
* the user does not have a status, or if there was an error fetching the status.
|
||||
@@ -76,23 +65,5 @@ export function useUserStatus(userId: string | undefined): UserStatus | undefine
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof rawUserStatus !== "object" || rawUserStatus === null) {
|
||||
logger.warn(`value of "org.matrix.msc4426.status" was not an object for ${userId}`);
|
||||
return;
|
||||
}
|
||||
if ("emoji" in rawUserStatus === false || typeof rawUserStatus.emoji !== "string" || !rawUserStatus.emoji) {
|
||||
logger.warn(`"emoji" property was not a valid string for ${userId}`);
|
||||
return;
|
||||
}
|
||||
if ("text" in rawUserStatus === false || typeof rawUserStatus.text !== "string" || !rawUserStatus.text) {
|
||||
logger.warn(`"text" property was not a valid string for ${userId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
emoji: rawUserStatus.emoji,
|
||||
text: userStatusTextWithinMaxLength(rawUserStatus.text)
|
||||
? rawUserStatus.text
|
||||
: `${rawUserStatus.text.slice(0, MAX_STATUS_TEXT_BYTES)}…`,
|
||||
};
|
||||
return validateUserStatus(rawUserStatus);
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ import { Command, CommandCategories, splitAtFirstSpace } from "./SlashCommands";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import { reject, success } from "./utils";
|
||||
import { UserFriendlyError } from "../languageHandler";
|
||||
import { userStatusTextWithinMaxLength } from "../hooks/useUserStatus";
|
||||
import { TimelineRenderingType } from "../contexts/RoomContext";
|
||||
import { userStatusTextWithinMaxLength } from "../utils/userStatus";
|
||||
|
||||
export const statusCommand = new Command({
|
||||
command: "status",
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
type User,
|
||||
UserEvent,
|
||||
EventType,
|
||||
ClientEvent,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { throttle } from "lodash";
|
||||
|
||||
@@ -22,11 +23,14 @@ import defaultDispatcher from "../dispatcher/dispatcher";
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
import { _t } from "../languageHandler";
|
||||
import { mediaFromMxc } from "../customisations/Media";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import { type UserStatus, validateUserStatus } from "../utils/userStatus";
|
||||
|
||||
interface IState {
|
||||
displayName?: string;
|
||||
avatarUrl?: string;
|
||||
fetchedAt?: number;
|
||||
userStatus?: UserStatus;
|
||||
}
|
||||
|
||||
const KEY_DISPLAY_NAME = "mx_profile_displayname";
|
||||
@@ -81,6 +85,10 @@ export class OwnProfileStore extends AsyncStoreWithClient<IState> {
|
||||
return this.state.avatarUrl || null;
|
||||
}
|
||||
|
||||
public get userStatus(): UserStatus | undefined {
|
||||
return this.state.userStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the user's avatar as an HTTP URL of the given size. If the user's
|
||||
* avatar is not present, this returns null.
|
||||
@@ -105,6 +113,9 @@ export class OwnProfileStore extends AsyncStoreWithClient<IState> {
|
||||
this.monitoredUser.removeListener(UserEvent.AvatarUrl, this.onProfileUpdate);
|
||||
}
|
||||
this.matrixClient?.removeListener(RoomStateEvent.Events, this.onStateEvents);
|
||||
if (SettingsStore.getValue("feature_user_status")) {
|
||||
this.matrixClient?.removeListener(ClientEvent.UserProfileUpdate, this.onExtendedProfileUpdate);
|
||||
}
|
||||
await this.reset({});
|
||||
}
|
||||
|
||||
@@ -117,11 +128,16 @@ export class OwnProfileStore extends AsyncStoreWithClient<IState> {
|
||||
this.monitoredUser.on(UserEvent.AvatarUrl, this.onProfileUpdate);
|
||||
}
|
||||
|
||||
if (SettingsStore.getValue("feature_user_status")) {
|
||||
this.matrixClient.on(ClientEvent.UserProfileUpdate, this.onExtendedProfileUpdate);
|
||||
}
|
||||
|
||||
// We also have to listen for membership events for ourselves as the above User events
|
||||
// are fired only with presence, which matrix.org (and many others) has disabled.
|
||||
this.matrixClient.on(RoomStateEvent.Events, this.onStateEvents);
|
||||
|
||||
await this.onProfileUpdate(); // trigger an initial update
|
||||
await this.refreshUserStatus(); // trigger an update for the user status
|
||||
}
|
||||
|
||||
protected async onAction(payload: ActionPayload): Promise<void> {
|
||||
@@ -175,6 +191,23 @@ export class OwnProfileStore extends AsyncStoreWithClient<IState> {
|
||||
{ trailing: true, leading: true },
|
||||
);
|
||||
|
||||
private onExtendedProfileUpdate = async (syncedUserId: string): Promise<void> => {
|
||||
if (syncedUserId === this.matrixClient?.getSafeUserId()) {
|
||||
await this.refreshUserStatus();
|
||||
}
|
||||
};
|
||||
|
||||
private refreshUserStatus = async (): Promise<void> => {
|
||||
if (!this.matrixClient) return;
|
||||
if (!SettingsStore.getValue("feature_user_status")) return;
|
||||
|
||||
const rawUserStatus = await this.matrixClient.getExtendedProfileProperty(
|
||||
this.matrixClient.getSafeUserId(),
|
||||
"org.matrix.msc4426.status",
|
||||
);
|
||||
await this.updateState({ userStatus: validateUserStatus(rawUserStatus) });
|
||||
};
|
||||
|
||||
private onStateEvents = async (ev: MatrixEvent): Promise<void> => {
|
||||
const myUserId = MatrixClientPeg.safeGet().getUserId();
|
||||
if (ev.getType() === EventType.RoomMember && ev.getSender() === myUserId && ev.getStateKey() === myUserId) {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
const MAX_STATUS_TEXT_BYTES = 256;
|
||||
|
||||
export interface UserStatus {
|
||||
emoji: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export function userStatusTextWithinMaxLength(text: string): boolean {
|
||||
const textEncoder = new TextEncoder();
|
||||
return textEncoder.encode(text).length <= MAX_STATUS_TEXT_BYTES;
|
||||
}
|
||||
|
||||
export function validateUserStatus(rawUserStatus: unknown): UserStatus | undefined {
|
||||
if (typeof rawUserStatus !== "object" || rawUserStatus === null) {
|
||||
return undefined;
|
||||
}
|
||||
if ("emoji" in rawUserStatus === false || typeof rawUserStatus.emoji !== "string" || !rawUserStatus.emoji) {
|
||||
return undefined;
|
||||
}
|
||||
if ("text" in rawUserStatus === false || typeof rawUserStatus.text !== "string" || !rawUserStatus.text) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
emoji: rawUserStatus.emoji,
|
||||
text: userStatusTextWithinMaxLength(rawUserStatus.text)
|
||||
? rawUserStatus.text
|
||||
: `${rawUserStatus.text.slice(0, MAX_STATUS_TEXT_BYTES)}…`,
|
||||
};
|
||||
}
|
||||
@@ -42,6 +42,7 @@ export class UserMenuViewModel extends BaseViewModel<UserMenuSnapshot, undefined
|
||||
expanded: !isPanelCollapsed,
|
||||
manageAccountHref: accountManagementEndpoint,
|
||||
showAvatar: isAuthenticated,
|
||||
statusEmoji: OwnProfileStore.instance.userStatus?.emoji,
|
||||
actions: {
|
||||
createAccount: !isAuthenticated,
|
||||
signIn: !isAuthenticated,
|
||||
@@ -72,7 +73,8 @@ export class UserMenuViewModel extends BaseViewModel<UserMenuSnapshot, undefined
|
||||
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 });
|
||||
const statusEmoji = OwnProfileStore.instance.userStatus?.emoji;
|
||||
this.snapshot.merge({ displayName, avatarUrl, statusEmoji });
|
||||
};
|
||||
|
||||
public readonly setOpen = (isOpen: boolean): void => {
|
||||
|
||||
@@ -15,7 +15,7 @@ import { type MouseEvent } from "react";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { getUserNameColorClass } from "../../../../utils/FormattingUtils";
|
||||
import UserIdentifier from "../../../../customisations/UserIdentifier";
|
||||
import type { UserStatus } from "../../../../hooks/useUserStatus";
|
||||
import { type UserStatus } from "../../../../utils/userStatus";
|
||||
|
||||
/**
|
||||
* Information about a member for disambiguation purposes.
|
||||
|
||||
@@ -359,6 +359,7 @@ export function createTestClient(): MatrixClient {
|
||||
sendTextMessage: jest.fn(),
|
||||
deleteRoomTag: jest.fn().mockResolvedValue({}),
|
||||
setRoomTag: jest.fn().mockResolvedValue({}),
|
||||
getExtendedProfileProperty: jest.fn(),
|
||||
} as unknown as MatrixClient;
|
||||
|
||||
client.reEmitter = new ReEmitter(client);
|
||||
|
||||
+15
-11
@@ -7,27 +7,31 @@ exports[`<SpacePanel /> should show all activated MetaSpaces in the correct orde
|
||||
class="mx_SpacePanel collapsed newUi"
|
||||
>
|
||||
<div
|
||||
class="_wrapper_1yvpq_8 mx_UserMenu"
|
||||
class="_wrapper_gc5p4_8 mx_UserMenu"
|
||||
>
|
||||
<button
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
aria-label="User menu"
|
||||
class="_triggerButton_1yvpq_57"
|
||||
class="_triggerButton_gc5p4_80"
|
||||
data-state="closed"
|
||||
id="radix-react-use-id-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="@test:test"
|
||||
class="_avatar_va14e_8 _avatar-imageless_va14e_55"
|
||||
data-color="5"
|
||||
data-type="round"
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 36px;"
|
||||
<div
|
||||
class="_avatarWrapper_gc5p4_57"
|
||||
>
|
||||
t
|
||||
</span>
|
||||
<span
|
||||
aria-label="@test:test"
|
||||
class="_avatar_va14e_8 _avatar-imageless_va14e_55"
|
||||
data-color="5"
|
||||
data-type="round"
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 36px;"
|
||||
>
|
||||
t
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -9,10 +9,11 @@ import React from "react";
|
||||
import { renderHook, waitFor } from "jest-matrix-react";
|
||||
import { ClientEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { useUserStatus, userStatusTextWithinMaxLength } from "../../../src/hooks/useUserStatus";
|
||||
import { useUserStatus } from "../../../src/hooks/useUserStatus";
|
||||
import { getMockClientWithEventEmitter, mockClientMethodsUser, mockClientMethodsServer } from "../../test-utils";
|
||||
import { MatrixClientContextProvider } from "../../../src/components/structures/MatrixClientContextProvider";
|
||||
import SettingsStore from "../../../src/settings/SettingsStore";
|
||||
import { userStatusTextWithinMaxLength } from "../../../src/utils/userStatus";
|
||||
|
||||
const userId = "@alice:example.com";
|
||||
|
||||
|
||||
@@ -6,12 +6,13 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix";
|
||||
import { ClientEvent, type MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix";
|
||||
import { type MockedObject, mocked } from "jest-mock";
|
||||
|
||||
import { stubClient } from "../../test-utils";
|
||||
import { OwnProfileStore } from "../../../src/stores/OwnProfileStore";
|
||||
import { UPDATE_EVENT } from "../../../src/stores/AsyncStore";
|
||||
import SettingsStore from "../../../src/settings/SettingsStore";
|
||||
|
||||
describe("OwnProfileStore", () => {
|
||||
let client: MockedObject<MatrixClient>;
|
||||
@@ -76,4 +77,61 @@ describe("OwnProfileStore", () => {
|
||||
expect(ownProfileStore.displayName).toBe(client.getSafeUserId());
|
||||
expect(ownProfileStore.avatarMxc).toBeNull();
|
||||
});
|
||||
|
||||
describe("userStatus", () => {
|
||||
beforeEach(() => {
|
||||
client.getProfileInfo.mockResolvedValue({});
|
||||
});
|
||||
|
||||
it("should be undefined if the feature is disabled", async () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
|
||||
await ownProfileStore.start();
|
||||
|
||||
expect(ownProfileStore.userStatus).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should be undefined if no status is set", async () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(true);
|
||||
client.getExtendedProfileProperty.mockResolvedValue(undefined);
|
||||
await ownProfileStore.start();
|
||||
|
||||
expect(ownProfileStore.userStatus).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should fetch and expose the user status on start", async () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(true);
|
||||
client.getExtendedProfileProperty.mockResolvedValue({ emoji: "🏝️", text: "On a tropical holiday" });
|
||||
await ownProfileStore.start();
|
||||
|
||||
expect(client.getExtendedProfileProperty).toHaveBeenCalledWith(
|
||||
client.getSafeUserId(),
|
||||
"org.matrix.msc4426.status",
|
||||
);
|
||||
expect(ownProfileStore.userStatus).toEqual({ emoji: "🏝️", text: "On a tropical holiday" });
|
||||
});
|
||||
|
||||
it("should update the user status when a UserProfileUpdate event is received for the current user", async () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(true);
|
||||
client.getExtendedProfileProperty.mockResolvedValue({ emoji: "🏝️", text: "On a tropical holiday" });
|
||||
await ownProfileStore.start();
|
||||
|
||||
client.getExtendedProfileProperty.mockResolvedValue({ emoji: "🏡", text: "Back home" });
|
||||
client.emit(ClientEvent.UserProfileUpdate, client.getSafeUserId(), null);
|
||||
await new Promise(process.nextTick);
|
||||
|
||||
expect(ownProfileStore.userStatus).toEqual({ emoji: "🏡", text: "Back home" });
|
||||
});
|
||||
|
||||
it("should not update the user status when a UserProfileUpdate event is received for another user", async () => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(true);
|
||||
client.getExtendedProfileProperty.mockResolvedValue({ emoji: "🏝️", text: "On a tropical holiday" });
|
||||
await ownProfileStore.start();
|
||||
|
||||
client.getExtendedProfileProperty.mockResolvedValue({ emoji: "🐭", text: "Is a mouse" });
|
||||
client.emit(ClientEvent.UserProfileUpdate, "@other:example.com", null);
|
||||
await new Promise(process.nextTick);
|
||||
|
||||
expect(ownProfileStore.userStatus).toEqual({ emoji: "🏝️", text: "On a tropical holiday" });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ exports[`UserMenuViewModel should generate a menu options for a guest 1`] = `
|
||||
"manageAccountHref": undefined,
|
||||
"open": true,
|
||||
"showAvatar": false,
|
||||
"statusEmoji": undefined,
|
||||
"userId": "@alice:domain",
|
||||
}
|
||||
`;
|
||||
@@ -38,6 +39,7 @@ exports[`UserMenuViewModel should generate a menu options for a logged in client
|
||||
"manageAccountHref": undefined,
|
||||
"open": true,
|
||||
"showAvatar": true,
|
||||
"statusEmoji": undefined,
|
||||
"userId": "@alice:domain",
|
||||
}
|
||||
`;
|
||||
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -54,6 +54,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
.avatarWrapper {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.statusEmoji {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
/* bottom edges aligned with the avatar */
|
||||
bottom: 0;
|
||||
/* center of circle on the right edge of the avatar */
|
||||
right: -10px;
|
||||
background-color: var(--cpd-color-bg-canvas-default);
|
||||
}
|
||||
|
||||
.triggerButton {
|
||||
border: none;
|
||||
background: none;
|
||||
|
||||
@@ -170,6 +170,12 @@ export const GuestOpen: Story = {
|
||||
tags: ["!dev", "!autodocs"],
|
||||
};
|
||||
|
||||
export const WithStatusEmoji: Story = {
|
||||
args: {
|
||||
statusEmoji: "🐹",
|
||||
},
|
||||
};
|
||||
|
||||
export const AllOptions: Story = {
|
||||
args: {
|
||||
displayName: "Alice",
|
||||
|
||||
@@ -13,7 +13,7 @@ import userEvent from "@testing-library/user-event";
|
||||
|
||||
import * as stories from "./UserMenu.stories.tsx";
|
||||
|
||||
const { Default, LongerName, Condensed, Guest, Open, NoAvatar } = composeStories(stories);
|
||||
const { Default, LongerName, Condensed, Guest, Open, NoAvatar, WithStatusEmoji } = composeStories(stories);
|
||||
|
||||
describe("UserMenu", () => {
|
||||
it("renders a button", async () => {
|
||||
@@ -28,6 +28,10 @@ describe("UserMenu", () => {
|
||||
const { container } = render(<Condensed />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
it("renders the status emoji when set", async () => {
|
||||
const { container } = render(<WithStatusEmoji />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
it("renders a menu", async () => {
|
||||
const { baseElement, getByRole } = render(<Default />);
|
||||
await userEvent.click(getByRole("button"));
|
||||
|
||||
@@ -50,6 +50,10 @@ export interface UserMenuViewSnapshot {
|
||||
* Account management URL if the user is using OIDC.
|
||||
*/
|
||||
manageAccountHref?: string;
|
||||
/**
|
||||
* The user status emoji to display, or undefined for no icon.
|
||||
*/
|
||||
statusEmoji?: string;
|
||||
/**
|
||||
* A set of actions that the user can perform from the menu.
|
||||
*/
|
||||
@@ -108,11 +112,15 @@ export type UserMenuViewProps = {
|
||||
};
|
||||
|
||||
export function UserMenuView({ vm, className }: UserMenuViewProps): JSX.Element {
|
||||
const { userId, displayName, avatarUrl, expanded, open, manageAccountHref, actions, showAvatar } = useViewModel(vm);
|
||||
const { userId, displayName, avatarUrl, expanded, open, manageAccountHref, actions, showAvatar, statusEmoji } =
|
||||
useViewModel(vm);
|
||||
const { translate: _t } = useI18n();
|
||||
const trigger = (
|
||||
<button className={styles.triggerButton} aria-label={_t("menus|user_menu|title")}>
|
||||
<Avatar id={userId} name={displayName} type="round" size="36px" src={avatarUrl} />
|
||||
<div className={styles.avatarWrapper}>
|
||||
<Avatar id={userId} name={displayName} type="round" size="36px" src={avatarUrl} />
|
||||
{statusEmoji && <span className={styles.statusEmoji}>{statusEmoji}</span>}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
|
||||
|
||||
+199
-119
@@ -14,25 +14,29 @@ exports[`UserMenu > renders a button 1`] = `
|
||||
id="radix-react-use-id-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="@person-name:homeserver.com"
|
||||
class="_avatar_va14e_8"
|
||||
data-color="1"
|
||||
data-type="round"
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 36px;"
|
||||
<div
|
||||
class="UserMenu-module_avatarWrapper"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_va14e_43"
|
||||
<span
|
||||
aria-label="@person-name:homeserver.com"
|
||||
class="_avatar_va14e_8"
|
||||
data-color="1"
|
||||
data-type="round"
|
||||
height="36px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="/static/element.png"
|
||||
width="36px"
|
||||
/>
|
||||
</span>
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 36px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_va14e_43"
|
||||
data-type="round"
|
||||
height="36px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="/static/element.png"
|
||||
width="36px"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
@@ -57,25 +61,29 @@ exports[`UserMenu > renders a button with a longer name 1`] = `
|
||||
id="radix-react-use-id-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="@person-name:homeserver.com"
|
||||
class="_avatar_va14e_8"
|
||||
data-color="1"
|
||||
data-type="round"
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 36px;"
|
||||
<div
|
||||
class="UserMenu-module_avatarWrapper"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_va14e_43"
|
||||
<span
|
||||
aria-label="@person-name:homeserver.com"
|
||||
class="_avatar_va14e_8"
|
||||
data-color="1"
|
||||
data-type="round"
|
||||
height="36px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="/static/element.png"
|
||||
width="36px"
|
||||
/>
|
||||
</span>
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 36px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_va14e_43"
|
||||
data-type="round"
|
||||
height="36px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="/static/element.png"
|
||||
width="36px"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
@@ -105,25 +113,29 @@ exports[`UserMenu > renders a guest menu 1`] = `
|
||||
id="radix-react-use-id-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="@guest:attendees.example.org"
|
||||
class="_avatar_va14e_8"
|
||||
data-color="4"
|
||||
data-type="round"
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 36px;"
|
||||
<div
|
||||
class="UserMenu-module_avatarWrapper"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_va14e_43"
|
||||
<span
|
||||
aria-label="@guest:attendees.example.org"
|
||||
class="_avatar_va14e_8"
|
||||
data-color="4"
|
||||
data-type="round"
|
||||
height="36px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="/static/element.png"
|
||||
width="36px"
|
||||
/>
|
||||
</span>
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 36px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_va14e_43"
|
||||
data-type="round"
|
||||
height="36px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="/static/element.png"
|
||||
width="36px"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
@@ -154,25 +166,29 @@ exports[`UserMenu > renders a menu 1`] = `
|
||||
id="radix-react-use-id-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="@person-name:homeserver.com"
|
||||
class="_avatar_va14e_8"
|
||||
data-color="1"
|
||||
data-type="round"
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 36px;"
|
||||
<div
|
||||
class="UserMenu-module_avatarWrapper"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_va14e_43"
|
||||
<span
|
||||
aria-label="@person-name:homeserver.com"
|
||||
class="_avatar_va14e_8"
|
||||
data-color="1"
|
||||
data-type="round"
|
||||
height="36px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="/static/element.png"
|
||||
width="36px"
|
||||
/>
|
||||
</span>
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 36px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_va14e_43"
|
||||
data-type="round"
|
||||
height="36px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="/static/element.png"
|
||||
width="36px"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
@@ -217,25 +233,29 @@ exports[`UserMenu > renders a menu without an avatar 1`] = `
|
||||
id="radix-react-use-id-2"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="@person-name:homeserver.com"
|
||||
class="_avatar_va14e_8"
|
||||
data-color="1"
|
||||
data-type="round"
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 36px;"
|
||||
<div
|
||||
class="UserMenu-module_avatarWrapper"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_va14e_43"
|
||||
<span
|
||||
aria-label="@person-name:homeserver.com"
|
||||
class="_avatar_va14e_8"
|
||||
data-color="1"
|
||||
data-type="round"
|
||||
height="36px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="/static/element.png"
|
||||
width="36px"
|
||||
/>
|
||||
</span>
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 36px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_va14e_43"
|
||||
data-type="round"
|
||||
height="36px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="/static/element.png"
|
||||
width="36px"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
@@ -526,25 +546,29 @@ exports[`UserMenu > renders an open menu 1`] = `
|
||||
id="radix-react-use-id-2"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="@person-name:homeserver.com"
|
||||
class="_avatar_va14e_8"
|
||||
data-color="1"
|
||||
data-type="round"
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 36px;"
|
||||
<div
|
||||
class="UserMenu-module_avatarWrapper"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_va14e_43"
|
||||
<span
|
||||
aria-label="@person-name:homeserver.com"
|
||||
class="_avatar_va14e_8"
|
||||
data-color="1"
|
||||
data-type="round"
|
||||
height="36px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="/static/element.png"
|
||||
width="36px"
|
||||
/>
|
||||
</span>
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 36px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_va14e_43"
|
||||
data-type="round"
|
||||
height="36px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="/static/element.png"
|
||||
width="36px"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
@@ -835,26 +859,82 @@ exports[`UserMenu > renders condensed view 1`] = `
|
||||
id="radix-react-use-id-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-label="@person-name:homeserver.com"
|
||||
class="_avatar_va14e_8"
|
||||
data-color="1"
|
||||
data-type="round"
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 36px;"
|
||||
<div
|
||||
class="UserMenu-module_avatarWrapper"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_va14e_43"
|
||||
<span
|
||||
aria-label="@person-name:homeserver.com"
|
||||
class="_avatar_va14e_8"
|
||||
data-color="1"
|
||||
data-type="round"
|
||||
height="36px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="/static/element.png"
|
||||
width="36px"
|
||||
/>
|
||||
</span>
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 36px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_va14e_43"
|
||||
data-type="round"
|
||||
height="36px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="/static/element.png"
|
||||
width="36px"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`UserMenu > renders the status emoji when set 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="UserMenu-module_wrapper"
|
||||
>
|
||||
<button
|
||||
aria-expanded="false"
|
||||
aria-haspopup="menu"
|
||||
aria-label="User menu"
|
||||
class="UserMenu-module_triggerButton"
|
||||
data-state="closed"
|
||||
id="radix-react-use-id-1"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="UserMenu-module_avatarWrapper"
|
||||
>
|
||||
<span
|
||||
aria-label="@person-name:homeserver.com"
|
||||
class="_avatar_va14e_8"
|
||||
data-color="1"
|
||||
data-type="round"
|
||||
role="img"
|
||||
style="--cpd-avatar-size: 36px;"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="_image_va14e_43"
|
||||
data-type="round"
|
||||
height="36px"
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
src="/static/element.png"
|
||||
width="36px"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="UserMenu-module_statusEmoji"
|
||||
>
|
||||
🐹
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
<span
|
||||
class="_typography_6v6n8_153 _font-heading-sm-semibold_6v6n8_93"
|
||||
>
|
||||
Sally Sanderson
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user