From 595647a6cb4342dc2416d804dabe5c5b2bdd4af8 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Wed, 1 May 2024 11:16:23 +0530 Subject: [PATCH] add menu to suggest or remove space children --- src/app/features/lobby/HierarchyItemMenu.tsx | 97 +++++++++++++++ src/app/features/lobby/Lobby.tsx | 49 ++++++-- src/app/features/lobby/RoomItem.tsx | 6 +- src/app/features/lobby/SpaceItem.css.ts | 1 + src/app/features/lobby/SpaceItem.tsx | 124 ++++++++++++------- 5 files changed, 222 insertions(+), 55 deletions(-) create mode 100644 src/app/features/lobby/HierarchyItemMenu.tsx diff --git a/src/app/features/lobby/HierarchyItemMenu.tsx b/src/app/features/lobby/HierarchyItemMenu.tsx new file mode 100644 index 00000000..58e8266b --- /dev/null +++ b/src/app/features/lobby/HierarchyItemMenu.tsx @@ -0,0 +1,97 @@ +import React, { MouseEventHandler, useState } from 'react'; +import FocusTrap from 'focus-trap-react'; +import { + Box, + IconButton, + Icon, + Icons, + PopOut, + Menu, + MenuItem, + Text, + RectCords, + config, +} from 'folds'; +import { HierarchyItem } from '../../hooks/useSpaceHierarchy'; +import { useMatrixClient } from '../../hooks/useMatrixClient'; +import { MSpaceChildContent, StateEvent } from '../../../types/matrix/room'; + +type HierarchyItemMenuProps = { + item: HierarchyItem & { + parentId: string; + }; +}; +export function HierarchyItemMenu({ item }: HierarchyItemMenuProps) { + const mx = useMatrixClient(); + const { roomId, parentId, content } = item; + const [menuAnchor, setMenuAnchor] = useState(); + + const handleOpenMenu: MouseEventHandler = (evt) => { + setMenuAnchor(evt.currentTarget.getBoundingClientRect()); + }; + const handleToggleSuggested = () => { + const newContent: MSpaceChildContent = { ...content, suggested: !content.suggested }; + mx.sendStateEvent(parentId, StateEvent.SpaceChild, newContent, roomId); + setMenuAnchor(undefined); + }; + + const handleRemove = () => { + mx.sendStateEvent(parentId, StateEvent.SpaceChild, {}, roomId); + setMenuAnchor(undefined); + }; + + return ( + + + + + {menuAnchor && ( + setMenuAnchor(undefined), + clickOutsideDeactivates: true, + isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown', + isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp', + }} + > + + + + + {content.suggested ? 'Unset Suggested' : 'Set Suggested'} + + + + + Remove + + + + + + } + /> + )} + + ); +} diff --git a/src/app/features/lobby/Lobby.tsx b/src/app/features/lobby/Lobby.tsx index 44c2543c..39440832 100644 --- a/src/app/features/lobby/Lobby.tsx +++ b/src/app/features/lobby/Lobby.tsx @@ -3,6 +3,7 @@ import { Box, Icon, IconButton, Icons, Line, Scroll, config } from 'folds'; import { useVirtualizer } from '@tanstack/react-virtual'; import { useAtom, useAtomValue } from 'jotai'; import { useNavigate } from 'react-router-dom'; +import { Room } from 'matrix-js-sdk'; import { useSpace } from '../../hooks/useSpace'; import { Page, PageContent, PageContentCenter, PageHeroSection } from '../../components/page'; import { HierarchyItem, useSpaceHierarchy } from '../../hooks/useSpaceHierarchy'; @@ -16,7 +17,13 @@ import { LobbyHeader } from './LobbyHeader'; import { LobbyHero } from './LobbyHero'; import { ScrollTopContainer } from '../../components/scroll-top-container'; import { useElementSizeObserver } from '../../hooks/useElementSizeObserver'; -import { PowerLevelsContextProvider, usePowerLevels } from '../../hooks/usePowerLevels'; +import { + DefaultPowerLevels, + PowerLevelsContextProvider, + powerLevelAPI, + usePowerLevels, + useRoomsPowerLevels, +} from '../../hooks/usePowerLevels'; import { RoomItemCard } from './RoomItem'; import { mDirectAtom } from '../../state/mDirectList'; import { SpaceItemCard } from './SpaceItem'; @@ -26,15 +33,18 @@ import { useMatrixClient } from '../../hooks/useMatrixClient'; import { allRoomsAtom } from '../../state/room-list/roomList'; import { getCanonicalAliasOrRoomId } from '../../utils/matrix'; import { getSpaceRoomPath } from '../../pages/pathUtils'; +import { HierarchyItemMenu } from './HierarchyItemMenu'; +import { StateEvent } from '../../../types/matrix/room'; export function Lobby() { + const navigate = useNavigate(); const mx = useMatrixClient(); const mDirects = useAtomValue(mDirectAtom); const allRooms = useAtomValue(allRoomsAtom); const allJoinedRooms = useMemo(() => new Set(allRooms), [allRooms]); - const navigate = useNavigate(); - const space = useSpace(); + const powerLevels = usePowerLevels(space); + const scrollRef = useRef(null); const heroSectionRef = useRef(null); const [heroSectionHeight, setHeroSectionHeight] = useState(); @@ -42,7 +52,6 @@ export function Lobby() { const [isDrawer] = useSetting(settingsAtom, 'isPeopleDrawer'); const screenSize = useScreenSize(); const [onTop, setOnTop] = useState(true); - const powerLevelAPI = usePowerLevels(space); const [closedCategories, setClosedCategories] = useAtom(closedLobbyCategoriesAtom); useElementSizeObserver( @@ -77,6 +86,15 @@ export function Lobby() { }); const vItems = virtualizer.getVirtualItems(); + const hierarchySpaces: Room[] = useMemo( + () => + flattenHierarchy + .filter((i) => i.space && allJoinedRooms.has(i.roomId) && !!mx.getRoom(i.roomId)) + .map((i) => mx.getRoom(i.parentId ?? i.roomId)) as Room[], + [mx, allJoinedRooms, flattenHierarchy] + ); + const roomsPowerLevels = useRoomsPowerLevels(hierarchySpaces); + const addSpaceRoom = (roomId: string) => setSpaceRooms({ type: 'PUT', roomId }); const handleCategoryClick = useCategoryHandler(setClosedCategories, (categoryId) => @@ -84,14 +102,14 @@ export function Lobby() { ); const handleOpenRoom: MouseEventHandler = (evt) => { - const rId = evt.currentTarget.getAttribute('data-roomId'); + const rId = evt.currentTarget.getAttribute('data-room-id'); if (!rId) return; const pSpaceIdOrAlias = getCanonicalAliasOrRoomId(mx, space.roomId); navigate(getSpaceRoomPath(pSpaceIdOrAlias, getCanonicalAliasOrRoomId(mx, rId))); }; return ( - + @@ -126,8 +144,17 @@ export function Lobby() { {vItems.map((vItem) => { const item = flattenHierarchy[vItem.index]; + const { parentId } = item; if (!item) return null; - + const parentPowerLevel = + parentId && (roomsPowerLevels.get(parentId) ?? DefaultPowerLevels); + const canEditSpaceChild = + parentPowerLevel && + powerLevelAPI.canSendStateEvent( + parentPowerLevel, + StateEvent.SpaceChild, + powerLevelAPI.getPowerLevel(parentPowerLevel, mx.getUserId() ?? undefined) + ); if (item.space) { const categoryId = makeLobbyCategoryId(space.roomId, item.roomId); @@ -146,6 +173,11 @@ export function Lobby() { categoryId={categoryId} closed={closedCategories.has(categoryId)} handleClose={handleCategoryClick} + options={ + parentId && canEditSpaceChild ? ( + + ) : undefined + } /> ); @@ -167,6 +199,9 @@ export function Lobby() { firstChild={!prevItem || prevItem.space === true} lastChild={!nextItem || nextItem.space === true} onOpen={handleOpenRoom} + options={ + canEditSpaceChild ? : undefined + } /> ); diff --git a/src/app/features/lobby/RoomItem.tsx b/src/app/features/lobby/RoomItem.tsx index 12734d73..a3bf963e 100644 --- a/src/app/features/lobby/RoomItem.tsx +++ b/src/app/features/lobby/RoomItem.tsx @@ -301,9 +301,10 @@ type RoomItemCardProps = { firstChild?: boolean; lastChild?: boolean; onOpen: MouseEventHandler; + options?: ReactNode; }; export const RoomItemCard = as<'div', RoomItemCardProps>( - ({ item, onSpaceFound, dm, firstChild, lastChild, onOpen, ...props }, ref) => { + ({ item, onSpaceFound, dm, firstChild, lastChild, onOpen, options, ...props }, ref) => { const mx = useMatrixClient(); const { roomId, content } = item; const room = mx.getRoom(roomId); @@ -337,7 +338,7 @@ export const RoomItemCard = as<'div', RoomItemCardProps>( joined ? ( ( )} )} + {options} ); } diff --git a/src/app/features/lobby/SpaceItem.css.ts b/src/app/features/lobby/SpaceItem.css.ts index 801ad932..588f337c 100644 --- a/src/app/features/lobby/SpaceItem.css.ts +++ b/src/app/features/lobby/SpaceItem.css.ts @@ -16,6 +16,7 @@ export const SpaceItemCard = recipe({ }, }); export const HeaderChip = style({ + paddingLeft: config.space.S200, selectors: { [`&[data-ui-before="true"]`]: { paddingLeft: config.space.S100, diff --git a/src/app/features/lobby/SpaceItem.tsx b/src/app/features/lobby/SpaceItem.tsx index 893ba400..df477ee7 100644 --- a/src/app/features/lobby/SpaceItem.tsx +++ b/src/app/features/lobby/SpaceItem.tsx @@ -1,4 +1,4 @@ -import React, { MouseEventHandler, useCallback } from 'react'; +import React, { MouseEventHandler, ReactNode, useCallback } from 'react'; import { Box, Avatar, Text, Chip, Icon, Icons, as, Badge, toRem, Spinner } from 'folds'; import classNames from 'classnames'; import { MatrixError, Room } from 'matrix-js-sdk'; @@ -193,15 +193,40 @@ function SpaceProfile({ ); } +type RootSpaceProfileProps = { + closed: boolean; + categoryId: string; + handleClose?: MouseEventHandler; +}; +function RootSpaceProfile({ closed, categoryId, handleClose }: RootSpaceProfileProps) { + return ( + } + > + + + Rooms + + + + ); +} + type SpaceItemCardProps = { item: HierarchyItem; joined?: boolean; categoryId: string; closed: boolean; handleClose?: MouseEventHandler; + options?: ReactNode; }; export const SpaceItemCard = as<'div', SpaceItemCardProps>( - ({ className, joined, closed, categoryId, item, handleClose, ...props }, ref) => { + ({ className, joined, closed, categoryId, item, handleClose, options, ...props }, ref) => { const mx = useMatrixClient(); const { roomId, content } = item; const space = mx.getRoom(roomId); @@ -213,56 +238,63 @@ export const SpaceItemCard = as<'div', SpaceItemCardProps>( {...props} ref={ref} > - {space ? ( - - {(localSummary) => ( - - )} - - ) : ( - - {(summaryState) => ( - <> - {summaryState.status === AsyncStatus.Loading && } - {summaryState.status === AsyncStatus.Error && - (summaryState.error.name === ErrorCode.M_FORBIDDEN ? ( - - ) : ( + + {space ? ( + + {(localSummary) => + item.parentId ? ( + + ) : ( + + ) + } + + ) : ( + + {(summaryState) => ( + <> + {summaryState.status === AsyncStatus.Loading && } + {summaryState.status === AsyncStatus.Error && + (summaryState.error.name === ErrorCode.M_FORBIDDEN ? ( + + ) : ( + + ))} + {summaryState.status === AsyncStatus.Success && ( - ))} - {summaryState.status === AsyncStatus.Success && ( - - )} - - )} - - )} - - {/* }> - Some - */} + )} + + )} + + )} + {options} ); }