mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-02-22 21:23:09 +01:00
add menu to suggest or remove space children
This commit is contained in:
parent
d51041869e
commit
595647a6cb
5 changed files with 222 additions and 55 deletions
97
src/app/features/lobby/HierarchyItemMenu.tsx
Normal file
97
src/app/features/lobby/HierarchyItemMenu.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue