diff --git a/src/app/components/user-profile/UserChips.tsx b/src/app/components/user-profile/UserChips.tsx
index 53e6618b..d06750dd 100644
--- a/src/app/components/user-profile/UserChips.tsx
+++ b/src/app/components/user-profile/UserChips.tsx
@@ -19,6 +19,9 @@ import {
Box,
Scroll,
Avatar,
+ TooltipProvider,
+ Tooltip,
+ Badge,
} from 'folds';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { getMxIdServer } from '../../utils/matrix';
@@ -41,6 +44,7 @@ import { useTimeoutToggle } from '../../hooks/useTimeoutToggle';
import { useIgnoredUsers } from '../../hooks/useIgnoredUsers';
import { CutoutCard } from '../cutout-card';
import { SettingTile } from '../setting-tile';
+import { useInterval } from '../../hooks/useInterval';
export function ServerChip({ server }: { server: string }) {
const mx = useMatrixClient();
@@ -512,3 +516,66 @@ export function OptionsChip({ userId }: { userId: string }) {
);
}
+
+export function TimezoneChip({ timezone }: { timezone: string }) {
+ const shortFormat = useMemo(
+ () =>
+ new Intl.DateTimeFormat(undefined, {
+ dateStyle: undefined,
+ timeStyle: 'short',
+ timeZone: timezone,
+ }),
+ [timezone]
+ );
+ const longFormat = useMemo(
+ () =>
+ new Intl.DateTimeFormat(undefined, {
+ dateStyle: 'long',
+ timeStyle: 'short',
+ timeZone: timezone,
+ }),
+ [timezone]
+ );
+ const [shortTime, setShortTime] = useState(shortFormat.format());
+ const [longTime, setLongTime] = useState(longFormat.format());
+
+ useInterval(() => {
+ setShortTime(shortFormat.format());
+ setLongTime(longFormat.format());
+ }, 1000);
+
+ return (
+
+
+
+ Timezone:
+
+ {timezone}
+
+
+ {longTime}
+
+
+ }
+ >
+ {(triggerRef) => (
+ }
+ >
+
+ {shortTime}
+
+
+ )}
+
+ );
+}
diff --git a/src/app/components/user-profile/UserHero.tsx b/src/app/components/user-profile/UserHero.tsx
index c042b3ec..54e40402 100644
--- a/src/app/components/user-profile/UserHero.tsx
+++ b/src/app/components/user-profile/UserHero.tsx
@@ -21,7 +21,7 @@ import { UserPresence } from '../../hooks/useUserPresence';
import { AvatarPresence, PresenceBadge } from '../presence';
import { ImageViewer } from '../image-viewer';
import { stopPropagation } from '../../utils/keyboard';
-import { extendedProfileFields } from '../../hooks/useExtendedProfile';
+import { ExtendedProfile } from '../../hooks/useExtendedProfile';
type UserHeroProps = {
userId: string;
@@ -96,7 +96,7 @@ export function UserHero({ userId, avatarUrl, presence }: UserHeroProps) {
type UserHeroNameProps = {
displayName?: string;
userId: string;
- extendedProfile?: extendedProfileFields;
+ extendedProfile?: ExtendedProfile;
};
export function UserHeroName({ displayName, userId, extendedProfile }: UserHeroNameProps) {
const username = getMxIdLocalPart(userId);
diff --git a/src/app/components/user-profile/UserRoomProfile.tsx b/src/app/components/user-profile/UserRoomProfile.tsx
index 26b687eb..ce985e94 100644
--- a/src/app/components/user-profile/UserRoomProfile.tsx
+++ b/src/app/components/user-profile/UserRoomProfile.tsx
@@ -1,5 +1,5 @@
import { Box, Button, config, Icon, Icons, Text } from 'folds';
-import React, { useEffect } from 'react';
+import React, { useEffect, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { UserHero, UserHeroName } from './UserHero';
import { getMxIdServer, mxcUrlToHttp } from '../../utils/matrix';
@@ -9,7 +9,7 @@ import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { usePowerLevels } from '../../hooks/usePowerLevels';
import { useRoom } from '../../hooks/useRoom';
import { useUserPresence } from '../../hooks/useUserPresence';
-import { IgnoredUserAlert, MutualRoomsChip, OptionsChip, ServerChip, ShareChip } from './UserChips';
+import { IgnoredUserAlert, MutualRoomsChip, OptionsChip, ServerChip, ShareChip, TimezoneChip } from './UserChips';
import { useCloseUserRoomProfile } from '../../state/hooks/userRoomProfile';
import { PowerChip } from './PowerChip';
import { UserInviteAlert, UserBanAlert, UserModeration, UserKickAlert } from './UserModeration';
@@ -60,6 +60,16 @@ export function UserRoomProfile({ userId }: UserRoomProfileProps) {
const avatarUrl = (avatarMxc && mxcUrlToHttp(mx, avatarMxc, useAuthentication)) ?? undefined;
const [extendedProfileState, refreshExtendedProfile] = useExtendedProfile(userId);
const extendedProfile = extendedProfileState.status === AsyncStatus.Success ? extendedProfileState.data : undefined;
+ const timezone = useMemo(() => {
+ // @ts-expect-error Intl.supportedValuesOf isn't in the types yet
+ const supportedTimezones = Intl.supportedValuesOf('timeZone') as string[];
+ const profileTimezone = extendedProfile?.['us.cloke.msc4175.tz'];
+ if (profileTimezone && supportedTimezones.includes(profileTimezone)) {
+ return profileTimezone;
+ }
+ return undefined;
+
+ }, [extendedProfile]);
const presence = useUserPresence(userId);
@@ -107,6 +117,7 @@ export function UserRoomProfile({ userId }: UserRoomProfileProps) {
{creator ? : }
{userId !== myUserId && }
{userId !== myUserId && }
+ {timezone && }
{ignored && }
diff --git a/src/app/features/settings/account/Profile.tsx b/src/app/features/settings/account/Profile.tsx
index d00ab076..fbff9f7a 100644
--- a/src/app/features/settings/account/Profile.tsx
+++ b/src/app/features/settings/account/Profile.tsx
@@ -1,4 +1,12 @@
-import React, { ChangeEventHandler, ReactNode, useCallback, useMemo, useState } from 'react';
+import React, {
+ ChangeEventHandler,
+ ReactNode,
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
import {
Box,
Text,
@@ -13,13 +21,17 @@ import {
Modal,
config,
Spinner,
+ toRem,
+ Dialog,
+ Header,
+ MenuItem,
} from 'folds';
import FocusTrap from 'focus-trap-react';
import { UserEvent } from 'matrix-js-sdk';
import { SequenceCard } from '../../../components/sequence-card';
import { SettingTile } from '../../../components/setting-tile';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
-import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix';
+import { getMxIdLocalPart, getMxIdServer, mxcUrlToHttp } from '../../../utils/matrix';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
import { useFilePicker } from '../../../hooks/useFilePicker';
import { useObjectURL } from '../../../hooks/useObjectURL';
@@ -37,6 +49,9 @@ import {
import { ProfileFieldContextProvider, useProfileField } from './ProfileFieldContext';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { FilterByValues } from '../../../../types/utils';
+import { CutoutCard } from '../../../components/cutout-card';
+import { ServerChip, ShareChip, TimezoneChip } from '../../../components/user-profile/UserChips';
+import { SequenceCardStyle } from '../styles.css';
function ProfileAvatar() {
const mx = useMatrixClient();
@@ -74,7 +89,13 @@ function ProfileAvatar() {
};
return (
-
+
+ Avatar
+
+ }
+ >
{uploadAtom ? (
(null);
+ const scrollRef = useRef(null);
+ const [overlayOpen, setOverlayOpen] = useState(false);
+ const [query, setQuery] = useState('');
+
+ // @ts-expect-error Intl.supportedValuesOf isn't in the types yet
+ const timezones = useMemo(() => Intl.supportedValuesOf('timeZone') as string[], []);
+ const filteredTimezones = timezones.filter(
+ (timezone) =>
+ query.length === 0 || timezone.toLowerCase().replace('_', ' ').includes(query.toLowerCase())
+ );
+
+ const handleSelect = useCallback(
+ (timezone: string) => {
+ setOverlayOpen(false);
+ setValue(timezone);
+ },
+ [setOverlayOpen, setValue]
+ );
+
+ useEffect(() => {
+ if (overlayOpen) {
+ const scrollView = scrollRef.current;
+ const focusedItem = scrollView?.querySelector(`[data-tz="${value}"]`);
+
+ if (value && focusedItem && scrollView) {
+ focusedItem.scrollIntoView({
+ block: 'center',
+ });
+ }
+ }
+ }, [scrollRef, value, overlayOpen]);
+
+ return (
+
+ Timezone
+
+ }
+ >
+ }>
+
+ inputRef.current,
+ allowOutsideClick: true,
+ clickOutsideDeactivates: true,
+ onDeactivate: () => setOverlayOpen(false),
+ escapeDeactivates: (evt) => {
+ evt.stopPropagation();
+ return true;
+ },
+ }}
+ >
+
+
+
+
+
+
+ {value && (
+
+ )}
+
+
+ );
+}
+
export function Profile() {
const mx = useMatrixClient();
const userId = mx.getUserId() as string;
+ const server = getMxIdServer(userId);
const [extendedProfileState, refreshExtendedProfile] = useExtendedProfile(userId);
const extendedProfile =
@@ -234,10 +412,10 @@ export function Profile() {
// XXX: synthesise a profile update for ourselves because Synapse is broken and won't
const user = mx.getUser(userId);
if (user) {
- user.displayName = fields.displayname;
- user.avatarUrl = fields.avatar_url;
- user.emit(UserEvent.DisplayName, user.events.presence, user);
- user.emit(UserEvent.AvatarUrl, user.events.presence, user);
+ user.displayName = fields.displayname;
+ user.avatarUrl = fields.avatar_url;
+ user.emit(UserEvent.DisplayName, user.events.presence, user);
+ user.emit(UserEvent.AvatarUrl, user.events.presence, user);
}
},
[mx, userId, refreshExtendedProfile]
@@ -252,7 +430,8 @@ export function Profile() {
Profile
{(save, reset, hasChanges, fields) => {
const heroAvatarUrl =
- (fields.avatar_url &&
- mxcUrlToHttp(mx, fields.avatar_url, useAuthentication)) ??
+ (fields.avatar_url && mxcUrlToHttp(mx, fields.avatar_url, useAuthentication)) ??
undefined;
return (
<>
@@ -275,8 +453,26 @@ export function Profile() {
extendedProfile={fields}
/>
+
+ {server && }
+
+ {fields['us.cloke.msc4175.tz'] && (
+
+ )}
+
+
+
+
-
+
>
);
}}