diff --git a/apps/web/src/stores/OwnProfileStore.ts b/apps/web/src/stores/OwnProfileStore.ts index b3178e3492..3b1647ab8a 100644 --- a/apps/web/src/stores/OwnProfileStore.ts +++ b/apps/web/src/stores/OwnProfileStore.ts @@ -25,7 +25,7 @@ import { MatrixClientPeg } from "../MatrixClientPeg"; import { _t } from "../languageHandler"; import { mediaFromMxc } from "../customisations/Media"; import SettingsStore from "../settings/SettingsStore"; -import { userStatusFromProfile } from "../utils/userStatus"; +import { fetchUserStatus } from "../utils/userStatus"; interface IState { displayName?: string; @@ -202,12 +202,8 @@ export class OwnProfileStore extends AsyncStoreWithClient { if (!this.matrixClient) return; if (!SettingsStore.getValue("feature_user_status")) return; - const rawUserStatus = await this.matrixClient.getExtendedProfileProperty( - this.matrixClient.getSafeUserId(), - "org.matrix.msc4426.status", - ); - // We don't show our own "on a call" status so we pass undefined for the call status. - await this.updateState({ userStatus: userStatusFromProfile(rawUserStatus, undefined) }); + const userStatus = await fetchUserStatus(this.matrixClient, this.matrixClient.getSafeUserId()); + await this.updateState({ userStatus }); }; private onStateEvents = async (ev: MatrixEvent): Promise => { diff --git a/apps/web/src/utils/userStatus.test.ts b/apps/web/src/utils/userStatus.test.ts index ee40ad7a39..db4ca6fb3d 100644 --- a/apps/web/src/utils/userStatus.test.ts +++ b/apps/web/src/utils/userStatus.test.ts @@ -12,7 +12,7 @@ import { type MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix"; import { stubClient } from "test-utils"; import { - clearUserStatus, + clearAllUserStatus, fetchUserStatus, setUserOnCall, setUserStatus, @@ -143,7 +143,7 @@ describe("userStatus utils", () => { }); }); - describe("clearUserStatus", () => { + describe("clearAllUserStatus", () => { let client: MatrixClient; beforeEach(() => { @@ -151,10 +151,20 @@ describe("userStatus utils", () => { }); it("clears the user status", async () => { - clearUserStatus(client); + vi.mocked(client.getExtendedProfileProperty).mockResolvedValue({ emoji: "๐Ÿฉ", text: "Arbitrary Status" }); + + await clearAllUserStatus(client); expect(client.setExtendedProfileProperty).toHaveBeenCalledWith("org.matrix.msc4426.status", null); }); + + it("clears the call status", async () => { + vi.mocked(client.getExtendedProfileProperty).mockResolvedValue({ call_joined_ts: 12345 }); + + await clearAllUserStatus(client); + + expect(client.setExtendedProfileProperty).toHaveBeenCalledWith("org.matrix.msc4426.call", null); + }); }); describe("setUserOnCall", () => { diff --git a/apps/web/src/utils/userStatus.ts b/apps/web/src/utils/userStatus.ts index 162e403c17..67027a82cc 100644 --- a/apps/web/src/utils/userStatus.ts +++ b/apps/web/src/utils/userStatus.ts @@ -139,12 +139,22 @@ export function setUserStatus(client: MatrixClient, userStatus: UserStatus): Pro } /** - * Clears the MSC4426 user status for the given user. + * Clears all MSC4426 user status for the given user, including their m.status and m.call status, + * if anything is set in those fields. * * @param client The Matrix client to use. + * @throws If either request fails, in which case the other may also not have been cleared. */ -export function clearUserStatus(client: MatrixClient): Promise { - return client.setExtendedProfileProperty("org.matrix.msc4426.status", null); +export async function clearAllUserStatus(client: MatrixClient): Promise { + const rawUserStatus = await client.getExtendedProfileProperty(client.getSafeUserId(), "org.matrix.msc4426.status"); + if (rawUserStatus) { + await client.setExtendedProfileProperty("org.matrix.msc4426.status", null); + } + + const rawCallStatus = await client.getExtendedProfileProperty(client.getSafeUserId(), "org.matrix.msc4426.call"); + if (rawCallStatus) { + await setUserOnCall(client, false); + } } /** diff --git a/apps/web/src/viewmodels/menus/UserMenuViewModel.ts b/apps/web/src/viewmodels/menus/UserMenuViewModel.ts index 4630ca5564..a94d5c4308 100644 --- a/apps/web/src/viewmodels/menus/UserMenuViewModel.ts +++ b/apps/web/src/viewmodels/menus/UserMenuViewModel.ts @@ -19,7 +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"; +import { clearAllUserStatus } from "../../utils/userStatus"; import { type SetStatusViewModel, UserMenuSetStatusViewModel } from "../status/SetStatusViewModel"; import SettingsStore from "../../settings/SettingsStore"; @@ -153,7 +153,7 @@ export class UserMenuViewModel public readonly clearStatus = (): void => { this.setOpen(false); - clearUserStatus(this.client).catch((err) => { + clearAllUserStatus(this.client).catch((err) => { logger.warn("Failed to clear user status", err); }); }; diff --git a/apps/web/src/viewmodels/status/SetStatusViewModel.test.ts b/apps/web/src/viewmodels/status/SetStatusViewModel.test.ts index bdba5104a7..2b6ebeafbb 100644 --- a/apps/web/src/viewmodels/status/SetStatusViewModel.test.ts +++ b/apps/web/src/viewmodels/status/SetStatusViewModel.test.ts @@ -39,6 +39,7 @@ describe("SetStatusViewModel", () => { client = getMockClientWithEventEmitter({ ...mockClientMethodsUser(), ...mockClientMethodsServer(), + getExtendedProfileProperty: vi.fn().mockResolvedValue(undefined), setExtendedProfileProperty: vi.fn().mockResolvedValue(undefined), }); vi.mocked(mockOwnProfileStoreInstance).userStatus = undefined; @@ -127,6 +128,7 @@ describe("SetStatusViewModel", () => { }); it("calls setExtendedProfileProperty with null", async () => { + client.getExtendedProfileProperty.mockResolvedValue(STATUS); const vm = new SetStatusViewModel({ client, ownProfileStore: mockOwnProfileStoreInstance }); vm.clearStatus(); await waitFor(() => @@ -145,6 +147,7 @@ describe("SetStatusViewModel", () => { it("rolls back the snapshot on failure", async () => { vi.mocked(mockOwnProfileStoreInstance).userStatus = STATUS; + client.getExtendedProfileProperty.mockResolvedValue(STATUS); client.setExtendedProfileProperty.mockRejectedValue(new Error("network error")); const vm = new SetStatusViewModel({ client, ownProfileStore: mockOwnProfileStoreInstance }); vm.clearStatus(); @@ -169,6 +172,7 @@ describe("UserMenuSetStatusViewModel", () => { client = getMockClientWithEventEmitter({ ...mockClientMethodsUser(), ...mockClientMethodsServer(), + getExtendedProfileProperty: vi.fn().mockResolvedValue(undefined), setExtendedProfileProperty: vi.fn().mockResolvedValue(undefined), }); vi.mocked(mockOwnProfileStoreInstance).userStatus = undefined; @@ -203,6 +207,7 @@ describe("UserMenuSetStatusViewModel", () => { }); it("inherits clearStatus from SetStatusViewModel", async () => { + client.getExtendedProfileProperty.mockResolvedValue(STATUS); const vm = new UserMenuSetStatusViewModel({ client, ownProfileStore: mockOwnProfileStoreInstance }); vm.clearStatus(); await waitFor(() => diff --git a/apps/web/src/viewmodels/status/SetStatusViewModel.ts b/apps/web/src/viewmodels/status/SetStatusViewModel.ts index 0ba10c1c2a..a285dcfb51 100644 --- a/apps/web/src/viewmodels/status/SetStatusViewModel.ts +++ b/apps/web/src/viewmodels/status/SetStatusViewModel.ts @@ -14,7 +14,7 @@ import { type UserStatus, } from "@element-hq/web-shared-components"; -import { clearUserStatus, setUserStatus } from "../../utils/userStatus"; +import { clearAllUserStatus, setUserStatus } from "../../utils/userStatus"; import { UPDATE_EVENT } from "../../stores/AsyncStore"; import dis from "../../dispatcher/dispatcher"; import { UserTab } from "../../components/views/dialogs/UserTab"; @@ -59,7 +59,7 @@ export class SetStatusViewModel const oldStatus = this.snapshot.current.userStatus; this.snapshot.merge({ userStatus: undefined }); - clearUserStatus(this.props.client).catch((err) => { + clearAllUserStatus(this.props.client).catch((err) => { this.snapshot.merge({ userStatus: oldStatus }); logger.warn("Failed to clear user status", err); }); diff --git a/apps/web/test/unit-tests/stores/OwnProfileStore-test.ts b/apps/web/test/unit-tests/stores/OwnProfileStore-test.ts index 32f77bc6be..f93e6a58a1 100644 --- a/apps/web/test/unit-tests/stores/OwnProfileStore-test.ts +++ b/apps/web/test/unit-tests/stores/OwnProfileStore-test.ts @@ -110,6 +110,20 @@ describe("OwnProfileStore", () => { expect(ownProfileStore.userStatus).toEqual({ emoji: "๐Ÿ๏ธ", text: "On a tropical holiday" }); }); + it("should reflect our own on-a-call status if no other status is set", async () => { + jest.spyOn(SettingsStore, "getValue").mockReturnValue(true); + client.getExtendedProfileProperty.mockImplementation(async (_userId, key) => + key === "org.matrix.msc4426.call" ? { call_joined_ts: 12345 } : undefined, + ); + await ownProfileStore.start(); + + expect(client.getExtendedProfileProperty).toHaveBeenCalledWith( + client.getSafeUserId(), + "org.matrix.msc4426.call", + ); + expect(ownProfileStore.userStatus).toEqual({ emoji: "๐Ÿ“ž", text: "On a call" }); + }); + 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" }); diff --git a/apps/web/test/viewmodels/menus/UserMenuViewModel-test.ts b/apps/web/test/viewmodels/menus/UserMenuViewModel-test.ts index 53e4922a87..8bcc763651 100644 --- a/apps/web/test/viewmodels/menus/UserMenuViewModel-test.ts +++ b/apps/web/test/viewmodels/menus/UserMenuViewModel-test.ts @@ -32,6 +32,7 @@ describe("UserMenuViewModel", () => { ...mockClientMethodsUser(), ...mockClientMethodsServer(), getAuthMetadata: jest.fn().mockRejectedValue(new MatrixError({ errcode: "M_UNRECOGNIZED" }, 404)), + getExtendedProfileProperty: jest.fn().mockResolvedValue(undefined), setExtendedProfileProperty: jest.fn().mockResolvedValue(undefined), }); sdkContext = new TestSDKContext(); @@ -178,6 +179,7 @@ describe("UserMenuViewModel", () => { }); it("can clear a user status", async () => { + client.getExtendedProfileProperty.mockResolvedValue({ emoji: "๐Ÿงช", text: "Testing" }); const vm = new UserMenuViewModel({ ownProfileStore: mockOwnProfileStore }, dispatcher, client, true); vm.setOpen(true); vm.clearStatus();