From 576e6d77c11f84e81dea2006a148777278b0501c Mon Sep 17 00:00:00 2001 From: Max Litruv Boonzaayer Date: Sat, 21 Feb 2026 01:28:11 +1100 Subject: [PATCH] feat(sidebar): implement home visibility toggle with hide/unhide functionality --- src/app/hooks/useSidebarItems.ts | 65 +++++++++ src/app/pages/client/SidebarNav.tsx | 4 +- src/app/pages/client/sidebar/HomeTab.tsx | 37 ++++- src/app/pages/client/sidebar/SpaceTabs.tsx | 153 +++++++++++++++++---- 4 files changed, 226 insertions(+), 33 deletions(-) diff --git a/src/app/hooks/useSidebarItems.ts b/src/app/hooks/useSidebarItems.ts index 69b7fbb..c5d13cc 100644 --- a/src/app/hooks/useSidebarItems.ts +++ b/src/app/hooks/useSidebarItems.ts @@ -18,6 +18,7 @@ export type InCinnySpacesContent = { shortcut?: string[]; sidebar?: SidebarItems; hidden?: string[]; + hideHome?: boolean; }; export const parseSidebar = ( @@ -238,3 +239,67 @@ export const makeUnhideSpaceContent = ( hidden: currentHidden.filter((id) => id !== spaceId), }; }; + +/** + * Create content for hiding home + */ +export const makeHideHomeContent = (mx: MatrixClient): InCinnySpacesContent => { + const currentContent = + getAccountData(mx, AccountDataEvent.CinnySpaces)?.getContent() ?? {}; + + return { + ...currentContent, + hideHome: true, + }; +}; + +/** + * Create content for unhiding home + */ +export const makeUnhideHomeContent = (mx: MatrixClient): InCinnySpacesContent => { + const currentContent = + getAccountData(mx, AccountDataEvent.CinnySpaces)?.getContent() ?? {}; + + return { + ...currentContent, + hideHome: false, + }; +}; + +/** + * Hook to get and manage home hidden state + */ +export const useHomeHidden = (): [boolean, Dispatch>] => { + const mx = useMatrixClient(); + + const [homeHidden, setHomeHidden] = useState(() => { + const content = getAccountData( + mx, + AccountDataEvent.CinnySpaces + )?.getContent(); + return content?.hideHome ?? false; + }); + + useEffect(() => { + const content = getAccountData( + mx, + AccountDataEvent.CinnySpaces + )?.getContent(); + setHomeHidden(content?.hideHome ?? false); + }, [mx]); + + useAccountDataCallback( + mx, + useCallback( + (mEvent) => { + if (mEvent.getType() === AccountDataEvent.CinnySpaces) { + const newContent = mEvent.getContent(); + setHomeHidden(newContent?.hideHome ?? false); + } + }, + [mx] + ) + ); + + return [homeHidden, setHomeHidden]; +}; diff --git a/src/app/pages/client/SidebarNav.tsx b/src/app/pages/client/SidebarNav.tsx index 221463a..68dc36c 100644 --- a/src/app/pages/client/SidebarNav.tsx +++ b/src/app/pages/client/SidebarNav.tsx @@ -19,9 +19,11 @@ import { } from './sidebar'; import { CreateTab } from './sidebar/CreateTab'; import { HiddenSpacesTabs } from './sidebar/SpaceTabs'; +import { useHomeHidden } from '../../hooks/useSidebarItems'; export function SidebarNav() { const scrollRef = useRef(null); + const [homeHidden] = useHomeHidden(); return ( @@ -29,7 +31,7 @@ export function SidebarNav() { scrollable={ - + {!homeHidden && } diff --git a/src/app/pages/client/sidebar/HomeTab.tsx b/src/app/pages/client/sidebar/HomeTab.tsx index 778b729..73d5051 100644 --- a/src/app/pages/client/sidebar/HomeTab.tsx +++ b/src/app/pages/client/sidebar/HomeTab.tsx @@ -1,6 +1,6 @@ import React, { MouseEventHandler, forwardRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { Box, Icon, Icons, Menu, MenuItem, PopOut, RectCords, Text, config, toRem } from 'folds'; +import { Box, Icon, Icons, Line, Menu, MenuItem, PopOut, RectCords, Text, config, toRem } from 'folds'; import { useAtomValue } from 'jotai'; import FocusTrap from 'focus-trap-react'; import { useOrphanRooms } from '../../../state/hooks/roomList'; @@ -26,11 +26,15 @@ import { markAsRead } from '../../../utils/notifications'; import { stopPropagation } from '../../../utils/keyboard'; import { useSetting } from '../../../state/hooks/settings'; import { settingsAtom } from '../../../state/settings'; +import { makeHideHomeContent, parseHiddenSpaces, parseSidebar, useHiddenSpaces, useSidebarItems } from '../../../hooks/useSidebarItems'; +import { AccountDataEvent } from '../../../../types/matrix/accountData'; +import { useOrphanSpaces } from '../../../state/hooks/roomList'; type HomeMenuProps = { requestClose: () => void; + onHide?: () => void; }; -const HomeMenu = forwardRef(({ requestClose }, ref) => { +const HomeMenu = forwardRef(({ requestClose, onHide }, ref) => { const orphanRooms = useHomeRooms(); const [hideActivity] = useSetting(settingsAtom, 'hideActivity'); const unread = useRoomsUnread(orphanRooms, roomToUnreadAtom); @@ -42,6 +46,11 @@ const HomeMenu = forwardRef(({ requestClose }, re requestClose(); }; + const handleHide = () => { + onHide?.(); + requestClose(); + }; + return ( @@ -56,6 +65,18 @@ const HomeMenu = forwardRef(({ requestClose }, re Mark as Read + {onHide && ( + } + > + + Hide Home + + + )} ); @@ -70,9 +91,12 @@ export function HomeTab() { const mDirects = useAtomValue(mDirectAtom); const roomToParents = useAtomValue(roomToParentsAtom); const orphanRooms = useOrphanRooms(mx, allRoomsAtom, mDirects, roomToParents); + const orphanSpaces = useOrphanSpaces(mx, allRoomsAtom, roomToParents); const homeUnread = useRoomsUnread(orphanRooms, roomToUnreadAtom); const homeSelected = useHomeSelected(); const [menuAnchor, setMenuAnchor] = useState(); + const [, localEchoSidebarItem] = useSidebarItems(orphanSpaces); + const [, localEchoHiddenSpaces] = useHiddenSpaces(); const handleHomeClick = () => { const activePath = navToActivePath.get('home'); @@ -93,6 +117,13 @@ export function HomeTab() { }); }; + const handleHide = () => { + const newContent = makeHideHomeContent(mx); + localEchoSidebarItem(parseSidebar(mx, orphanSpaces, newContent)); + localEchoHiddenSpaces(parseHiddenSpaces(mx, newContent)); + mx.setAccountData(AccountDataEvent.CinnySpaces, newContent); + }; + return ( @@ -130,7 +161,7 @@ export function HomeTab() { escapeDeactivates: stopPropagation, }} > - setMenuAnchor(undefined)} /> + setMenuAnchor(undefined)} onHide={handleHide} /> } /> diff --git a/src/app/pages/client/sidebar/SpaceTabs.tsx b/src/app/pages/client/sidebar/SpaceTabs.tsx index 843a693..a43182a 100644 --- a/src/app/pages/client/sidebar/SpaceTabs.tsx +++ b/src/app/pages/client/sidebar/SpaceTabs.tsx @@ -40,14 +40,16 @@ import { autoScrollForElements } from '@atlaskit/pragmatic-drag-and-drop-auto-sc import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine'; import FocusTrap from 'focus-trap-react'; import { + useOrphanRooms, useOrphanSpaces, useRecursiveChildScopeFactory, useSpaceChildren, } from '../../../state/hooks/roomList'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; +import { mDirectAtom } from '../../../state/mDirectList'; import { roomToParentsAtom } from '../../../state/room/roomToParents'; import { allRoomsAtom } from '../../../state/room-list/roomList'; -import { getSpaceLobbyPath, getSpacePath, joinPathComponent } from '../../pathUtils'; +import { getHomePath, getSpaceLobbyPath, getSpacePath, joinPathComponent } from '../../pathUtils'; import { SidebarAvatar, SidebarItem, @@ -60,6 +62,7 @@ import { } from '../../../components/sidebar'; import { RoomUnreadProvider, RoomsUnreadProvider } from '../../../components/RoomUnreadProvider'; import { useSelectedSpace } from '../../../hooks/router/useSelectedSpace'; +import { useHomeSelected } from '../../../hooks/router/useHomeSelected'; import { UnreadBadge } from '../../../components/unread-badge'; import { getCanonicalAliasOrRoomId, isRoomAlias } from '../../../utils/matrix'; import { RoomAvatar } from '../../../components/room-avatar'; @@ -69,12 +72,15 @@ import { SidebarItems, TSidebarItem, makeCinnySpacesContent, + makeHideHomeContent, makeHideSpaceContent, + makeUnhideHomeContent, makeUnhideSpaceContent, parseHiddenSpaces, parseSidebar, sidebarItemWithout, useHiddenSpaces, + useHomeHidden, useSidebarItems, } from '../../../hooks/useSidebarItems'; import { AccountDataEvent } from '../../../../types/matrix/accountData'; @@ -670,10 +676,13 @@ function ClosedSpaceFolder({ type HiddenSpacesFolderProps = { hiddenSpaces: string[]; + homeHidden?: boolean; open: boolean; onToggle: () => void; onSpaceClick: MouseEventHandler; + onHomeClick?: MouseEventHandler; onUnhide: (roomId: string) => void; + onUnhideHome?: () => void; selectedSpaceId?: string; }; @@ -682,21 +691,42 @@ type HiddenSpacesFolderProps = { */ function HiddenSpacesFolder({ hiddenSpaces, + homeHidden, open, onToggle, onSpaceClick, + onHomeClick, onUnhide, + onUnhideHome, selectedSpaceId, }: HiddenSpacesFolderProps) { const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); + const homeSelected = useHomeSelected(); const [menuAnchor, setMenuAnchor] = useState(); const [menuSpaceId, setMenuSpaceId] = useState(); + const [menuIsHome, setMenuIsHome] = useState(false); + + const mDirects = useAtomValue(mDirectAtom); + const roomToParents = useAtomValue(roomToParentsAtom); + const orphanRooms = useOrphanRooms(mx, allRoomsAtom, mDirects, roomToParents); + const homeUnread = useRoomsUnread(orphanRooms, roomToUnreadAtom); const handleContextMenu = (spaceId: string): MouseEventHandler => (evt) => { evt.preventDefault(); const cords = evt.currentTarget.getBoundingClientRect(); setMenuSpaceId(spaceId); + setMenuIsHome(false); + setMenuAnchor((currentState) => { + if (currentState) return undefined; + return cords; + }); + }; + + const handleHomeContextMenu: MouseEventHandler = (evt) => { + evt.preventDefault(); + const cords = evt.currentTarget.getBoundingClientRect(); + setMenuIsHome(true); setMenuAnchor((currentState) => { if (currentState) return undefined; return cords; @@ -704,7 +734,9 @@ function HiddenSpacesFolder({ }; const handleUnhideClick = () => { - if (menuSpaceId) { + if (menuIsHome) { + onUnhideHome?.(); + } else if (menuSpaceId) { onUnhide(menuSpaceId); } setMenuAnchor(undefined); @@ -719,35 +751,53 @@ function HiddenSpacesFolder({ }); if (!open) { - // Only show when hovered, has unreads, or a hidden space is selected - const isVisible = hovered || hasUnreads || (!!selectedSpaceId && hiddenSpaces.includes(selectedSpaceId)); + // Only show when hovered, has unreads (from spaces or home), or a hidden space/home is selected + const isVisible = + hovered || + hasUnreads || + (homeHidden && !!homeUnread) || + (!!selectedSpaceId && hiddenSpaces.includes(selectedSpaceId)) || + (homeHidden && homeSelected); return ( - {(unread) => ( - setHovered(true)} - onMouseLeave={() => setHovered(false)} - style={{ - opacity: isVisible ? 1 : 0, - transition: 'opacity 0.15s ease', - }} - > - - {(tooltipRef) => ( - - - + {(unread) => { + // Combine space unreads with home unreads if home is hidden + const totalUnread = homeHidden && homeUnread + ? { + total: (unread?.total ?? 0) + homeUnread.total, + highlight: (unread?.highlight ?? 0) + homeUnread.highlight, + } + : unread; + + return ( + setHovered(true)} + onMouseLeave={() => setHovered(false)} + style={{ + opacity: isVisible ? 1 : 0, + transition: 'opacity 0.15s ease', + }} + > + + {(tooltipRef) => ( + + + + )} + + {totalUnread && ( + 0}> + 0} count={totalUnread.total} /> + )} - - {unread && ( - 0}> - 0} count={unread.total} /> - - )} - - )} + + ); + }} ); } @@ -759,6 +809,29 @@ function HiddenSpacesFolder({ + {homeHidden && ( + + + {(triggerRef) => ( + + + + )} + + {homeUnread && ( + 0}> + 0} count={homeUnread.total} /> + + )} + + )} {hiddenSpaces.map((spaceId) => { const space = mx.getRoom(spaceId); if (!space) return null; @@ -825,7 +898,7 @@ function HiddenSpacesFolder({ after={} > - Unhide Space + {menuIsHome ? 'Unhide Home' : 'Unhide Space'} @@ -1121,6 +1194,7 @@ export function HiddenSpacesTabs() { const navToActivePath = useAtomValue(useNavToActivePathAtom()); const orphanSpaces = useOrphanSpaces(mx, allRoomsAtom, useAtomValue(roomToParentsAtom)); const [hiddenSpaces, localEchoHiddenSpaces] = useHiddenSpaces(); + const [homeHidden, localEchoHomeHidden] = useHomeHidden(); const [, localEchoSidebarItem] = useSidebarItems(orphanSpaces); const [hiddenFolderOpen, setHiddenFolderOpen] = useState(false); @@ -1146,6 +1220,16 @@ export function HiddenSpacesTabs() { navigate(getSpaceLobbyPath(getCanonicalAliasOrRoomId(mx, targetSpaceId))); }; + const handleHomeClick = () => { + const activePath = navToActivePath.get('home'); + if (activePath && screenSize !== ScreenSize.Mobile) { + navigate(joinPathComponent(activePath)); + return; + } + + navigate(getHomePath()); + }; + const handleUnhide = useCallback( (roomId: string) => { const newContent = makeUnhideSpaceContent(mx, roomId); @@ -1156,7 +1240,15 @@ export function HiddenSpacesTabs() { [mx, orphanSpaces, localEchoSidebarItem, localEchoHiddenSpaces] ); - if (hiddenSpaces.length === 0) return null; + const handleUnhideHome = useCallback(() => { + const newContent = makeUnhideHomeContent(mx); + localEchoHomeHidden(false); + localEchoSidebarItem(parseSidebar(mx, orphanSpaces, newContent)); + localEchoHiddenSpaces(parseHiddenSpaces(mx, newContent)); + mx.setAccountData(AccountDataEvent.CinnySpaces, newContent); + }, [mx, orphanSpaces, localEchoHomeHidden, localEchoSidebarItem, localEchoHiddenSpaces]); + + if (hiddenSpaces.length === 0 && !homeHidden) return null; return ( <> @@ -1164,10 +1256,13 @@ export function HiddenSpacesTabs() { setHiddenFolderOpen(!hiddenFolderOpen)} onSpaceClick={handleSpaceClick} + onHomeClick={handleHomeClick} onUnhide={handleUnhide} + onUnhideHome={handleUnhideHome} selectedSpaceId={selectedSpaceId} />