feat(sidebar): implement home visibility toggle with hide/unhide functionality

This commit is contained in:
2026-02-21 01:28:11 +11:00
parent 0bb1b411f4
commit 576e6d77c1
4 changed files with 226 additions and 33 deletions

View File

@@ -18,6 +18,7 @@ export type InCinnySpacesContent = {
shortcut?: string[]; shortcut?: string[];
sidebar?: SidebarItems; sidebar?: SidebarItems;
hidden?: string[]; hidden?: string[];
hideHome?: boolean;
}; };
export const parseSidebar = ( export const parseSidebar = (
@@ -238,3 +239,67 @@ export const makeUnhideSpaceContent = (
hidden: currentHidden.filter((id) => id !== spaceId), hidden: currentHidden.filter((id) => id !== spaceId),
}; };
}; };
/**
* Create content for hiding home
*/
export const makeHideHomeContent = (mx: MatrixClient): InCinnySpacesContent => {
const currentContent =
getAccountData(mx, AccountDataEvent.CinnySpaces)?.getContent<InCinnySpacesContent>() ?? {};
return {
...currentContent,
hideHome: true,
};
};
/**
* Create content for unhiding home
*/
export const makeUnhideHomeContent = (mx: MatrixClient): InCinnySpacesContent => {
const currentContent =
getAccountData(mx, AccountDataEvent.CinnySpaces)?.getContent<InCinnySpacesContent>() ?? {};
return {
...currentContent,
hideHome: false,
};
};
/**
* Hook to get and manage home hidden state
*/
export const useHomeHidden = (): [boolean, Dispatch<SetStateAction<boolean>>] => {
const mx = useMatrixClient();
const [homeHidden, setHomeHidden] = useState(() => {
const content = getAccountData(
mx,
AccountDataEvent.CinnySpaces
)?.getContent<InCinnySpacesContent>();
return content?.hideHome ?? false;
});
useEffect(() => {
const content = getAccountData(
mx,
AccountDataEvent.CinnySpaces
)?.getContent<InCinnySpacesContent>();
setHomeHidden(content?.hideHome ?? false);
}, [mx]);
useAccountDataCallback(
mx,
useCallback(
(mEvent) => {
if (mEvent.getType() === AccountDataEvent.CinnySpaces) {
const newContent = mEvent.getContent<InCinnySpacesContent>();
setHomeHidden(newContent?.hideHome ?? false);
}
},
[mx]
)
);
return [homeHidden, setHomeHidden];
};

View File

@@ -19,9 +19,11 @@ import {
} from './sidebar'; } from './sidebar';
import { CreateTab } from './sidebar/CreateTab'; import { CreateTab } from './sidebar/CreateTab';
import { HiddenSpacesTabs } from './sidebar/SpaceTabs'; import { HiddenSpacesTabs } from './sidebar/SpaceTabs';
import { useHomeHidden } from '../../hooks/useSidebarItems';
export function SidebarNav() { export function SidebarNav() {
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
const [homeHidden] = useHomeHidden();
return ( return (
<Sidebar> <Sidebar>
@@ -29,7 +31,7 @@ export function SidebarNav() {
scrollable={ scrollable={
<Scroll ref={scrollRef} variant="Background" size="0"> <Scroll ref={scrollRef} variant="Background" size="0">
<SidebarStack> <SidebarStack>
<HomeTab /> {!homeHidden && <HomeTab />}
<DirectTab /> <DirectTab />
</SidebarStack> </SidebarStack>
<SpaceTabs scrollRef={scrollRef} /> <SpaceTabs scrollRef={scrollRef} />

View File

@@ -1,6 +1,6 @@
import React, { MouseEventHandler, forwardRef, useState } from 'react'; import React, { MouseEventHandler, forwardRef, useState } from 'react';
import { useNavigate } from 'react-router-dom'; 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 { useAtomValue } from 'jotai';
import FocusTrap from 'focus-trap-react'; import FocusTrap from 'focus-trap-react';
import { useOrphanRooms } from '../../../state/hooks/roomList'; import { useOrphanRooms } from '../../../state/hooks/roomList';
@@ -26,11 +26,15 @@ import { markAsRead } from '../../../utils/notifications';
import { stopPropagation } from '../../../utils/keyboard'; import { stopPropagation } from '../../../utils/keyboard';
import { useSetting } from '../../../state/hooks/settings'; import { useSetting } from '../../../state/hooks/settings';
import { settingsAtom } from '../../../state/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 = { type HomeMenuProps = {
requestClose: () => void; requestClose: () => void;
onHide?: () => void;
}; };
const HomeMenu = forwardRef<HTMLDivElement, HomeMenuProps>(({ requestClose }, ref) => { const HomeMenu = forwardRef<HTMLDivElement, HomeMenuProps>(({ requestClose, onHide }, ref) => {
const orphanRooms = useHomeRooms(); const orphanRooms = useHomeRooms();
const [hideActivity] = useSetting(settingsAtom, 'hideActivity'); const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const unread = useRoomsUnread(orphanRooms, roomToUnreadAtom); const unread = useRoomsUnread(orphanRooms, roomToUnreadAtom);
@@ -42,6 +46,11 @@ const HomeMenu = forwardRef<HTMLDivElement, HomeMenuProps>(({ requestClose }, re
requestClose(); requestClose();
}; };
const handleHide = () => {
onHide?.();
requestClose();
};
return ( return (
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}> <Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}> <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
@@ -56,6 +65,18 @@ const HomeMenu = forwardRef<HTMLDivElement, HomeMenuProps>(({ requestClose }, re
Mark as Read Mark as Read
</Text> </Text>
</MenuItem> </MenuItem>
{onHide && (
<MenuItem
size="300"
radii="300"
onClick={handleHide}
after={<Icon size="100" src={Icons.EyeBlind} />}
>
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
Hide Home
</Text>
</MenuItem>
)}
</Box> </Box>
</Menu> </Menu>
); );
@@ -70,9 +91,12 @@ export function HomeTab() {
const mDirects = useAtomValue(mDirectAtom); const mDirects = useAtomValue(mDirectAtom);
const roomToParents = useAtomValue(roomToParentsAtom); const roomToParents = useAtomValue(roomToParentsAtom);
const orphanRooms = useOrphanRooms(mx, allRoomsAtom, mDirects, roomToParents); const orphanRooms = useOrphanRooms(mx, allRoomsAtom, mDirects, roomToParents);
const orphanSpaces = useOrphanSpaces(mx, allRoomsAtom, roomToParents);
const homeUnread = useRoomsUnread(orphanRooms, roomToUnreadAtom); const homeUnread = useRoomsUnread(orphanRooms, roomToUnreadAtom);
const homeSelected = useHomeSelected(); const homeSelected = useHomeSelected();
const [menuAnchor, setMenuAnchor] = useState<RectCords>(); const [menuAnchor, setMenuAnchor] = useState<RectCords>();
const [, localEchoSidebarItem] = useSidebarItems(orphanSpaces);
const [, localEchoHiddenSpaces] = useHiddenSpaces();
const handleHomeClick = () => { const handleHomeClick = () => {
const activePath = navToActivePath.get('home'); 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 ( return (
<SidebarItem active={homeSelected}> <SidebarItem active={homeSelected}>
<SidebarItemTooltip tooltip="Home"> <SidebarItemTooltip tooltip="Home">
@@ -130,7 +161,7 @@ export function HomeTab() {
escapeDeactivates: stopPropagation, escapeDeactivates: stopPropagation,
}} }}
> >
<HomeMenu requestClose={() => setMenuAnchor(undefined)} /> <HomeMenu requestClose={() => setMenuAnchor(undefined)} onHide={handleHide} />
</FocusTrap> </FocusTrap>
} }
/> />

View File

@@ -40,14 +40,16 @@ import { autoScrollForElements } from '@atlaskit/pragmatic-drag-and-drop-auto-sc
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine'; import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
import FocusTrap from 'focus-trap-react'; import FocusTrap from 'focus-trap-react';
import { import {
useOrphanRooms,
useOrphanSpaces, useOrphanSpaces,
useRecursiveChildScopeFactory, useRecursiveChildScopeFactory,
useSpaceChildren, useSpaceChildren,
} from '../../../state/hooks/roomList'; } from '../../../state/hooks/roomList';
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { mDirectAtom } from '../../../state/mDirectList';
import { roomToParentsAtom } from '../../../state/room/roomToParents'; import { roomToParentsAtom } from '../../../state/room/roomToParents';
import { allRoomsAtom } from '../../../state/room-list/roomList'; import { allRoomsAtom } from '../../../state/room-list/roomList';
import { getSpaceLobbyPath, getSpacePath, joinPathComponent } from '../../pathUtils'; import { getHomePath, getSpaceLobbyPath, getSpacePath, joinPathComponent } from '../../pathUtils';
import { import {
SidebarAvatar, SidebarAvatar,
SidebarItem, SidebarItem,
@@ -60,6 +62,7 @@ import {
} from '../../../components/sidebar'; } from '../../../components/sidebar';
import { RoomUnreadProvider, RoomsUnreadProvider } from '../../../components/RoomUnreadProvider'; import { RoomUnreadProvider, RoomsUnreadProvider } from '../../../components/RoomUnreadProvider';
import { useSelectedSpace } from '../../../hooks/router/useSelectedSpace'; import { useSelectedSpace } from '../../../hooks/router/useSelectedSpace';
import { useHomeSelected } from '../../../hooks/router/useHomeSelected';
import { UnreadBadge } from '../../../components/unread-badge'; import { UnreadBadge } from '../../../components/unread-badge';
import { getCanonicalAliasOrRoomId, isRoomAlias } from '../../../utils/matrix'; import { getCanonicalAliasOrRoomId, isRoomAlias } from '../../../utils/matrix';
import { RoomAvatar } from '../../../components/room-avatar'; import { RoomAvatar } from '../../../components/room-avatar';
@@ -69,12 +72,15 @@ import {
SidebarItems, SidebarItems,
TSidebarItem, TSidebarItem,
makeCinnySpacesContent, makeCinnySpacesContent,
makeHideHomeContent,
makeHideSpaceContent, makeHideSpaceContent,
makeUnhideHomeContent,
makeUnhideSpaceContent, makeUnhideSpaceContent,
parseHiddenSpaces, parseHiddenSpaces,
parseSidebar, parseSidebar,
sidebarItemWithout, sidebarItemWithout,
useHiddenSpaces, useHiddenSpaces,
useHomeHidden,
useSidebarItems, useSidebarItems,
} from '../../../hooks/useSidebarItems'; } from '../../../hooks/useSidebarItems';
import { AccountDataEvent } from '../../../../types/matrix/accountData'; import { AccountDataEvent } from '../../../../types/matrix/accountData';
@@ -670,10 +676,13 @@ function ClosedSpaceFolder({
type HiddenSpacesFolderProps = { type HiddenSpacesFolderProps = {
hiddenSpaces: string[]; hiddenSpaces: string[];
homeHidden?: boolean;
open: boolean; open: boolean;
onToggle: () => void; onToggle: () => void;
onSpaceClick: MouseEventHandler<HTMLButtonElement>; onSpaceClick: MouseEventHandler<HTMLButtonElement>;
onHomeClick?: MouseEventHandler<HTMLButtonElement>;
onUnhide: (roomId: string) => void; onUnhide: (roomId: string) => void;
onUnhideHome?: () => void;
selectedSpaceId?: string; selectedSpaceId?: string;
}; };
@@ -682,21 +691,42 @@ type HiddenSpacesFolderProps = {
*/ */
function HiddenSpacesFolder({ function HiddenSpacesFolder({
hiddenSpaces, hiddenSpaces,
homeHidden,
open, open,
onToggle, onToggle,
onSpaceClick, onSpaceClick,
onHomeClick,
onUnhide, onUnhide,
onUnhideHome,
selectedSpaceId, selectedSpaceId,
}: HiddenSpacesFolderProps) { }: HiddenSpacesFolderProps) {
const mx = useMatrixClient(); const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication(); const useAuthentication = useMediaAuthentication();
const homeSelected = useHomeSelected();
const [menuAnchor, setMenuAnchor] = useState<RectCords>(); const [menuAnchor, setMenuAnchor] = useState<RectCords>();
const [menuSpaceId, setMenuSpaceId] = useState<string>(); const [menuSpaceId, setMenuSpaceId] = useState<string>();
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<HTMLButtonElement> => (evt) => { const handleContextMenu = (spaceId: string): MouseEventHandler<HTMLButtonElement> => (evt) => {
evt.preventDefault(); evt.preventDefault();
const cords = evt.currentTarget.getBoundingClientRect(); const cords = evt.currentTarget.getBoundingClientRect();
setMenuSpaceId(spaceId); setMenuSpaceId(spaceId);
setMenuIsHome(false);
setMenuAnchor((currentState) => {
if (currentState) return undefined;
return cords;
});
};
const handleHomeContextMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
evt.preventDefault();
const cords = evt.currentTarget.getBoundingClientRect();
setMenuIsHome(true);
setMenuAnchor((currentState) => { setMenuAnchor((currentState) => {
if (currentState) return undefined; if (currentState) return undefined;
return cords; return cords;
@@ -704,7 +734,9 @@ function HiddenSpacesFolder({
}; };
const handleUnhideClick = () => { const handleUnhideClick = () => {
if (menuSpaceId) { if (menuIsHome) {
onUnhideHome?.();
} else if (menuSpaceId) {
onUnhide(menuSpaceId); onUnhide(menuSpaceId);
} }
setMenuAnchor(undefined); setMenuAnchor(undefined);
@@ -719,35 +751,53 @@ function HiddenSpacesFolder({
}); });
if (!open) { if (!open) {
// Only show when hovered, has unreads, or a hidden space is selected // Only show when hovered, has unreads (from spaces or home), or a hidden space/home is selected
const isVisible = hovered || hasUnreads || (!!selectedSpaceId && hiddenSpaces.includes(selectedSpaceId)); const isVisible =
hovered ||
hasUnreads ||
(homeHidden && !!homeUnread) ||
(!!selectedSpaceId && hiddenSpaces.includes(selectedSpaceId)) ||
(homeHidden && homeSelected);
return ( return (
<RoomsUnreadProvider rooms={hiddenSpaces}> <RoomsUnreadProvider rooms={hiddenSpaces}>
{(unread) => ( {(unread) => {
<SidebarItem // Combine space unreads with home unreads if home is hidden
active={!!selectedSpaceId && hiddenSpaces.includes(selectedSpaceId)} const totalUnread = homeHidden && homeUnread
onMouseEnter={() => setHovered(true)} ? {
onMouseLeave={() => setHovered(false)} total: (unread?.total ?? 0) + homeUnread.total,
style={{ highlight: (unread?.highlight ?? 0) + homeUnread.highlight,
opacity: isVisible ? 1 : 0, }
transition: 'opacity 0.15s ease', : unread;
}}
> return (
<SidebarItemTooltip tooltip="Hidden Spaces"> <SidebarItem
{(tooltipRef) => ( active={
<SidebarAvatar as="button" ref={tooltipRef} outlined onClick={onToggle}> (!!selectedSpaceId && hiddenSpaces.includes(selectedSpaceId)) ||
<Icon src={Icons.EyeBlind} /> (homeHidden && homeSelected)
</SidebarAvatar> }
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
style={{
opacity: isVisible ? 1 : 0,
transition: 'opacity 0.15s ease',
}}
>
<SidebarItemTooltip tooltip="Hidden Spaces">
{(tooltipRef) => (
<SidebarAvatar as="button" ref={tooltipRef} outlined onClick={onToggle}>
<Icon src={Icons.EyeBlind} />
</SidebarAvatar>
)}
</SidebarItemTooltip>
{totalUnread && (
<SidebarItemBadge hasCount={totalUnread.total > 0}>
<UnreadBadge highlight={totalUnread.highlight > 0} count={totalUnread.total} />
</SidebarItemBadge>
)} )}
</SidebarItemTooltip> </SidebarItem>
{unread && ( );
<SidebarItemBadge hasCount={unread.total > 0}> }}
<UnreadBadge highlight={unread.highlight > 0} count={unread.total} />
</SidebarItemBadge>
)}
</SidebarItem>
)}
</RoomsUnreadProvider> </RoomsUnreadProvider>
); );
} }
@@ -759,6 +809,29 @@ function HiddenSpacesFolder({
<Icon size="400" src={Icons.ChevronTop} filled /> <Icon size="400" src={Icons.ChevronTop} filled />
</IconButton> </IconButton>
</SidebarAvatar> </SidebarAvatar>
{homeHidden && (
<SidebarItem active={homeSelected} data-inside-folder>
<SidebarItemTooltip tooltip="Home">
{(triggerRef) => (
<SidebarAvatar
as="button"
ref={triggerRef}
size="300"
outlined
onClick={onHomeClick}
onContextMenu={handleHomeContextMenu}
>
<Icon src={Icons.Home} filled={homeSelected} />
</SidebarAvatar>
)}
</SidebarItemTooltip>
{homeUnread && (
<SidebarItemBadge hasCount={homeUnread.total > 0}>
<UnreadBadge highlight={homeUnread.highlight > 0} count={homeUnread.total} />
</SidebarItemBadge>
)}
</SidebarItem>
)}
{hiddenSpaces.map((spaceId) => { {hiddenSpaces.map((spaceId) => {
const space = mx.getRoom(spaceId); const space = mx.getRoom(spaceId);
if (!space) return null; if (!space) return null;
@@ -825,7 +898,7 @@ function HiddenSpacesFolder({
after={<Icon size="100" src={Icons.Eye} />} after={<Icon size="100" src={Icons.Eye} />}
> >
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate> <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
Unhide Space {menuIsHome ? 'Unhide Home' : 'Unhide Space'}
</Text> </Text>
</MenuItem> </MenuItem>
</Box> </Box>
@@ -1121,6 +1194,7 @@ export function HiddenSpacesTabs() {
const navToActivePath = useAtomValue(useNavToActivePathAtom()); const navToActivePath = useAtomValue(useNavToActivePathAtom());
const orphanSpaces = useOrphanSpaces(mx, allRoomsAtom, useAtomValue(roomToParentsAtom)); const orphanSpaces = useOrphanSpaces(mx, allRoomsAtom, useAtomValue(roomToParentsAtom));
const [hiddenSpaces, localEchoHiddenSpaces] = useHiddenSpaces(); const [hiddenSpaces, localEchoHiddenSpaces] = useHiddenSpaces();
const [homeHidden, localEchoHomeHidden] = useHomeHidden();
const [, localEchoSidebarItem] = useSidebarItems(orphanSpaces); const [, localEchoSidebarItem] = useSidebarItems(orphanSpaces);
const [hiddenFolderOpen, setHiddenFolderOpen] = useState(false); const [hiddenFolderOpen, setHiddenFolderOpen] = useState(false);
@@ -1146,6 +1220,16 @@ export function HiddenSpacesTabs() {
navigate(getSpaceLobbyPath(getCanonicalAliasOrRoomId(mx, targetSpaceId))); 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( const handleUnhide = useCallback(
(roomId: string) => { (roomId: string) => {
const newContent = makeUnhideSpaceContent(mx, roomId); const newContent = makeUnhideSpaceContent(mx, roomId);
@@ -1156,7 +1240,15 @@ export function HiddenSpacesTabs() {
[mx, orphanSpaces, localEchoSidebarItem, localEchoHiddenSpaces] [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 ( return (
<> <>
@@ -1164,10 +1256,13 @@ export function HiddenSpacesTabs() {
<SidebarStack> <SidebarStack>
<HiddenSpacesFolder <HiddenSpacesFolder
hiddenSpaces={hiddenSpaces} hiddenSpaces={hiddenSpaces}
homeHidden={homeHidden}
open={hiddenFolderOpen} open={hiddenFolderOpen}
onToggle={() => setHiddenFolderOpen(!hiddenFolderOpen)} onToggle={() => setHiddenFolderOpen(!hiddenFolderOpen)}
onSpaceClick={handleSpaceClick} onSpaceClick={handleSpaceClick}
onHomeClick={handleHomeClick}
onUnhide={handleUnhide} onUnhide={handleUnhide}
onUnhideHome={handleUnhideHome}
selectedSpaceId={selectedSpaceId} selectedSpaceId={selectedSpaceId}
/> />
</SidebarStack> </SidebarStack>