feat: Integrate forum layout support across various components and enhance room handling logic
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { MouseEventHandler, forwardRef, useState } from 'react';
|
||||
import React, { MouseEventHandler, forwardRef, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
@@ -38,6 +38,12 @@ import { useOpenSpaceSettings } from '../../state/hooks/spaceSettings';
|
||||
import { useRoomCreators } from '../../hooks/useRoomCreators';
|
||||
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
|
||||
import { InviteUserPrompt } from '../../components/invite-user-prompt';
|
||||
import { AddExistingModal } from '../add-existing';
|
||||
import { SpeechBubble } from '../../components/speech-bubble/SpeechBubble';
|
||||
import { SpaceOptionsMenu, type OpenAddExistingOptions } from '../space/SpaceOptionsMenu';
|
||||
import type { HierarchyItemSpace } from '../../hooks/useSpaceHierarchy';
|
||||
import { StateEvent } from '../../../types/matrix/room';
|
||||
import { useForumBoardContextOptionally } from '../forum/ForumBoardContext';
|
||||
|
||||
type LobbyMenuProps = {
|
||||
powerLevels: IPowerLevels;
|
||||
@@ -137,14 +143,19 @@ const LobbyMenu = forwardRef<HTMLDivElement, LobbyMenuProps>(
|
||||
|
||||
type LobbyHeaderProps = {
|
||||
showProfile?: boolean;
|
||||
/** Space name on the left + add room/space + ⋮ menu (forum toolbar). */
|
||||
showSpaceToolbar?: boolean;
|
||||
powerLevels: IPowerLevels;
|
||||
};
|
||||
export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
|
||||
export function LobbyHeader({ showProfile, showSpaceToolbar, powerLevels }: LobbyHeaderProps) {
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const space = useSpace();
|
||||
const creators = useRoomCreators(space);
|
||||
const spacePermissions = useRoomPermissions(creators, powerLevels);
|
||||
const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer');
|
||||
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
|
||||
const [addExisting, setAddExisting] = useState<OpenAddExistingOptions | null>(null);
|
||||
const screenSize = useScreenSizeContext();
|
||||
|
||||
const name = useRoomName(space);
|
||||
@@ -153,10 +164,192 @@ export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
|
||||
? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined
|
||||
: undefined;
|
||||
|
||||
const forumBoard = useForumBoardContextOptionally();
|
||||
const menuWrapperRef = useRef<HTMLDivElement | null>(null);
|
||||
const menuTriggerElRef = useRef<HTMLButtonElement | null>(null);
|
||||
const addParent = useMemo(() => {
|
||||
const rootId = space.roomId;
|
||||
if (!forumBoard) {
|
||||
return { roomId: rootId, showAddSpace: true };
|
||||
}
|
||||
if (forumBoard.query.category) {
|
||||
const section = forumBoard.sections.find((s) => s.title === forumBoard.query.category);
|
||||
if (section?.spaceId) {
|
||||
return { roomId: section.spaceId, showAddSpace: false };
|
||||
}
|
||||
}
|
||||
return { roomId: rootId, showAddSpace: true };
|
||||
}, [forumBoard, space.roomId]);
|
||||
|
||||
const addParentItem: HierarchyItemSpace = useMemo(
|
||||
() => ({
|
||||
roomId: addParent.roomId,
|
||||
content: { via: [] },
|
||||
ts: 0,
|
||||
space: true,
|
||||
parentId: addParent.roomId === space.roomId ? undefined : space.roomId,
|
||||
}),
|
||||
[addParent.roomId, space.roomId]
|
||||
);
|
||||
|
||||
const canEditChildren = Boolean(
|
||||
spacePermissions.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId())
|
||||
);
|
||||
|
||||
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
menuTriggerElRef.current = evt.currentTarget;
|
||||
setMenuAnchor(evt.currentTarget.getBoundingClientRect());
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!showSpaceToolbar || !menuAnchor || addExisting) return;
|
||||
|
||||
let outsideTimer: number | undefined;
|
||||
const clearOutsideTimer = () => {
|
||||
if (outsideTimer) {
|
||||
window.clearTimeout(outsideTimer);
|
||||
outsideTimer = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const closeMenu = () => {
|
||||
clearOutsideTimer();
|
||||
setMenuAnchor(undefined);
|
||||
};
|
||||
|
||||
const isInside = (node: Node | null) => {
|
||||
if (!node) return false;
|
||||
return Boolean(
|
||||
menuWrapperRef.current?.contains(node) || menuTriggerElRef.current?.contains(node)
|
||||
);
|
||||
};
|
||||
|
||||
const handlePointerDown = (evt: MouseEvent | TouchEvent) => {
|
||||
const node = evt.target as Node | null;
|
||||
if (!isInside(node)) {
|
||||
closeMenu();
|
||||
}
|
||||
};
|
||||
|
||||
const handlePointerMove = (evt: PointerEvent | MouseEvent) => {
|
||||
const node = evt.target as Node | null;
|
||||
const inside = isInside(node);
|
||||
if (inside) {
|
||||
clearOutsideTimer();
|
||||
return;
|
||||
}
|
||||
if (!outsideTimer) {
|
||||
outsideTimer = window.setTimeout(closeMenu, 3000);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handlePointerDown, true);
|
||||
document.addEventListener('touchstart', handlePointerDown, true);
|
||||
document.addEventListener('pointermove', handlePointerMove, true);
|
||||
document.addEventListener('mousemove', handlePointerMove, true);
|
||||
|
||||
return () => {
|
||||
clearOutsideTimer();
|
||||
document.removeEventListener('mousedown', handlePointerDown, true);
|
||||
document.removeEventListener('touchstart', handlePointerDown, true);
|
||||
document.removeEventListener('pointermove', handlePointerMove, true);
|
||||
document.removeEventListener('mousemove', handlePointerMove, true);
|
||||
};
|
||||
}, [addExisting, menuAnchor, showSpaceToolbar]);
|
||||
|
||||
if (showSpaceToolbar) {
|
||||
return (
|
||||
<>
|
||||
<PageHeader outlined>
|
||||
<Box grow="Yes" alignItems="Center" gap="300">
|
||||
<Avatar size="300" shrink="No">
|
||||
<RoomAvatar
|
||||
roomId={space.roomId}
|
||||
src={avatarUrl}
|
||||
alt={name}
|
||||
renderFallback={() => <Text size="H4">{nameInitials(name)}</Text>}
|
||||
/>
|
||||
</Avatar>
|
||||
<Text size="H3" truncate style={{ minWidth: 0 }}>
|
||||
{name}
|
||||
</Text>
|
||||
<Box grow="Yes" />
|
||||
<TooltipProvider
|
||||
position="Bottom"
|
||||
align="End"
|
||||
offset={4}
|
||||
tooltip={
|
||||
<Tooltip>
|
||||
<Text>More Options</Text>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
{(triggerRef) => (
|
||||
<IconButton
|
||||
onClick={handleOpenMenu}
|
||||
ref={triggerRef}
|
||||
aria-pressed={!!menuAnchor}
|
||||
variant="SurfaceVariant"
|
||||
fill="None"
|
||||
size="300"
|
||||
radii="300"
|
||||
>
|
||||
<Icon size="400" src={Icons.VerticalDots} />
|
||||
</IconButton>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
<PopOut
|
||||
anchor={menuAnchor}
|
||||
position="Bottom"
|
||||
align="End"
|
||||
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,
|
||||
}}
|
||||
>
|
||||
<div ref={menuWrapperRef}>
|
||||
<SpeechBubble variant="menu" tail="top" tailX="calc(100% - 22px)">
|
||||
<SpaceOptionsMenu
|
||||
room={space}
|
||||
requestClose={() => setMenuAnchor(undefined)}
|
||||
onOpenAddExisting={(options) => {
|
||||
setAddExisting(options);
|
||||
setMenuAnchor(undefined);
|
||||
}}
|
||||
forumAdd={
|
||||
forumBoard && canEditChildren
|
||||
? {
|
||||
item: addParentItem,
|
||||
showAddSpace: addParent.showAddSpace,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</SpeechBubble>
|
||||
</div>
|
||||
</FocusTrap>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</PageHeader>
|
||||
{addExisting && (
|
||||
<AddExistingModal
|
||||
parentId={addExisting.parentId}
|
||||
space={addExisting.space}
|
||||
requestClose={() => setAddExisting(null)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PageHeader className={showProfile ? undefined : css.Header} balance>
|
||||
<Box grow="Yes" alignItems="Center" gap="200">
|
||||
|
||||
Reference in New Issue
Block a user