From ec6880a64683c5ce5d446afffcc69e1264323218 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 8 Jul 2026 15:30:08 +0100 Subject: [PATCH 1/3] Add user status to DM rooms in the room list --- apps/web/src/hooks/useUserStatus.ts | 24 ++++----- apps/web/src/utils/userStatus.ts | 51 ++++++++++++++++++- .../room-list/RoomListItemViewModel.ts | 47 ++++++++++++++++- .../shared-components/src/core/userStatus.ts | 6 +++ .../RoomListItemView/RoomListItemContent.tsx | 10 +++- .../RoomListItemView.module.css | 9 ++++ .../RoomListItemView.stories.tsx | 9 ++++ .../RoomListItemView/RoomListItemView.tsx | 3 ++ .../DisambiguatedProfileView.tsx | 2 +- 9 files changed, 141 insertions(+), 20 deletions(-) diff --git a/apps/web/src/hooks/useUserStatus.ts b/apps/web/src/hooks/useUserStatus.ts index 131e43f9aec..4b4afaff9aa 100644 --- a/apps/web/src/hooks/useUserStatus.ts +++ b/apps/web/src/hooks/useUserStatus.ts @@ -6,14 +6,14 @@ Please see LICENSE files in the repository root for full details. */ import { useEffect, useState } from "react"; -import { ClientEvent, MatrixError } from "matrix-js-sdk/src/matrix"; +import { ClientEvent } from "matrix-js-sdk/src/matrix"; import { logger as rootLogger } from "matrix-js-sdk/src/logger"; import { type UserStatus } from "@element-hq/web-shared-components"; import { useMatrixClientContext } from "../contexts/MatrixClientContext"; import { useTypedEventEmitter } from "./useEventEmitter"; import { useFeatureEnabled } from "./useSettings"; -import { validateUserStatus } from "../utils/userStatus"; +import { fetchUserStatus, validateUserStatus } from "../utils/userStatus"; const logger = rootLogger.getChild("useUserStatus"); @@ -27,14 +27,14 @@ const logger = rootLogger.getChild("useUserStatus"); export function useUserStatus(userId: string | undefined): UserStatus | undefined { const isEnabled = useFeatureEnabled("feature_user_status"); const matrixClient = useMatrixClientContext(); - const [rawUserStatus, setRawUserStatus] = useState(); + const [userStatus, setUserStatus] = useState(); useTypedEventEmitter(matrixClient, ClientEvent.UserProfileUpdate, (syncedUserId, syncProfile) => { if (syncedUserId !== userId) { return; } - setRawUserStatus(syncProfile["org.matrix.msc4426.status"]); + setUserStatus(validateUserStatus(syncProfile["org.matrix.msc4426.status"])); }); useEffect(() => { (async () => { @@ -42,22 +42,18 @@ export function useUserStatus(userId: string | undefined): UserStatus | undefine return; } if (!userId) { - setRawUserStatus(undefined); + setUserStatus(undefined); return; } if ((await matrixClient.doesServerSupportExtendedProfiles()) === false) { - setRawUserStatus(undefined); + setUserStatus(undefined); return; } try { - const result = await matrixClient.getExtendedProfileProperty(userId, "org.matrix.msc4426.status"); - setRawUserStatus(result); + const result = await fetchUserStatus(matrixClient, userId); + setUserStatus(result); } catch (ex) { - if (ex instanceof MatrixError && ex.errcode === "M_NOT_FOUND") { - setRawUserStatus(undefined); - } else { - logger.warn(`Failed to get userStatus for ${userId}`, ex); - } + logger.warn(`Failed to get userStatus for ${userId}`, ex); } })(); }, [isEnabled, userId, matrixClient]); @@ -65,5 +61,5 @@ export function useUserStatus(userId: string | undefined): UserStatus | undefine return; } - return validateUserStatus(rawUserStatus); + return userStatus; } diff --git a/apps/web/src/utils/userStatus.ts b/apps/web/src/utils/userStatus.ts index 6020b6bf893..5315f7d9187 100644 --- a/apps/web/src/utils/userStatus.ts +++ b/apps/web/src/utils/userStatus.ts @@ -6,17 +6,32 @@ Please see LICENSE files in the repository root for full details. */ import { type UserStatus } from "@element-hq/web-shared-components"; -import { type MatrixClient } from "matrix-js-sdk/src/matrix"; +import { type MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix"; +import { logger } from "matrix-js-sdk/src/logger"; // MSC4426 defines the maximum length of a status to be 256 bytes of UTF-8, // so we truncate anything longer than that. const MAX_STATUS_TEXT_BYTES = 256; +// Static Intl.Segmenter for grabbing the first grapheme of a user status emoji. +// We make one and keep it for performance. +const intlSegmenter = new Intl.Segmenter(); + +/** + * Checks if a given text is within the maximum allowed length for a user status. + * @param text The text to check. + * @returns True if the text is within the maximum length, false otherwise. + */ export function userStatusTextWithinMaxLength(text: string): boolean { const textEncoder = new TextEncoder(); return textEncoder.encode(text).length <= MAX_STATUS_TEXT_BYTES; } +/** + * Validates an object from a user profile and returns a UserStatus object if it contains a valid user status. + * @param rawUserStatus The raw user status object to validate. + * @returns A UserStatus object if valid, otherwise undefined. + */ export function validateUserStatus(rawUserStatus: unknown): UserStatus | undefined { if (typeof rawUserStatus !== "object" || rawUserStatus === null) { return undefined; @@ -28,13 +43,40 @@ export function validateUserStatus(rawUserStatus: unknown): UserStatus | undefin return undefined; } return { - emoji: rawUserStatus.emoji, + emoji: [...intlSegmenter.segment(rawUserStatus.emoji)][0]?.segment, text: userStatusTextWithinMaxLength(rawUserStatus.text) ? rawUserStatus.text : `${rawUserStatus.text.slice(0, MAX_STATUS_TEXT_BYTES)}…`, }; } +/** + * Fetch the MSC4426 user status of the given user. Returns undefined if the server does not + * support extended profiles, the user has no (valid) status, or the status could not be fetched. + * + * @param client The Matrix client to fetch the status with. + * @param userId The ID of the user whose status is being fetched. + */ +export async function fetchUserStatus(client: MatrixClient, userId: string): Promise { + if ((await client.doesServerSupportExtendedProfiles()) === false) { + return undefined; + } + try { + return validateUserStatus(await client.getExtendedProfileProperty(userId, "org.matrix.msc4426.status")); + } catch (ex) { + if (!(ex instanceof MatrixError && ex.errcode === "M_NOT_FOUND")) { + logger.warn(`Failed to get userStatus for ${userId}`, ex); + } + return undefined; + } +} + +/** + * Sets the MSC4426 user status for the given user. + * + * @param client The Matrix client to use. + * @param userStatus The user status to set. + */ export function setUserStatus(client: MatrixClient, userStatus: UserStatus): Promise { return client.setExtendedProfileProperty("org.matrix.msc4426.status", { emoji: userStatus.emoji, @@ -42,6 +84,11 @@ export function setUserStatus(client: MatrixClient, userStatus: UserStatus): Pro }); } +/** + * Clears the MSC4426 user status for the given user. + * + * @param client The Matrix client to use. + */ export function clearUserStatus(client: MatrixClient): Promise { return client.setExtendedProfileProperty("org.matrix.msc4426.status", null); } diff --git a/apps/web/src/viewmodels/room-list/RoomListItemViewModel.ts b/apps/web/src/viewmodels/room-list/RoomListItemViewModel.ts index f7f285271ab..fc6d21c6946 100644 --- a/apps/web/src/viewmodels/room-list/RoomListItemViewModel.ts +++ b/apps/web/src/viewmodels/room-list/RoomListItemViewModel.ts @@ -11,11 +11,13 @@ import { type RoomListItemViewSnapshot, type RoomListItemViewActions, type Section, + type UserStatus, } from "@element-hq/web-shared-components"; -import { RoomEvent } from "matrix-js-sdk/src/matrix"; +import { ClientEvent, RoomEvent } from "matrix-js-sdk/src/matrix"; import { CallType } from "matrix-js-sdk/src/webrtc/call"; +import { logger } from "matrix-js-sdk/src/logger"; -import type { Room, MatrixClient, RoomMember } from "matrix-js-sdk/src/matrix"; +import type { Room, MatrixClient, RoomMember, ClientEventHandlerMap } from "matrix-js-sdk/src/matrix"; import type { RoomNotificationState } from "../../stores/notifications/RoomNotificationState"; import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore"; import { NotificationStateEvents } from "../../stores/notifications/NotificationState"; @@ -41,6 +43,7 @@ import { type Call, CallEvent } from "../../models/Call"; import RoomListStoreV3 from "../../stores/room-list-v3/RoomListStoreV3"; import { getCustomSectionData, isDefaultSectionTag } from "../../stores/room-list-v3/section"; import { _t } from "../../languageHandler"; +import { fetchUserStatus, validateUserStatus } from "../../utils/userStatus"; /** * View section type without `isSelected` field @@ -75,6 +78,11 @@ export class RoomListItemViewModel */ private availableSections: Sections; + /** + * The user ID of the other user if this room is a DM, used to show their user status. + */ + private readonly dmUserId?: string; + public constructor(props: RoomItemProps) { // Get notification state first so we can generate a complete initial snapshot const notifState = RoomNotificationStateStore.instance.getRoomState(props.room); @@ -141,6 +149,17 @@ export class RoomListItemViewModel // Load message preview asynchronously (sync data is already complete) void this.loadAndSetMessagePreview(); + + // If this room is a DM, show the MSC4426 user status of the other user + this.dmUserId = DMRoomMap.shared().getUserIdForRoomId(props.room.roomId) ?? undefined; + if (this.dmUserId) { + props.client.on(ClientEvent.UserProfileUpdate, this.onUserProfileUpdate); + this.disposables.track(() => { + props.client.off(ClientEvent.UserProfileUpdate, this.onUserProfileUpdate); + }); + + this.updateUserStatus(); + } } public dispose(): void { @@ -268,6 +287,30 @@ export class RoomListItemViewModel this.snapshot.merge({ messagePreview }); } + /** + * Handler for profile updates received via sync, to keep the DM user's status up to date. + */ + private onUserProfileUpdate: ClientEventHandlerMap[ClientEvent.UserProfileUpdate] = (userId, profile) => { + if (userId !== this.dmUserId || !SettingsStore.getValue("feature_user_status")) return; + this.snapshot.merge({ userStatus: validateUserStatus(profile?.["org.matrix.msc4426.status"]) }); + }; + + /** + * Fetch and set the user status of the DM user, if the feature is enabled. + */ + private updateUserStatus(): void { + (async () => { + let userStatus: UserStatus | undefined; + if (this.dmUserId && SettingsStore.getValue("feature_user_status")) { + userStatus = await fetchUserStatus(this.props.client, this.dmUserId); + } + if (this.disposables.isDisposed) return; + this.snapshot.merge({ userStatus }); + })().catch((ex) => { + logger.warn(`Failed to update userStatus for ${this.dmUserId}`, ex); + }); + } + /** * Generate a complete RoomListItem with all synchronous data. * Message preview is loaded separately to avoid blocking initial render. diff --git a/packages/shared-components/src/core/userStatus.ts b/packages/shared-components/src/core/userStatus.ts index 6aada947ef0..65144bd7d5c 100644 --- a/packages/shared-components/src/core/userStatus.ts +++ b/packages/shared-components/src/core/userStatus.ts @@ -10,6 +10,12 @@ Please see LICENSE files in the repository root for full details. * The emoji should be a single grapheme cluster. */ export interface UserStatus { + /** + * The emoji representing the user's status. This must be a single grapheme cluster. + */ emoji: string; + /** + * The text representing the user's status. + */ text: string; } diff --git a/packages/shared-components/src/room-list/VirtualizedRoomListView/RoomListItemWrapper/RoomListItemView/RoomListItemContent.tsx b/packages/shared-components/src/room-list/VirtualizedRoomListView/RoomListItemWrapper/RoomListItemView/RoomListItemContent.tsx index 4843a3463f5..d1937ef7a00 100644 --- a/packages/shared-components/src/room-list/VirtualizedRoomListView/RoomListItemWrapper/RoomListItemView/RoomListItemContent.tsx +++ b/packages/shared-components/src/room-list/VirtualizedRoomListView/RoomListItemWrapper/RoomListItemView/RoomListItemContent.tsx @@ -6,7 +6,7 @@ */ import React, { type JSX, memo, type ReactNode } from "react"; -import { Text } from "@vector-im/compound-web"; +import { Text, Tooltip } from "@vector-im/compound-web"; import classNames from "classnames"; import { Flex } from "../../../../core/utils/Flex"; @@ -54,7 +54,15 @@ export const RoomListItemContent = memo(function RoomListItemContent({
{item.name} + {item.userStatus && ( + + + {item.userStatus.emoji} + + + )}
+ {item.messagePreview && ( {item.messagePreview} diff --git a/packages/shared-components/src/room-list/VirtualizedRoomListView/RoomListItemWrapper/RoomListItemView/RoomListItemView.module.css b/packages/shared-components/src/room-list/VirtualizedRoomListView/RoomListItemWrapper/RoomListItemView/RoomListItemView.module.css index 4359634380e..f6a46413989 100644 --- a/packages/shared-components/src/room-list/VirtualizedRoomListView/RoomListItemWrapper/RoomListItemView/RoomListItemView.module.css +++ b/packages/shared-components/src/room-list/VirtualizedRoomListView/RoomListItemWrapper/RoomListItemView/RoomListItemView.module.css @@ -99,6 +99,15 @@ text-overflow: ellipsis; } +.roomName { + /* Allow the room name to shrink and truncate when displayed next to the user status emoji */ + min-width: 0; +} + +.userStatusEmoji { + padding-left: var(--cpd-space-1-5x); +} + .selected { .container { background-color: var(--cpd-color-bg-action-tertiary-selected); diff --git a/packages/shared-components/src/room-list/VirtualizedRoomListView/RoomListItemWrapper/RoomListItemView/RoomListItemView.stories.tsx b/packages/shared-components/src/room-list/VirtualizedRoomListView/RoomListItemWrapper/RoomListItemView/RoomListItemView.stories.tsx index 1cd951673d4..6834c552418 100644 --- a/packages/shared-components/src/room-list/VirtualizedRoomListView/RoomListItemWrapper/RoomListItemView/RoomListItemView.stories.tsx +++ b/packages/shared-components/src/room-list/VirtualizedRoomListView/RoomListItemWrapper/RoomListItemView/RoomListItemView.stories.tsx @@ -314,3 +314,12 @@ export const SectionDisabled: Story = { areSectionsEnabled: false, }, }; + +export const WithUserStatus: Story = { + args: { + userStatus: { + emoji: "🌭", + text: "Hot", + }, + }, +}; diff --git a/packages/shared-components/src/room-list/VirtualizedRoomListView/RoomListItemWrapper/RoomListItemView/RoomListItemView.tsx b/packages/shared-components/src/room-list/VirtualizedRoomListView/RoomListItemWrapper/RoomListItemView/RoomListItemView.tsx index 1f3e4bf86c6..2f395df6560 100644 --- a/packages/shared-components/src/room-list/VirtualizedRoomListView/RoomListItemWrapper/RoomListItemView/RoomListItemView.tsx +++ b/packages/shared-components/src/room-list/VirtualizedRoomListView/RoomListItemWrapper/RoomListItemView/RoomListItemView.tsx @@ -16,6 +16,7 @@ import { RoomListItemContent } from "./RoomListItemContent"; import { type RoomNotifState } from "./RoomNotifs"; import styles from "./RoomListItemView.module.css"; import { useViewModel, type ViewModel } from "../../../../core/viewmodel"; +import { type UserStatus } from "../../../../core/userStatus"; import { _t } from "../../../../core/i18n/i18n"; /** @@ -72,6 +73,8 @@ export interface RoomListItemViewSnapshot { isBold: boolean; /** Optional message preview text */ messagePreview?: string; + /** The MSC4426 user status of the other user in a DM room, if any */ + userStatus?: UserStatus; /** Notification decoration data */ notification: NotificationDecorationData; /** Whether the more options menu should be shown */ diff --git a/packages/shared-components/src/room/timeline/event-tile/EventTileView/DisambiguatedProfile/DisambiguatedProfileView.tsx b/packages/shared-components/src/room/timeline/event-tile/EventTileView/DisambiguatedProfile/DisambiguatedProfileView.tsx index f7a3a2b62a9..fd710674433 100644 --- a/packages/shared-components/src/room/timeline/event-tile/EventTileView/DisambiguatedProfile/DisambiguatedProfileView.tsx +++ b/packages/shared-components/src/room/timeline/event-tile/EventTileView/DisambiguatedProfile/DisambiguatedProfileView.tsx @@ -91,7 +91,7 @@ interface DisambiguatedProfileViewProps { export function DisambiguatedProfileView({ vm, className }: Readonly): JSX.Element { const { displayName, colorClass, displayIdentifier, title, emphasizeDisplayName, userStatus } = useViewModel(vm); - const userStatusEmoji = userStatus && [...new Intl.Segmenter().segment(userStatus.emoji)][0]?.segment; + const userStatusEmoji = userStatus && userStatus.emoji; const displayNameClasses = classNames(colorClass, { [styles.disambiguatedProfile_displayName]: emphasizeDisplayName, From e559f0703fed894648f514d1f0da53ed7f13831e Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 8 Jul 2026 15:40:54 +0100 Subject: [PATCH 2/3] Screenshot --- .../with-user-status-auto.png | Bin 0 -> 21514 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 packages/shared-components/__vis__/linux/__baselines__/room-list/VirtualizedRoomListView/RoomListItemWrapper/RoomListItemView/RoomListItemView.stories.tsx/with-user-status-auto.png diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/VirtualizedRoomListView/RoomListItemWrapper/RoomListItemView/RoomListItemView.stories.tsx/with-user-status-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/VirtualizedRoomListView/RoomListItemWrapper/RoomListItemView/RoomListItemView.stories.tsx/with-user-status-auto.png new file mode 100644 index 0000000000000000000000000000000000000000..fddb8cd7ef9aa8c9a2866060b3145942f300dc3d GIT binary patch literal 21514 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 literal 0 HcmV?d00001 From 5b0a2327994f219f4495d81f1b3d0a300a68c16b Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 8 Jul 2026 16:50:40 +0100 Subject: [PATCH 3/3] Tests for fetchUserStatus --- .../test/unit-tests/utils/userStatus-test.ts | 64 ++++++++++++++++++- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/apps/web/test/unit-tests/utils/userStatus-test.ts b/apps/web/test/unit-tests/utils/userStatus-test.ts index 5ae29dcded9..6c07bfc6983 100644 --- a/apps/web/test/unit-tests/utils/userStatus-test.ts +++ b/apps/web/test/unit-tests/utils/userStatus-test.ts @@ -5,9 +5,15 @@ 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 } from "matrix-js-sdk/src/matrix"; +import { type MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix"; +import { mocked } from "jest-mock"; -import { clearUserStatus, setUserStatus, userStatusTextWithinMaxLength } from "../../../src/utils/userStatus"; +import { + clearUserStatus, + fetchUserStatus, + setUserStatus, + userStatusTextWithinMaxLength, +} from "../../../src/utils/userStatus"; import { stubClient } from "../../test-utils"; describe("userStatus utils", () => { @@ -39,6 +45,60 @@ describe("userStatus utils", () => { }); }); + describe("fetchUserStatus", () => { + let client: MatrixClient; + + beforeEach(() => { + client = stubClient(); + client.doesServerSupportExtendedProfiles = jest.fn(); + }); + + it("returns undefined if the server does not support extended profiles", async () => { + mocked(client.doesServerSupportExtendedProfiles).mockResolvedValue(false); + + await expect(fetchUserStatus(client, "@alice:example.com")).resolves.toBeUndefined(); + expect(client.getExtendedProfileProperty).not.toHaveBeenCalled(); + }); + + it("returns the validated status if the server supports extended profiles and has a status set", async () => { + mocked(client.doesServerSupportExtendedProfiles).mockResolvedValue(true); + mocked(client.getExtendedProfileProperty).mockResolvedValue({ emoji: "🐳", text: "Feeling a little blue" }); + + await expect(fetchUserStatus(client, "@alice:example.com")).resolves.toEqual({ + emoji: "🐳", + text: "Feeling a little blue", + }); + expect(client.getExtendedProfileProperty).toHaveBeenCalledWith( + "@alice:example.com", + "org.matrix.msc4426.status", + ); + }); + + it("returns undefined if the status is invalid", async () => { + mocked(client.doesServerSupportExtendedProfiles).mockResolvedValue(true); + mocked(client.getExtendedProfileProperty).mockResolvedValue({ text: "Feeling a little blue" }); + + await expect(fetchUserStatus(client, "@alice:example.com")).resolves.toBeUndefined(); + }); + + it("returns undefined if the user has no status set", async () => { + mocked(client.doesServerSupportExtendedProfiles).mockResolvedValue(true); + mocked(client.getExtendedProfileProperty).mockRejectedValue( + new MatrixError({ errcode: "M_NOT_FOUND" }, 404), + ); + + await expect(fetchUserStatus(client, "@alice:example.com")).resolves.toBeUndefined(); + }); + + it("returns undefined and logs a warning if fetching the status fails unexpectedly", async () => { + mocked(client.doesServerSupportExtendedProfiles).mockResolvedValue(true); + const error = new Error("network error"); + mocked(client.getExtendedProfileProperty).mockRejectedValue(error); + + await expect(fetchUserStatus(client, "@alice:example.com")).resolves.toBeUndefined(); + }); + }); + describe("clearUserStatus", () => { let client: MatrixClient;