diff --git a/apps/web/src/hooks/useUserStatus.ts b/apps/web/src/hooks/useUserStatus.ts index 39c86f897c..131e43f9ae 100644 --- a/apps/web/src/hooks/useUserStatus.ts +++ b/apps/web/src/hooks/useUserStatus.ts @@ -8,11 +8,12 @@ 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 { 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 { type UserStatus, validateUserStatus } from "../utils/userStatus"; +import { validateUserStatus } from "../utils/userStatus"; const logger = rootLogger.getChild("useUserStatus"); @@ -32,9 +33,8 @@ export function useUserStatus(userId: string | undefined): UserStatus | undefine if (syncedUserId !== userId) { return; } - if (syncProfile["org.matrix.msc4426.status"]) { - setRawUserStatus(syncProfile["org.matrix.msc4426.status"]); - } + + setRawUserStatus(syncProfile["org.matrix.msc4426.status"]); }); useEffect(() => { (async () => { diff --git a/apps/web/src/slash-commands/status.ts b/apps/web/src/slash-commands/status.ts index e52e1e5c2e..e19c73ec9d 100644 --- a/apps/web/src/slash-commands/status.ts +++ b/apps/web/src/slash-commands/status.ts @@ -12,7 +12,7 @@ import SettingsStore from "../settings/SettingsStore"; import { reject, success } from "./utils"; import { UserFriendlyError } from "../languageHandler"; import { TimelineRenderingType } from "../contexts/RoomContext"; -import { userStatusTextWithinMaxLength } from "../utils/userStatus"; +import { setUserStatus, userStatusTextWithinMaxLength } from "../utils/userStatus"; export const statusCommand = new Command({ command: "status", @@ -40,7 +40,7 @@ export const statusCommand = new Command({ return reject(new UserFriendlyError("slash_command|status|too_long_text")); } return success( - cli.setExtendedProfileProperty("org.matrix.msc4426.status", { + setUserStatus(cli, { emoji: emoji.segment, text, }), diff --git a/apps/web/src/stores/OwnProfileStore.ts b/apps/web/src/stores/OwnProfileStore.ts index 788e8b81cb..a1a2d8ebe6 100644 --- a/apps/web/src/stores/OwnProfileStore.ts +++ b/apps/web/src/stores/OwnProfileStore.ts @@ -16,6 +16,7 @@ import { ClientEvent, } from "matrix-js-sdk/src/matrix"; import { throttle } from "lodash"; +import { type UserStatus } from "@element-hq/web-shared-components"; import { type ActionPayload } from "../dispatcher/payloads"; import { AsyncStoreWithClient } from "./AsyncStoreWithClient"; @@ -24,7 +25,7 @@ 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"; +import { validateUserStatus } from "../utils/userStatus"; interface IState { displayName?: string; diff --git a/apps/web/src/utils/userStatus.ts b/apps/web/src/utils/userStatus.ts index 66a4a6f3cb..6020b6bf89 100644 --- a/apps/web/src/utils/userStatus.ts +++ b/apps/web/src/utils/userStatus.ts @@ -5,12 +5,12 @@ 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. */ -const MAX_STATUS_TEXT_BYTES = 256; +import { type UserStatus } from "@element-hq/web-shared-components"; +import { type MatrixClient } from "matrix-js-sdk/src/matrix"; -export interface UserStatus { - emoji: string; - text: string; -} +// 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; export function userStatusTextWithinMaxLength(text: string): boolean { const textEncoder = new TextEncoder(); @@ -34,3 +34,14 @@ export function validateUserStatus(rawUserStatus: unknown): UserStatus | undefin : `${rawUserStatus.text.slice(0, MAX_STATUS_TEXT_BYTES)}…`, }; } + +export function setUserStatus(client: MatrixClient, userStatus: UserStatus): Promise { + return client.setExtendedProfileProperty("org.matrix.msc4426.status", { + emoji: userStatus.emoji, + text: userStatus.text, + }); +} + +export function clearUserStatus(client: MatrixClient): Promise { + return client.setExtendedProfileProperty("org.matrix.msc4426.status", null); +} diff --git a/apps/web/src/viewmodels/menus/UserMenuViewModel.ts b/apps/web/src/viewmodels/menus/UserMenuViewModel.ts index a97ee5a4ab..ee021d3f46 100644 --- a/apps/web/src/viewmodels/menus/UserMenuViewModel.ts +++ b/apps/web/src/viewmodels/menus/UserMenuViewModel.ts @@ -6,6 +6,7 @@ */ import { BaseViewModel, type UserMenuSnapshot, type UserMenuViewActions } from "@element-hq/web-shared-components"; +import { logger } from "matrix-js-sdk/src/logger"; import { OwnProfileStore } from "../../stores/OwnProfileStore"; import { UPDATE_EVENT } from "../../stores/AsyncStore"; @@ -18,6 +19,7 @@ import { shouldShowFeedback } from "../../utils/Feedback"; import { getHomePageUrl } from "../../utils/pages"; import SdkConfig from "../../SdkConfig"; import type { MatrixClient } from "matrix-js-sdk/src/matrix"; +import { clearUserStatus } from "../../utils/userStatus"; // Matches maximum size of an avatar in the UserMenu const AVATAR_PX = 88; @@ -42,7 +44,7 @@ export class UserMenuViewModel extends BaseViewModel { const displayName = OwnProfileStore.instance.displayName || this.snapshot.current.userId; const avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(AVATAR_PX) ?? undefined; - const statusEmoji = OwnProfileStore.instance.userStatus?.emoji; - this.snapshot.merge({ displayName, avatarUrl, statusEmoji }); + const userStatus = OwnProfileStore.instance.userStatus; + this.snapshot.merge({ displayName, avatarUrl, userStatus }); }; public readonly setOpen = (isOpen: boolean): void => { @@ -128,4 +130,11 @@ export class UserMenuViewModel extends BaseViewModel { + this.setOpen(false); + clearUserStatus(this.client).catch((err) => { + logger.warn("Failed to clear user status", err); + }); + }; } diff --git a/apps/web/src/viewmodels/room/timeline/event-tile/DisambiguatedProfileViewModel.ts b/apps/web/src/viewmodels/room/timeline/event-tile/DisambiguatedProfileViewModel.ts index ec7cc97702..76a9939087 100644 --- a/apps/web/src/viewmodels/room/timeline/event-tile/DisambiguatedProfileViewModel.ts +++ b/apps/web/src/viewmodels/room/timeline/event-tile/DisambiguatedProfileViewModel.ts @@ -9,13 +9,13 @@ import { type DisambiguatedProfileViewActions, type DisambiguatedProfileViewSnapshot, type DisambiguatedProfileViewModel as DisambiguatedProfileViewModelInterface, + type UserStatus, } from "@element-hq/web-shared-components"; import { type MouseEvent } from "react"; import { _t } from "../../../../languageHandler"; import { getUserNameColorClass } from "../../../../utils/FormattingUtils"; import UserIdentifier from "../../../../customisations/UserIdentifier"; -import { type UserStatus } from "../../../../utils/userStatus"; /** * Information about a member for disambiguation purposes. diff --git a/apps/web/test/slash-commands/status-test.ts b/apps/web/test/slash-commands/status-test.ts index 2f81af17c6..e2ff04d03f 100644 --- a/apps/web/test/slash-commands/status-test.ts +++ b/apps/web/test/slash-commands/status-test.ts @@ -16,7 +16,6 @@ describe("/status", () => { beforeEach(() => { client = stubClient(); - client.setExtendedProfileProperty = jest.fn().mockResolvedValue(undefined); }); function run(args?: string) { diff --git a/apps/web/test/test-utils/test-utils.ts b/apps/web/test/test-utils/test-utils.ts index b0e6dccce2..dd3800f766 100644 --- a/apps/web/test/test-utils/test-utils.ts +++ b/apps/web/test/test-utils/test-utils.ts @@ -360,6 +360,7 @@ export function createTestClient(): MatrixClient { deleteRoomTag: jest.fn().mockResolvedValue({}), setRoomTag: jest.fn().mockResolvedValue({}), getExtendedProfileProperty: jest.fn(), + setExtendedProfileProperty: jest.fn().mockResolvedValue(undefined), } as unknown as MatrixClient; client.reEmitter = new ReEmitter(client); diff --git a/apps/web/test/unit-tests/components/views/spaces/__snapshots__/SpacePanel-test.tsx.snap b/apps/web/test/unit-tests/components/views/spaces/__snapshots__/SpacePanel-test.tsx.snap index 8f3449cee6..c2ba1f8a10 100644 --- a/apps/web/test/unit-tests/components/views/spaces/__snapshots__/SpacePanel-test.tsx.snap +++ b/apps/web/test/unit-tests/components/views/spaces/__snapshots__/SpacePanel-test.tsx.snap @@ -7,19 +7,19 @@ exports[` should show all activated MetaSpaces in the correct orde class="mx_SpacePanel collapsed newUi" >
); @@ -143,6 +175,7 @@ export function UserMenuView({ vm, className }: UserMenuViewProps): JSX.Element {displayName} + {userStatus && } {userId} diff --git a/packages/shared-components/src/menus/UserMenu/__snapshots__/UserMenu.test.tsx.snap b/packages/shared-components/src/menus/UserMenu/__snapshots__/UserMenu.test.tsx.snap index f1de919ab3..c23fe6619b 100644 --- a/packages/shared-components/src/menus/UserMenu/__snapshots__/UserMenu.test.tsx.snap +++ b/packages/shared-components/src/menus/UserMenu/__snapshots__/UserMenu.test.tsx.snap @@ -845,6 +845,62 @@ exports[`UserMenu > renders an open menu 1`] = ` `; +exports[`UserMenu > renders an open menu with user status 1`] = ` + +`; + exports[`UserMenu > renders condensed view 1`] = `
renders the status emoji when set 1`] = ` width="36px" /> - 🐹 - +