diff --git a/src/app/features/common-settings/cosmetics/Cosmetics.tsx b/src/app/features/common-settings/cosmetics/Cosmetics.tsx index 70c4cacd01..a286f6ab1c 100644 --- a/src/app/features/common-settings/cosmetics/Cosmetics.tsx +++ b/src/app/features/common-settings/cosmetics/Cosmetics.tsx @@ -1,5 +1,31 @@ -import { useCallback } from 'react'; -import { Box, Text, IconButton, Icon, Icons, Scroll, Switch } from 'folds'; +import { + ChangeEventHandler, + FormEventHandler, + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; +import { + Box, + Text, + IconButton, + Icon, + Icons, + Scroll, + Switch, + Avatar, + Input, + config, + Button, + Spinner, + OverlayBackdrop, + Overlay, + OverlayCenter, + Modal, + Dialog, + Header, +} from 'folds'; import { Page, PageContent, PageHeader } from '$components/page'; import { SequenceCard } from '$components/sequence-card'; import { SettingTile } from '$components/setting-tile'; @@ -11,17 +37,285 @@ import { useRoomCreators } from '$hooks/useRoomCreators'; import { useRoomPermissions } from '$hooks/useRoomPermissions'; import { createLogger } from '$utils/debug'; import { SequenceCardStyle } from '$features/common-settings/styles.css'; +import { UserAvatar } from '$components/user-avatar'; +import { nameInitials } from '$utils/common'; +import { useMediaAuthentication } from '$hooks/useMediaAuthentication'; +import { UserProfile, useUserProfile } from '$hooks/useUserProfile'; +import { getMxIdLocalPart, mxcUrlToHttp } from '$utils/matrix'; +import { AsyncStatus, useAsyncCallback } from '$hooks/useAsyncCallback'; +import { Room, RoomMember } from '$types/matrix-sdk'; +import { Command, useCommands } from '$hooks/useCommands'; +import { useCapabilities } from '$hooks/useCapabilities'; +import { useObjectURL } from '$hooks/useObjectURL'; +import { createUploadAtom, UploadSuccess } from '$state/upload'; +import { useFilePicker } from '$hooks/useFilePicker'; +import { CompactUploadCardRenderer } from '$components/upload-card'; +import FocusTrap from 'focus-trap-react'; +import { ImageEditor } from '$components/image-editor'; +import { stopPropagation } from '$utils/keyboard'; +import { ModalWide } from '$styles/Modal.css'; -type CosmeticsProps = { - requestClose: () => void; +const log = createLogger('Cosmetics'); + +type CosmeticsSettingProps = { + profile: UserProfile; + member: RoomMember; + userId: string; + room: Room; }; +export function CosmeticsAvatar({ profile, member, userId, room }: CosmeticsSettingProps) { + const mx = useMatrixClient(); + const useAuthentication = useMediaAuthentication(); + const capabilities = useCapabilities(); + const [alertRemove, setAlertRemove] = useState(false); + const disableSetAvatar = capabilities['m.set_avatar_url']?.enabled === false; -const log = createLogger('Cosmetics'); + const avatarMxc = member.getMxcAvatarUrl(); + const avatarUrl = + avatarMxc && (mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined); + + const [imageFile, setImageFile] = useState(); + const imageFileURL = useObjectURL(imageFile); + const uploadAtom = useMemo(() => { + if (imageFile) return createUploadAtom(imageFile); + return undefined; + }, [imageFile]); + + const pickFile = useFilePicker(setImageFile, false); + + const handleRemoveUpload = useCallback(() => { + setImageFile(undefined); + }, []); + + const myRoomAvatar = useCommands(mx, room)[Command.MyRoomAvatar]; + const handleUploaded = useCallback( + (upload: UploadSuccess) => { + const { mxc } = upload; + myRoomAvatar.exe(mxc); + handleRemoveUpload(); + }, + [myRoomAvatar, handleRemoveUpload] + ); + + const handleRemoveAvatar = () => { + myRoomAvatar.exe(''); + setAlertRemove(false); + }; + + return ( + + ( + {nameInitials(room.getMember(userId)!.rawDisplayName)} + )} + /> + + } + > + {uploadAtom ? ( + + + + ) : ( + + + {avatarUrl && + avatarUrl !== + mxcUrlToHttp(mx, profile.avatarUrl ?? '', useAuthentication, 96, 96, 'crop') && ( + + )} + + )} + + {imageFileURL && ( + }> + + + + + + + + + )} + + }> + + setAlertRemove(false), + clickOutsideDeactivates: true, + escapeDeactivates: stopPropagation, + }} + > + +
+ + Remove Room Avatar + + setAlertRemove(false)} radii="300"> + + +
+ + + Are you sure you want to remove room avatar? + + + +
+
+
+
+
+ ); +} + +export function CosmeticsNickname({ profile, member, userId, room }: CosmeticsSettingProps) { + const mx = useMatrixClient(); + + const defaultDisplayName = member.rawDisplayName; + const [displayName, setDisplayName] = useState(defaultDisplayName); + const hasChanges = displayName !== defaultDisplayName; + + const myRoomNick = useCommands(mx, room)[Command.MyRoomNick]; + const [changeState, changeDisplayName] = useAsyncCallback((name: string) => myRoomNick.exe(name)); + const changingDisplayName = changeState.status === AsyncStatus.Loading; + + useEffect(() => { + setDisplayName(defaultDisplayName); + }, [defaultDisplayName]); + + const handleChange: ChangeEventHandler = (evt) => { + const name = evt.currentTarget.value; + setDisplayName(name); + }; + + const handleReset = () => { + if (hasChanges) { + setDisplayName(defaultDisplayName); + } else { + setDisplayName(profile.displayName ?? getMxIdLocalPart(userId) ?? userId); + } + }; + const handleSubmit: FormEventHandler = (evt) => { + evt.preventDefault(); + if (changingDisplayName) return; + + const target = evt.target as HTMLFormElement | undefined; + const displayNameInput = target?.displayNameInput as HTMLInputElement | undefined; + const name = displayNameInput?.value; + + changeDisplayName(name ?? ''); + }; + + return ( + + + + + + + + ) + } + /> + + + + + + ); +} + +type CosmeticsProps = { + requestClose: () => void; +}; export function Cosmetics({ requestClose }: CosmeticsProps) { const mx = useMatrixClient(); + const userId = mx.getUserId()!; + const profile = useUserProfile(userId); const room = useRoom(); const creators = useRoomCreators(room); + const member = room.getMember(userId)!; const powerLevels = usePowerLevels(room); const isSpace = room.isSpaceRoom(); @@ -70,9 +364,74 @@ export function Cosmetics({ requestClose }: CosmeticsProps) { + + Profile + {!isSpace && ( + + + + )} + {!isSpace && ( + + + + )} + + + + + + + + + + Settings - - - - - {/* --- COMMAND REFERENCE SECTION --- */} - - Commands - - - - - - - - - - - - - diff --git a/src/app/features/room-settings/RoomSettings.tsx b/src/app/features/room-settings/RoomSettings.tsx index 8707cefa6d..7c973d9954 100644 --- a/src/app/features/room-settings/RoomSettings.tsx +++ b/src/app/features/room-settings/RoomSettings.tsx @@ -24,6 +24,7 @@ type RoomSettingsMenuItem = { page: RoomSettingsPage; name: string; icon: IconSrc; + activeIcon?: IconSrc; }; const useRoomSettingsMenuItems = (): RoomSettingsMenuItem[] => @@ -48,6 +49,7 @@ const useRoomSettingsMenuItems = (): RoomSettingsMenuItem[] => page: RoomSettingsPage.CosmeticsPage, name: 'Cosmetics', icon: Icons.Alphabet, + activeIcon: Icons.AlphabetUnderline, }, { page: RoomSettingsPage.EmojisStickersPage, @@ -141,29 +143,34 @@ export function RoomSettings({ initialPage, requestClose }: RoomSettingsProps) {
- {menuItems.map((item) => ( - - } - onClick={() => setActivePage(item.page)} - > - { + const currentIcon = + activePage === item.page && item.activeIcon ? item.activeIcon : item.icon; + + return ( + + } + onClick={() => setActivePage(item.page)} > - {item.name} - - - ))} + + {item.name} + + + ); + })}
diff --git a/src/app/features/space-settings/SpaceSettings.tsx b/src/app/features/space-settings/SpaceSettings.tsx index 79ab84429d..ddd1d5c74e 100644 --- a/src/app/features/space-settings/SpaceSettings.tsx +++ b/src/app/features/space-settings/SpaceSettings.tsx @@ -23,6 +23,7 @@ type SpaceSettingsMenuItem = { page: SpaceSettingsPage; name: string; icon: IconSrc; + activeIcon?: IconSrc; }; const useSpaceSettingsMenuItems = (): SpaceSettingsMenuItem[] => @@ -47,6 +48,7 @@ const useSpaceSettingsMenuItems = (): SpaceSettingsMenuItem[] => page: SpaceSettingsPage.CosmeticsPage, name: 'Cosmetics', icon: Icons.Alphabet, + activeIcon: Icons.AlphabetUnderline, }, { page: SpaceSettingsPage.EmojisStickersPage, @@ -132,26 +134,34 @@ export function SpaceSettings({ initialPage, requestClose }: SpaceSettingsProps)
- {menuItems.map((item) => ( - } - onClick={() => setActivePage(item.page)} - > - { + const currentIcon = + activePage === item.page && item.activeIcon ? item.activeIcon : item.icon; + + return ( + + } + onClick={() => setActivePage(item.page)} > - {item.name} - - - ))} + + {item.name} + + + ); + })}