Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 93b669d

Browse files
committed
Allow user to set timezone
1 parent 72e0d10 commit 93b669d

File tree

9 files changed

+114
-5
lines changed

9 files changed

+114
-5
lines changed

src/DateUtils.ts

+14-3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ limitations under the License.
1919
import { Optional } from "matrix-events-sdk";
2020

2121
import { _t, getUserLanguage } from "./languageHandler";
22+
import { getUserTimezone } from "./TimezoneHandler";
2223

2324
export const MINUTE_MS = 60000;
2425
export const HOUR_MS = MINUTE_MS * 60;
@@ -77,6 +78,7 @@ export function formatDate(date: Date, showTwelveHour = false, locale?: string):
7778
weekday: "short",
7879
hour: "numeric",
7980
minute: "2-digit",
81+
timeZone: getUserTimezone(),
8082
}).format(date);
8183
} else if (now.getFullYear() === date.getFullYear()) {
8284
return new Intl.DateTimeFormat(_locale, {
@@ -86,6 +88,7 @@ export function formatDate(date: Date, showTwelveHour = false, locale?: string):
8688
day: "numeric",
8789
hour: "numeric",
8890
minute: "2-digit",
91+
timeZone: getUserTimezone(),
8992
}).format(date);
9093
}
9194
return formatFullDate(date, showTwelveHour, false, _locale);
@@ -104,6 +107,7 @@ export function formatFullDateNoTime(date: Date, locale?: string): string {
104107
month: "short",
105108
day: "numeric",
106109
year: "numeric",
110+
timeZone: getUserTimezone(),
107111
}).format(date);
108112
}
109113

@@ -127,6 +131,7 @@ export function formatFullDate(date: Date, showTwelveHour = false, showSeconds =
127131
hour: "numeric",
128132
minute: "2-digit",
129133
second: showSeconds ? "2-digit" : undefined,
134+
timeZone: getUserTimezone(),
130135
}).format(date);
131136
}
132137

@@ -160,6 +165,7 @@ export function formatFullTime(date: Date, showTwelveHour = false, locale?: stri
160165
hour: "numeric",
161166
minute: "2-digit",
162167
second: "2-digit",
168+
timeZone: getUserTimezone(),
163169
}).format(date);
164170
}
165171

@@ -178,6 +184,7 @@ export function formatTime(date: Date, showTwelveHour = false, locale?: string):
178184
...getTwelveHourOptions(showTwelveHour),
179185
hour: "numeric",
180186
minute: "2-digit",
187+
timeZone: getUserTimezone(),
181188
}).format(date);
182189
}
183190

@@ -285,6 +292,7 @@ export function formatFullDateNoDayNoTime(date: Date, locale?: string): string {
285292
year: "numeric",
286293
month: "numeric",
287294
day: "numeric",
295+
timeZone: getUserTimezone(),
288296
}).format(date);
289297
}
290298

@@ -354,6 +362,9 @@ export function formatPreciseDuration(durationMs: number): string {
354362
* @returns {string} formattedDate
355363
*/
356364
export const formatLocalDateShort = (timestamp: number, locale?: string): string =>
357-
new Intl.DateTimeFormat(locale ?? getUserLanguage(), { day: "2-digit", month: "2-digit", year: "2-digit" }).format(
358-
timestamp,
359-
);
365+
new Intl.DateTimeFormat(locale ?? getUserLanguage(), {
366+
day: "2-digit",
367+
month: "2-digit",
368+
year: "2-digit",
369+
timeZone: getUserTimezone(),
370+
}).format(timestamp);

src/TimezoneHandler.ts

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
Copyright 2024 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { SettingLevel } from "./settings/SettingLevel";
18+
import SettingsStore from "./settings/SettingsStore";
19+
20+
export const USER_TIMEZONE_KEY = "userTimezone";
21+
22+
export function getUserTimezone(): string | undefined {
23+
const tz = SettingsStore.getValueAt(SettingLevel.DEVICE, USER_TIMEZONE_KEY);
24+
return tz ? tz : undefined;
25+
}
26+
27+
export function setUserTimezone(timezone: string): Promise<void> {
28+
return SettingsStore.setValue(USER_TIMEZONE_KEY, null, SettingLevel.DEVICE, timezone);
29+
}
30+
31+
export function getAllTimezones(): string[] {
32+
return Intl.supportedValuesOf("timeZone");
33+
}

src/components/structures/RoomView.tsx

+6
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import { ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/Ro
4747

4848
import shouldHideEvent from "../../shouldHideEvent";
4949
import { _t } from "../../languageHandler";
50+
import * as TimezoneHandler from "../../TimezoneHandler";
5051
import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
5152
import ResizeNotifier from "../../utils/ResizeNotifier";
5253
import ContentMessages from "../../ContentMessages";
@@ -227,6 +228,7 @@ export interface IRoomState {
227228
lowBandwidth: boolean;
228229
alwaysShowTimestamps: boolean;
229230
showTwelveHourTimestamps: boolean;
231+
userTimezone: string | undefined;
230232
readMarkerInViewThresholdMs: number;
231233
readMarkerOutOfViewThresholdMs: number;
232234
showHiddenEvents: boolean;
@@ -454,6 +456,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
454456
lowBandwidth: SettingsStore.getValue("lowBandwidth"),
455457
alwaysShowTimestamps: SettingsStore.getValue("alwaysShowTimestamps"),
456458
showTwelveHourTimestamps: SettingsStore.getValue("showTwelveHourTimestamps"),
459+
userTimezone: TimezoneHandler.getUserTimezone(),
457460
readMarkerInViewThresholdMs: SettingsStore.getValue("readMarkerInViewThresholdMs"),
458461
readMarkerOutOfViewThresholdMs: SettingsStore.getValue("readMarkerOutOfViewThresholdMs"),
459462
showHiddenEvents: SettingsStore.getValue("showHiddenEventsInTimeline"),
@@ -511,6 +514,9 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
511514
SettingsStore.watchSetting("showTwelveHourTimestamps", null, (...[, , , value]) =>
512515
this.setState({ showTwelveHourTimestamps: value as boolean }),
513516
),
517+
SettingsStore.watchSetting(TimezoneHandler.USER_TIMEZONE_KEY, null, (...[, , , value]) =>
518+
this.setState({ userTimezone: value as string }),
519+
),
514520
SettingsStore.watchSetting("readMarkerInViewThresholdMs", null, (...[, , , value]) =>
515521
this.setState({ readMarkerInViewThresholdMs: value as number }),
516522
),

src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx

+50-1
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@ See the License for the specific language governing permissions and
1515
limitations under the License.
1616
*/
1717

18-
import React, { useCallback, useEffect, useState } from "react";
18+
import React, { ReactElement, useCallback, useEffect, useState } from "react";
1919

20+
import { NonEmptyArray } from "../../../../../@types/common";
2021
import { _t, getCurrentLanguage } from "../../../../../languageHandler";
2122
import { UseCase } from "../../../../../settings/enums/UseCase";
2223
import SettingsStore from "../../../../../settings/SettingsStore";
2324
import Field from "../../../elements/Field";
25+
import Dropdown from "../../../elements/Dropdown";
2426
import { SettingLevel } from "../../../../../settings/SettingLevel";
2527
import SettingsFlag from "../../../elements/SettingsFlag";
2628
import AccessibleButton from "../../../elements/AccessibleButton";
@@ -38,12 +40,16 @@ import PlatformPeg from "../../../../../PlatformPeg";
3840
import { IS_MAC } from "../../../../../Keyboard";
3941
import SpellCheckSettings from "../../SpellCheckSettings";
4042
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
43+
import * as TimezoneHandler from "../../../../../TimezoneHandler";
44+
4145

4246
interface IProps {
4347
closeSettingsFn(success: boolean): void;
4448
}
4549

4650
interface IState {
51+
timezone: string | undefined;
52+
timezoneSearch: string | undefined;
4753
autocompleteDelay: string;
4854
readMarkerInViewThresholdMs: string;
4955
readMarkerOutOfViewThresholdMs: string;
@@ -173,10 +179,16 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
173179
// Autocomplete delay (niche text box)
174180
];
175181

182+
private allTimezones: string[];
183+
176184
public constructor(props: IProps) {
177185
super(props);
178186

187+
this.allTimezones = TimezoneHandler.getAllTimezones();
188+
179189
this.state = {
190+
timezone: TimezoneHandler.getUserTimezone(),
191+
timezoneSearch: undefined,
180192
autocompleteDelay: SettingsStore.getValueAt(SettingLevel.DEVICE, "autocompleteDelay").toString(10),
181193
readMarkerInViewThresholdMs: SettingsStore.getValueAt(
182194
SettingLevel.DEVICE,
@@ -189,6 +201,15 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
189201
};
190202
}
191203

204+
private onTimezoneChange = (tz: string): void => {
205+
this.setState({ timezone: tz });
206+
TimezoneHandler.setUserTimezone(tz);
207+
};
208+
209+
private onTimezoneSearchChange = (search: string): void => {
210+
this.setState({ timezoneSearch: search.toLowerCase() });
211+
};
212+
192213
private onAutocompleteDelayChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
193214
this.setState({ autocompleteDelay: e.target.value });
194215
SettingsStore.setValue("autocompleteDelay", null, SettingLevel.DEVICE, e.target.value);
@@ -221,6 +242,17 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
221242
// Only show the user onboarding setting if the user should see the user onboarding page
222243
.filter((it) => it !== "FTUE.userOnboardingButton" || showUserOnboardingPage(useCase));
223244

245+
const timezones = (
246+
!this.state.timezoneSearch ? this.allTimezones : this.allTimezones.filter((tz) => {
247+
return tz.toLowerCase().includes(this.state.timezoneSearch!)
248+
})
249+
).map((tz) => {
250+
return <div key={tz}>{tz}</div>;
251+
});
252+
timezones.unshift(
253+
<div key="">{_t("settings|preferences|defaultTimezone")}</div>
254+
);
255+
224256
return (
225257
<SettingsTab data-testid="mx_PreferencesUserSettingsTab">
226258
<SettingsSection>
@@ -259,6 +291,23 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
259291

260292
<SettingsSubsection heading={_t("settings|preferences|time_heading")}>
261293
{this.renderGroup(PreferencesUserSettingsTab.TIME_SETTINGS)}
294+
295+
<div className="mx_SettingsFlag">
296+
<label className="mx_SettingsFlag_label" htmlFor="mx_dropdownUserTimezone">
297+
<span className="mx_SettingsFlag_labelText">{_t("settings|preferences|userTimezone")}</span>
298+
</label>
299+
<Dropdown
300+
id="mx_dropdownUserTimezone"
301+
searchEnabled={true}
302+
value={this.state.timezone}
303+
label={_t("settings|preferences|userTimezone")}
304+
placeholder={_t("settings|preferences|defaultTimezone")}
305+
onOptionChange={this.onTimezoneChange}
306+
onSearchChange={this.onTimezoneSearchChange}
307+
>
308+
{ timezones as NonEmptyArray<ReactElement & { key: string }> }
309+
</Dropdown>
310+
</div>
262311
</SettingsSubsection>
263312

264313
<SettingsSubsection

src/contexts/RoomContext.ts

+1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ const RoomContext = createContext<
5555
lowBandwidth: false,
5656
alwaysShowTimestamps: false,
5757
showTwelveHourTimestamps: false,
58+
userTimezone: undefined,
5859
readMarkerInViewThresholdMs: 3000,
5960
readMarkerOutOfViewThresholdMs: 30000,
6061
showHiddenEvents: false,

src/i18n/strings/en_EN.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -2690,7 +2690,9 @@
26902690
"show_checklist_shortcuts": "Show shortcut to welcome checklist above the room list",
26912691
"show_polls_button": "Show polls button",
26922692
"surround_text": "Surround selected text when typing special characters",
2693-
"time_heading": "Displaying time"
2693+
"time_heading": "Displaying time",
2694+
"userTimezone": "Set timezone",
2695+
"defaultTimezone": "Browser default"
26942696
},
26952697
"prompt_invite": "Prompt before sending invites to potentially invalid matrix IDs",
26962698
"replace_plain_emoji": "Automatically replace plain text Emoji",

src/settings/Settings.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,11 @@ export const SETTINGS: { [setting: string]: ISetting } = {
652652
displayName: _td("settings|always_show_message_timestamps"),
653653
default: false,
654654
},
655+
"userTimezone": {
656+
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
657+
displayName: _td("settings|preferences|userTimezone"),
658+
default: "",
659+
},
655660
"autoplayGifs": {
656661
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
657662
displayName: _td("settings|autoplay_gifs"),

test/components/views/rooms/SendMessageComposer-test.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ describe("<SendMessageComposer/>", () => {
6666
lowBandwidth: false,
6767
alwaysShowTimestamps: false,
6868
showTwelveHourTimestamps: false,
69+
userTimezone: undefined,
6970
readMarkerInViewThresholdMs: 3000,
7071
readMarkerOutOfViewThresholdMs: 30000,
7172
showHiddenEvents: false,

test/test-utils/room.ts

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export function getRoomContext(room: Room, override: Partial<IRoomState>): IRoom
7272
layout: Layout.Group,
7373
lowBandwidth: false,
7474
alwaysShowTimestamps: false,
75+
userTimezone: undefined,
7576
showTwelveHourTimestamps: false,
7677
readMarkerInViewThresholdMs: 3000,
7778
readMarkerOutOfViewThresholdMs: 30000,

0 commit comments

Comments
 (0)