Add timezone to user profile (#20)

* [create-pull-request] automated change (#12966)

Co-authored-by: github-merge-queue <github-merge-queue@users.noreply.github.com>

* Add timezone to right panel profile.

* Add setting to publish timezone

* Add string for timezone publish

* Automatically update timezone when setting changes.

* Refactor to using a hook

And automatically refresh the timezone every minute.

* Check for feature support for extended profiles.

* lint

* Add timezone

* Remove unintentional changes

* Use browser default timezone.

* lint

* tweaks

* Set timezone publish at the device level to prevent all devices writing to the timezone field.

* Update hook to use external client.

* Add test for user timezone.

* Update snapshot for preferences tab.

* Hide timezone info if not provided.

* Stablize test

* Fix date test types.

* prettier

* Add timezone tests

* Add test for invalid timezone.

* Update screenshot

* Remove check for profile.

---------

Co-authored-by: ElementRobot <releases@riot.im>
Co-authored-by: github-merge-queue <github-merge-queue@users.noreply.github.com>
This commit is contained in:
Will Hunt 2024-09-12 14:18:25 +01:00 committed by GitHub
parent f31776378d
commit eae9d9e248
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 426 additions and 156 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 198 KiB

View File

@ -124,6 +124,10 @@ Please see LICENSE files in the repository root for full details.
}
}
.mx_UserInfo_timezone {
margin: var(--cpd-space-1x) 0;
}
.mx_PresenceLabel {
font: var(--cpd-font-body-sm-regular);
opacity: 1;

View File

@ -131,6 +131,7 @@ class LoggedInView extends React.Component<IProps, IState> {
protected layoutWatcherRef?: string;
protected compactLayoutWatcherRef?: string;
protected backgroundImageWatcherRef?: string;
protected timezoneProfileUpdateRef?: string[];
protected resizer?: Resizer<ICollapseConfig, CollapseItem>;
public constructor(props: IProps) {
@ -182,6 +183,11 @@ class LoggedInView extends React.Component<IProps, IState> {
this.refreshBackgroundImage,
);
this.timezoneProfileUpdateRef = [
SettingsStore.watchSetting("userTimezonePublish", null, this.onTimezoneUpdate),
SettingsStore.watchSetting("userTimezone", null, this.onTimezoneUpdate),
];
this.resizer = this.createResizer();
this.resizer.attach();
@ -190,6 +196,31 @@ class LoggedInView extends React.Component<IProps, IState> {
this.refreshBackgroundImage();
}
private onTimezoneUpdate = async (): Promise<void> => {
if (!SettingsStore.getValue("userTimezonePublish")) {
// Ensure it's deleted
try {
await this._matrixClient.deleteExtendedProfileProperty("us.cloke.msc4175.tz");
} catch (ex) {
console.warn("Failed to delete timezone from user profile", ex);
}
return;
}
const currentTimezone =
SettingsStore.getValue("userTimezone") ||
// If the timezone is empty, then use the browser timezone.
// eslint-disable-next-line new-cap
Intl.DateTimeFormat().resolvedOptions().timeZone;
if (!currentTimezone || typeof currentTimezone !== "string") {
return;
}
try {
await this._matrixClient.setExtendedProfileProperty("us.cloke.msc4175.tz", currentTimezone);
} catch (ex) {
console.warn("Failed to update user profile with current timezone", ex);
}
};
public componentWillUnmount(): void {
document.removeEventListener("keydown", this.onNativeKeyDown, false);
LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallState, this.onCallState);
@ -200,6 +231,7 @@ class LoggedInView extends React.Component<IProps, IState> {
if (this.layoutWatcherRef) SettingsStore.unwatchSetting(this.layoutWatcherRef);
if (this.compactLayoutWatcherRef) SettingsStore.unwatchSetting(this.compactLayoutWatcherRef);
if (this.backgroundImageWatcherRef) SettingsStore.unwatchSetting(this.backgroundImageWatcherRef);
this.timezoneProfileUpdateRef?.forEach((s) => SettingsStore.unwatchSetting(s));
this.resizer?.detach();
}

View File

@ -26,7 +26,7 @@ import { KnownMembership } from "matrix-js-sdk/src/types";
import { UserVerificationStatus, VerificationRequest } from "matrix-js-sdk/src/crypto-api";
import { logger } from "matrix-js-sdk/src/logger";
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
import { Heading, MenuItem, Text } from "@vector-im/compound-web";
import { Heading, MenuItem, Text, Tooltip } from "@vector-im/compound-web";
import ChatIcon from "@vector-im/compound-design-tokens/assets/web/icons/chat";
import CheckIcon from "@vector-im/compound-design-tokens/assets/web/icons/check";
import ShareIcon from "@vector-im/compound-design-tokens/assets/web/icons/share";
@ -85,7 +85,7 @@ import { SdkContextClass } from "../../../contexts/SDKContext";
import { asyncSome } from "../../../utils/arrays";
import { Flex } from "../../utils/Flex";
import CopyableText from "../elements/CopyableText";
import { useUserTimezone } from "../../../hooks/useUserTimezone";
export interface IDevice extends Device {
ambiguous?: boolean;
}
@ -1694,6 +1694,8 @@ export const UserInfoHeader: React.FC<{
);
}
const timezoneInfo = useUserTimezone(cli, member.userId);
const e2eIcon = e2eStatus ? <E2EIcon size={18} status={e2eStatus} isUser={true} /> : null;
const userIdentifier = UserIdentifierCustomisations.getDisplayUserIdentifier?.(member.userId, {
roomId,
@ -1727,6 +1729,15 @@ export const UserInfoHeader: React.FC<{
</Flex>
</Heading>
{presenceLabel}
{timezoneInfo && (
<Tooltip label={timezoneInfo?.timezone ?? ""}>
<span className="mx_UserInfo_timezone">
<Text size="sm" weight="regular">
{timezoneInfo?.friendly ?? ""}
</Text>
</span>
</Tooltip>
)}
<Text size="sm" weight="semibold" className="mx_UserInfo_profile_mxid">
<CopyableText getTextToCopy={() => userIdentifier} border={false}>
{userIdentifier}

View File

@ -302,6 +302,7 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
</div>
{this.renderGroup(PreferencesUserSettingsTab.TIME_SETTINGS)}
<SettingsFlag name="userTimezonePublish" level={SettingLevel.DEVICE} />
</SettingsSubsection>
<SettingsSubsection

View File

@ -0,0 +1,106 @@
/*
Copyright 2024 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { useEffect, useState } from "react";
import { MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix";
/**
* Fetch a user's delclared timezone through their profile, and return
* a friendly string of the current time for that user. This will keep
* in sync with the current time, and will be refreshed once a minute.
*
* @param cli The Matrix Client instance.
* @param userId The userID to fetch the timezone for.
* @returns A timezone name and friendly string for the user's timezone, or
* null if the user has no timezone or the timezone was not recognised
* by the browser.
*/
export const useUserTimezone = (cli: MatrixClient, userId: string): { timezone: string; friendly: string } | null => {
const [timezone, setTimezone] = useState<string>();
const [updateInterval, setUpdateInterval] = useState<number>();
const [friendly, setFriendly] = useState<string>();
const [supported, setSupported] = useState<boolean>();
useEffect(() => {
if (!cli || supported !== undefined) {
return;
}
cli.doesServerSupportExtendedProfiles()
.then(setSupported)
.catch((ex) => {
console.warn("Unable to determine if extended profiles are supported", ex);
});
}, [supported, cli]);
useEffect(() => {
return () => {
if (updateInterval) {
clearInterval(updateInterval);
}
};
}, [updateInterval]);
useEffect(() => {
if (supported !== true) {
return;
}
(async () => {
console.log("Trying to fetch TZ");
try {
const tz = await cli.getExtendedProfileProperty(userId, "us.cloke.msc4175.tz");
if (typeof tz !== "string") {
// Err, definitely not a tz.
throw Error("Timezone value was not a string");
}
// This will validate the timezone for us.
// eslint-disable-next-line new-cap
Intl.DateTimeFormat(undefined, { timeZone: tz });
const updateTime = (): void => {
const currentTime = new Date();
const friendly = currentTime.toLocaleString(undefined, {
timeZone: tz,
hour12: true,
hour: "2-digit",
minute: "2-digit",
timeZoneName: "shortOffset",
});
setTimezone(tz);
setFriendly(friendly);
setUpdateInterval(setTimeout(updateTime, (60 - currentTime.getSeconds()) * 1000));
};
updateTime();
} catch (ex) {
setTimezone(undefined);
setFriendly(undefined);
setUpdateInterval(undefined);
if (ex instanceof MatrixError && ex.errcode === "M_NOT_FOUND") {
// No timezone set, ignore.
return;
}
console.error("Could not render current timezone for user", ex);
}
})();
}, [supported, userId, cli]);
if (!timezone || !friendly) {
return null;
}
return {
friendly,
timezone,
};
};

View File

@ -1426,6 +1426,7 @@
"element_call_video_rooms": "Element Call video rooms",
"experimental_description": "Feeling experimental? Try out our latest ideas in development. These features are not finalised; they may be unstable, may change, or may be dropped altogether. <a>Learn more</a>.",
"experimental_section": "Early previews",
"extended_profiles_msc_support": "Requires your server to support MSC4133",
"feature_disable_call_per_sender_encryption": "Disable per-sender encryption for Element Call",
"feature_wysiwyg_composer_description": "Use rich text instead of Markdown in the message composer.",
"group_calls": "New group call experience",
@ -2719,6 +2720,7 @@
"keyboard_view_shortcuts_button": "To view all keyboard shortcuts, <a>click here</a>.",
"media_heading": "Images, GIFs and videos",
"presence_description": "Share your activity and status with others.",
"publish_timezone": "Publish timezone on public profile",
"rm_lifetime": "Read Marker lifetime (ms)",
"rm_lifetime_offscreen": "Read Marker off-screen lifetime (ms)",
"room_directory_heading": "Room directory",

View File

@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details.
*/
import React, { ReactNode } from "react";
import { UNSTABLE_MSC4133_EXTENDED_PROFILES } from "matrix-js-sdk/src/matrix";
import { _t, _td, TranslationKey } from "../languageHandler";
import {
@ -646,6 +647,19 @@ export const SETTINGS: { [setting: string]: ISetting } = {
displayName: _td("settings|preferences|user_timezone"),
default: "",
},
"userTimezonePublish": {
// This is per-device so you can avoid having devices overwrite each other.
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
displayName: _td("settings|preferences|publish_timezone"),
default: false,
controller: new ServerSupportUnstableFeatureController(
"userTimezonePublish",
defaultWatchManager,
[[UNSTABLE_MSC4133_EXTENDED_PROFILES]],
undefined,
_td("labs|extended_profiles_msc_support"),
),
},
"autoplayGifs": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td("settings|autoplay_gifs"),

View File

@ -24,6 +24,7 @@ import SettingsStore from "../../../src/settings/SettingsStore";
import { SettingLevel } from "../../../src/settings/SettingLevel";
import { Action } from "../../../src/dispatcher/actions";
import Modal from "../../../src/Modal";
import { SETTINGS } from "../../../src/settings/Settings";
describe("<LoggedInView />", () => {
const userId = "@alice:domain.org";
@ -37,6 +38,9 @@ describe("<LoggedInView />", () => {
setPushRuleEnabled: jest.fn(),
setPushRuleActions: jest.fn(),
getCrypto: jest.fn().mockReturnValue(undefined),
setExtendedProfileProperty: jest.fn().mockResolvedValue(undefined),
deleteExtendedProfileProperty: jest.fn().mockResolvedValue(undefined),
doesServerSupportExtendedProfiles: jest.fn().mockResolvedValue(true),
});
const mediaHandler = new MediaHandler(mockClient);
const mockSdkContext = new TestSdkContext();
@ -409,4 +413,48 @@ describe("<LoggedInView />", () => {
await userEvent.keyboard("{Control>}{Alt>}h</Alt>{/Control}");
expect(defaultDispatcher.dispatch).not.toHaveBeenCalledWith({ action: Action.ViewHomePage });
});
describe("timezone updates", () => {
const userTimezone = "Europe/London";
const originalController = SETTINGS["userTimezonePublish"].controller;
beforeEach(async () => {
SETTINGS["userTimezonePublish"].controller = undefined;
await SettingsStore.setValue("userTimezonePublish", null, SettingLevel.DEVICE, false);
await SettingsStore.setValue("userTimezone", null, SettingLevel.DEVICE, userTimezone);
});
afterEach(() => {
SETTINGS["userTimezonePublish"].controller = originalController;
});
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.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);
});
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);
await SettingsStore.setValue("userTimezone", null, SettingLevel.DEVICE, newTimezone);
expect(mockClient.setExtendedProfileProperty).toHaveBeenCalledWith("us.cloke.msc4175.tz", 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);
await SettingsStore.setValue("userTimezonePublish", null, SettingLevel.DEVICE, false);
expect(mockClient.deleteExtendedProfileProperty).toHaveBeenCalledWith("us.cloke.msc4175.tz");
});
});
});

View File

@ -92,6 +92,7 @@ let mockRoom: Mocked<Room>;
let mockSpace: Mocked<Room>;
let mockClient: Mocked<MatrixClient>;
let mockCrypto: Mocked<CryptoApi>;
const origDate = global.Date.prototype.toLocaleString;
beforeEach(() => {
mockRoom = mocked({
@ -150,6 +151,8 @@ beforeEach(() => {
isSynapseAdministrator: jest.fn().mockResolvedValue(false),
isRoomEncrypted: jest.fn().mockReturnValue(false),
doesServerSupportUnstableFeature: jest.fn().mockReturnValue(false),
doesServerSupportExtendedProfiles: jest.fn().mockResolvedValue(false),
getExtendedProfileProperty: jest.fn().mockRejectedValue(new Error("Not supported")),
mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"),
removeListener: jest.fn(),
currentState: {
@ -229,6 +232,28 @@ describe("<UserInfo />", () => {
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);
});
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("renders encryption info panel without pending verification", () => {
renderComponent({ phase: RightPanelPhases.EncryptionPanel });
expect(screen.getByRole("heading", { name: /encryption/i })).toBeInTheDocument();

View File

@ -307,6 +307,33 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
/>
</div>
</div>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_GQvdMWe954DV"
>
<span
class="mx_SettingsFlag_labelText"
>
Publish timezone on public profile
</span>
</label>
<div
aria-checked="false"
aria-disabled="true"
aria-label="Publish timezone on public profile"
class="mx_AccessibleButton mx_ToggleSwitch"
id="mx_SettingsFlag_GQvdMWe954DV"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
</div>
</div>
<div
@ -338,7 +365,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_GQvdMWe954DV"
for="mx_SettingsFlag_IAu5CsiHRD7n"
>
<span
class="mx_SettingsFlag_labelText"
@ -351,7 +378,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
aria-disabled="true"
aria-label="Send read receipts"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_GQvdMWe954DV"
id="mx_SettingsFlag_IAu5CsiHRD7n"
role="switch"
tabindex="0"
>
@ -365,7 +392,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_IAu5CsiHRD7n"
for="mx_SettingsFlag_yrA2ohjWVJIP"
>
<span
class="mx_SettingsFlag_labelText"
@ -378,7 +405,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
aria-disabled="true"
aria-label="Send typing notifications"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_IAu5CsiHRD7n"
id="mx_SettingsFlag_yrA2ohjWVJIP"
role="switch"
tabindex="0"
>
@ -409,7 +436,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_yrA2ohjWVJIP"
for="mx_SettingsFlag_auy1OmnTidX4"
>
<span
class="mx_SettingsFlag_labelText"
@ -422,7 +449,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
aria-disabled="true"
aria-label="Automatically replace plain text Emoji"
class="mx_AccessibleButton mx_ToggleSwitch"
id="mx_SettingsFlag_yrA2ohjWVJIP"
id="mx_SettingsFlag_auy1OmnTidX4"
role="switch"
tabindex="0"
>
@ -436,7 +463,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_auy1OmnTidX4"
for="mx_SettingsFlag_ePDS0OpWwAHG"
>
<span
class="mx_SettingsFlag_labelText"
@ -460,33 +487,6 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
aria-disabled="true"
aria-label="Enable Markdown"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_auy1OmnTidX4"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_ePDS0OpWwAHG"
>
<span
class="mx_SettingsFlag_labelText"
>
Enable Emoji suggestions while typing
</span>
</label>
<div
aria-checked="true"
aria-disabled="true"
aria-label="Enable Emoji suggestions while typing"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_ePDS0OpWwAHG"
role="switch"
tabindex="0"
@ -506,14 +506,14 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<span
class="mx_SettingsFlag_labelText"
>
Use Ctrl + Enter to send a message
Enable Emoji suggestions while typing
</span>
</label>
<div
aria-checked="false"
aria-checked="true"
aria-disabled="true"
aria-label="Use Ctrl + Enter to send a message"
class="mx_AccessibleButton mx_ToggleSwitch"
aria-label="Enable Emoji suggestions while typing"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_75JNTNkNU64r"
role="switch"
tabindex="0"
@ -533,13 +533,13 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<span
class="mx_SettingsFlag_labelText"
>
Surround selected text when typing special characters
Use Ctrl + Enter to send a message
</span>
</label>
<div
aria-checked="false"
aria-disabled="true"
aria-label="Surround selected text when typing special characters"
aria-label="Use Ctrl + Enter to send a message"
class="mx_AccessibleButton mx_ToggleSwitch"
id="mx_SettingsFlag_aTLcRsQRlYy7"
role="switch"
@ -560,14 +560,14 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<span
class="mx_SettingsFlag_labelText"
>
Show stickers button
Surround selected text when typing special characters
</span>
</label>
<div
aria-checked="true"
aria-checked="false"
aria-disabled="true"
aria-label="Show stickers button"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
aria-label="Surround selected text when typing special characters"
class="mx_AccessibleButton mx_ToggleSwitch"
id="mx_SettingsFlag_5nfv5bOEPN1s"
role="switch"
tabindex="0"
@ -583,6 +583,33 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_u1JYVtOyR5kb"
>
<span
class="mx_SettingsFlag_labelText"
>
Show stickers button
</span>
</label>
<div
aria-checked="true"
aria-disabled="true"
aria-label="Show stickers button"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_u1JYVtOyR5kb"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_u3pEwuLn9Enn"
>
<span
class="mx_SettingsFlag_labelText"
@ -595,7 +622,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
aria-disabled="true"
aria-label="Insert a trailing colon after user mentions at the start of a message"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_u1JYVtOyR5kb"
id="mx_SettingsFlag_u3pEwuLn9Enn"
role="switch"
tabindex="0"
>
@ -626,7 +653,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_u3pEwuLn9Enn"
for="mx_SettingsFlag_YuxfFEpOsztW"
>
<span
class="mx_SettingsFlag_labelText"
@ -639,33 +666,6 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
aria-disabled="true"
aria-label="Enable automatic language detection for syntax highlighting"
class="mx_AccessibleButton mx_ToggleSwitch"
id="mx_SettingsFlag_u3pEwuLn9Enn"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_YuxfFEpOsztW"
>
<span
class="mx_SettingsFlag_labelText"
>
Expand code blocks by default
</span>
</label>
<div
aria-checked="false"
aria-disabled="true"
aria-label="Expand code blocks by default"
class="mx_AccessibleButton mx_ToggleSwitch"
id="mx_SettingsFlag_YuxfFEpOsztW"
role="switch"
tabindex="0"
@ -681,6 +681,33 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_hQkBerF1ejc4"
>
<span
class="mx_SettingsFlag_labelText"
>
Expand code blocks by default
</span>
</label>
<div
aria-checked="false"
aria-disabled="true"
aria-label="Expand code blocks by default"
class="mx_AccessibleButton mx_ToggleSwitch"
id="mx_SettingsFlag_hQkBerF1ejc4"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_GFes1UFzOK2n"
>
<span
class="mx_SettingsFlag_labelText"
@ -693,7 +720,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
aria-disabled="true"
aria-label="Show line numbers in code blocks"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_hQkBerF1ejc4"
id="mx_SettingsFlag_GFes1UFzOK2n"
role="switch"
tabindex="0"
>
@ -724,7 +751,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_GFes1UFzOK2n"
for="mx_SettingsFlag_vfGFMldL2r2v"
>
<span
class="mx_SettingsFlag_labelText"
@ -737,33 +764,6 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
aria-disabled="true"
aria-label="Enable inline URL previews by default"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_GFes1UFzOK2n"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_vfGFMldL2r2v"
>
<span
class="mx_SettingsFlag_labelText"
>
Autoplay GIFs
</span>
</label>
<div
aria-checked="false"
aria-disabled="true"
aria-label="Autoplay GIFs"
class="mx_AccessibleButton mx_ToggleSwitch"
id="mx_SettingsFlag_vfGFMldL2r2v"
role="switch"
tabindex="0"
@ -783,13 +783,13 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<span
class="mx_SettingsFlag_labelText"
>
Autoplay videos
Autoplay GIFs
</span>
</label>
<div
aria-checked="false"
aria-disabled="true"
aria-label="Autoplay videos"
aria-label="Autoplay GIFs"
class="mx_AccessibleButton mx_ToggleSwitch"
id="mx_SettingsFlag_bsSwicmKUiOB"
role="switch"
@ -806,6 +806,33 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_dvqsxEaZtl3A"
>
<span
class="mx_SettingsFlag_labelText"
>
Autoplay videos
</span>
</label>
<div
aria-checked="false"
aria-disabled="true"
aria-label="Autoplay videos"
class="mx_AccessibleButton mx_ToggleSwitch"
id="mx_SettingsFlag_dvqsxEaZtl3A"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_NIiWzqsApP1c"
>
<span
class="mx_SettingsFlag_labelText"
@ -818,7 +845,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
aria-disabled="true"
aria-label="Show previews/thumbnails for images"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_dvqsxEaZtl3A"
id="mx_SettingsFlag_NIiWzqsApP1c"
role="switch"
tabindex="0"
>
@ -849,7 +876,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_NIiWzqsApP1c"
for="mx_SettingsFlag_q1SIAPqLMVXh"
>
<span
class="mx_SettingsFlag_labelText"
@ -862,33 +889,6 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
aria-disabled="true"
aria-label="Show typing notifications"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_NIiWzqsApP1c"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_q1SIAPqLMVXh"
>
<span
class="mx_SettingsFlag_labelText"
>
Show a placeholder for removed messages
</span>
</label>
<div
aria-checked="true"
aria-disabled="true"
aria-label="Show a placeholder for removed messages"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_q1SIAPqLMVXh"
role="switch"
tabindex="0"
@ -908,13 +908,13 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<span
class="mx_SettingsFlag_labelText"
>
Show read receipts sent by other users
Show a placeholder for removed messages
</span>
</label>
<div
aria-checked="true"
aria-disabled="true"
aria-label="Show read receipts sent by other users"
aria-label="Show a placeholder for removed messages"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_dXFDGgBsKXay"
role="switch"
@ -935,13 +935,13 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<span
class="mx_SettingsFlag_labelText"
>
Show join/leave messages (invites/removes/bans unaffected)
Show read receipts sent by other users
</span>
</label>
<div
aria-checked="true"
aria-disabled="true"
aria-label="Show join/leave messages (invites/removes/bans unaffected)"
aria-label="Show read receipts sent by other users"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_7Az0xw4Bs4Tt"
role="switch"
@ -962,13 +962,13 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<span
class="mx_SettingsFlag_labelText"
>
Show display name changes
Show join/leave messages (invites/removes/bans unaffected)
</span>
</label>
<div
aria-checked="true"
aria-disabled="true"
aria-label="Show display name changes"
aria-label="Show join/leave messages (invites/removes/bans unaffected)"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_8jmzPIlPoBCv"
role="switch"
@ -989,13 +989,13 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<span
class="mx_SettingsFlag_labelText"
>
Show chat effects (animations when receiving e.g. confetti)
Show display name changes
</span>
</label>
<div
aria-checked="true"
aria-disabled="true"
aria-label="Show chat effects (animations when receiving e.g. confetti)"
aria-label="Show display name changes"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_enFRaTjdsFou"
role="switch"
@ -1016,13 +1016,13 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<span
class="mx_SettingsFlag_labelText"
>
Show profile picture changes
Show chat effects (animations when receiving e.g. confetti)
</span>
</label>
<div
aria-checked="true"
aria-disabled="true"
aria-label="Show profile picture changes"
aria-label="Show chat effects (animations when receiving e.g. confetti)"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_bfwnd5rz4XNX"
role="switch"
@ -1043,13 +1043,13 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<span
class="mx_SettingsFlag_labelText"
>
Show avatars in user, room and event mentions
Show profile picture changes
</span>
</label>
<div
aria-checked="true"
aria-disabled="true"
aria-label="Show avatars in user, room and event mentions"
aria-label="Show profile picture changes"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_gs5uWEzYzZrS"
role="switch"
@ -1070,13 +1070,13 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<span
class="mx_SettingsFlag_labelText"
>
Enable big emoji in chat
Show avatars in user, room and event mentions
</span>
</label>
<div
aria-checked="true"
aria-disabled="true"
aria-label="Enable big emoji in chat"
aria-label="Show avatars in user, room and event mentions"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_qWg7OgID1yRR"
role="switch"
@ -1097,13 +1097,13 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<span
class="mx_SettingsFlag_labelText"
>
Jump to the bottom of the timeline when you send a message
Enable big emoji in chat
</span>
</label>
<div
aria-checked="true"
aria-disabled="true"
aria-label="Jump to the bottom of the timeline when you send a message"
aria-label="Enable big emoji in chat"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_pOPewl7rtMbV"
role="switch"
@ -1120,6 +1120,33 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_cmt3PZSyNp3v"
>
<span
class="mx_SettingsFlag_labelText"
>
Jump to the bottom of the timeline when you send a message
</span>
</label>
<div
aria-checked="true"
aria-disabled="true"
aria-label="Jump to the bottom of the timeline when you send a message"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_cmt3PZSyNp3v"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_dJJz3lHUv9XX"
>
<span
class="mx_SettingsFlag_labelText"
@ -1132,7 +1159,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
aria-disabled="true"
aria-label="Show current profile picture and name for users in message history"
class="mx_AccessibleButton mx_ToggleSwitch"
id="mx_SettingsFlag_cmt3PZSyNp3v"
id="mx_SettingsFlag_dJJz3lHUv9XX"
role="switch"
tabindex="0"
>
@ -1163,7 +1190,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_dJJz3lHUv9XX"
for="mx_SettingsFlag_SBSSOZDRlzlA"
>
<span
class="mx_SettingsFlag_labelText"
@ -1176,7 +1203,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
aria-disabled="true"
aria-label="Show NSFW content"
class="mx_AccessibleButton mx_ToggleSwitch"
id="mx_SettingsFlag_dJJz3lHUv9XX"
id="mx_SettingsFlag_SBSSOZDRlzlA"
role="switch"
tabindex="0"
>
@ -1207,7 +1234,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_SBSSOZDRlzlA"
for="mx_SettingsFlag_FLEpLCb0jpp6"
>
<span
class="mx_SettingsFlag_labelText"
@ -1220,7 +1247,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
aria-disabled="true"
aria-label="Prompt before sending invites to potentially invalid matrix IDs"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
id="mx_SettingsFlag_SBSSOZDRlzlA"
id="mx_SettingsFlag_FLEpLCb0jpp6"
role="switch"
tabindex="0"
>