From f3b61bc5f0edb3737fba4d1c1d94097d43a1dfe2 Mon Sep 17 00:00:00 2001 From: Olivier 'reivilibre Date: Wed, 26 Aug 2026 16:34:37 +0000 Subject: [PATCH] Don't set custom profile fields to null when deletion is meant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Null is not interoperable as an allowable field value: > Servers MAY reject null values. — https://spec.matrix.org/v1.19/client-server-api/#put_matrixclientv3profileuseridkeyname Synapse will enforce this to help in https://github.com/element-hq/synapse/pull/20153 --- apps/web/src/utils/userStatus.test.ts | 6 +++--- apps/web/src/utils/userStatus.ts | 15 ++++++--------- .../viewmodels/menus/UserMenuViewModel.test.ts | 3 ++- .../viewmodels/status/SetStatusViewModel.test.ts | 10 ++++++---- apps/web/test/test-utils/test-utils.ts | 1 + 5 files changed, 18 insertions(+), 17 deletions(-) diff --git a/apps/web/src/utils/userStatus.test.ts b/apps/web/src/utils/userStatus.test.ts index 04b047f0316..5289700fc18 100644 --- a/apps/web/src/utils/userStatus.test.ts +++ b/apps/web/src/utils/userStatus.test.ts @@ -117,7 +117,7 @@ describe("userStatus utils", () => { await clearAllUserStatus(client); - expect(client.setExtendedProfileProperty).toHaveBeenCalledWith("org.matrix.msc4426.status", null); + expect(client.deleteExtendedProfileProperty).toHaveBeenCalledWith("org.matrix.msc4426.status"); }); it("clears the call status", async () => { @@ -125,7 +125,7 @@ describe("userStatus utils", () => { await clearAllUserStatus(client); - expect(client.setExtendedProfileProperty).toHaveBeenCalledWith("org.matrix.msc4426.call", null); + expect(client.deleteExtendedProfileProperty).toHaveBeenCalledWith("org.matrix.msc4426.call"); }); }); @@ -151,7 +151,7 @@ describe("userStatus utils", () => { it("clears the call status if onCall is false", async () => { await setUserOnCall(client, false); - expect(client.setExtendedProfileProperty).toHaveBeenCalledWith("org.matrix.msc4426.call", null); + expect(client.deleteExtendedProfileProperty).toHaveBeenCalledWith("org.matrix.msc4426.call"); }); }); }); diff --git a/apps/web/src/utils/userStatus.ts b/apps/web/src/utils/userStatus.ts index 26005c80f32..6d1c47e2014 100644 --- a/apps/web/src/utils/userStatus.ts +++ b/apps/web/src/utils/userStatus.ts @@ -148,7 +148,7 @@ export function setUserStatus(client: MatrixClient, userStatus: UserStatus): Pro 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); + await client.deleteExtendedProfileProperty("org.matrix.msc4426.status"); } const rawCallStatus = await client.getExtendedProfileProperty(client.getSafeUserId(), "org.matrix.msc4426.call"); @@ -165,12 +165,9 @@ export async function clearAllUserStatus(client: MatrixClient): Promise { * @param onCall Whether the user is currently on a call. */ export function setUserOnCall(client: MatrixClient, onCall: boolean): Promise { - return client.setExtendedProfileProperty( - "org.matrix.msc4426.call", - onCall - ? { - call_joined_ts: Date.now(), - } - : null, - ); + if (!onCall) { + return client.deleteExtendedProfileProperty("org.matrix.msc4426.call"); + } + + return client.setExtendedProfileProperty("org.matrix.msc4426.call", { call_joined_ts: Date.now() }); } diff --git a/apps/web/src/viewmodels/menus/UserMenuViewModel.test.ts b/apps/web/src/viewmodels/menus/UserMenuViewModel.test.ts index 2dd2db97abf..f2042e491de 100644 --- a/apps/web/src/viewmodels/menus/UserMenuViewModel.test.ts +++ b/apps/web/src/viewmodels/menus/UserMenuViewModel.test.ts @@ -37,6 +37,7 @@ describe("UserMenuViewModel", () => { getAuthMetadata: vi.fn().mockRejectedValue(new MatrixError({ errcode: "M_UNRECOGNIZED" }, 404)), getExtendedProfileProperty: vi.fn().mockResolvedValue(undefined), setExtendedProfileProperty: vi.fn().mockResolvedValue(undefined), + deleteExtendedProfileProperty: vi.fn().mockResolvedValue(undefined), }); sdkContext = new TestSDKContext(); // @ts-ignore UserMenuViewModel uses SDKContext in the constructor @@ -187,7 +188,7 @@ describe("UserMenuViewModel", () => { vm.setOpen(true); vm.clearStatus(); await waitFor(() => - expect(client.setExtendedProfileProperty).toHaveBeenCalledWith("org.matrix.msc4426.status", null), + expect(client.deleteExtendedProfileProperty).toHaveBeenCalledWith("org.matrix.msc4426.status"), ); }); diff --git a/apps/web/src/viewmodels/status/SetStatusViewModel.test.ts b/apps/web/src/viewmodels/status/SetStatusViewModel.test.ts index 62640e57ec1..4e4e882bf0e 100644 --- a/apps/web/src/viewmodels/status/SetStatusViewModel.test.ts +++ b/apps/web/src/viewmodels/status/SetStatusViewModel.test.ts @@ -47,6 +47,7 @@ describe("SetStatusViewModel", () => { ...mockClientMethodsServer(), getExtendedProfileProperty: vi.fn().mockResolvedValue(undefined), setExtendedProfileProperty: vi.fn().mockResolvedValue(undefined), + deleteExtendedProfileProperty: vi.fn().mockResolvedValue(undefined), }); vi.mocked(mockOwnProfileStoreInstance).userStatus = undefined; vi.mocked(recent.get).mockReturnValue([]); @@ -159,12 +160,12 @@ describe("SetStatusViewModel", () => { expect(vm.getSnapshot().userStatus).toBeUndefined(); }); - it("calls setExtendedProfileProperty with null", async () => { + it("calls deleteExtendedProfileProperty", async () => { client.getExtendedProfileProperty.mockResolvedValue(STATUS); const vm = new SetStatusViewModel({ client, ownProfileStore: mockOwnProfileStoreInstance }); vm.clearStatus(); await waitFor(() => - expect(client.setExtendedProfileProperty).toHaveBeenCalledWith("org.matrix.msc4426.status", null), + expect(client.deleteExtendedProfileProperty).toHaveBeenCalledWith("org.matrix.msc4426.status"), ); }); @@ -180,7 +181,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")); + client.deleteExtendedProfileProperty.mockRejectedValue(new Error("network error")); const vm = new SetStatusViewModel({ client, ownProfileStore: mockOwnProfileStoreInstance }); vm.clearStatus(); expect(vm.getSnapshot().userStatus).toBeUndefined(); @@ -206,6 +207,7 @@ describe("UserMenuSetStatusViewModel", () => { ...mockClientMethodsServer(), getExtendedProfileProperty: vi.fn().mockResolvedValue(undefined), setExtendedProfileProperty: vi.fn().mockResolvedValue(undefined), + deleteExtendedProfileProperty: vi.fn().mockResolvedValue(undefined), }); vi.mocked(mockOwnProfileStoreInstance).userStatus = undefined; dispatchSpy = vi.spyOn(dis, "dispatch").mockImplementation(() => {}); @@ -244,7 +246,7 @@ describe("UserMenuSetStatusViewModel", () => { const vm = new UserMenuSetStatusViewModel({ client, ownProfileStore: mockOwnProfileStoreInstance }); vm.clearStatus(); await waitFor(() => - expect(client.setExtendedProfileProperty).toHaveBeenCalledWith("org.matrix.msc4426.status", null), + expect(client.deleteExtendedProfileProperty).toHaveBeenCalledWith("org.matrix.msc4426.status"), ); }); }); diff --git a/apps/web/test/test-utils/test-utils.ts b/apps/web/test/test-utils/test-utils.ts index b74eb077cf6..d4dd03755a4 100644 --- a/apps/web/test/test-utils/test-utils.ts +++ b/apps/web/test/test-utils/test-utils.ts @@ -370,6 +370,7 @@ export function createTestClient(): MatrixClient { setRoomTag: vi.fn().mockResolvedValue({}), getExtendedProfileProperty: vi.fn(), setExtendedProfileProperty: vi.fn().mockResolvedValue(undefined), + deleteExtendedProfileProperty: vi.fn().mockResolvedValue(undefined), doesServerSupportExtendedProfiles: vi.fn(), } as unknown as MatrixClient;