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
|
@ -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
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 { _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 }),
|
||||
),
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -55,6 +55,7 @@ const RoomContext = createContext<
|
|||
lowBandwidth: false,
|
||||
alwaysShowTimestamps: false,
|
||||
showTwelveHourTimestamps: false,
|
||||
userTimezone: undefined,
|
||||
readMarkerInViewThresholdMs: 3000,
|
||||
readMarkerOutOfViewThresholdMs: 30000,
|
||||
showHiddenEvents: false,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue