Show & clear your own on-a-call status (#34613)

* Show & clear your own on-a-call status

The user menu / settings now reflects your own on-a-call status
and pressing the 'clear' button will clear both m.status and m.call,
whichever are set.

* Update function name

* Fix tests

And make the clear status function simpler by just throwing if either fails.

* Fix more tests
This commit is contained in:
David Baker
2026-08-10 14:13:42 +00:00
committed by GitHub
parent 0b264a482e
commit fef1f0e0dd
8 changed files with 54 additions and 17 deletions
+3 -7
View File
@@ -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<IState> {
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<void> => {
+13 -3
View File
@@ -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", () => {
+13 -3
View File
@@ -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<void> {
return client.setExtendedProfileProperty("org.matrix.msc4426.status", null);
export async function clearAllUserStatus(client: MatrixClient): Promise<void> {
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);
}
}
/**
@@ -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);
});
};
@@ -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(() =>
@@ -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);
});
@@ -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" });
@@ -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();