From 68389697924b30c89b2fbfff1b7ace1a7940b8ad Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Mon, 13 Oct 2025 12:41:57 +0100 Subject: [PATCH] Stabilise user profile timezones (#30815) * Fix imports * lint * update test * log * Update comment --- src/components/structures/LoggedInView.tsx | 9 +++- src/hooks/useUserTimezone.ts | 20 +++++-- .../structures/LoggedInView-test.tsx | 20 ++++--- .../views/right_panel/UserInfo-test.tsx | 54 +++++++++++-------- .../__snapshots__/UserInfo-test.tsx.snap | 6 +-- 5 files changed, 71 insertions(+), 38 deletions(-) diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 871085c24f..907e40dede 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -17,6 +17,8 @@ import { type SyncStateData, SyncState, EventType, + ProfileKeyTimezone, + ProfileKeyMSC4175Timezone, } from "matrix-js-sdk/src/matrix"; import { type MatrixCall } from "matrix-js-sdk/src/webrtc/call"; import classNames from "classnames"; @@ -197,10 +199,12 @@ class LoggedInView extends React.Component { } private onTimezoneUpdate = async (): Promise => { + // TODO: In a future app release, remove support for legacy key. if (!SettingsStore.getValue("userTimezonePublish")) { // Ensure it's deleted try { - await this._matrixClient.deleteExtendedProfileProperty("us.cloke.msc4175.tz"); + await this._matrixClient.deleteExtendedProfileProperty(ProfileKeyMSC4175Timezone); + await this._matrixClient.deleteExtendedProfileProperty(ProfileKeyTimezone); } catch (ex) { console.warn("Failed to delete timezone from user profile", ex); } @@ -215,7 +219,8 @@ class LoggedInView extends React.Component { return; } try { - await this._matrixClient.setExtendedProfileProperty("us.cloke.msc4175.tz", currentTimezone); + await this._matrixClient.setExtendedProfileProperty(ProfileKeyTimezone, currentTimezone); + await this._matrixClient.setExtendedProfileProperty(ProfileKeyMSC4175Timezone, currentTimezone); } catch (ex) { console.warn("Failed to update user profile with current timezone", ex); } diff --git a/src/hooks/useUserTimezone.ts b/src/hooks/useUserTimezone.ts index 0e5f046d74..04de7c60e1 100644 --- a/src/hooks/useUserTimezone.ts +++ b/src/hooks/useUserTimezone.ts @@ -5,11 +5,19 @@ 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 { useEffect, useState } from "react"; -import { type MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix"; +import { + type MatrixClient, + MatrixError, + ProfileKeyMSC4175Timezone, + ProfileKeyTimezone, +} from "matrix-js-sdk/src/matrix"; +import { logger } from "matrix-js-sdk/src/logger"; import { getTwelveHourOptions } from "../DateUtils.ts"; import { useSettingValue } from "./useSettings.ts"; +const log = logger.getChild("useUserTimezone"); + /** * Fetch a user's delclared timezone through their profile, and return * a friendly string of the current time for that user. This will keep @@ -52,11 +60,13 @@ export const useUserTimezone = (cli: MatrixClient, userId: string): { timezone: return; } (async () => { - console.log("Trying to fetch TZ"); + log.debug("Trying to fetch TZ for", userId); try { - const tz = await cli.getExtendedProfileProperty(userId, "us.cloke.msc4175.tz"); + const userProfile = await cli.getExtendedProfile(userId); + // In a future spec release, remove support for legacy key. + const tz = userProfile[ProfileKeyTimezone] ?? userProfile[ProfileKeyMSC4175Timezone]; if (typeof tz !== "string") { - // Err, definitely not a tz. + // Definitely not a tz. throw Error("Timezone value was not a string"); } // This will validate the timezone for us. @@ -85,7 +95,7 @@ export const useUserTimezone = (cli: MatrixClient, userId: string): { timezone: // No timezone set, ignore. return; } - console.error("Could not render current timezone for user", ex); + log.warn(`Could not render current timezone for ${userId}`, ex); } })(); }, [supported, userId, cli, showTwelveHour]); diff --git a/test/unit-tests/components/structures/LoggedInView-test.tsx b/test/unit-tests/components/structures/LoggedInView-test.tsx index dd88625509..1bbbd5b6d3 100644 --- a/test/unit-tests/components/structures/LoggedInView-test.tsx +++ b/test/unit-tests/components/structures/LoggedInView-test.tsx @@ -15,6 +15,8 @@ import { MatrixEvent, ClientEvent, PushRuleKind, + ProfileKeyTimezone, + ProfileKeyMSC4175Timezone, } from "matrix-js-sdk/src/matrix"; import { MediaHandler } from "matrix-js-sdk/src/webrtc/mediaHandler"; import { logger } from "matrix-js-sdk/src/logger"; @@ -470,30 +472,36 @@ describe("", () => { it("does not update the timezone when userTimezonePublish is off", async () => { getComponent(); await SettingsStore.setValue("userTimezonePublish", null, SettingLevel.DEVICE, false); - expect(mockClient.deleteExtendedProfileProperty).toHaveBeenCalledWith("us.cloke.msc4175.tz"); + expect(mockClient.deleteExtendedProfileProperty).toHaveBeenCalledWith(ProfileKeyTimezone); + expect(mockClient.deleteExtendedProfileProperty).toHaveBeenCalledWith(ProfileKeyMSC4175Timezone); expect(mockClient.setExtendedProfileProperty).not.toHaveBeenCalled(); }); it("should set the user timezone when userTimezonePublish is enabled", async () => { getComponent(); await SettingsStore.setValue("userTimezonePublish", null, SettingLevel.DEVICE, true); - expect(mockClient.setExtendedProfileProperty).toHaveBeenCalledWith("us.cloke.msc4175.tz", userTimezone); + expect(mockClient.setExtendedProfileProperty).toHaveBeenCalledWith(ProfileKeyTimezone, userTimezone); + expect(mockClient.setExtendedProfileProperty).toHaveBeenCalledWith(ProfileKeyMSC4175Timezone, userTimezone); }); it("should set the user timezone when the timezone is changed", async () => { const newTimezone = "Europe/Paris"; getComponent(); await SettingsStore.setValue("userTimezonePublish", null, SettingLevel.DEVICE, true); - expect(mockClient.setExtendedProfileProperty).toHaveBeenCalledWith("us.cloke.msc4175.tz", userTimezone); + expect(mockClient.setExtendedProfileProperty).toHaveBeenCalledWith(ProfileKeyTimezone, userTimezone); + expect(mockClient.setExtendedProfileProperty).toHaveBeenCalledWith(ProfileKeyMSC4175Timezone, userTimezone); await SettingsStore.setValue("userTimezone", null, SettingLevel.DEVICE, newTimezone); - expect(mockClient.setExtendedProfileProperty).toHaveBeenCalledWith("us.cloke.msc4175.tz", newTimezone); + expect(mockClient.setExtendedProfileProperty).toHaveBeenCalledWith(ProfileKeyTimezone, newTimezone); + expect(mockClient.setExtendedProfileProperty).toHaveBeenCalledWith(ProfileKeyMSC4175Timezone, newTimezone); }); it("should clear the timezone when the publish feature is turned off", async () => { getComponent(); await SettingsStore.setValue("userTimezonePublish", null, SettingLevel.DEVICE, true); - expect(mockClient.setExtendedProfileProperty).toHaveBeenCalledWith("us.cloke.msc4175.tz", userTimezone); + expect(mockClient.setExtendedProfileProperty).toHaveBeenCalledWith(ProfileKeyTimezone, userTimezone); + expect(mockClient.setExtendedProfileProperty).toHaveBeenCalledWith(ProfileKeyMSC4175Timezone, userTimezone); await SettingsStore.setValue("userTimezonePublish", null, SettingLevel.DEVICE, false); - expect(mockClient.deleteExtendedProfileProperty).toHaveBeenCalledWith("us.cloke.msc4175.tz"); + expect(mockClient.deleteExtendedProfileProperty).toHaveBeenCalledWith(ProfileKeyTimezone); + expect(mockClient.deleteExtendedProfileProperty).toHaveBeenCalledWith(ProfileKeyMSC4175Timezone); }); }); diff --git a/test/unit-tests/components/views/right_panel/UserInfo-test.tsx b/test/unit-tests/components/views/right_panel/UserInfo-test.tsx index df417e1950..1b5efc2868 100644 --- a/test/unit-tests/components/views/right_panel/UserInfo-test.tsx +++ b/test/unit-tests/components/views/right_panel/UserInfo-test.tsx @@ -10,7 +10,15 @@ import React from "react"; import { render, screen, act, waitForElementToBeRemoved } from "jest-matrix-react"; import userEvent from "@testing-library/user-event"; import { type Mocked, mocked } from "jest-mock"; -import { type Room, User, type MatrixClient, RoomMember, Device } from "matrix-js-sdk/src/matrix"; +import { + type Room, + User, + type MatrixClient, + RoomMember, + Device, + ProfileKeyTimezone, + ProfileKeyMSC4175Timezone, +} from "matrix-js-sdk/src/matrix"; import { EventEmitter } from "events"; import { UserVerificationStatus, @@ -120,7 +128,7 @@ beforeEach(() => { isSynapseAdministrator: jest.fn().mockResolvedValue(false), doesServerSupportUnstableFeature: jest.fn().mockReturnValue(false), doesServerSupportExtendedProfiles: jest.fn().mockResolvedValue(false), - getExtendedProfileProperty: jest.fn().mockRejectedValue(new Error("Not supported")), + getExtendedProfile: jest.fn().mockRejectedValue(new Error("Not supported")), mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"), removeListener: jest.fn(), currentState: { @@ -199,29 +207,31 @@ describe("", () => { expect(screen.getByRole("heading", { name: defaultUserId })).toBeInTheDocument(); }); - it("renders user timezone if set", async () => { - // For timezone, force a consistent locale. - jest.spyOn(global.Date.prototype, "toLocaleString").mockImplementation(function ( - this: Date, - _locale, - opts, - ) { - return origDate.call(this, "en-US", { - ...opts, - hourCycle: "h12", + describe.each([[ProfileKeyTimezone], [ProfileKeyMSC4175Timezone]])("timezone rendering (%s)", (profileKey) => { + it("renders user timezone if set", async () => { + // For timezone, force a consistent locale. + jest.spyOn(global.Date.prototype, "toLocaleString").mockImplementation(function ( + this: Date, + _locale, + opts, + ) { + return origDate.call(this, "en-US", { + ...opts, + hourCycle: "h12", + }); }); + mockClient.doesServerSupportExtendedProfiles.mockResolvedValue(true); + mockClient.getExtendedProfile.mockResolvedValue({ [profileKey]: "Europe/London" }); + renderComponent(); + await expect(screen.findByText(/\d\d:\d\d (AM|PM)/)).resolves.toBeInTheDocument(); }); - mockClient.doesServerSupportExtendedProfiles.mockResolvedValue(true); - mockClient.getExtendedProfileProperty.mockResolvedValue("Europe/London"); - renderComponent(); - await expect(screen.findByText(/\d\d:\d\d (AM|PM)/)).resolves.toBeInTheDocument(); - }); - it("does not renders user timezone if timezone is invalid", async () => { - mockClient.doesServerSupportExtendedProfiles.mockResolvedValue(true); - mockClient.getExtendedProfileProperty.mockResolvedValue("invalid-tz"); - renderComponent(); - expect(screen.queryByText(/\d\d:\d\d (AM|PM)/)).not.toBeInTheDocument(); + it("does not renders user timezone if timezone is invalid", async () => { + mockClient.doesServerSupportExtendedProfiles.mockResolvedValue(true); + mockClient.getExtendedProfile.mockResolvedValue({ [profileKey]: "invalid-tz" }); + renderComponent(); + expect(screen.queryByText(/\d\d:\d\d (AM|PM)/)).not.toBeInTheDocument(); + }); }); it("renders encryption info panel without pending verification", () => { diff --git a/test/unit-tests/components/views/right_panel/__snapshots__/UserInfo-test.tsx.snap b/test/unit-tests/components/views/right_panel/__snapshots__/UserInfo-test.tsx.snap index c096526782..7a8ace1bc3 100644 --- a/test/unit-tests/components/views/right_panel/__snapshots__/UserInfo-test.tsx.snap +++ b/test/unit-tests/components/views/right_panel/__snapshots__/UserInfo-test.tsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Jest Snapshot v1, https://goo.gl/fbAQLP exports[` with crypto enabled renders 1`] = `
@@ -19,7 +19,7 @@ exports[` with crypto enabled renders 1`] = `