add menu to suggest or remove space children

This commit is contained in:
Ajay Bura 2024-05-01 11:16:23 +05:30
parent d51041869e
commit 595647a6cb
5 changed files with 222 additions and 55 deletions

View file

@ -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<RectCords>();
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (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 (
<Box gap="200" alignItems="Center" shrink="No">
<IconButton
onClick={handleOpenMenu}
size="300"
variant="SurfaceVariant"
fill="None"
radii="300"
aria-pressed={!!menuAnchor}
>
<Icon size="50" src={Icons.VerticalDots} />
</IconButton>
{menuAnchor && (
<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',
}}
>
<Menu>
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
<MenuItem onClick={handleToggleSuggested} size="300" radii="300">
<Text as="span" size="T300" truncate>
{content.suggested ? 'Unset Suggested' : 'Set Suggested'}
</Text>
</MenuItem>
<MenuItem
onClick={handleRemove}
variant="Critical"
fill="None"
size="300"
radii="300"
>
<Text as="span" size="T300" truncate>
Remove
</Text>
</MenuItem>
</Box>
</Menu>
</FocusTrap>
}
/>
)}
</Box>
);
}

View file

@ -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<HTMLDivElement>(null);
const heroSectionRef = useRef<HTMLDivElement>(null);
const [heroSectionHeight, setHeroSectionHeight] = useState<number>();
@ -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<HTMLButtonElement> = (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 (
<PowerLevelsContextProvider value={powerLevelAPI}>
<PowerLevelsContextProvider value={powerLevels}>
<Box grow="Yes">
<Page>
<LobbyHeader showProfile={!onTop} />
@ -126,8 +144,17 @@ export function Lobby() {
</PageHeroSection>
{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 ? (
<HierarchyItemMenu item={{ ...item, parentId }} />
) : undefined
}
/>
</VirtualTile>
);
@ -167,6 +199,9 @@ export function Lobby() {
firstChild={!prevItem || prevItem.space === true}
lastChild={!nextItem || nextItem.space === true}
onOpen={handleOpenRoom}
options={
canEditSpaceChild ? <HierarchyItemMenu item={item} /> : undefined
}
/>
</VirtualTile>
);

View file

@ -301,9 +301,10 @@ type RoomItemCardProps = {
firstChild?: boolean;
lastChild?: boolean;
onOpen: MouseEventHandler<HTMLButtonElement>;
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 ? (
<Box shrink="No" gap="100" alignItems="Center">
<Chip
data-roomId={roomId}
data-room-id={roomId}
onClick={onOpen}
variant="Secondary"
fill="None"
@ -396,6 +397,7 @@ export const RoomItemCard = as<'div', RoomItemCardProps>(
)}
</HierarchyRoomSummaryLoader>
)}
{options}
</SequenceCard>
);
}

View file

@ -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,

View file

@ -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<HTMLButtonElement>;
};
function RootSpaceProfile({ closed, categoryId, handleClose }: RootSpaceProfileProps) {
return (
<Chip
data-category-id={categoryId}
onClick={handleClose}
className={css.HeaderChip}
variant="Surface"
size="500"
after={<Icon src={closed ? Icons.ChevronRight : Icons.ChevronBottom} size="50" />}
>
<Box alignItems="Center" gap="200">
<Text size="H4" truncate>
Rooms
</Text>
</Box>
</Chip>
);
}
type SpaceItemCardProps = {
item: HierarchyItem;
joined?: boolean;
categoryId: string;
closed: boolean;
handleClose?: MouseEventHandler<HTMLButtonElement>;
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 ? (
<LocalRoomSummaryLoader room={space}>
{(localSummary) => (
<SpaceProfile
name={localSummary.name}
avatarUrl={getRoomAvatarUrl(mx, space, 96)}
suggested={content.suggested}
closed={closed}
categoryId={categoryId}
handleClose={handleClose}
/>
)}
</LocalRoomSummaryLoader>
) : (
<HierarchyRoomSummaryLoader roomId={roomId}>
{(summaryState) => (
<>
{summaryState.status === AsyncStatus.Loading && <SpaceProfileLoading />}
{summaryState.status === AsyncStatus.Error &&
(summaryState.error.name === ErrorCode.M_FORBIDDEN ? (
<UnknownPrivateSpaceProfile suggested={content.suggested} />
) : (
<Box grow="Yes">
{space ? (
<LocalRoomSummaryLoader room={space}>
{(localSummary) =>
item.parentId ? (
<SpaceProfile
name={localSummary.name}
avatarUrl={getRoomAvatarUrl(mx, space, 96)}
suggested={content.suggested}
closed={closed}
categoryId={categoryId}
handleClose={handleClose}
/>
) : (
<RootSpaceProfile
closed={closed}
categoryId={categoryId}
handleClose={handleClose}
/>
)
}
</LocalRoomSummaryLoader>
) : (
<HierarchyRoomSummaryLoader roomId={roomId}>
{(summaryState) => (
<>
{summaryState.status === AsyncStatus.Loading && <SpaceProfileLoading />}
{summaryState.status === AsyncStatus.Error &&
(summaryState.error.name === ErrorCode.M_FORBIDDEN ? (
<UnknownPrivateSpaceProfile suggested={content.suggested} />
) : (
<UnknownSpaceProfile
roomId={roomId}
via={item.content.via}
suggested={content.suggested}
/>
))}
{summaryState.status === AsyncStatus.Success && (
<UnknownSpaceProfile
roomId={roomId}
via={item.content.via}
name={summaryState.data.name || roomId}
avatarUrl={
summaryState.data?.avatar_url
? mx.mxcUrlToHttp(summaryState.data.avatar_url, 96, 96, 'crop') ??
undefined
: undefined
}
suggested={content.suggested}
/>
))}
{summaryState.status === AsyncStatus.Success && (
<UnknownSpaceProfile
roomId={roomId}
via={item.content.via}
name={summaryState.data.name || roomId}
avatarUrl={
summaryState.data?.avatar_url
? mx.mxcUrlToHttp(summaryState.data.avatar_url, 96, 96, 'crop') ?? undefined
: undefined
}
suggested={content.suggested}
/>
)}
</>
)}
</HierarchyRoomSummaryLoader>
)}
<Box shrink="No">
{/* <Chip variant="Primary" radii="Pill" before={<Icon size="100" src={Icons.CheckTwice} />}>
<Text size="T200">Some</Text>
</Chip> */}
)}
</>
)}
</HierarchyRoomSummaryLoader>
)}
</Box>
{options}
</Box>
);
}