diff --git a/src/app/components/setting-tile/SettingTile.tsx b/src/app/components/setting-tile/SettingTile.tsx index 2656903a..5c09732d 100644 --- a/src/app/components/setting-tile/SettingTile.tsx +++ b/src/app/components/setting-tile/SettingTile.tsx @@ -2,7 +2,7 @@ import React, { ReactNode } from 'react'; import { Box, Text } from 'folds'; type SettingTileProps = { - title: ReactNode; + title?: ReactNode; description?: ReactNode; before?: ReactNode; after?: ReactNode; @@ -13,7 +13,7 @@ export function SettingTile({ title, description, before, after, children }: Set {before && {before}} - {title} + {title && {title}} {description && ( {description} diff --git a/src/app/features/settings/Account.tsx b/src/app/features/settings/Account.tsx new file mode 100644 index 00000000..7ab14aa6 --- /dev/null +++ b/src/app/features/settings/Account.tsx @@ -0,0 +1,184 @@ +import React, { useCallback, useEffect } from 'react'; +import { Box, Text, IconButton, Icon, Icons, Scroll, Input, Avatar, Button, Chip } from 'folds'; +import { Page, PageContent, PageHeader } from '../../components/page'; +import { SequenceCard } from '../../components/sequence-card'; +import { SequenceCardStyle } from './styles.css'; +import { SettingTile } from '../../components/setting-tile'; +import { useMatrixClient } from '../../hooks/useMatrixClient'; +import { useUserProfile } from '../../hooks/useUserProfile'; +import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix'; +import { UserAvatar } from '../../components/user-avatar'; +import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; +import { nameInitials } from '../../utils/common'; +import { copyToClipboard } from '../../utils/dom'; +import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; + +function MatrixId() { + const mx = useMatrixClient(); + const userId = mx.getUserId()!; + + return ( + + Matrix ID + + copyToClipboard(userId)}> + Copy + + } + /> + + + ); +} + +function Profile() { + const mx = useMatrixClient(); + const useAuthentication = useMediaAuthentication(); + const userId = mx.getUserId()!; + const profile = useUserProfile(userId); + + const defaultDisplayName = profile.displayName ?? getMxIdLocalPart(userId) ?? userId; + const avatarUrl = profile.avatarUrl + ? mxcUrlToHttp(mx, profile.avatarUrl, useAuthentication, 96, 96, 'crop') ?? undefined + : undefined; + + return ( + + Profile + + + Avatar + + } + before={ + + {nameInitials(defaultDisplayName)}} + /> + + } + > + + + {avatarUrl && ( + + )} + + + + Display Name + + } + > + + + + + + + + + + + + + ); +} + +function ContactInformation() { + const mx = useMatrixClient(); + const [threePIdsState, loadThreePIds] = useAsyncCallback( + useCallback(() => mx.getThreePids(), [mx]) + ); + const threePIds = + threePIdsState.status === AsyncStatus.Success ? threePIdsState.data.threepids : undefined; + + const emailIds = threePIds?.filter((id) => id.medium === 'email'); + + useEffect(() => { + loadThreePIds(); + }, [loadThreePIds]); + + return ( + + Contact Information + + + + {emailIds?.map((email) => ( + + {email.address} + + ))} + + {/* */} + + + + ); +} + +type AccountProps = { + requestClose: () => void; +}; +export function Account({ requestClose }: AccountProps) { + return ( + + + + + + Account + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/src/app/features/settings/General.tsx b/src/app/features/settings/General.tsx index 4a2840f5..1ad15d32 100644 --- a/src/app/features/settings/General.tsx +++ b/src/app/features/settings/General.tsx @@ -1,6 +1,5 @@ import React, { ChangeEventHandler, - CSSProperties, KeyboardEventHandler, MouseEventHandler, useState, @@ -45,10 +44,7 @@ import { import { stopPropagation } from '../../utils/keyboard'; import { useMessageLayoutItems } from '../../hooks/useMessageLayout'; import { useMessageSpacingItems } from '../../hooks/useMessageSpacing'; - -const SequenceCardStyle: CSSProperties = { - padding: config.space.S300, -}; +import { SequenceCardStyle } from './styles.css'; type ThemeSelectorProps = { themeNames: Record; @@ -286,7 +282,7 @@ function PageZoomInput() { return ( Appearance - + } - + - + } /> - + } /> @@ -347,7 +348,7 @@ function Editor() { return ( Editor - + } /> - + } @@ -521,13 +522,13 @@ function Messages() { return ( Messages - + } /> - + } /> - + - + - + } /> - + } /> - + } /> - + void; }; export function Settings({ requestClose }: SettingsProps) { + const mx = useMatrixClient(); + const useAuthentication = useMediaAuthentication(); + const userId = mx.getUserId()!; + const profile = useUserProfile(userId); + const displayName = profile.displayName ?? getMxIdLocalPart(userId) ?? userId; + const avatarUrl = profile.avatarUrl + ? mxcUrlToHttp(mx, profile.avatarUrl, useAuthentication, 96, 96, 'crop') ?? undefined + : undefined; + const screenSize = useScreenSizeContext(); const [activePage, setActivePage] = useState( screenSize === ScreenSize.Mobile ? undefined : SettingsPages.GeneralPage @@ -92,8 +108,17 @@ export function Settings({ requestClose }: SettingsProps) { screenSize === ScreenSize.Mobile && activePage !== undefined ? undefined : ( - - Settings + + + {nameInitials(displayName)}} + /> + + + Settings + {screenSize === ScreenSize.Mobile && ( @@ -133,6 +158,9 @@ export function Settings({ requestClose }: SettingsProps) { {activePage === SettingsPages.GeneralPage && ( )} + {activePage === SettingsPages.AccountPage && ( + + )} ); } diff --git a/src/app/features/settings/index.ts b/src/app/features/settings/index.ts index 4fddb27e..90e26973 100644 --- a/src/app/features/settings/index.ts +++ b/src/app/features/settings/index.ts @@ -1,2 +1 @@ export * from './Settings'; -export * from './General'; diff --git a/src/app/features/settings/styles.css.ts b/src/app/features/settings/styles.css.ts new file mode 100644 index 00000000..ce89c16e --- /dev/null +++ b/src/app/features/settings/styles.css.ts @@ -0,0 +1,6 @@ +import { style } from '@vanilla-extract/css'; +import { config } from 'folds'; + +export const SequenceCardStyle = style({ + padding: config.space.S300, +}); diff --git a/src/app/hooks/useUserProfile.ts b/src/app/hooks/useUserProfile.ts new file mode 100644 index 00000000..c7cb7487 --- /dev/null +++ b/src/app/hooks/useUserProfile.ts @@ -0,0 +1,51 @@ +import { useEffect, useState } from 'react'; +import { UserEvent, UserEventHandlerMap } from 'matrix-js-sdk'; +import { useMatrixClient } from './useMatrixClient'; + +export type UserProfile = { + avatarUrl?: string; + displayName?: string; +}; +export const useUserProfile = (userId: string): UserProfile => { + const mx = useMatrixClient(); + + const [profile, setProfile] = useState(() => { + const user = mx.getUser(userId); + return { + avatarUrl: user?.avatarUrl, + displayName: user?.displayName, + }; + }); + + useEffect(() => { + const user = mx.getUser(userId); + const onAvatarChange: UserEventHandlerMap[UserEvent.AvatarUrl] = (event, myUser) => { + setProfile((cp) => ({ + ...cp, + avatarUrl: myUser.avatarUrl, + })); + }; + const onDisplayNameChange: UserEventHandlerMap[UserEvent.DisplayName] = (event, myUser) => { + setProfile((cp) => ({ + ...cp, + displayName: myUser.displayName, + })); + }; + + mx.getProfileInfo(userId).then((info) => + setProfile({ + avatarUrl: info.avatar_url, + displayName: info.displayname, + }) + ); + + user?.on(UserEvent.AvatarUrl, onAvatarChange); + user?.on(UserEvent.DisplayName, onDisplayNameChange); + return () => { + user?.removeListener(UserEvent.AvatarUrl, onAvatarChange); + user?.removeListener(UserEvent.DisplayName, onDisplayNameChange); + }; + }, [mx, userId]); + + return profile; +}; diff --git a/src/app/pages/client/sidebar/SettingsTab.tsx b/src/app/pages/client/sidebar/SettingsTab.tsx index bd00dee6..f19b9221 100644 --- a/src/app/pages/client/sidebar/SettingsTab.tsx +++ b/src/app/pages/client/sidebar/SettingsTab.tsx @@ -1,7 +1,6 @@ -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { Modal, Overlay, OverlayBackdrop, OverlayCenter, Text } from 'folds'; import FocusTrap from 'focus-trap-react'; -import { UserEvent, UserEventHandlerMap } from 'matrix-js-sdk'; import { SidebarItem, SidebarItemTooltip, SidebarAvatar } from '../../../components/sidebar'; import { UserAvatar } from '../../../components/user-avatar'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; @@ -10,54 +9,7 @@ import { nameInitials } from '../../../utils/common'; import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; import { Settings } from '../../../features/settings'; import { stopPropagation } from '../../../utils/keyboard'; - -type UserProfile = { - avatarUrl?: string; - displayName?: string; -}; -const useUserProfile = (userId: string): UserProfile => { - const mx = useMatrixClient(); - - const [profile, setProfile] = useState(() => { - const user = mx.getUser(userId); - return { - avatarUrl: user?.avatarUrl, - displayName: user?.displayName, - }; - }); - - useEffect(() => { - const user = mx.getUser(userId); - const onAvatarChange: UserEventHandlerMap[UserEvent.AvatarUrl] = (event, myUser) => { - setProfile((cp) => ({ - ...cp, - avatarUrl: myUser.avatarUrl, - })); - }; - const onDisplayNameChange: UserEventHandlerMap[UserEvent.DisplayName] = (event, myUser) => { - setProfile((cp) => ({ - ...cp, - displayName: myUser.displayName, - })); - }; - - mx.getProfileInfo(userId).then((info) => - setProfile({ - avatarUrl: info.avatar_url, - displayName: info.displayname, - }) - ); - - user?.on(UserEvent.AvatarUrl, onAvatarChange); - user?.on(UserEvent.DisplayName, onDisplayNameChange); - return () => { - user?.removeListener(UserEvent.AvatarUrl, onAvatarChange); - user?.removeListener(UserEvent.DisplayName, onDisplayNameChange); - }; - }, [mx, userId]); - - return profile; -}; +import { useUserProfile } from '../../../hooks/useUserProfile'; export function SettingsTab() { const mx = useMatrixClient();