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:
Timshel 2024-09-02 11:07:07 +02:00 committed by GitHub
parent acc7342758
commit ae15bbe6e0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 256 additions and 9 deletions

View file

@ -19,6 +19,7 @@ limitations under the License.
import { Optional } from "matrix-events-sdk";
import { _t, getUserLanguage } from "./languageHandler";
import { getUserTimezone } from "./TimezoneHandler";
export const MINUTE_MS = 60000;
export const HOUR_MS = MINUTE_MS * 60;
@ -77,6 +78,7 @@ export function formatDate(date: Date, showTwelveHour = false, locale?: string):
weekday: "short",
hour: "numeric",
minute: "2-digit",
timeZone: getUserTimezone(),
}).format(date);
} else if (now.getFullYear() === date.getFullYear()) {
return new Intl.DateTimeFormat(_locale, {
@ -86,6 +88,7 @@ export function formatDate(date: Date, showTwelveHour = false, locale?: string):
day: "numeric",
hour: "numeric",
minute: "2-digit",
timeZone: getUserTimezone(),
}).format(date);
}
return formatFullDate(date, showTwelveHour, false, _locale);
@ -104,6 +107,7 @@ export function formatFullDateNoTime(date: Date, locale?: string): string {
month: "short",
day: "numeric",
year: "numeric",
timeZone: getUserTimezone(),
}).format(date);
}
@ -127,6 +131,7 @@ export function formatFullDate(date: Date, showTwelveHour = false, showSeconds =
hour: "numeric",
minute: "2-digit",
second: showSeconds ? "2-digit" : undefined,
timeZone: getUserTimezone(),
}).format(date);
}
@ -160,6 +165,7 @@ export function formatFullTime(date: Date, showTwelveHour = false, locale?: stri
hour: "numeric",
minute: "2-digit",
second: "2-digit",
timeZone: getUserTimezone(),
}).format(date);
}
@ -178,6 +184,7 @@ export function formatTime(date: Date, showTwelveHour = false, locale?: string):
...getTwelveHourOptions(showTwelveHour),
hour: "numeric",
minute: "2-digit",
timeZone: getUserTimezone(),
}).format(date);
}
@ -285,6 +292,7 @@ export function formatFullDateNoDayNoTime(date: Date, locale?: string): string {
year: "numeric",
month: "numeric",
day: "numeric",
timeZone: getUserTimezone(),
}).format(date);
}
@ -354,6 +362,9 @@ export function formatPreciseDuration(durationMs: number): string {
* @returns {string} formattedDate
*/
export const formatLocalDateShort = (timestamp: number, locale?: string): string =>
new Intl.DateTimeFormat(locale ?? getUserLanguage(), { day: "2-digit", month: "2-digit", year: "2-digit" }).format(
timestamp,
);
new Intl.DateTimeFormat(locale ?? getUserLanguage(), {
day: "2-digit",
month: "2-digit",
year: "2-digit",
timeZone: getUserTimezone(),
}).format(timestamp);

55
src/TimezoneHandler.ts Normal file
View 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"
);
}

View file

@ -47,6 +47,7 @@ import { ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/Ro
import shouldHideEvent from "../../shouldHideEvent";
import { _t } from "../../languageHandler";
import * as TimezoneHandler from "../../TimezoneHandler";
import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
import ResizeNotifier from "../../utils/ResizeNotifier";
import ContentMessages from "../../ContentMessages";
@ -228,6 +229,7 @@ export interface IRoomState {
lowBandwidth: boolean;
alwaysShowTimestamps: boolean;
showTwelveHourTimestamps: boolean;
userTimezone: string | undefined;
readMarkerInViewThresholdMs: number;
readMarkerOutOfViewThresholdMs: number;
showHiddenEvents: boolean;
@ -455,6 +457,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
lowBandwidth: SettingsStore.getValue("lowBandwidth"),
alwaysShowTimestamps: SettingsStore.getValue("alwaysShowTimestamps"),
showTwelveHourTimestamps: SettingsStore.getValue("showTwelveHourTimestamps"),
userTimezone: TimezoneHandler.getUserTimezone(),
readMarkerInViewThresholdMs: SettingsStore.getValue("readMarkerInViewThresholdMs"),
readMarkerOutOfViewThresholdMs: SettingsStore.getValue("readMarkerOutOfViewThresholdMs"),
showHiddenEvents: SettingsStore.getValue("showHiddenEventsInTimeline"),
@ -512,6 +515,9 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
SettingsStore.watchSetting("showTwelveHourTimestamps", null, (...[, , , value]) =>
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]) =>
this.setState({ readMarkerInViewThresholdMs: value as number }),
),

View file

@ -15,12 +15,14 @@ See the License for the specific language governing permissions and
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 { UseCase } from "../../../../../settings/enums/UseCase";
import SettingsStore from "../../../../../settings/SettingsStore";
import Field from "../../../elements/Field";
import Dropdown from "../../../elements/Dropdown";
import { SettingLevel } from "../../../../../settings/SettingLevel";
import SettingsFlag from "../../../elements/SettingsFlag";
import AccessibleButton from "../../../elements/AccessibleButton";
@ -38,12 +40,16 @@ import PlatformPeg from "../../../../../PlatformPeg";
import { IS_MAC } from "../../../../../Keyboard";
import SpellCheckSettings from "../../SpellCheckSettings";
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
import * as TimezoneHandler from "../../../../../TimezoneHandler";
interface IProps {
closeSettingsFn(success: boolean): void;
}
interface IState {
timezone: string | undefined;
timezones: string[];
timezoneSearch: string | undefined;
autocompleteDelay: string;
readMarkerInViewThresholdMs: string;
readMarkerOutOfViewThresholdMs: string;
@ -68,7 +74,7 @@ const LanguageSection: React.FC = () => {
);
return (
<div className="mx_SettingsSubsection_contentStretch">
<div className="mx_SettingsSubsection_dropdown">
{_t("settings|general|application_language")}
<LanguageDropdown onOptionChange={onLanguageChange} value={language} />
<div className="mx_PreferencesUserSettingsTab_section_hint">
@ -173,6 +179,9 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
super(props);
this.state = {
timezone: TimezoneHandler.getUserTimezone(),
timezones: TimezoneHandler.getAllTimezones(),
timezoneSearch: undefined,
autocompleteDelay: SettingsStore.getValueAt(SettingLevel.DEVICE, "autocompleteDelay").toString(10),
readMarkerInViewThresholdMs: SettingsStore.getValueAt(
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 => {
this.setState({ autocompleteDelay: 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
.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 (
<SettingsTab data-testid="mx_PreferencesUserSettingsTab">
<SettingsSection>
@ -254,6 +292,23 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
</SettingsSubsection>
<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)}
</SettingsSubsection>

View file

@ -55,6 +55,7 @@ const RoomContext = createContext<
lowBandwidth: false,
alwaysShowTimestamps: false,
showTwelveHourTimestamps: false,
userTimezone: undefined,
readMarkerInViewThresholdMs: 3000,
readMarkerOutOfViewThresholdMs: 30000,
showHiddenEvents: false,

View file

@ -2703,6 +2703,7 @@
"code_blocks_heading": "Code blocks",
"compact_modern": "Use a more compact 'Modern' layout",
"composer_heading": "Composer",
"default_timezone": "Browser default (%(timezone)s)",
"dialog_title": "<strong>Settings:</strong> Preferences",
"enable_hardware_acceleration": "Enable hardware acceleration",
"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_polls_button": "Show polls button",
"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",
"replace_plain_emoji": "Automatically replace plain text Emoji",

View file

@ -649,6 +649,11 @@ export const SETTINGS: { [setting: string]: ISetting } = {
displayName: _td("settings|always_show_message_timestamps"),
default: false,
},
"userTimezone": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
displayName: _td("settings|preferences|user_timezone"),
default: "",
},
"autoplayGifs": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td("settings|autoplay_gifs"),