feat(sidebar): implement home visibility toggle with hide/unhide functionality
This commit is contained in:
@@ -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];
|
||||
};
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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,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 (
|
||||
<RoomsUnreadProvider rooms={hiddenSpaces}>
|
||||
{(unread) => (
|
||||
<SidebarItem
|
||||
active={!!selectedSpaceId && hiddenSpaces.includes(selectedSpaceId)}
|
||||
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>
|
||||
{(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)) ||
|
||||
(homeHidden && homeSelected)
|
||||
}
|
||||
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>
|
||||
{unread && (
|
||||
<SidebarItemBadge hasCount={unread.total > 0}>
|
||||
<UnreadBadge highlight={unread.highlight > 0} count={unread.total} />
|
||||
</SidebarItemBadge>
|
||||
)}
|
||||
</SidebarItem>
|
||||
)}
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user