Skip to content
This repository was archived by the owner on Oct 22, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
33c9c04
[create-pull-request] automated change (#12966)
RiotRobot Sep 6, 2024
05b1826
Add timezone to right panel profile.
Half-Shot Sep 6, 2024
c103109
Add setting to publish timezone
Half-Shot Sep 6, 2024
d9238c7
Add string for timezone publish
Half-Shot Sep 6, 2024
e78dfc2
Automatically update timezone when setting changes.
Half-Shot Sep 6, 2024
6989162
Refactor to using a hook
Half-Shot Sep 8, 2024
34d3176
Check for feature support for extended profiles.
Half-Shot Sep 8, 2024
8e39e06
lint
Half-Shot Sep 9, 2024
6fc5450
Add timezone
Half-Shot Sep 9, 2024
96d7fda
Remove unintentional changes
Half-Shot Sep 9, 2024
8ec6294
Use browser default timezone.
Half-Shot Sep 9, 2024
45285f2
lint
Half-Shot Sep 9, 2024
5ae6e2e
tweaks
Half-Shot Sep 9, 2024
b12e4cd
Set timezone publish at the device level to prevent all devices writi…
Half-Shot Sep 9, 2024
8c18fce
Update hook to use external client.
Half-Shot Sep 10, 2024
4e11e7d
Add test for user timezone.
Half-Shot Sep 10, 2024
b176ca8
Update snapshot for preferences tab.
Half-Shot Sep 10, 2024
2a14aee
Hide timezone info if not provided.
Half-Shot Sep 10, 2024
c43048d
Stablize test
Half-Shot Sep 10, 2024
5ffe3d4
Fix date test types.
Half-Shot Sep 10, 2024
ea26d0a
prettier
Half-Shot Sep 10, 2024
2703d55
Merge branch 'develop' into hs/timezone-publish-to-profile
Half-Shot Sep 10, 2024
b40eee7
Add timezone tests
Half-Shot Sep 10, 2024
f365143
Add test for invalid timezone.
Half-Shot Sep 10, 2024
256cd49
Update screenshot
Half-Shot Sep 12, 2024
ea3cad1
Remove check for profile.
Half-Shot Sep 12, 2024
c218b35
Merge branch 'develop' into hs/timezone-publish-to-profile
Half-Shot Sep 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions res/css/views/right_panel/_UserInfo.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,10 @@ limitations under the License.
}
}

.mx_UserInfo_timezone {
margin: var(--cpd-space-1x) 0;
}

.mx_PresenceLabel {
font: var(--cpd-font-body-sm-regular);
opacity: 1;
Expand Down
32 changes: 32 additions & 0 deletions src/components/structures/LoggedInView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,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) {
Expand Down Expand Up @@ -190,6 +191,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();

Expand All @@ -198,6 +204,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);
Expand All @@ -208,6 +239,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();
}

Expand Down
15 changes: 13 additions & 2 deletions src/components/views/right_panel/UserInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,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";
Expand Down Expand Up @@ -93,7 +93,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;
}
Expand Down Expand Up @@ -1702,6 +1702,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,
Expand Down Expand Up @@ -1735,6 +1737,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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
</div>

{this.renderGroup(PreferencesUserSettingsTab.TIME_SETTINGS)}
<SettingsFlag name="userTimezonePublish" level={SettingLevel.DEVICE} />
</SettingsSubsection>

<SettingsSubsection
Expand Down
106 changes: 106 additions & 0 deletions src/hooks/useUserTimezone.ts
Original file line number Diff line number Diff line change
@@ -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,
};
};
2 changes: 2 additions & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -2721,6 +2722,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",
Expand Down
36 changes: 34 additions & 2 deletions src/i18n/strings/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -1841,10 +1841,25 @@
"files_button": "Pliki",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changes came from 33c9c04, I don't know whether I should remove them from my commit list to make the CI pass or just ignore it?

"info": "Info",
"pinned_messages": {
"empty_description": "Znajdź wiadomość i wybierz „%(pinAction)s”, aby załączyć ją tutaj.",
"empty_title": "Przypinaj ważne wiadomości, aby można było je łatwo znaleźć",
"header": {
"one": "Przypięto 1 wiadomość",
"few": "Przypięto %(count)s wiadomości",
"many": "Przypięto %(count)s wiadomości"
},
"limits": {
"other": "Możesz przypiąć do %(count)s widżetów"
},
"title": "Przypięte wiadomości"
"menu": "Otwórz menu",
"reply_thread": "Odpowiedz na <link>wiadomość w wątku</link>",
"title": "Przypięte wiadomości",
"unpin_all": {
"button": "Odepnij wszystkie wiadomości",
"content": "Upewnij się, czy naprawdę chcesz usunąć wszystkie przypięte wiadomości. Tej akcji nie można cofnąć.",
"title": "Odpiąć wszystkie wiadomości?"
},
"view": "Wyświetl na osi czasu"
},
"pinned_messages_button": "Przypięte wiadomości",
"poll": {
Expand Down Expand Up @@ -2036,6 +2051,21 @@
"not_found_title": "Ten pokój lub przestrzeń nie istnieje.",
"not_found_title_name": "%(roomName)s nie istnieje.",
"peek_join_prompt": "Przeglądasz %(roomName)s. Czy chcesz dołączyć do pokoju?",
"pinned_message_banner": {
"button_close_list": "Zamknij listę",
"button_view_all": "Pokaż wszystkie",
"description": "Ten pokój ma przypięte wiadomości. Kliknij, aby je wyświetlić.",
"go_to_message": "Wyświetl przypiętą wiadomość na osi czasu.",
"prefix": {
"audio": "Audio",
"file": "Plik",
"image": "Obraz",
"poll": "Ankieta",
"video": "Wideo"
},
"preview": "<bold>%(prefix)s:</bold> %(preview)s",
"title": "<bold>%(index)s z %(length)s</bold> przypiętych wiadomości"
},
"read_topic": "Kliknij, aby przeczytać temat",
"rejecting": "Odrzucanie zaproszenia…",
"rejoin_button": "Dołącz ponownie",
Expand Down Expand Up @@ -2685,6 +2715,7 @@
"code_blocks_heading": "Bloki kodu",
"compact_modern": "Użyj bardziej kompaktowego, \"nowoczesnego\" wyglądu",
"composer_heading": "Kompozytor",
"default_timezone": "Ustawienie przeglądarki (%(timezone)s)",
"dialog_title": "<strong>Ustawienia:</strong> Preferencje",
"enable_hardware_acceleration": "Włącz przyspieszenie sprzętowe",
"enable_tray_icon": "Pokaż ikonę w zasobniku systemowym i zminimalizuj okno do niej zamiast zamknięcia",
Expand All @@ -2700,7 +2731,8 @@
"show_checklist_shortcuts": "Pokaż skrót do listy powitalnej nad listą pokojów",
"show_polls_button": "Pokaż przycisk ankiet",
"surround_text": "Otocz zaznaczony tekst podczas wpisywania specjalnych znaków",
"time_heading": "Wyświetlanie czasu"
"time_heading": "Wyświetlanie czasu",
"user_timezone": "Ustaw strefę czasową"
},
"prompt_invite": "Powiadamiaj przed wysłaniem zaproszenia do potencjalnie nieprawidłowych ID matrix",
"replace_plain_emoji": "Automatycznie zastępuj tekstowe emotikony",
Expand Down
14 changes: 14 additions & 0 deletions src/settings/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ limitations under the License.
*/

import React, { ReactNode } from "react";
import { UNSTABLE_MSC4133_EXTENDED_PROFILES } from "matrix-js-sdk/src/matrix";

import { _t, _td, TranslationKey } from "../languageHandler";
import {
Expand Down Expand Up @@ -654,6 +655,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"),
Expand Down
14 changes: 14 additions & 0 deletions test/components/views/right_panel/UserInfo-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
let mockSpace: Mocked<Room>;
let mockClient: Mocked<MatrixClient>;
let mockCrypto: Mocked<CryptoApi>;
const origDate = global.Date.prototype.toLocaleString;

beforeEach(() => {
mockRoom = mocked({
Expand Down Expand Up @@ -158,6 +159,8 @@
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: {
Expand Down Expand Up @@ -232,11 +235,22 @@
expect(screen.queryByTestId("space-header")).not.toBeInTheDocument();
});

it("renders user info", () => {

Check failure on line 238 in test/components/views/right_panel/UserInfo-test.tsx

View workflow job for this annotation

GitHub Actions / Typescript Syntax Check

'this' implicitly has type 'any' because it does not have a type annotation.
renderComponent();
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 (_locale, opts) {
return origDate.call(this, "en-US", opts); // eslint-disable-line @typescript-eslint/no-invalid-this
});
mockClient.doesServerSupportExtendedProfiles.mockResolvedValue(true);
mockClient.getExtendedProfileProperty.mockResolvedValue("Europe/London");
renderComponent();
await expect(screen.findByText(/\d\d:\d\d (AM|PM)/)).resolves.toBeInTheDocument();
});

it("renders encryption info panel without pending verification", () => {
renderComponent({ phase: RightPanelPhases.EncryptionPanel });
expect(screen.getByRole("heading", { name: /encryption/i })).toBeInTheDocument();
Expand Down
Loading
Loading