feat(call): add docked call panel and remote cursor overlay
- Implemented DockedCallPanel component to render the docked call interface when an active call is present. - Created RemoteCursorOverlay component to display remote cursor positions during screen sharing. - Added hooks for managing docked call state and remote cursor functionality. - Introduced pending drag state management for seamless transitions between docked and floating states. - Developed embed filter management components for personal and room-wide settings, allowing users to customize URL embed visibility. - Implemented auto-joining functionality for rooms within spaces, enhancing user experience during space navigation. - Added state management for tracking joining progress of rooms in spaces.
This commit is contained in:
@@ -3,7 +3,6 @@ import { Provider as JotaiProvider } from 'jotai';
|
||||
import { OverlayContainerProvider, PopOutContainerProvider, TooltipContainerProvider } from 'folds';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
|
||||
import { ClientConfigLoader } from '../components/ClientConfigLoader';
|
||||
import { ClientConfigProvider } from '../hooks/useClientConfig';
|
||||
@@ -39,7 +38,6 @@ function App() {
|
||||
<JotaiProvider>
|
||||
<RouterProvider router={createRouter(clientConfig, screenSize)} />
|
||||
</JotaiProvider>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
</ClientConfigProvider>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { Box } from 'folds';
|
||||
import { DockedCallPanel } from '../../features/call/DockedCallPanel';
|
||||
|
||||
type ClientLayoutProps = {
|
||||
nav: ReactNode;
|
||||
@@ -10,6 +11,7 @@ export function ClientLayout({ nav, children }: ClientLayoutProps) {
|
||||
<Box grow="Yes">
|
||||
<Box shrink="No">{nav}</Box>
|
||||
<Box grow="Yes">{children}</Box>
|
||||
<DockedCallPanel />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
SearchTab,
|
||||
} from './sidebar';
|
||||
import { CreateTab } from './sidebar/CreateTab';
|
||||
import { HiddenSpacesTabs } from './sidebar/SpaceTabs';
|
||||
|
||||
export function SidebarNav() {
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
@@ -37,6 +38,7 @@ export function SidebarNav() {
|
||||
<ExploreTab />
|
||||
<CreateTab />
|
||||
</SidebarStack>
|
||||
<HiddenSpacesTabs />
|
||||
</Scroll>
|
||||
}
|
||||
sticky={
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { Room } from 'matrix-js-sdk';
|
||||
import { factoryRoomIdByActivity, factoryRoomIdByAtoZ } from '../../../utils/sort';
|
||||
import {
|
||||
NavButton,
|
||||
@@ -64,44 +65,73 @@ import {
|
||||
} from '../../../hooks/useRoomsNotificationPreferences';
|
||||
import { UseStateProvider } from '../../../components/UseStateProvider';
|
||||
import { JoinAddressPrompt } from '../../../components/join-address-prompt';
|
||||
import { ManageRoomsPrompt } from '../../../components/manage-rooms-prompt';
|
||||
import { _RoomSearchParams } from '../../paths';
|
||||
|
||||
type HomeMenuProps = {
|
||||
requestClose: () => void;
|
||||
onManageRooms: () => void;
|
||||
};
|
||||
const HomeMenu = forwardRef<HTMLDivElement, HomeMenuProps>(({ requestClose }, ref) => {
|
||||
const orphanRooms = useHomeRooms();
|
||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||
const unread = useRoomsUnread(orphanRooms, roomToUnreadAtom);
|
||||
const mx = useMatrixClient();
|
||||
const HomeMenu = forwardRef<HTMLDivElement, HomeMenuProps>(
|
||||
({ requestClose, onManageRooms }, ref) => {
|
||||
const orphanRooms = useHomeRooms();
|
||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||
const unread = useRoomsUnread(orphanRooms, roomToUnreadAtom);
|
||||
const mx = useMatrixClient();
|
||||
|
||||
const handleMarkAsRead = () => {
|
||||
if (!unread) return;
|
||||
orphanRooms.forEach((rId) => markAsRead(mx, rId, hideActivity));
|
||||
requestClose();
|
||||
};
|
||||
const handleMarkAsRead = () => {
|
||||
if (!unread) return;
|
||||
orphanRooms.forEach((rId) => markAsRead(mx, rId, hideActivity));
|
||||
requestClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
<MenuItem
|
||||
onClick={handleMarkAsRead}
|
||||
size="300"
|
||||
after={<Icon size="100" src={Icons.CheckTwice} />}
|
||||
radii="300"
|
||||
aria-disabled={!unread}
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||
Mark as Read
|
||||
</Text>
|
||||
</MenuItem>
|
||||
</Box>
|
||||
</Menu>
|
||||
);
|
||||
});
|
||||
const handleManageRooms = () => {
|
||||
requestClose();
|
||||
onManageRooms();
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
<MenuItem
|
||||
onClick={handleMarkAsRead}
|
||||
size="300"
|
||||
after={<Icon size="100" src={Icons.CheckTwice} />}
|
||||
radii="300"
|
||||
aria-disabled={!unread}
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||
Mark as Read
|
||||
</Text>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={handleManageRooms}
|
||||
size="300"
|
||||
after={<Icon size="100" src={Icons.Setting} />}
|
||||
radii="300"
|
||||
aria-disabled={orphanRooms.length === 0}
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||
Manage Rooms
|
||||
</Text>
|
||||
</MenuItem>
|
||||
</Box>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
function HomeHeader() {
|
||||
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
|
||||
const [showManageRooms, setShowManageRooms] = useState(false);
|
||||
const mx = useMatrixClient();
|
||||
const orphanRooms = useHomeRooms();
|
||||
|
||||
const rooms = useMemo(() => {
|
||||
return orphanRooms
|
||||
.map((roomId) => mx.getRoom(roomId))
|
||||
.filter((room): room is Room => room !== null);
|
||||
}, [mx, orphanRooms]);
|
||||
|
||||
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
const cords = evt.currentTarget.getBoundingClientRect();
|
||||
@@ -144,10 +174,20 @@ function HomeHeader() {
|
||||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<HomeMenu requestClose={() => setMenuAnchor(undefined)} />
|
||||
<HomeMenu
|
||||
requestClose={() => setMenuAnchor(undefined)}
|
||||
onManageRooms={() => setShowManageRooms(true)}
|
||||
/>
|
||||
</FocusTrap>
|
||||
}
|
||||
/>
|
||||
{showManageRooms && (
|
||||
<ManageRoomsPrompt
|
||||
rooms={rooms}
|
||||
onDone={() => setShowManageRooms(false)}
|
||||
onCancel={() => setShowManageRooms(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,8 +69,12 @@ import {
|
||||
SidebarItems,
|
||||
TSidebarItem,
|
||||
makeCinnySpacesContent,
|
||||
makeHideSpaceContent,
|
||||
makeUnhideSpaceContent,
|
||||
parseHiddenSpaces,
|
||||
parseSidebar,
|
||||
sidebarItemWithout,
|
||||
useHiddenSpaces,
|
||||
useSidebarItems,
|
||||
} from '../../../hooks/useSidebarItems';
|
||||
import { AccountDataEvent } from '../../../../types/matrix/accountData';
|
||||
@@ -93,14 +97,17 @@ import { useOpenSpaceSettings } from '../../../state/hooks/spaceSettings';
|
||||
import { useRoomCreators } from '../../../hooks/useRoomCreators';
|
||||
import { useRoomPermissions } from '../../../hooks/useRoomPermissions';
|
||||
import { InviteUserPrompt } from '../../../components/invite-user-prompt';
|
||||
import { UseStateProvider } from '../../../components/UseStateProvider';
|
||||
import { LeaveSpacePrompt } from '../../../components/leave-space-prompt';
|
||||
|
||||
type SpaceMenuProps = {
|
||||
room: Room;
|
||||
requestClose: () => void;
|
||||
onUnpin?: (roomId: string) => void;
|
||||
onHide?: (roomId: string) => void;
|
||||
};
|
||||
const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(
|
||||
({ room, requestClose, onUnpin }, ref) => {
|
||||
({ room, requestClose, onUnpin, onHide }, ref) => {
|
||||
const mx = useMatrixClient();
|
||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||
@@ -146,6 +153,11 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(
|
||||
requestClose();
|
||||
};
|
||||
|
||||
const handleHide = () => {
|
||||
onHide?.(room.roomId);
|
||||
requestClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
|
||||
{invitePrompt && room && (
|
||||
@@ -181,6 +193,18 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(
|
||||
</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 Space
|
||||
</Text>
|
||||
</MenuItem>
|
||||
)}
|
||||
</Box>
|
||||
<Line variant="Surface" size="300" />
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
@@ -219,6 +243,42 @@ const SpaceMenu = forwardRef<HTMLDivElement, SpaceMenuProps>(
|
||||
</Text>
|
||||
</MenuItem>
|
||||
</Box>
|
||||
<Line variant="Surface" size="300" />
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
<UseStateProvider initial={false}>
|
||||
{(promptLeave, setPromptLeave) => (
|
||||
<>
|
||||
<MenuItem
|
||||
onClick={(evt) => {
|
||||
if (evt.shiftKey) {
|
||||
mx.leave(room.roomId);
|
||||
requestClose();
|
||||
} else {
|
||||
setPromptLeave(true);
|
||||
}
|
||||
}}
|
||||
variant="Critical"
|
||||
fill="None"
|
||||
size="300"
|
||||
after={<Icon size="100" src={Icons.ArrowGoLeft} />}
|
||||
radii="300"
|
||||
aria-pressed={promptLeave}
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||
Leave Space
|
||||
</Text>
|
||||
</MenuItem>
|
||||
{promptLeave && (
|
||||
<LeaveSpacePrompt
|
||||
roomId={room.roomId}
|
||||
onDone={requestClose}
|
||||
onCancel={() => setPromptLeave(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</UseStateProvider>
|
||||
</Box>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
@@ -390,6 +450,7 @@ type SpaceTabProps = {
|
||||
onDragging: (dragItem?: SidebarDraggable) => void;
|
||||
disabled?: boolean;
|
||||
onUnpin?: (roomId: string) => void;
|
||||
onHide?: (roomId: string) => void;
|
||||
};
|
||||
function SpaceTab({
|
||||
space,
|
||||
@@ -399,6 +460,7 @@ function SpaceTab({
|
||||
onDragging,
|
||||
disabled,
|
||||
onUnpin,
|
||||
onHide,
|
||||
}: SpaceTabProps) {
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
@@ -489,6 +551,7 @@ function SpaceTab({
|
||||
room={space}
|
||||
requestClose={() => setMenuAnchor(undefined)}
|
||||
onUnpin={onUnpin}
|
||||
onHide={onHide}
|
||||
/>
|
||||
</FocusTrap>
|
||||
}
|
||||
@@ -605,6 +668,176 @@ function ClosedSpaceFolder({
|
||||
);
|
||||
}
|
||||
|
||||
type HiddenSpacesFolderProps = {
|
||||
hiddenSpaces: string[];
|
||||
open: boolean;
|
||||
onToggle: () => void;
|
||||
onSpaceClick: MouseEventHandler<HTMLButtonElement>;
|
||||
onUnhide: (roomId: string) => void;
|
||||
selectedSpaceId?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A special folder that displays hidden spaces
|
||||
*/
|
||||
function HiddenSpacesFolder({
|
||||
hiddenSpaces,
|
||||
open,
|
||||
onToggle,
|
||||
onSpaceClick,
|
||||
onUnhide,
|
||||
selectedSpaceId,
|
||||
}: HiddenSpacesFolderProps) {
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
|
||||
const [menuSpaceId, setMenuSpaceId] = useState<string>();
|
||||
|
||||
const handleContextMenu = (spaceId: string): MouseEventHandler<HTMLButtonElement> => (evt) => {
|
||||
evt.preventDefault();
|
||||
const cords = evt.currentTarget.getBoundingClientRect();
|
||||
setMenuSpaceId(spaceId);
|
||||
setMenuAnchor((currentState) => {
|
||||
if (currentState) return undefined;
|
||||
return cords;
|
||||
});
|
||||
};
|
||||
|
||||
const handleUnhideClick = () => {
|
||||
if (menuSpaceId) {
|
||||
onUnhide(menuSpaceId);
|
||||
}
|
||||
setMenuAnchor(undefined);
|
||||
};
|
||||
|
||||
const [hovered, setHovered] = useState(false);
|
||||
|
||||
// Check if any hidden space has unreads
|
||||
const hasUnreads = hiddenSpaces.some((spaceId) => {
|
||||
const space = mx.getRoom(spaceId);
|
||||
return space && space.getUnreadNotificationCount() > 0;
|
||||
});
|
||||
|
||||
if (!open) {
|
||||
// Only show when hovered, has unreads, or a hidden space is selected
|
||||
const isVisible = hovered || hasUnreads || (!!selectedSpaceId && hiddenSpaces.includes(selectedSpaceId));
|
||||
|
||||
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>
|
||||
)}
|
||||
</SidebarItemTooltip>
|
||||
{unread && (
|
||||
<SidebarItemBadge hasCount={unread.total > 0}>
|
||||
<UnreadBadge highlight={unread.highlight > 0} count={unread.total} />
|
||||
</SidebarItemBadge>
|
||||
)}
|
||||
</SidebarItem>
|
||||
)}
|
||||
</RoomsUnreadProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarFolder state="Open">
|
||||
<SidebarAvatar size="300">
|
||||
<IconButton size="300" variant="Background" onClick={onToggle}>
|
||||
<Icon size="400" src={Icons.ChevronTop} filled />
|
||||
</IconButton>
|
||||
</SidebarAvatar>
|
||||
{hiddenSpaces.map((spaceId) => {
|
||||
const space = mx.getRoom(spaceId);
|
||||
if (!space) return null;
|
||||
const selected = space.roomId === selectedSpaceId;
|
||||
|
||||
return (
|
||||
<RoomUnreadProvider key={space.roomId} roomId={space.roomId}>
|
||||
{(unread) => (
|
||||
<SidebarItem active={selected} data-inside-folder>
|
||||
<SidebarItemTooltip tooltip={space.name}>
|
||||
{(triggerRef) => (
|
||||
<SidebarAvatar
|
||||
as="button"
|
||||
data-id={space.roomId}
|
||||
ref={triggerRef}
|
||||
size="300"
|
||||
onClick={onSpaceClick}
|
||||
onContextMenu={handleContextMenu(space.roomId)}
|
||||
>
|
||||
<RoomAvatar
|
||||
roomId={space.roomId}
|
||||
src={getRoomAvatarUrl(mx, space, 96, useAuthentication) ?? undefined}
|
||||
alt={space.name}
|
||||
renderFallback={() => (
|
||||
<Text size="H6">{nameInitials(space.name, 2)}</Text>
|
||||
)}
|
||||
/>
|
||||
</SidebarAvatar>
|
||||
)}
|
||||
</SidebarItemTooltip>
|
||||
{unread && (
|
||||
<SidebarItemBadge hasCount={unread.total > 0}>
|
||||
<UnreadBadge highlight={unread.highlight > 0} count={unread.total} />
|
||||
</SidebarItemBadge>
|
||||
)}
|
||||
</SidebarItem>
|
||||
)}
|
||||
</RoomUnreadProvider>
|
||||
);
|
||||
})}
|
||||
{menuAnchor && (
|
||||
<PopOut
|
||||
anchor={menuAnchor}
|
||||
position="Right"
|
||||
align="Start"
|
||||
content={
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
returnFocusOnDeactivate: false,
|
||||
onDeactivate: () => setMenuAnchor(undefined),
|
||||
clickOutsideDeactivates: true,
|
||||
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
||||
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
||||
escapeDeactivates: stopPropagation,
|
||||
}}
|
||||
>
|
||||
<Menu style={{ maxWidth: toRem(160), width: '100vw' }}>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
<MenuItem
|
||||
size="300"
|
||||
radii="300"
|
||||
onClick={handleUnhideClick}
|
||||
after={<Icon size="100" src={Icons.Eye} />}
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||
Unhide Space
|
||||
</Text>
|
||||
</MenuItem>
|
||||
</Box>
|
||||
</Menu>
|
||||
</FocusTrap>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</SidebarFolder>
|
||||
);
|
||||
}
|
||||
|
||||
type SpaceTabsProps = {
|
||||
scrollRef: RefObject<HTMLDivElement>;
|
||||
};
|
||||
@@ -615,6 +848,7 @@ export function SpaceTabs({ scrollRef }: SpaceTabsProps) {
|
||||
const roomToParents = useAtomValue(roomToParentsAtom);
|
||||
const orphanSpaces = useOrphanSpaces(mx, allRoomsAtom, roomToParents);
|
||||
const [sidebarItems, localEchoSidebarItem] = useSidebarItems(orphanSpaces);
|
||||
const [, localEchoHiddenSpaces] = useHiddenSpaces();
|
||||
const navToActivePath = useAtomValue(useNavToActivePathAtom());
|
||||
const [openedFolder, setOpenedFolder] = useAtom(useOpenedSidebarFolderAtom());
|
||||
const [draggingItem, setDraggingItem] = useState<SidebarDraggable>();
|
||||
@@ -797,6 +1031,16 @@ export function SpaceTabs({ scrollRef }: SpaceTabsProps) {
|
||||
[mx, sidebarItems, orphanSpaces, localEchoSidebarItem]
|
||||
);
|
||||
|
||||
const handleHide = useCallback(
|
||||
(roomId: string) => {
|
||||
const newContent = makeHideSpaceContent(mx, roomId);
|
||||
localEchoSidebarItem(parseSidebar(mx, orphanSpaces, newContent));
|
||||
localEchoHiddenSpaces(parseHiddenSpaces(mx, newContent));
|
||||
mx.setAccountData(AccountDataEvent.CinnySpaces, newContent);
|
||||
},
|
||||
[mx, orphanSpaces, localEchoSidebarItem, localEchoHiddenSpaces]
|
||||
);
|
||||
|
||||
if (sidebarItems.length === 0) return null;
|
||||
return (
|
||||
<>
|
||||
@@ -824,6 +1068,7 @@ export function SpaceTabs({ scrollRef }: SpaceTabsProps) {
|
||||
: false
|
||||
}
|
||||
onUnpin={orphanSpaces.includes(space.roomId) ? undefined : handleUnpin}
|
||||
onHide={handleHide}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@@ -857,6 +1102,7 @@ export function SpaceTabs({ scrollRef }: SpaceTabsProps) {
|
||||
onDragging={setDraggingItem}
|
||||
disabled={typeof draggingItem === 'string' ? draggingItem === space.roomId : false}
|
||||
onUnpin={orphanSpaces.includes(space.roomId) ? undefined : handleUnpin}
|
||||
onHide={handleHide}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@@ -864,3 +1110,67 @@ export function SpaceTabs({ scrollRef }: SpaceTabsProps) {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Separate component for hidden spaces that can be rendered after other sidebar items
|
||||
*/
|
||||
export function HiddenSpacesTabs() {
|
||||
const navigate = useNavigate();
|
||||
const mx = useMatrixClient();
|
||||
const screenSize = useScreenSizeContext();
|
||||
const navToActivePath = useAtomValue(useNavToActivePathAtom());
|
||||
const orphanSpaces = useOrphanSpaces(mx, allRoomsAtom, useAtomValue(roomToParentsAtom));
|
||||
const [hiddenSpaces, localEchoHiddenSpaces] = useHiddenSpaces();
|
||||
const [, localEchoSidebarItem] = useSidebarItems(orphanSpaces);
|
||||
const [hiddenFolderOpen, setHiddenFolderOpen] = useState(false);
|
||||
|
||||
const selectedSpaceId = useSelectedSpace();
|
||||
|
||||
const handleSpaceClick: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
const target = evt.currentTarget;
|
||||
const targetSpaceId = target.getAttribute('data-id');
|
||||
if (!targetSpaceId) return;
|
||||
|
||||
const spacePath = getSpacePath(getCanonicalAliasOrRoomId(mx, targetSpaceId));
|
||||
if (screenSize === ScreenSize.Mobile) {
|
||||
navigate(spacePath);
|
||||
return;
|
||||
}
|
||||
|
||||
const activePath = navToActivePath.get(targetSpaceId);
|
||||
if (activePath && activePath.pathname.startsWith(spacePath)) {
|
||||
navigate(joinPathComponent(activePath));
|
||||
return;
|
||||
}
|
||||
|
||||
navigate(getSpaceLobbyPath(getCanonicalAliasOrRoomId(mx, targetSpaceId)));
|
||||
};
|
||||
|
||||
const handleUnhide = useCallback(
|
||||
(roomId: string) => {
|
||||
const newContent = makeUnhideSpaceContent(mx, roomId);
|
||||
localEchoSidebarItem(parseSidebar(mx, orphanSpaces, newContent));
|
||||
localEchoHiddenSpaces(parseHiddenSpaces(mx, newContent));
|
||||
mx.setAccountData(AccountDataEvent.CinnySpaces, newContent);
|
||||
},
|
||||
[mx, orphanSpaces, localEchoSidebarItem, localEchoHiddenSpaces]
|
||||
);
|
||||
|
||||
if (hiddenSpaces.length === 0) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidebarStackSeparator />
|
||||
<SidebarStack>
|
||||
<HiddenSpacesFolder
|
||||
hiddenSpaces={hiddenSpaces}
|
||||
open={hiddenFolderOpen}
|
||||
onToggle={() => setHiddenFolderOpen(!hiddenFolderOpen)}
|
||||
onSpaceClick={handleSpaceClick}
|
||||
onUnhide={handleUnhide}
|
||||
selectedSpaceId={selectedSpaceId}
|
||||
/>
|
||||
</SidebarStack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user