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

View File

@@ -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<HTMLDivElement, HomeMenuProps>(({ requestClose }, ref) => {
const HomeMenu = forwardRef<HTMLDivElement, HomeMenuProps>(({ requestClose, onHide }, ref) => {
const orphanRooms = useHomeRooms();
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const unread = useRoomsUnread(orphanRooms, roomToUnreadAtom);
@@ -42,6 +46,11 @@ const HomeMenu = forwardRef<HTMLDivElement, HomeMenuProps>(({ requestClose }, re
requestClose();
};
const handleHide = () => {
onHide?.();
requestClose();
};
return (
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
@@ -56,6 +65,18 @@ const HomeMenu = forwardRef<HTMLDivElement, HomeMenuProps>(({ requestClose }, re
Mark as Read
</Text>
</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>
</Menu>
);
@@ -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<RectCords>();
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 (
<SidebarItem active={homeSelected}>
<SidebarItemTooltip tooltip="Home">
@@ -130,7 +161,7 @@ export function HomeTab() {
escapeDeactivates: stopPropagation,
}}
>
<HomeMenu requestClose={() => setMenuAnchor(undefined)} />
<HomeMenu requestClose={() => setMenuAnchor(undefined)} onHide={handleHide} />
</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 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<HTMLButtonElement>;
onHomeClick?: MouseEventHandler<HTMLButtonElement>;
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<RectCords>();
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) => {
evt.preventDefault();
const cords = evt.currentTarget.getBoundingClientRect();
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) => {
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,14 +751,31 @@ 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 (
<RoomsUnreadProvider rooms={hiddenSpaces}>
{(unread) => (
{(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 (
<SidebarItem
active={!!selectedSpaceId && hiddenSpaces.includes(selectedSpaceId)}
active={
(!!selectedSpaceId && hiddenSpaces.includes(selectedSpaceId)) ||
(homeHidden && homeSelected)
}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
style={{
@@ -741,13 +790,14 @@ function HiddenSpacesFolder({
</SidebarAvatar>
)}
</SidebarItemTooltip>
{unread && (
<SidebarItemBadge hasCount={unread.total > 0}>
<UnreadBadge highlight={unread.highlight > 0} count={unread.total} />
{totalUnread && (
<SidebarItemBadge hasCount={totalUnread.total > 0}>
<UnreadBadge highlight={totalUnread.highlight > 0} count={totalUnread.total} />
</SidebarItemBadge>
)}
</SidebarItem>
)}
);
}}
</RoomsUnreadProvider>
);
}
@@ -759,6 +809,29 @@ function HiddenSpacesFolder({
<Icon size="400" src={Icons.ChevronTop} filled />
</IconButton>
</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) => {
const space = mx.getRoom(spaceId);
if (!space) return null;
@@ -825,7 +898,7 @@ function HiddenSpacesFolder({
after={<Icon size="100" src={Icons.Eye} />}
>
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
Unhide Space
{menuIsHome ? 'Unhide Home' : 'Unhide Space'}
</Text>
</MenuItem>
</Box>
@@ -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() {
<SidebarStack>
<HiddenSpacesFolder
hiddenSpaces={hiddenSpaces}
homeHidden={homeHidden}
open={hiddenFolderOpen}
onToggle={() => setHiddenFolderOpen(!hiddenFolderOpen)}
onSpaceClick={handleSpaceClick}
onHomeClick={handleHomeClick}
onUnhide={handleUnhide}
onUnhideHome={handleUnhideHome}
selectedSpaceId={selectedSpaceId}
/>
</SidebarStack>