mirror of
https://github.com/element-hq/element-web
synced 2024-11-21 16:55:34 +03:00
Allow user to set timezone (#12775)
* Allow user to set timezone * Update test snapshots --------- Co-authored-by: Florian Duros <florianduros@element.io>
This commit is contained in:
parent
acc7342758
commit
ae15bbe6e0
15 changed files with 256 additions and 9 deletions
|
@ -17,6 +17,11 @@ limitations under the License.
|
||||||
|
|
||||||
import { test, expect } from "../../element-web-test";
|
import { test, expect } from "../../element-web-test";
|
||||||
|
|
||||||
|
test.use({
|
||||||
|
locale: "en-GB",
|
||||||
|
timezoneId: "Europe/London",
|
||||||
|
});
|
||||||
|
|
||||||
test.describe("Preferences user settings tab", () => {
|
test.describe("Preferences user settings tab", () => {
|
||||||
test.use({
|
test.use({
|
||||||
displayName: "Bob",
|
displayName: "Bob",
|
||||||
|
@ -26,9 +31,9 @@ test.describe("Preferences user settings tab", () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should be rendered properly", async ({ app, user }) => {
|
test("should be rendered properly", async ({ app, page, user }) => {
|
||||||
|
page.setViewportSize({ width: 1024, height: 3300 });
|
||||||
const tab = await app.settings.openUserSettings("Preferences");
|
const tab = await app.settings.openUserSettings("Preferences");
|
||||||
|
|
||||||
// Assert that the top heading is rendered
|
// Assert that the top heading is rendered
|
||||||
await expect(tab.getByRole("heading", { name: "Preferences" })).toBeVisible();
|
await expect(tab.getByRole("heading", { name: "Preferences" })).toBeVisible();
|
||||||
await expect(tab).toMatchScreenshot("Preferences-user-settings-tab-should-be-rendered-properly-1.png");
|
await expect(tab).toMatchScreenshot("Preferences-user-settings-tab-should-be-rendered-properly-1.png");
|
||||||
|
@ -53,4 +58,19 @@ test.describe("Preferences user settings tab", () => {
|
||||||
// Assert that the default value is rendered again
|
// Assert that the default value is rendered again
|
||||||
await expect(languageInput.getByText("English")).toBeVisible();
|
await expect(languageInput.getByText("English")).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should be able to change the timezone", async ({ uut, user }) => {
|
||||||
|
// Check language and region setting dropdown
|
||||||
|
const timezoneInput = uut.locator(".mx_dropdownUserTimezone");
|
||||||
|
const timezoneValue = uut.locator("#mx_dropdownUserTimezone_value");
|
||||||
|
await timezoneInput.scrollIntoViewIfNeeded();
|
||||||
|
// Check the default value
|
||||||
|
await expect(timezoneValue.getByText("Browser default")).toBeVisible();
|
||||||
|
// Click the button to display the dropdown menu
|
||||||
|
await timezoneInput.getByRole("button", { name: "Set timezone" }).click();
|
||||||
|
// Select a different value
|
||||||
|
timezoneInput.getByRole("option", { name: /Africa\/Abidjan/ }).click();
|
||||||
|
// Check the new value
|
||||||
|
await expect(timezoneValue.getByText("Africa/Abidjan")).toBeVisible();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 195 KiB |
|
@ -64,4 +64,8 @@ limitations under the License.
|
||||||
gap: var(--cpd-space-6x);
|
gap: var(--cpd-space-6x);
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_SettingsSubsection_dropdown {
|
||||||
|
min-width: 360px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ limitations under the License.
|
||||||
import { Optional } from "matrix-events-sdk";
|
import { Optional } from "matrix-events-sdk";
|
||||||
|
|
||||||
import { _t, getUserLanguage } from "./languageHandler";
|
import { _t, getUserLanguage } from "./languageHandler";
|
||||||
|
import { getUserTimezone } from "./TimezoneHandler";
|
||||||
|
|
||||||
export const MINUTE_MS = 60000;
|
export const MINUTE_MS = 60000;
|
||||||
export const HOUR_MS = MINUTE_MS * 60;
|
export const HOUR_MS = MINUTE_MS * 60;
|
||||||
|
@ -77,6 +78,7 @@ export function formatDate(date: Date, showTwelveHour = false, locale?: string):
|
||||||
weekday: "short",
|
weekday: "short",
|
||||||
hour: "numeric",
|
hour: "numeric",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
|
timeZone: getUserTimezone(),
|
||||||
}).format(date);
|
}).format(date);
|
||||||
} else if (now.getFullYear() === date.getFullYear()) {
|
} else if (now.getFullYear() === date.getFullYear()) {
|
||||||
return new Intl.DateTimeFormat(_locale, {
|
return new Intl.DateTimeFormat(_locale, {
|
||||||
|
@ -86,6 +88,7 @@ export function formatDate(date: Date, showTwelveHour = false, locale?: string):
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
hour: "numeric",
|
hour: "numeric",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
|
timeZone: getUserTimezone(),
|
||||||
}).format(date);
|
}).format(date);
|
||||||
}
|
}
|
||||||
return formatFullDate(date, showTwelveHour, false, _locale);
|
return formatFullDate(date, showTwelveHour, false, _locale);
|
||||||
|
@ -104,6 +107,7 @@ export function formatFullDateNoTime(date: Date, locale?: string): string {
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
|
timeZone: getUserTimezone(),
|
||||||
}).format(date);
|
}).format(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,6 +131,7 @@ export function formatFullDate(date: Date, showTwelveHour = false, showSeconds =
|
||||||
hour: "numeric",
|
hour: "numeric",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
second: showSeconds ? "2-digit" : undefined,
|
second: showSeconds ? "2-digit" : undefined,
|
||||||
|
timeZone: getUserTimezone(),
|
||||||
}).format(date);
|
}).format(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,6 +165,7 @@ export function formatFullTime(date: Date, showTwelveHour = false, locale?: stri
|
||||||
hour: "numeric",
|
hour: "numeric",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
second: "2-digit",
|
second: "2-digit",
|
||||||
|
timeZone: getUserTimezone(),
|
||||||
}).format(date);
|
}).format(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,6 +184,7 @@ export function formatTime(date: Date, showTwelveHour = false, locale?: string):
|
||||||
...getTwelveHourOptions(showTwelveHour),
|
...getTwelveHourOptions(showTwelveHour),
|
||||||
hour: "numeric",
|
hour: "numeric",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
|
timeZone: getUserTimezone(),
|
||||||
}).format(date);
|
}).format(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -285,6 +292,7 @@ export function formatFullDateNoDayNoTime(date: Date, locale?: string): string {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "numeric",
|
month: "numeric",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
|
timeZone: getUserTimezone(),
|
||||||
}).format(date);
|
}).format(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -354,6 +362,9 @@ export function formatPreciseDuration(durationMs: number): string {
|
||||||
* @returns {string} formattedDate
|
* @returns {string} formattedDate
|
||||||
*/
|
*/
|
||||||
export const formatLocalDateShort = (timestamp: number, locale?: string): string =>
|
export const formatLocalDateShort = (timestamp: number, locale?: string): string =>
|
||||||
new Intl.DateTimeFormat(locale ?? getUserLanguage(), { day: "2-digit", month: "2-digit", year: "2-digit" }).format(
|
new Intl.DateTimeFormat(locale ?? getUserLanguage(), {
|
||||||
timestamp,
|
day: "2-digit",
|
||||||
);
|
month: "2-digit",
|
||||||
|
year: "2-digit",
|
||||||
|
timeZone: getUserTimezone(),
|
||||||
|
}).format(timestamp);
|
||||||
|
|
55
src/TimezoneHandler.ts
Normal file
55
src/TimezoneHandler.ts
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 { SettingLevel } from "./settings/SettingLevel";
|
||||||
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
|
|
||||||
|
export const USER_TIMEZONE_KEY = "userTimezone";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returning `undefined` ensure that if unset the browser default will be used in `DateTimeFormat`.
|
||||||
|
* @returns The user specified timezone or `undefined`
|
||||||
|
*/
|
||||||
|
export function getUserTimezone(): string | undefined {
|
||||||
|
const tz = SettingsStore.getValueAt(SettingLevel.DEVICE, USER_TIMEZONE_KEY);
|
||||||
|
return tz || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set in the settings the given timezone
|
||||||
|
* @timezone
|
||||||
|
*/
|
||||||
|
export function setUserTimezone(timezone: string): Promise<void> {
|
||||||
|
return SettingsStore.setValue(USER_TIMEZONE_KEY, null, SettingLevel.DEVICE, timezone);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all the available timezones
|
||||||
|
*/
|
||||||
|
export function getAllTimezones(): string[] {
|
||||||
|
return Intl.supportedValuesOf("timeZone");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the current timezone in a short human readable way
|
||||||
|
*/
|
||||||
|
export function shortBrowserTimezone(): string {
|
||||||
|
return (
|
||||||
|
new Intl.DateTimeFormat(undefined, { timeZoneName: "short" })
|
||||||
|
.formatToParts(new Date())
|
||||||
|
.find((x) => x.type === "timeZoneName")?.value ?? "GMT"
|
||||||
|
);
|
||||||
|
}
|
|
@ -47,6 +47,7 @@ import { ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/Ro
|
||||||
|
|
||||||
import shouldHideEvent from "../../shouldHideEvent";
|
import shouldHideEvent from "../../shouldHideEvent";
|
||||||
import { _t } from "../../languageHandler";
|
import { _t } from "../../languageHandler";
|
||||||
|
import * as TimezoneHandler from "../../TimezoneHandler";
|
||||||
import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
|
import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
|
||||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||||
import ContentMessages from "../../ContentMessages";
|
import ContentMessages from "../../ContentMessages";
|
||||||
|
@ -228,6 +229,7 @@ export interface IRoomState {
|
||||||
lowBandwidth: boolean;
|
lowBandwidth: boolean;
|
||||||
alwaysShowTimestamps: boolean;
|
alwaysShowTimestamps: boolean;
|
||||||
showTwelveHourTimestamps: boolean;
|
showTwelveHourTimestamps: boolean;
|
||||||
|
userTimezone: string | undefined;
|
||||||
readMarkerInViewThresholdMs: number;
|
readMarkerInViewThresholdMs: number;
|
||||||
readMarkerOutOfViewThresholdMs: number;
|
readMarkerOutOfViewThresholdMs: number;
|
||||||
showHiddenEvents: boolean;
|
showHiddenEvents: boolean;
|
||||||
|
@ -455,6 +457,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
lowBandwidth: SettingsStore.getValue("lowBandwidth"),
|
lowBandwidth: SettingsStore.getValue("lowBandwidth"),
|
||||||
alwaysShowTimestamps: SettingsStore.getValue("alwaysShowTimestamps"),
|
alwaysShowTimestamps: SettingsStore.getValue("alwaysShowTimestamps"),
|
||||||
showTwelveHourTimestamps: SettingsStore.getValue("showTwelveHourTimestamps"),
|
showTwelveHourTimestamps: SettingsStore.getValue("showTwelveHourTimestamps"),
|
||||||
|
userTimezone: TimezoneHandler.getUserTimezone(),
|
||||||
readMarkerInViewThresholdMs: SettingsStore.getValue("readMarkerInViewThresholdMs"),
|
readMarkerInViewThresholdMs: SettingsStore.getValue("readMarkerInViewThresholdMs"),
|
||||||
readMarkerOutOfViewThresholdMs: SettingsStore.getValue("readMarkerOutOfViewThresholdMs"),
|
readMarkerOutOfViewThresholdMs: SettingsStore.getValue("readMarkerOutOfViewThresholdMs"),
|
||||||
showHiddenEvents: SettingsStore.getValue("showHiddenEventsInTimeline"),
|
showHiddenEvents: SettingsStore.getValue("showHiddenEventsInTimeline"),
|
||||||
|
@ -512,6 +515,9 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
SettingsStore.watchSetting("showTwelveHourTimestamps", null, (...[, , , value]) =>
|
SettingsStore.watchSetting("showTwelveHourTimestamps", null, (...[, , , value]) =>
|
||||||
this.setState({ showTwelveHourTimestamps: value as boolean }),
|
this.setState({ showTwelveHourTimestamps: value as boolean }),
|
||||||
),
|
),
|
||||||
|
SettingsStore.watchSetting(TimezoneHandler.USER_TIMEZONE_KEY, null, (...[, , , value]) =>
|
||||||
|
this.setState({ userTimezone: value as string }),
|
||||||
|
),
|
||||||
SettingsStore.watchSetting("readMarkerInViewThresholdMs", null, (...[, , , value]) =>
|
SettingsStore.watchSetting("readMarkerInViewThresholdMs", null, (...[, , , value]) =>
|
||||||
this.setState({ readMarkerInViewThresholdMs: value as number }),
|
this.setState({ readMarkerInViewThresholdMs: value as number }),
|
||||||
),
|
),
|
||||||
|
|
|
@ -15,12 +15,14 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback, useEffect, useState } from "react";
|
import React, { ReactElement, useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { NonEmptyArray } from "../../../../../@types/common";
|
||||||
import { _t, getCurrentLanguage } from "../../../../../languageHandler";
|
import { _t, getCurrentLanguage } from "../../../../../languageHandler";
|
||||||
import { UseCase } from "../../../../../settings/enums/UseCase";
|
import { UseCase } from "../../../../../settings/enums/UseCase";
|
||||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||||
import Field from "../../../elements/Field";
|
import Field from "../../../elements/Field";
|
||||||
|
import Dropdown from "../../../elements/Dropdown";
|
||||||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||||
import SettingsFlag from "../../../elements/SettingsFlag";
|
import SettingsFlag from "../../../elements/SettingsFlag";
|
||||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||||
|
@ -38,12 +40,16 @@ import PlatformPeg from "../../../../../PlatformPeg";
|
||||||
import { IS_MAC } from "../../../../../Keyboard";
|
import { IS_MAC } from "../../../../../Keyboard";
|
||||||
import SpellCheckSettings from "../../SpellCheckSettings";
|
import SpellCheckSettings from "../../SpellCheckSettings";
|
||||||
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
||||||
|
import * as TimezoneHandler from "../../../../../TimezoneHandler";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
closeSettingsFn(success: boolean): void;
|
closeSettingsFn(success: boolean): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
timezone: string | undefined;
|
||||||
|
timezones: string[];
|
||||||
|
timezoneSearch: string | undefined;
|
||||||
autocompleteDelay: string;
|
autocompleteDelay: string;
|
||||||
readMarkerInViewThresholdMs: string;
|
readMarkerInViewThresholdMs: string;
|
||||||
readMarkerOutOfViewThresholdMs: string;
|
readMarkerOutOfViewThresholdMs: string;
|
||||||
|
@ -68,7 +74,7 @@ const LanguageSection: React.FC = () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_SettingsSubsection_contentStretch">
|
<div className="mx_SettingsSubsection_dropdown">
|
||||||
{_t("settings|general|application_language")}
|
{_t("settings|general|application_language")}
|
||||||
<LanguageDropdown onOptionChange={onLanguageChange} value={language} />
|
<LanguageDropdown onOptionChange={onLanguageChange} value={language} />
|
||||||
<div className="mx_PreferencesUserSettingsTab_section_hint">
|
<div className="mx_PreferencesUserSettingsTab_section_hint">
|
||||||
|
@ -173,6 +179,9 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
timezone: TimezoneHandler.getUserTimezone(),
|
||||||
|
timezones: TimezoneHandler.getAllTimezones(),
|
||||||
|
timezoneSearch: undefined,
|
||||||
autocompleteDelay: SettingsStore.getValueAt(SettingLevel.DEVICE, "autocompleteDelay").toString(10),
|
autocompleteDelay: SettingsStore.getValueAt(SettingLevel.DEVICE, "autocompleteDelay").toString(10),
|
||||||
readMarkerInViewThresholdMs: SettingsStore.getValueAt(
|
readMarkerInViewThresholdMs: SettingsStore.getValueAt(
|
||||||
SettingLevel.DEVICE,
|
SettingLevel.DEVICE,
|
||||||
|
@ -185,6 +194,25 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onTimezoneChange = (tz: string): void => {
|
||||||
|
this.setState({ timezone: tz });
|
||||||
|
TimezoneHandler.setUserTimezone(tz);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If present filter the time zones matching the search term
|
||||||
|
*/
|
||||||
|
private onTimezoneSearchChange = (search: string): void => {
|
||||||
|
const timezoneSearch = search.toLowerCase();
|
||||||
|
const timezones = timezoneSearch
|
||||||
|
? TimezoneHandler.getAllTimezones().filter((tz) => {
|
||||||
|
return tz.toLowerCase().includes(timezoneSearch);
|
||||||
|
})
|
||||||
|
: TimezoneHandler.getAllTimezones();
|
||||||
|
|
||||||
|
this.setState({ timezones, timezoneSearch });
|
||||||
|
};
|
||||||
|
|
||||||
private onAutocompleteDelayChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
private onAutocompleteDelayChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
this.setState({ autocompleteDelay: e.target.value });
|
this.setState({ autocompleteDelay: e.target.value });
|
||||||
SettingsStore.setValue("autocompleteDelay", null, SettingLevel.DEVICE, e.target.value);
|
SettingsStore.setValue("autocompleteDelay", null, SettingLevel.DEVICE, e.target.value);
|
||||||
|
@ -217,6 +245,16 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
||||||
// Only show the user onboarding setting if the user should see the user onboarding page
|
// Only show the user onboarding setting if the user should see the user onboarding page
|
||||||
.filter((it) => it !== "FTUE.userOnboardingButton" || showUserOnboardingPage(useCase));
|
.filter((it) => it !== "FTUE.userOnboardingButton" || showUserOnboardingPage(useCase));
|
||||||
|
|
||||||
|
const browserTimezoneLabel: string = _t("settings|preferences|default_timezone", {
|
||||||
|
timezone: TimezoneHandler.shortBrowserTimezone(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Always Preprend the default option
|
||||||
|
const timezones = this.state.timezones.map((tz) => {
|
||||||
|
return <div key={tz}>{tz}</div>;
|
||||||
|
});
|
||||||
|
timezones.unshift(<div key="">{browserTimezoneLabel}</div>);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsTab data-testid="mx_PreferencesUserSettingsTab">
|
<SettingsTab data-testid="mx_PreferencesUserSettingsTab">
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
|
@ -254,6 +292,23 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
||||||
</SettingsSubsection>
|
</SettingsSubsection>
|
||||||
|
|
||||||
<SettingsSubsection heading={_t("settings|preferences|time_heading")}>
|
<SettingsSubsection heading={_t("settings|preferences|time_heading")}>
|
||||||
|
<div className="mx_SettingsSubsection_dropdown">
|
||||||
|
{_t("settings|preferences|user_timezone")}
|
||||||
|
<Dropdown
|
||||||
|
id="mx_dropdownUserTimezone"
|
||||||
|
className="mx_dropdownUserTimezone"
|
||||||
|
data-testid="mx_dropdownUserTimezone"
|
||||||
|
searchEnabled={true}
|
||||||
|
value={this.state.timezone}
|
||||||
|
label={_t("settings|preferences|user_timezone")}
|
||||||
|
placeholder={browserTimezoneLabel}
|
||||||
|
onOptionChange={this.onTimezoneChange}
|
||||||
|
onSearchChange={this.onTimezoneSearchChange}
|
||||||
|
>
|
||||||
|
{timezones as NonEmptyArray<ReactElement & { key: string }>}
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
{this.renderGroup(PreferencesUserSettingsTab.TIME_SETTINGS)}
|
{this.renderGroup(PreferencesUserSettingsTab.TIME_SETTINGS)}
|
||||||
</SettingsSubsection>
|
</SettingsSubsection>
|
||||||
|
|
||||||
|
|
|
@ -55,6 +55,7 @@ const RoomContext = createContext<
|
||||||
lowBandwidth: false,
|
lowBandwidth: false,
|
||||||
alwaysShowTimestamps: false,
|
alwaysShowTimestamps: false,
|
||||||
showTwelveHourTimestamps: false,
|
showTwelveHourTimestamps: false,
|
||||||
|
userTimezone: undefined,
|
||||||
readMarkerInViewThresholdMs: 3000,
|
readMarkerInViewThresholdMs: 3000,
|
||||||
readMarkerOutOfViewThresholdMs: 30000,
|
readMarkerOutOfViewThresholdMs: 30000,
|
||||||
showHiddenEvents: false,
|
showHiddenEvents: false,
|
||||||
|
|
|
@ -2703,6 +2703,7 @@
|
||||||
"code_blocks_heading": "Code blocks",
|
"code_blocks_heading": "Code blocks",
|
||||||
"compact_modern": "Use a more compact 'Modern' layout",
|
"compact_modern": "Use a more compact 'Modern' layout",
|
||||||
"composer_heading": "Composer",
|
"composer_heading": "Composer",
|
||||||
|
"default_timezone": "Browser default (%(timezone)s)",
|
||||||
"dialog_title": "<strong>Settings:</strong> Preferences",
|
"dialog_title": "<strong>Settings:</strong> Preferences",
|
||||||
"enable_hardware_acceleration": "Enable hardware acceleration",
|
"enable_hardware_acceleration": "Enable hardware acceleration",
|
||||||
"enable_tray_icon": "Show tray icon and minimise window to it on close",
|
"enable_tray_icon": "Show tray icon and minimise window to it on close",
|
||||||
|
@ -2718,7 +2719,8 @@
|
||||||
"show_checklist_shortcuts": "Show shortcut to welcome checklist above the room list",
|
"show_checklist_shortcuts": "Show shortcut to welcome checklist above the room list",
|
||||||
"show_polls_button": "Show polls button",
|
"show_polls_button": "Show polls button",
|
||||||
"surround_text": "Surround selected text when typing special characters",
|
"surround_text": "Surround selected text when typing special characters",
|
||||||
"time_heading": "Displaying time"
|
"time_heading": "Displaying time",
|
||||||
|
"user_timezone": "Set timezone"
|
||||||
},
|
},
|
||||||
"prompt_invite": "Prompt before sending invites to potentially invalid matrix IDs",
|
"prompt_invite": "Prompt before sending invites to potentially invalid matrix IDs",
|
||||||
"replace_plain_emoji": "Automatically replace plain text Emoji",
|
"replace_plain_emoji": "Automatically replace plain text Emoji",
|
||||||
|
|
|
@ -649,6 +649,11 @@ export const SETTINGS: { [setting: string]: ISetting } = {
|
||||||
displayName: _td("settings|always_show_message_timestamps"),
|
displayName: _td("settings|always_show_message_timestamps"),
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
"userTimezone": {
|
||||||
|
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
||||||
|
displayName: _td("settings|preferences|user_timezone"),
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
"autoplayGifs": {
|
"autoplayGifs": {
|
||||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
displayName: _td("settings|autoplay_gifs"),
|
displayName: _td("settings|autoplay_gifs"),
|
||||||
|
|
31
test/TimezoneHandler-test.ts
Normal file
31
test/TimezoneHandler-test.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
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 * as tzh from "../src/TimezoneHandler";
|
||||||
|
|
||||||
|
describe("TimezoneHandler", () => {
|
||||||
|
it("should support setting a user timezone", async () => {
|
||||||
|
const tz = "Europe/Paris";
|
||||||
|
await tzh.setUserTimezone(tz);
|
||||||
|
expect(tzh.getUserTimezone()).toEqual(tz);
|
||||||
|
});
|
||||||
|
it("Return undefined with an empty TZ", async () => {
|
||||||
|
await tzh.setUserTimezone("");
|
||||||
|
expect(tzh.getUserTimezone()).toEqual(undefined);
|
||||||
|
});
|
||||||
|
});
|
|
@ -66,6 +66,7 @@ describe("<SendMessageComposer/>", () => {
|
||||||
lowBandwidth: false,
|
lowBandwidth: false,
|
||||||
alwaysShowTimestamps: false,
|
alwaysShowTimestamps: false,
|
||||||
showTwelveHourTimestamps: false,
|
showTwelveHourTimestamps: false,
|
||||||
|
userTimezone: undefined,
|
||||||
readMarkerInViewThresholdMs: 3000,
|
readMarkerInViewThresholdMs: 3000,
|
||||||
readMarkerOutOfViewThresholdMs: 30000,
|
readMarkerOutOfViewThresholdMs: 30000,
|
||||||
showHiddenEvents: false,
|
showHiddenEvents: false,
|
||||||
|
|
|
@ -55,6 +55,32 @@ describe("PreferencesUserSettingsTab", () => {
|
||||||
expect(reloadStub).toHaveBeenCalled();
|
expect(reloadStub).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should search and select a user timezone", async () => {
|
||||||
|
renderTab();
|
||||||
|
|
||||||
|
expect(await screen.findByText(/Browser default/)).toBeInTheDocument();
|
||||||
|
const timezoneDropdown = await screen.findByRole("button", { name: "Set timezone" });
|
||||||
|
await userEvent.click(timezoneDropdown);
|
||||||
|
|
||||||
|
// Without filtering `expect(screen.queryByRole("option" ...` take over 1s.
|
||||||
|
await fireEvent.change(screen.getByRole("combobox", { name: "Set timezone" }), {
|
||||||
|
target: { value: "Africa/Abidjan" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.queryByRole("option", { name: "Africa/Abidjan" })).toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole("option", { name: "Europe/Paris" })).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
await fireEvent.change(screen.getByRole("combobox", { name: "Set timezone" }), {
|
||||||
|
target: { value: "Europe/Paris" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.queryByRole("option", { name: "Africa/Abidjan" })).not.toBeInTheDocument();
|
||||||
|
const option = await screen.getByRole("option", { name: "Europe/Paris" });
|
||||||
|
await userEvent.click(option);
|
||||||
|
|
||||||
|
expect(await screen.findByText("Europe/Paris")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it("should not show spell check setting if unsupported", async () => {
|
it("should not show spell check setting if unsupported", async () => {
|
||||||
PlatformPeg.get()!.supportsSpellCheckSettings = jest.fn().mockReturnValue(false);
|
PlatformPeg.get()!.supportsSpellCheckSettings = jest.fn().mockReturnValue(false);
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
||||||
class="mx_SettingsSubsection_content"
|
class="mx_SettingsSubsection_content"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="mx_SettingsSubsection_contentStretch"
|
class="mx_SettingsSubsection_dropdown"
|
||||||
>
|
>
|
||||||
Application language
|
Application language
|
||||||
<div
|
<div
|
||||||
|
@ -224,6 +224,35 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
||||||
<div
|
<div
|
||||||
class="mx_SettingsSubsection_content"
|
class="mx_SettingsSubsection_content"
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
class="mx_SettingsSubsection_dropdown"
|
||||||
|
>
|
||||||
|
Set timezone
|
||||||
|
<div
|
||||||
|
class="mx_Dropdown mx_dropdownUserTimezone"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-describedby="mx_dropdownUserTimezone_value"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
aria-label="Set timezone"
|
||||||
|
aria-owns="mx_dropdownUserTimezone_input"
|
||||||
|
class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_Dropdown_option"
|
||||||
|
id="mx_dropdownUserTimezone_value"
|
||||||
|
>
|
||||||
|
Browser default (UTC)
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="mx_Dropdown_arrow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="mx_SettingsFlag"
|
class="mx_SettingsFlag"
|
||||||
>
|
>
|
||||||
|
|
|
@ -72,6 +72,7 @@ export function getRoomContext(room: Room, override: Partial<IRoomState>): IRoom
|
||||||
layout: Layout.Group,
|
layout: Layout.Group,
|
||||||
lowBandwidth: false,
|
lowBandwidth: false,
|
||||||
alwaysShowTimestamps: false,
|
alwaysShowTimestamps: false,
|
||||||
|
userTimezone: undefined,
|
||||||
showTwelveHourTimestamps: false,
|
showTwelveHourTimestamps: false,
|
||||||
readMarkerInViewThresholdMs: 3000,
|
readMarkerInViewThresholdMs: 3000,
|
||||||
readMarkerOutOfViewThresholdMs: 30000,
|
readMarkerOutOfViewThresholdMs: 30000,
|
||||||
|
|
Loading…
Reference in a new issue