diff --git a/.changeset/feat_right_click_rename_folder.md b/.changeset/feat_right_click_rename_folder.md new file mode 100644 index 000000000..4d34eef28 --- /dev/null +++ b/.changeset/feat_right_click_rename_folder.md @@ -0,0 +1,5 @@ +--- +default: minor +--- + +Added the ability to right click on a folder to rename it. diff --git a/src/app/hooks/useSidebarItems.ts b/src/app/hooks/useSidebarItems.ts index 13c79e17b..1b366ebb8 100644 --- a/src/app/hooks/useSidebarItems.ts +++ b/src/app/hooks/useSidebarItems.ts @@ -102,6 +102,18 @@ export const useSidebarItems = ( return [sidebarItems, setSidebarItems]; }; +export const renameSidebarFolderItem = ( + items: SidebarItems, + folderId: string, + name: string | undefined +): SidebarItems => { + const trimmed = name?.trim(); + const nextName = trimmed ? trimmed : undefined; + return items.map((item) => + typeof item === 'object' && item.id === folderId ? { ...item, name: nextName } : item + ); +}; + export const sidebarItemWithout = (items: SidebarItems, roomId: string) => { const newItems: SidebarItems = items .map((item) => { diff --git a/src/app/pages/client/sidebar/SpaceTabs.tsx b/src/app/pages/client/sidebar/SpaceTabs.tsx index 4ffd0b160..e9e2a6e5a 100644 --- a/src/app/pages/client/sidebar/SpaceTabs.tsx +++ b/src/app/pages/client/sidebar/SpaceTabs.tsx @@ -1,22 +1,29 @@ -import type { MouseEventHandler, ReactNode, RefObject } from 'react'; +import type { FormEventHandler, MouseEventHandler, ReactNode, RefObject, ChangeEvent } from 'react'; import { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import type { RectCords } from 'folds'; import { Box, + Button, + Dialog, + Header, Icon, IconButton, Icons, + Input, Line, Menu, MenuItem, + Overlay, + OverlayBackdrop, + OverlayCenter, PopOut, Text, config, toRem, } from 'folds'; import { useAtom, useAtomValue } from 'jotai'; -import type { Room } from '$types/matrix-sdk'; +import type { MatrixClient, Room } from '$types/matrix-sdk'; import { draggable, dropTargetForElements, @@ -58,6 +65,7 @@ import type { ISidebarFolder, SidebarItems, TSidebarItem } from '$hooks/useSideb import { makeCinnySpacesContent, parseSidebar, + renameSidebarFolderItem, sidebarItemWithout, useSidebarItems, } from '$hooks/useSidebarItems'; @@ -213,6 +221,121 @@ const SpaceMenu = forwardRef( } ); +type FolderMenuProps = { + requestClose: () => void; + onRename: () => void; +}; +const FolderMenu = forwardRef( + ({ requestClose, onRename }, ref) => ( + + + { + onRename(); + requestClose(); + }} + after={} + > + + Rename + + + + + ) +); + +const FOLDER_NAME_MAX_LENGTH = 200; + +const folderDefaultDisplayName = (mx: MatrixClient, folder: ISidebarFolder): string => { + const auto = folder.content.map((i) => mx.getRoom(i)?.name ?? '').join(', '); + return (folder.name ?? auto) || 'Unnamed'; +}; + +type RenameFolderDialogProps = { + mx: MatrixClient; + folder: ISidebarFolder; + onClose: () => void; + onSave: (name: string) => void; +}; +function RenameFolderDialog({ mx, folder, onClose, onSave }: Readonly) { + const [draft, setDraft] = useState(() => folderDefaultDisplayName(mx, folder)); + + useEffect(() => { + setDraft(folderDefaultDisplayName(mx, folder)); + }, [mx, folder]); + + const handleSubmit: FormEventHandler = (evt) => { + evt.preventDefault(); + onSave(draft); + }; + + return ( + }> + + + +
+ + Rename Folder + + + + +
+ + + Choose a short label for this folder. Leave empty to show space names again. + + + Folder name + ) => setDraft(e.target.value)} + autoFocus + /> + + + + + + +
+
+
+
+ ); +} + type InstructionType = Instruction['type']; type FolderDraggable = { folder: ISidebarFolder; @@ -495,9 +618,15 @@ function SpaceTab({ type OpenedSpaceFolderProps = { folder: ISidebarFolder; onClose: MouseEventHandler; + onFolderContextMenu?: MouseEventHandler; children?: ReactNode; }; -function OpenedSpaceFolder({ folder, onClose, children }: Readonly) { +function OpenedSpaceFolder({ + folder, + onClose, + onFolderContextMenu, + children, +}: Readonly) { const aboveTargetRef = useRef(null); const belowTargetRef = useRef(null); @@ -513,7 +642,7 @@ function OpenedSpaceFolder({ folder, onClose, children }: Readonly - + @@ -530,6 +659,7 @@ type ClosedSpaceFolderProps = { onOpen: MouseEventHandler; onDragging: (dragItem?: SidebarDraggable) => void; disabled?: boolean; + onFolderContextMenu?: MouseEventHandler; }; function ClosedSpaceFolder({ folder, @@ -537,6 +667,7 @@ function ClosedSpaceFolder({ onOpen, onDragging, disabled, + onFolderContextMenu, }: Readonly) { const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); @@ -547,8 +678,7 @@ function ClosedSpaceFolder({ const dropState = useDropTarget(spaceDraggable, handlerRef); const dropType = dropState?.type; - const tooltipName = - folder.name ?? folder.content.map((i) => mx.getRoom(i)?.name ?? '').join(', ') ?? 'Unnamed'; + const tooltipName = folderDefaultDisplayName(mx, folder); return ( @@ -563,7 +693,13 @@ function ClosedSpaceFolder({ > {(tooltipRef) => ( - + {folder.content.map((sId) => { const space = mx.getRoom(sId); if (!space) return null; @@ -612,6 +748,33 @@ export function SpaceTabs({ scrollRef }: Readonly) { const navToActivePath = useAtomValue(useNavToActivePathAtom()); const [openedFolder, setOpenedFolder] = useAtom(useOpenedSidebarFolderAtom()); const [draggingItem, setDraggingItem] = useState(); + const [folderMenuState, setFolderMenuState] = useState<{ + folder: ISidebarFolder; + anchor: RectCords; + }>(); + const [renameTargetFolder, setRenameTargetFolder] = useState(); + + const handleFolderContextMenu = useCallback( + (folder: ISidebarFolder): MouseEventHandler => + (evt) => { + evt.preventDefault(); + setFolderMenuState({ + folder, + anchor: evt.currentTarget.getBoundingClientRect(), + }); + }, + [] + ); + + const handleRenameFolderApply = useCallback( + (folderId: string, rawName: string) => { + const newItems = renameSidebarFolderItem(sidebarItems, folderId, rawName); + const newSpacesContent = makeCinnySpacesContent(mx, newItems); + localEchoSidebarItem(parseSidebar(mx, orphanSpaces, newSpacesContent)); + mx.setAccountData(CustomAccountDataEvent.CinnySpaces, newSpacesContent); + }, + [mx, sidebarItems, orphanSpaces, localEchoSidebarItem] + ); useDnDMonitor( scrollRef, @@ -797,13 +960,54 @@ export function SpaceTabs({ scrollRef }: Readonly) { if (sidebarItems.length === 0) return null; return ( <> + {folderMenuState && ( + setFolderMenuState(undefined), + clickOutsideDeactivates: true, + isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown', + isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp', + escapeDeactivates: stopPropagation, + }} + > + setFolderMenuState(undefined)} + onRename={() => setRenameTargetFolder(folderMenuState.folder)} + /> + + } + /> + )} + {renameTargetFolder && ( + setRenameTargetFolder(undefined)} + onSave={(name) => { + handleRenameFolderApply(renameTargetFolder.id, name); + setRenameTargetFolder(undefined); + }} + /> + )} {sidebarItems.map((item) => { if (typeof item === 'object') { if (openedFolder.has(item.id)) { return ( - + {item.content.map((sId) => { const space = mx.getRoom(sId); if (!space) return null; @@ -835,6 +1039,7 @@ export function SpaceTabs({ scrollRef }: Readonly) { selected={!!selectedSpaceId && item.content.includes(selectedSpaceId)} onOpen={handleFolderToggle} onDragging={setDraggingItem} + onFolderContextMenu={handleFolderContextMenu(item)} disabled={ typeof draggingItem === 'object' ? draggingItem.folder.id === item.id : false }