mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-10 17:20:28 +03:00
URL navigation in interface and other improvements (#1633)
* load room on url change * add direct room list * render space room list * fix css syntax error * update scroll virtualizer * render subspaces room list * improve sidebar notification badge perf * add nav category components * add space recursive direct component * use nav category component in home, direct and space room list * add empty home and direct list layout * fix unread room menu ref * add more navigation items in room, direct and space tab * add more navigation * fix unread room menu to links * fix space lobby and search link * add explore navigation section * add notifications navigation menu * redirect to initial path after login * include unsupported room in rooms * move router hooks in hooks/router folder * add featured explore - WIP * load featured room with room summary * fix room card topic line clamp * add react query * load room summary using react query * add join button in room card * add content component * use content component in featured community content * fix content width * add responsive room card grid * fix async callback error status * add room card error button * fix client drawer shrink * add room topic viewer * open room card topic in viewer * fix room topic close btn * add get orphan parent util * add room card error dialog * add view featured room or space btn * refactor orphanParent to orphanParents * WIP - explore server * show space hint in room card * add room type filters * add per page item limit popout * reset scroll on public rooms load * refactor explore ui * refactor public rooms component * reset search on server change * fix typo * add empty featured section info * display user server on top * make server room card view btn clickable * add user server as default redirect for explore path * make home empty btn clickable * add thirdparty instance filter in server explore * remove since param on instance change * add server button in explore menu * rename notifications path to inbox * update react-virtual * Add notification messages inbox - WIP * add scroll top container component * add useInterval hook * add visibility change callback prop to scroll top container component * auto refresh notifications every 10 seconds * make message related component reusable * refactor matrix event renderer hoook * render notification message content * refactor matrix event renderer hook * update sequence card styles * move room navigate hook in global hooks * add open message button in notifications * add mark room as read button in notification group * show error in notification messages * add more featured spaces * render reply in notification messages * make notification message reply clickable * add outline prop for attachments * make old settings dialog viewable * add open featured communities as default config option * add invite count notification badge in sidebar and inbox menu * add element size observer hook * improve element size observer hook props * improve screen size hook * fix room avatar util function * allow Text props in Time component * fix dm room util function * add invitations * add no invites and notification cards * fix inbox tab unread badge visible without invite count * update folds and change inbox icon * memo search param construction * add message search in home * fix default message search order * fix display edited message new content * highlight search text in search messages * fix message search loading * disable log in production * add use space context * add useRoom context * fix space room list * fix inbox tab active state * add hook to get space child room recursive * add search for space * add virtual tile component * virtualize home and directs room list * update nav category component * use virtual tile component in more places * fix message highlight when click on reply twice * virtualize space room list * fix space room list lag issue * update folds * add room nav item component in space room list * use room nav item in home and direct room list * make space categories closable and save it in local storage * show unread room when category is collapsed * make home and direct room list category closable * rename room nav item show avatar prop * fix explore server category text alignment * rename closedRoomCategories to closedNavCategories * add nav category handler hook * save and restore last navigation path on space select * filter space rooms category by activity when it is closed * save and restore home and direct nav path state * save and restore inbox active path on open * save and restore explore tab active path * remove notification badge unread menu * add join room or space before navigate screen * move room component to features folder and add new room header * update folds * add room header menu * fix home room list activity sorting * do not hide selected room item on category closed in home and direct tab * replace old select room/tab call with navigate hook * improve state event hooks * show room card summary for joined rooms * prevent room from opening in wrong tab * only show message sender id on hover in modern layout * revert state event hooks changes * add key prop to room provider components * add welcome page * prevent excessive redirects * fix sidebar style with no spaces * move room settings in popup window * remove invite option from room settings * fix open room list search * add leave room prompt * standardize room and user avatar * fix avatar text size * add new reply layout * rename space hierarchy hook * add room topic hook * add room name hook * add room avatar hook and add direct room avatar util * space lobby - WIP * hide invalid space child event from space hierarchy in lobby * move lobby to features * fix element size observer hook width and height * add lobby header and hero section * add hierarchy room item error and loading state * add first and last child prop in sequence card * redirect to lobby from index path * memo and retry hierarchy room summary error * fix hierarchy room item styles * rename lobby hierarchy item card to room item card * show direct room avatar in space lobby * add hierarchy space item * add space item unknown room join button * fix space hierarchy hook refresh after new space join * change user avatar color and fallback render to user icon * change room avatar fallback to room icon * rename room/user avatar renderInitial prop to renderFallback * add room join and view button in space lobby * make power level api more reusable * fix space hierarchy not updating on child update * add menu to suggest or remove space children * show reply arrow in place of reply bend in message * fix typeerror in search because of wrong js-sdk t.ds * do not refetch hierarchy room summary on window focus * make room/user avatar un-draggable * change welcome page support button copy * drag-and-drop ordering of lobby spaces/rooms - WIP * add ASCIILexicalTable algorithms * fix wrong power level check in lobby items options * fix lobby can drop checks * fix join button error crash * fix reply spacing * fix m direct updated with other account data * add option to open room/space settings from lobby * add option in lobby to add new or existing room/spaces * fix room nav item selected styles * add space children reorder mechanism * fix space child reorder bug * fix hierarchy item sort function * Apply reorder of lobby into room list * add and improve space lobby menu items * add existing spaces menu in lobby * change restricted room allow params when dragging outside space * move featured servers config from homeserver list * removed unused features from space settings * add canonical alias as name fallback in lobby item * fix unreliable unread count update bug * fix after login redirect * fix room card topic hover style * Add dnd and folders in sidebar spaces * fix orphan space not visible in sidebar * fix sso login has mix of icon and button * fix space children not visible in home upon leaving space * recalculate notification on updating any space child * fix user color saturation/lightness * add user color to user avatar * add background colors to room avatar * show 2 length initial in sidebar space avatar * improve link color * add nav button component * open legacy create room and create direct * improve page route structure * handle hash router in path utils * mobile friendly router and navigation * make room header member drawer icon mobile friendly * setup index redirect for inbox and explore server route * add leave space prompt * improve member drawer filter menu * add space context menu * add context menu in home * add leave button in lobby items * render user tab avatar on sidebar * force overwrite netlify - test * netlify test * fix reset-password path without server redirected to login * add message link copy button in message menu * reset unread on sync prepared * fix stuck typing notifications * show typing indication in room nav item * refactor closedNavCategories atom to use userId in store key * refactor closedLobbyCategoriesAtom to include userId in store key * refactor navToActivePathAtom to use userId in storage key * remove unused file * refactor openedSidebarFolderAtom to include userId in storage key * add context menu for sidebar space tab * fix eslint not working * add option to pin/unpin child spaces * add context menu for directs tab * add context menu for direct and home tab * show lock icon for non-public space in header * increase matrix max listener count * wrap lobby add space room in callback hook
This commit is contained in:
parent
2b7d825694
commit
4c76a7fd18
290 changed files with 17447 additions and 3224 deletions
91
src/app/features/lobby/DnD.css.ts
Normal file
91
src/app/features/lobby/DnD.css.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import { style } from '@vanilla-extract/css';
|
||||
import { color, config, toRem } from 'folds';
|
||||
import { ContainerColor } from '../../styles/ContainerColor.css';
|
||||
|
||||
export const ItemDraggableTarget = style([
|
||||
ContainerColor({ variant: 'SurfaceVariant' }),
|
||||
{
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
zIndex: 1,
|
||||
cursor: 'grab',
|
||||
borderRadius: config.radii.R400,
|
||||
opacity: config.opacity.P300,
|
||||
|
||||
':active': {
|
||||
cursor: 'ns-resize',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const LineHeight = 4;
|
||||
const DropTargetLine = style({
|
||||
selectors: {
|
||||
'&[data-hover=true]:before': {
|
||||
content: '',
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: '50%',
|
||||
zIndex: 1,
|
||||
transform: 'translateY(-50%)',
|
||||
|
||||
borderBottom: `${toRem(LineHeight)} solid currentColor`,
|
||||
},
|
||||
'&[data-hover=true]:after': {
|
||||
content: '',
|
||||
display: 'block',
|
||||
width: toRem(LineHeight * 3),
|
||||
height: toRem(LineHeight * 3),
|
||||
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: '50%',
|
||||
zIndex: 1,
|
||||
transform: 'translate(-50%, -50%)',
|
||||
|
||||
backgroundColor: color.Surface.Container,
|
||||
border: `${toRem(LineHeight)} solid currentColor`,
|
||||
borderRadius: '50%',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const BaseAfterRoomItemDropTarget = style({
|
||||
width: '100%',
|
||||
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
zIndex: 99,
|
||||
|
||||
color: color.Success.Main,
|
||||
|
||||
selectors: {
|
||||
'&[data-error=true]': {
|
||||
color: color.Critical.Main,
|
||||
},
|
||||
},
|
||||
});
|
||||
const RoomTargetHeight = 32;
|
||||
export const AfterRoomItemDropTarget = style([
|
||||
BaseAfterRoomItemDropTarget,
|
||||
{
|
||||
height: toRem(RoomTargetHeight),
|
||||
transform: `translateY(${toRem(RoomTargetHeight / 2 + LineHeight / 2)})`,
|
||||
},
|
||||
DropTargetLine,
|
||||
]);
|
||||
const SpaceTargetHeight = 14;
|
||||
export const AfterSpaceItemDropTarget = style([
|
||||
BaseAfterRoomItemDropTarget,
|
||||
{
|
||||
height: toRem(SpaceTargetHeight),
|
||||
transform: `translateY(calc(100% - ${toRem(4)}))`,
|
||||
},
|
||||
DropTargetLine,
|
||||
]);
|
||||
146
src/app/features/lobby/DnD.tsx
Normal file
146
src/app/features/lobby/DnD.tsx
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
import React, { RefObject, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
dropTargetForElements,
|
||||
draggable,
|
||||
monitorForElements,
|
||||
} from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import { autoScrollForElements } from '@atlaskit/pragmatic-drag-and-drop-auto-scroll/element';
|
||||
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
|
||||
import classNames from 'classnames';
|
||||
import { Box, Icon, Icons, as } from 'folds';
|
||||
import { HierarchyItem } from '../../hooks/useSpaceHierarchy';
|
||||
import * as css from './DnD.css';
|
||||
|
||||
export type DropContainerData = {
|
||||
item: HierarchyItem;
|
||||
nextRoomId?: string;
|
||||
};
|
||||
export type CanDropCallback = (item: HierarchyItem, container: DropContainerData) => boolean;
|
||||
|
||||
export const useDraggableItem = (
|
||||
item: HierarchyItem,
|
||||
targetRef: RefObject<HTMLElement>,
|
||||
onDragging: (item?: HierarchyItem) => void,
|
||||
dragHandleRef?: RefObject<HTMLElement>
|
||||
): boolean => {
|
||||
const [dragging, setDragging] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const target = targetRef.current;
|
||||
const dragHandle = dragHandleRef?.current ?? undefined;
|
||||
|
||||
return !target
|
||||
? undefined
|
||||
: draggable({
|
||||
element: target,
|
||||
dragHandle,
|
||||
getInitialData: () => item,
|
||||
onDragStart: () => {
|
||||
setDragging(true);
|
||||
onDragging(item);
|
||||
},
|
||||
onDrop: () => {
|
||||
setDragging(false);
|
||||
onDragging(undefined);
|
||||
},
|
||||
});
|
||||
}, [targetRef, dragHandleRef, item, onDragging]);
|
||||
|
||||
return dragging;
|
||||
};
|
||||
|
||||
export const ItemDraggableTarget = as<'div'>(({ className, ...props }, ref) => (
|
||||
<Box
|
||||
justifyContent="Center"
|
||||
alignItems="Center"
|
||||
className={classNames(css.ItemDraggableTarget, className)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
<Icon size="50" src={Icons.VerticalDots} />
|
||||
</Box>
|
||||
));
|
||||
|
||||
type AfterItemDropTargetProps = {
|
||||
item: HierarchyItem;
|
||||
afterSpace?: boolean;
|
||||
nextRoomId?: string;
|
||||
canDrop: CanDropCallback;
|
||||
};
|
||||
export function AfterItemDropTarget({
|
||||
item,
|
||||
afterSpace,
|
||||
nextRoomId,
|
||||
canDrop,
|
||||
}: AfterItemDropTargetProps) {
|
||||
const targetRef = useRef<HTMLDivElement>(null);
|
||||
const [dropState, setDropState] = useState<'idle' | 'allow' | 'not-allow'>('idle');
|
||||
|
||||
useEffect(() => {
|
||||
const target = targetRef.current;
|
||||
if (!target) {
|
||||
throw Error('drop target ref is not set properly');
|
||||
}
|
||||
|
||||
return dropTargetForElements({
|
||||
element: target,
|
||||
getData: () => {
|
||||
const container: DropContainerData = {
|
||||
item,
|
||||
nextRoomId,
|
||||
};
|
||||
return container;
|
||||
},
|
||||
onDragEnter: ({ source }) => {
|
||||
if (
|
||||
canDrop(source.data as HierarchyItem, {
|
||||
item,
|
||||
nextRoomId,
|
||||
})
|
||||
) {
|
||||
setDropState('allow');
|
||||
} else {
|
||||
setDropState('not-allow');
|
||||
}
|
||||
},
|
||||
onDragLeave: () => setDropState('idle'),
|
||||
onDrop: () => setDropState('idle'),
|
||||
});
|
||||
}, [item, nextRoomId, canDrop]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={afterSpace ? css.AfterSpaceItemDropTarget : css.AfterRoomItemDropTarget}
|
||||
data-hover={dropState !== 'idle'}
|
||||
data-error={dropState === 'not-allow'}
|
||||
ref={targetRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const useDnDMonitor = (
|
||||
scrollRef: RefObject<HTMLElement>,
|
||||
onDragging: (item?: HierarchyItem) => void,
|
||||
onReorder: (item: HierarchyItem, container: DropContainerData) => void
|
||||
) => {
|
||||
useEffect(() => {
|
||||
const scrollElement = scrollRef.current;
|
||||
if (!scrollElement) {
|
||||
throw Error('Scroll element ref not configured');
|
||||
}
|
||||
|
||||
return combine(
|
||||
monitorForElements({
|
||||
onDrop: ({ source, location }) => {
|
||||
onDragging(undefined);
|
||||
const { dropTargets } = location.current;
|
||||
if (dropTargets.length === 0) return;
|
||||
onReorder(source.data as HierarchyItem, dropTargets[0].data as DropContainerData);
|
||||
},
|
||||
}),
|
||||
autoScrollForElements({
|
||||
element: scrollElement,
|
||||
})
|
||||
);
|
||||
}, [scrollRef, onDragging, onReorder]);
|
||||
};
|
||||
306
src/app/features/lobby/HierarchyItemMenu.tsx
Normal file
306
src/app/features/lobby/HierarchyItemMenu.tsx
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
import React, { MouseEventHandler, useCallback, useEffect, useState } from 'react';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import {
|
||||
Box,
|
||||
IconButton,
|
||||
Icon,
|
||||
Icons,
|
||||
PopOut,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Text,
|
||||
RectCords,
|
||||
config,
|
||||
Line,
|
||||
Spinner,
|
||||
toRem,
|
||||
} from 'folds';
|
||||
import { HierarchyItem } from '../../hooks/useSpaceHierarchy';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { MSpaceChildContent, StateEvent } from '../../../types/matrix/room';
|
||||
import {
|
||||
openInviteUser,
|
||||
openSpaceSettings,
|
||||
toggleRoomSettings,
|
||||
} from '../../../client/action/navigation';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||
import { UseStateProvider } from '../../components/UseStateProvider';
|
||||
import { LeaveSpacePrompt } from '../../components/leave-space-prompt';
|
||||
import { LeaveRoomPrompt } from '../../components/leave-room-prompt';
|
||||
|
||||
type HierarchyItemWithParent = HierarchyItem & {
|
||||
parentId: string;
|
||||
};
|
||||
|
||||
function SuggestMenuItem({
|
||||
item,
|
||||
requestClose,
|
||||
}: {
|
||||
item: HierarchyItemWithParent;
|
||||
requestClose: () => void;
|
||||
}) {
|
||||
const mx = useMatrixClient();
|
||||
const { roomId, parentId, content } = item;
|
||||
|
||||
const [toggleState, handleToggleSuggested] = useAsyncCallback(
|
||||
useCallback(() => {
|
||||
const newContent: MSpaceChildContent = { ...content, suggested: !content.suggested };
|
||||
return mx.sendStateEvent(parentId, StateEvent.SpaceChild, newContent, roomId);
|
||||
}, [mx, parentId, roomId, content])
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (toggleState.status === AsyncStatus.Success) {
|
||||
requestClose();
|
||||
}
|
||||
}, [requestClose, toggleState]);
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
onClick={handleToggleSuggested}
|
||||
size="300"
|
||||
radii="300"
|
||||
before={toggleState.status === AsyncStatus.Loading && <Spinner size="100" />}
|
||||
disabled={toggleState.status === AsyncStatus.Loading}
|
||||
>
|
||||
<Text as="span" size="T300" truncate>
|
||||
{content.suggested ? 'Unset Suggested' : 'Set Suggested'}
|
||||
</Text>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
function RemoveMenuItem({
|
||||
item,
|
||||
requestClose,
|
||||
}: {
|
||||
item: HierarchyItemWithParent;
|
||||
requestClose: () => void;
|
||||
}) {
|
||||
const mx = useMatrixClient();
|
||||
const { roomId, parentId } = item;
|
||||
|
||||
const [removeState, handleRemove] = useAsyncCallback(
|
||||
useCallback(
|
||||
() => mx.sendStateEvent(parentId, StateEvent.SpaceChild, {}, roomId),
|
||||
[mx, parentId, roomId]
|
||||
)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (removeState.status === AsyncStatus.Success) {
|
||||
requestClose();
|
||||
}
|
||||
}, [requestClose, removeState]);
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
onClick={handleRemove}
|
||||
variant="Critical"
|
||||
fill="None"
|
||||
size="300"
|
||||
radii="300"
|
||||
before={
|
||||
removeState.status === AsyncStatus.Loading && (
|
||||
<Spinner variant="Critical" fill="Soft" size="100" />
|
||||
)
|
||||
}
|
||||
disabled={removeState.status === AsyncStatus.Loading}
|
||||
>
|
||||
<Text as="span" size="T300" truncate>
|
||||
Remove
|
||||
</Text>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
function InviteMenuItem({
|
||||
item,
|
||||
requestClose,
|
||||
disabled,
|
||||
}: {
|
||||
item: HierarchyItemWithParent;
|
||||
requestClose: () => void;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const handleInvite = () => {
|
||||
openInviteUser(item.roomId);
|
||||
requestClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
onClick={handleInvite}
|
||||
size="300"
|
||||
radii="300"
|
||||
variant="Primary"
|
||||
fill="None"
|
||||
disabled={disabled}
|
||||
>
|
||||
<Text as="span" size="T300" truncate>
|
||||
Invite
|
||||
</Text>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
function SettingsMenuItem({
|
||||
item,
|
||||
requestClose,
|
||||
disabled,
|
||||
}: {
|
||||
item: HierarchyItemWithParent;
|
||||
requestClose: () => void;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const handleSettings = () => {
|
||||
if (item.space) {
|
||||
openSpaceSettings(item.roomId);
|
||||
} else {
|
||||
toggleRoomSettings(item.roomId);
|
||||
}
|
||||
requestClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<MenuItem onClick={handleSettings} size="300" radii="300" disabled={disabled}>
|
||||
<Text as="span" size="T300" truncate>
|
||||
Settings
|
||||
</Text>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
type HierarchyItemMenuProps = {
|
||||
item: HierarchyItem & {
|
||||
parentId: string;
|
||||
};
|
||||
joined: boolean;
|
||||
canInvite: boolean;
|
||||
canEditChild: boolean;
|
||||
pinned?: boolean;
|
||||
onTogglePin?: (roomId: string) => void;
|
||||
};
|
||||
export function HierarchyItemMenu({
|
||||
item,
|
||||
joined,
|
||||
canInvite,
|
||||
canEditChild,
|
||||
pinned,
|
||||
onTogglePin,
|
||||
}: HierarchyItemMenuProps) {
|
||||
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
|
||||
|
||||
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
setMenuAnchor(evt.currentTarget.getBoundingClientRect());
|
||||
};
|
||||
|
||||
const handleRequestClose = useCallback(() => setMenuAnchor(undefined), []);
|
||||
|
||||
if (!joined && !canEditChild) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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 style={{ maxWidth: toRem(150), width: '100vw' }}>
|
||||
{joined && (
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
{onTogglePin && (
|
||||
<MenuItem
|
||||
size="300"
|
||||
radii="300"
|
||||
onClick={() => {
|
||||
onTogglePin(item.roomId);
|
||||
handleRequestClose();
|
||||
}}
|
||||
>
|
||||
<Text as="span" size="T300" truncate>
|
||||
{pinned ? 'Unpin from Sidebar' : 'Pin to Sidebar'}
|
||||
</Text>
|
||||
</MenuItem>
|
||||
)}
|
||||
<InviteMenuItem
|
||||
item={item}
|
||||
requestClose={handleRequestClose}
|
||||
disabled={!canInvite}
|
||||
/>
|
||||
<SettingsMenuItem item={item} requestClose={handleRequestClose} />
|
||||
<UseStateProvider initial={false}>
|
||||
{(promptLeave, setPromptLeave) => (
|
||||
<>
|
||||
<MenuItem
|
||||
onClick={() => 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
|
||||
</Text>
|
||||
</MenuItem>
|
||||
{promptLeave &&
|
||||
(item.space ? (
|
||||
<LeaveSpacePrompt
|
||||
roomId={item.roomId}
|
||||
onDone={handleRequestClose}
|
||||
onCancel={() => setPromptLeave(false)}
|
||||
/>
|
||||
) : (
|
||||
<LeaveRoomPrompt
|
||||
roomId={item.roomId}
|
||||
onDone={handleRequestClose}
|
||||
onCancel={() => setPromptLeave(false)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</UseStateProvider>
|
||||
</Box>
|
||||
)}
|
||||
{(joined || canEditChild) && (
|
||||
<Line size="300" variant="Surface" direction="Horizontal" />
|
||||
)}
|
||||
{canEditChild && (
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
<SuggestMenuItem item={item} requestClose={handleRequestClose} />
|
||||
<RemoveMenuItem item={item} requestClose={handleRequestClose} />
|
||||
</Box>
|
||||
)}
|
||||
</Menu>
|
||||
</FocusTrap>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
528
src/app/features/lobby/Lobby.tsx
Normal file
528
src/app/features/lobby/Lobby.tsx
Normal file
|
|
@ -0,0 +1,528 @@
|
|||
import React, { MouseEventHandler, useCallback, useMemo, useRef, useState } from 'react';
|
||||
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 { IJoinRuleEventContent, JoinRule, RestrictedAllowType, Room } from 'matrix-js-sdk';
|
||||
import { useSpace } from '../../hooks/useSpace';
|
||||
import { Page, PageContent, PageContentCenter, PageHeroSection } from '../../components/page';
|
||||
import { HierarchyItem, useSpaceHierarchy } from '../../hooks/useSpaceHierarchy';
|
||||
import { VirtualTile } from '../../components/virtualizer';
|
||||
import { spaceRoomsAtom } from '../../state/spaceRooms';
|
||||
import { MembersDrawer } from '../room/MembersDrawer';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import { LobbyHeader } from './LobbyHeader';
|
||||
import { LobbyHero } from './LobbyHero';
|
||||
import { ScrollTopContainer } from '../../components/scroll-top-container';
|
||||
import { useElementSizeObserver } from '../../hooks/useElementSizeObserver';
|
||||
import {
|
||||
IPowerLevels,
|
||||
PowerLevelsContextProvider,
|
||||
powerLevelAPI,
|
||||
usePowerLevels,
|
||||
useRoomsPowerLevels,
|
||||
} from '../../hooks/usePowerLevels';
|
||||
import { RoomItemCard } from './RoomItem';
|
||||
import { mDirectAtom } from '../../state/mDirectList';
|
||||
import { SpaceItemCard } from './SpaceItem';
|
||||
import { makeLobbyCategoryId } from '../../state/closedLobbyCategories';
|
||||
import { useCategoryHandler } from '../../hooks/useCategoryHandler';
|
||||
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';
|
||||
import { AfterItemDropTarget, CanDropCallback, useDnDMonitor } from './DnD';
|
||||
import { ASCIILexicalTable, orderKeys } from '../../utils/ASCIILexicalTable';
|
||||
import { getStateEvent } from '../../utils/room';
|
||||
import { useClosedLobbyCategoriesAtom } from '../../state/hooks/closedLobbyCategories';
|
||||
import {
|
||||
makeCinnySpacesContent,
|
||||
sidebarItemWithout,
|
||||
useSidebarItems,
|
||||
} from '../../hooks/useSidebarItems';
|
||||
import { useOrphanSpaces } from '../../state/hooks/roomList';
|
||||
import { roomToParentsAtom } from '../../state/room/roomToParents';
|
||||
import { AccountDataEvent } from '../../../types/matrix/accountData';
|
||||
|
||||
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 space = useSpace();
|
||||
const spacePowerLevels = usePowerLevels(space);
|
||||
const lex = useMemo(() => new ASCIILexicalTable(' '.charCodeAt(0), '~'.charCodeAt(0), 6), []);
|
||||
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const heroSectionRef = useRef<HTMLDivElement>(null);
|
||||
const [heroSectionHeight, setHeroSectionHeight] = useState<number>();
|
||||
const [spaceRooms, setSpaceRooms] = useAtom(spaceRoomsAtom);
|
||||
const [isDrawer] = useSetting(settingsAtom, 'isPeopleDrawer');
|
||||
const screenSize = useScreenSizeContext();
|
||||
const [onTop, setOnTop] = useState(true);
|
||||
const [closedCategories, setClosedCategories] = useAtom(useClosedLobbyCategoriesAtom());
|
||||
const [sidebarItems] = useSidebarItems(
|
||||
useOrphanSpaces(mx, allRoomsAtom, useAtomValue(roomToParentsAtom))
|
||||
);
|
||||
const sidebarSpaces = useMemo(() => {
|
||||
const sideSpaces = sidebarItems.flatMap((item) => {
|
||||
if (typeof item === 'string') return item;
|
||||
return item.content;
|
||||
});
|
||||
|
||||
return new Set(sideSpaces);
|
||||
}, [sidebarItems]);
|
||||
|
||||
useElementSizeObserver(
|
||||
useCallback(() => heroSectionRef.current, []),
|
||||
useCallback((w, height) => setHeroSectionHeight(height), [])
|
||||
);
|
||||
|
||||
const getRoom = useCallback(
|
||||
(rId: string) => {
|
||||
if (allJoinedRooms.has(rId)) {
|
||||
return mx.getRoom(rId) ?? undefined;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
[mx, allJoinedRooms]
|
||||
);
|
||||
|
||||
const canEditSpaceChild = useCallback(
|
||||
(powerLevels: IPowerLevels) =>
|
||||
powerLevelAPI.canSendStateEvent(
|
||||
powerLevels,
|
||||
StateEvent.SpaceChild,
|
||||
powerLevelAPI.getPowerLevel(powerLevels, mx.getUserId() ?? undefined)
|
||||
),
|
||||
[mx]
|
||||
);
|
||||
|
||||
const [draggingItem, setDraggingItem] = useState<HierarchyItem>();
|
||||
const flattenHierarchy = useSpaceHierarchy(
|
||||
space.roomId,
|
||||
spaceRooms,
|
||||
getRoom,
|
||||
useCallback(
|
||||
(childId) =>
|
||||
closedCategories.has(makeLobbyCategoryId(space.roomId, childId)) || !!draggingItem?.space,
|
||||
[closedCategories, space.roomId, draggingItem]
|
||||
)
|
||||
);
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: flattenHierarchy.length,
|
||||
getScrollElement: () => scrollRef.current,
|
||||
estimateSize: () => 1,
|
||||
overscan: 2,
|
||||
paddingStart: heroSectionHeight ?? 258,
|
||||
});
|
||||
const vItems = virtualizer.getVirtualItems();
|
||||
|
||||
const roomsPowerLevels = useRoomsPowerLevels(
|
||||
useMemo(
|
||||
() => flattenHierarchy.map((i) => mx.getRoom(i.roomId)).filter((r) => !!r) as Room[],
|
||||
[mx, flattenHierarchy]
|
||||
)
|
||||
);
|
||||
|
||||
const canDrop: CanDropCallback = useCallback(
|
||||
(item, container): boolean => {
|
||||
const restrictedItem = mx.getRoom(item.roomId)?.getJoinRule() === JoinRule.Restricted;
|
||||
if (item.roomId === container.item.roomId || item.roomId === container.nextRoomId) {
|
||||
// can not drop before or after itself
|
||||
return false;
|
||||
}
|
||||
|
||||
if (item.space) {
|
||||
if (!container.item.space) return false;
|
||||
const containerSpaceId = space.roomId;
|
||||
|
||||
if (
|
||||
getRoom(containerSpaceId) === undefined ||
|
||||
!canEditSpaceChild(roomsPowerLevels.get(containerSpaceId) ?? {})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const containerSpaceId = container.item.space
|
||||
? container.item.roomId
|
||||
: container.item.parentId;
|
||||
|
||||
const dropOutsideSpace = item.parentId !== containerSpaceId;
|
||||
|
||||
if (dropOutsideSpace && restrictedItem) {
|
||||
// do not allow restricted room to drop outside
|
||||
// current space if can't change join rule allow
|
||||
const itemPowerLevel = roomsPowerLevels.get(item.roomId) ?? {};
|
||||
const userPLInItem = powerLevelAPI.getPowerLevel(
|
||||
itemPowerLevel,
|
||||
mx.getUserId() ?? undefined
|
||||
);
|
||||
const canChangeJoinRuleAllow = powerLevelAPI.canSendStateEvent(
|
||||
itemPowerLevel,
|
||||
StateEvent.RoomJoinRules,
|
||||
userPLInItem
|
||||
);
|
||||
if (!canChangeJoinRuleAllow) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
getRoom(containerSpaceId) === undefined ||
|
||||
!canEditSpaceChild(roomsPowerLevels.get(containerSpaceId) ?? {})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
[getRoom, space.roomId, roomsPowerLevels, canEditSpaceChild, mx]
|
||||
);
|
||||
|
||||
const reorderSpace = useCallback(
|
||||
(item: HierarchyItem, containerItem: HierarchyItem) => {
|
||||
if (!item.parentId) return;
|
||||
|
||||
const childItems = flattenHierarchy
|
||||
.filter((i) => i.parentId && i.space)
|
||||
.filter((i) => i.roomId !== item.roomId);
|
||||
|
||||
const beforeIndex = childItems.findIndex((i) => i.roomId === containerItem.roomId);
|
||||
const insertIndex = beforeIndex + 1;
|
||||
|
||||
childItems.splice(insertIndex, 0, {
|
||||
...item,
|
||||
content: { ...item.content, order: undefined },
|
||||
});
|
||||
|
||||
const currentOrders = childItems.map((i) => {
|
||||
if (typeof i.content.order === 'string' && lex.has(i.content.order)) {
|
||||
return i.content.order;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const newOrders = orderKeys(lex, currentOrders);
|
||||
|
||||
newOrders?.forEach((orderKey, index) => {
|
||||
const itm = childItems[index];
|
||||
if (!itm || !itm.parentId) return;
|
||||
const parentPL = roomsPowerLevels.get(itm.parentId);
|
||||
const canEdit = parentPL && canEditSpaceChild(parentPL);
|
||||
if (canEdit && orderKey !== currentOrders[index]) {
|
||||
mx.sendStateEvent(
|
||||
itm.parentId,
|
||||
StateEvent.SpaceChild,
|
||||
{ ...itm.content, order: orderKey },
|
||||
itm.roomId
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
[mx, flattenHierarchy, lex, roomsPowerLevels, canEditSpaceChild]
|
||||
);
|
||||
|
||||
const reorderRoom = useCallback(
|
||||
(item: HierarchyItem, containerItem: HierarchyItem): void => {
|
||||
const itemRoom = mx.getRoom(item.roomId);
|
||||
if (!item.parentId) {
|
||||
return;
|
||||
}
|
||||
const containerParentId: string = containerItem.space
|
||||
? containerItem.roomId
|
||||
: containerItem.parentId;
|
||||
const itemContent = item.content;
|
||||
|
||||
if (item.parentId !== containerParentId) {
|
||||
mx.sendStateEvent(item.parentId, StateEvent.SpaceChild, {}, item.roomId);
|
||||
}
|
||||
|
||||
if (
|
||||
itemRoom &&
|
||||
itemRoom.getJoinRule() === JoinRule.Restricted &&
|
||||
item.parentId !== containerParentId
|
||||
) {
|
||||
// change join rule allow parameter when dragging
|
||||
// restricted room from one space to another
|
||||
const joinRuleContent = getStateEvent(
|
||||
itemRoom,
|
||||
StateEvent.RoomJoinRules
|
||||
)?.getContent<IJoinRuleEventContent>();
|
||||
|
||||
if (joinRuleContent) {
|
||||
const allow =
|
||||
joinRuleContent.allow?.filter((allowRule) => allowRule.room_id !== item.parentId) ?? [];
|
||||
allow.push({ type: RestrictedAllowType.RoomMembership, room_id: containerParentId });
|
||||
mx.sendStateEvent(itemRoom.roomId, StateEvent.RoomJoinRules, {
|
||||
...joinRuleContent,
|
||||
allow,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const childItems = flattenHierarchy
|
||||
.filter((i) => i.parentId === containerParentId && !i.space)
|
||||
.filter((i) => i.roomId !== item.roomId);
|
||||
|
||||
const beforeItem: HierarchyItem | undefined = containerItem.space ? undefined : containerItem;
|
||||
const beforeIndex = childItems.findIndex((i) => i.roomId === beforeItem?.roomId);
|
||||
const insertIndex = beforeIndex + 1;
|
||||
|
||||
childItems.splice(insertIndex, 0, {
|
||||
...item,
|
||||
parentId: containerParentId,
|
||||
content: { ...itemContent, order: undefined },
|
||||
});
|
||||
|
||||
const currentOrders = childItems.map((i) => {
|
||||
if (typeof i.content.order === 'string' && lex.has(i.content.order)) {
|
||||
return i.content.order;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const newOrders = orderKeys(lex, currentOrders);
|
||||
|
||||
newOrders?.forEach((orderKey, index) => {
|
||||
const itm = childItems[index];
|
||||
if (itm && orderKey !== currentOrders[index]) {
|
||||
mx.sendStateEvent(
|
||||
containerParentId,
|
||||
StateEvent.SpaceChild,
|
||||
{ ...itm.content, order: orderKey },
|
||||
itm.roomId
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
[mx, flattenHierarchy, lex]
|
||||
);
|
||||
|
||||
useDnDMonitor(
|
||||
scrollRef,
|
||||
setDraggingItem,
|
||||
useCallback(
|
||||
(item, container) => {
|
||||
if (!canDrop(item, container)) {
|
||||
return;
|
||||
}
|
||||
if (item.space) {
|
||||
reorderSpace(item, container.item);
|
||||
} else {
|
||||
reorderRoom(item, container.item);
|
||||
}
|
||||
},
|
||||
[reorderRoom, reorderSpace, canDrop]
|
||||
)
|
||||
);
|
||||
|
||||
const addSpaceRoom = useCallback(
|
||||
(roomId: string) => setSpaceRooms({ type: 'PUT', roomId }),
|
||||
[setSpaceRooms]
|
||||
);
|
||||
|
||||
const handleCategoryClick = useCategoryHandler(setClosedCategories, (categoryId) =>
|
||||
closedCategories.has(categoryId)
|
||||
);
|
||||
|
||||
const handleOpenRoom: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
const rId = evt.currentTarget.getAttribute('data-room-id');
|
||||
if (!rId) return;
|
||||
const pSpaceIdOrAlias = getCanonicalAliasOrRoomId(mx, space.roomId);
|
||||
navigate(getSpaceRoomPath(pSpaceIdOrAlias, getCanonicalAliasOrRoomId(mx, rId)));
|
||||
};
|
||||
|
||||
const togglePinToSidebar = useCallback(
|
||||
(rId: string) => {
|
||||
const newItems = sidebarItemWithout(sidebarItems, rId);
|
||||
if (!sidebarSpaces.has(rId)) {
|
||||
newItems.push(rId);
|
||||
}
|
||||
const newSpacesContent = makeCinnySpacesContent(mx, newItems);
|
||||
mx.setAccountData(AccountDataEvent.CinnySpaces, newSpacesContent);
|
||||
},
|
||||
[mx, sidebarItems, sidebarSpaces]
|
||||
);
|
||||
|
||||
return (
|
||||
<PowerLevelsContextProvider value={spacePowerLevels}>
|
||||
<Box grow="Yes">
|
||||
<Page>
|
||||
<LobbyHeader
|
||||
showProfile={!onTop}
|
||||
powerLevels={roomsPowerLevels.get(space.roomId) ?? {}}
|
||||
/>
|
||||
<Box style={{ position: 'relative' }} grow="Yes">
|
||||
<Scroll ref={scrollRef} hideTrack visibility="Hover">
|
||||
<PageContent>
|
||||
<PageContentCenter>
|
||||
<ScrollTopContainer
|
||||
scrollRef={scrollRef}
|
||||
anchorRef={heroSectionRef}
|
||||
onVisibilityChange={setOnTop}
|
||||
>
|
||||
<IconButton
|
||||
onClick={() => virtualizer.scrollToOffset(0)}
|
||||
variant="SurfaceVariant"
|
||||
radii="Pill"
|
||||
outlined
|
||||
size="300"
|
||||
aria-label="Scroll to Top"
|
||||
>
|
||||
<Icon src={Icons.ChevronTop} size="300" />
|
||||
</IconButton>
|
||||
</ScrollTopContainer>
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
height: virtualizer.getTotalSize(),
|
||||
}}
|
||||
>
|
||||
<PageHeroSection ref={heroSectionRef} style={{ paddingTop: 0 }}>
|
||||
<LobbyHero />
|
||||
</PageHeroSection>
|
||||
{vItems.map((vItem) => {
|
||||
const item = flattenHierarchy[vItem.index];
|
||||
if (!item) return null;
|
||||
const itemPowerLevel = roomsPowerLevels.get(item.roomId) ?? {};
|
||||
const userPLInItem = powerLevelAPI.getPowerLevel(
|
||||
itemPowerLevel,
|
||||
mx.getUserId() ?? undefined
|
||||
);
|
||||
const canInvite = powerLevelAPI.canDoAction(
|
||||
itemPowerLevel,
|
||||
'invite',
|
||||
userPLInItem
|
||||
);
|
||||
const isJoined = allJoinedRooms.has(item.roomId);
|
||||
|
||||
const nextRoomId: string | undefined =
|
||||
flattenHierarchy[vItem.index + 1]?.roomId;
|
||||
|
||||
const dragging =
|
||||
draggingItem?.roomId === item.roomId &&
|
||||
draggingItem.parentId === item.parentId;
|
||||
|
||||
if (item.space) {
|
||||
const categoryId = makeLobbyCategoryId(space.roomId, item.roomId);
|
||||
const { parentId } = item;
|
||||
const parentPowerLevels = parentId
|
||||
? roomsPowerLevels.get(parentId) ?? {}
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<VirtualTile
|
||||
virtualItem={vItem}
|
||||
style={{
|
||||
paddingTop: vItem.index === 0 ? 0 : config.space.S500,
|
||||
}}
|
||||
ref={virtualizer.measureElement}
|
||||
key={vItem.index}
|
||||
>
|
||||
<SpaceItemCard
|
||||
item={item}
|
||||
joined={allJoinedRooms.has(item.roomId)}
|
||||
categoryId={categoryId}
|
||||
closed={closedCategories.has(categoryId) || !!draggingItem?.space}
|
||||
handleClose={handleCategoryClick}
|
||||
getRoom={getRoom}
|
||||
canEditChild={canEditSpaceChild(
|
||||
roomsPowerLevels.get(item.roomId) ?? {}
|
||||
)}
|
||||
canReorder={
|
||||
parentPowerLevels ? canEditSpaceChild(parentPowerLevels) : false
|
||||
}
|
||||
options={
|
||||
parentId &&
|
||||
parentPowerLevels && (
|
||||
<HierarchyItemMenu
|
||||
item={{ ...item, parentId }}
|
||||
canInvite={canInvite}
|
||||
joined={isJoined}
|
||||
canEditChild={canEditSpaceChild(parentPowerLevels)}
|
||||
pinned={sidebarSpaces.has(item.roomId)}
|
||||
onTogglePin={togglePinToSidebar}
|
||||
/>
|
||||
)
|
||||
}
|
||||
before={item.parentId ? undefined : undefined}
|
||||
after={
|
||||
<AfterItemDropTarget
|
||||
item={item}
|
||||
nextRoomId={nextRoomId}
|
||||
afterSpace
|
||||
canDrop={canDrop}
|
||||
/>
|
||||
}
|
||||
onDragging={setDraggingItem}
|
||||
data-dragging={dragging}
|
||||
/>
|
||||
</VirtualTile>
|
||||
);
|
||||
}
|
||||
|
||||
const parentPowerLevels = roomsPowerLevels.get(item.parentId) ?? {};
|
||||
const prevItem: HierarchyItem | undefined = flattenHierarchy[vItem.index - 1];
|
||||
const nextItem: HierarchyItem | undefined = flattenHierarchy[vItem.index + 1];
|
||||
return (
|
||||
<VirtualTile
|
||||
virtualItem={vItem}
|
||||
style={{ paddingTop: config.space.S100 }}
|
||||
ref={virtualizer.measureElement}
|
||||
key={vItem.index}
|
||||
>
|
||||
<RoomItemCard
|
||||
item={item}
|
||||
onSpaceFound={addSpaceRoom}
|
||||
dm={mDirects.has(item.roomId)}
|
||||
firstChild={!prevItem || prevItem.space === true}
|
||||
lastChild={!nextItem || nextItem.space === true}
|
||||
onOpen={handleOpenRoom}
|
||||
getRoom={getRoom}
|
||||
canReorder={canEditSpaceChild(parentPowerLevels)}
|
||||
options={
|
||||
<HierarchyItemMenu
|
||||
item={item}
|
||||
canInvite={canInvite}
|
||||
joined={isJoined}
|
||||
canEditChild={canEditSpaceChild(parentPowerLevels)}
|
||||
/>
|
||||
}
|
||||
after={
|
||||
<AfterItemDropTarget
|
||||
item={item}
|
||||
nextRoomId={nextRoomId}
|
||||
canDrop={canDrop}
|
||||
/>
|
||||
}
|
||||
data-dragging={dragging}
|
||||
onDragging={setDraggingItem}
|
||||
/>
|
||||
</VirtualTile>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</PageContentCenter>
|
||||
</PageContent>
|
||||
</Scroll>
|
||||
</Box>
|
||||
</Page>
|
||||
{screenSize === ScreenSize.Desktop && isDrawer && (
|
||||
<>
|
||||
<Line variant="Background" direction="Vertical" size="300" />
|
||||
<MembersDrawer room={space} />
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</PowerLevelsContextProvider>
|
||||
);
|
||||
}
|
||||
13
src/app/features/lobby/LobbyHeader.css.ts
Normal file
13
src/app/features/lobby/LobbyHeader.css.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { style } from '@vanilla-extract/css';
|
||||
import { config } from 'folds';
|
||||
|
||||
export const Header = style({
|
||||
borderBottomColor: 'transparent',
|
||||
});
|
||||
export const HeaderTopic = style({
|
||||
':hover': {
|
||||
cursor: 'pointer',
|
||||
opacity: config.opacity.P500,
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
});
|
||||
214
src/app/features/lobby/LobbyHeader.tsx
Normal file
214
src/app/features/lobby/LobbyHeader.tsx
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
import React, { MouseEventHandler, forwardRef, useState } from 'react';
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Icon,
|
||||
IconButton,
|
||||
Icons,
|
||||
Line,
|
||||
Menu,
|
||||
MenuItem,
|
||||
PopOut,
|
||||
RectCords,
|
||||
Text,
|
||||
Tooltip,
|
||||
TooltipProvider,
|
||||
config,
|
||||
toRem,
|
||||
} from 'folds';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { PageHeader } from '../../components/page';
|
||||
import { useSetSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import { useRoomAvatar, useRoomName } from '../../hooks/useRoomMeta';
|
||||
import { useSpace } from '../../hooks/useSpace';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { RoomAvatar } from '../../components/room-avatar';
|
||||
import { nameInitials } from '../../utils/common';
|
||||
import * as css from './LobbyHeader.css';
|
||||
import { openInviteUser, openSpaceSettings } from '../../../client/action/navigation';
|
||||
import { IPowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels';
|
||||
import { UseStateProvider } from '../../components/UseStateProvider';
|
||||
import { LeaveSpacePrompt } from '../../components/leave-space-prompt';
|
||||
|
||||
type LobbyMenuProps = {
|
||||
roomId: string;
|
||||
powerLevels: IPowerLevels;
|
||||
requestClose: () => void;
|
||||
};
|
||||
const LobbyMenu = forwardRef<HTMLDivElement, LobbyMenuProps>(
|
||||
({ roomId, powerLevels, requestClose }, ref) => {
|
||||
const mx = useMatrixClient();
|
||||
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
|
||||
const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
|
||||
|
||||
const handleInvite = () => {
|
||||
openInviteUser(roomId);
|
||||
requestClose();
|
||||
};
|
||||
|
||||
const handleRoomSettings = () => {
|
||||
openSpaceSettings(roomId);
|
||||
requestClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
|
||||
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
||||
<MenuItem
|
||||
onClick={handleInvite}
|
||||
variant="Primary"
|
||||
fill="None"
|
||||
size="300"
|
||||
after={<Icon size="100" src={Icons.UserPlus} />}
|
||||
radii="300"
|
||||
disabled={!canInvite}
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||
Invite
|
||||
</Text>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={handleRoomSettings}
|
||||
size="300"
|
||||
after={<Icon size="100" src={Icons.Setting} />}
|
||||
radii="300"
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||
Space Settings
|
||||
</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={() => 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={roomId}
|
||||
onDone={requestClose}
|
||||
onCancel={() => setPromptLeave(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</UseStateProvider>
|
||||
</Box>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
type LobbyHeaderProps = {
|
||||
showProfile?: boolean;
|
||||
powerLevels: IPowerLevels;
|
||||
};
|
||||
export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) {
|
||||
const mx = useMatrixClient();
|
||||
const space = useSpace();
|
||||
const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer');
|
||||
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
|
||||
|
||||
const name = useRoomName(space);
|
||||
const avatarMxc = useRoomAvatar(space);
|
||||
const avatarUrl = avatarMxc ? mx.mxcUrlToHttp(avatarMxc, 96, 96, 'crop') ?? undefined : undefined;
|
||||
|
||||
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
setMenuAnchor(evt.currentTarget.getBoundingClientRect());
|
||||
};
|
||||
|
||||
return (
|
||||
<PageHeader className={showProfile ? undefined : css.Header}>
|
||||
<Box grow="Yes" alignItems="Center" gap="200">
|
||||
<Box grow="Yes" basis="No" />
|
||||
<Box justifyContent="Center" alignItems="Center" gap="300">
|
||||
{showProfile && (
|
||||
<>
|
||||
<Avatar size="300">
|
||||
<RoomAvatar
|
||||
roomId={space.roomId}
|
||||
src={avatarUrl}
|
||||
alt={name}
|
||||
renderFallback={() => <Text size="H4">{nameInitials(name)}</Text>}
|
||||
/>
|
||||
</Avatar>
|
||||
<Text size="H3" truncate>
|
||||
{name}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
<Box shrink="No" grow="Yes" basis="No" justifyContent="End">
|
||||
<TooltipProvider
|
||||
position="Bottom"
|
||||
offset={4}
|
||||
tooltip={
|
||||
<Tooltip>
|
||||
<Text>Members</Text>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
{(triggerRef) => (
|
||||
<IconButton ref={triggerRef} onClick={() => setPeopleDrawer((drawer) => !drawer)}>
|
||||
<Icon size="400" src={Icons.User} />
|
||||
</IconButton>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
<TooltipProvider
|
||||
position="Bottom"
|
||||
align="End"
|
||||
offset={4}
|
||||
tooltip={
|
||||
<Tooltip>
|
||||
<Text>More Options</Text>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
{(triggerRef) => (
|
||||
<IconButton onClick={handleOpenMenu} ref={triggerRef} aria-pressed={!!menuAnchor}>
|
||||
<Icon size="400" src={Icons.VerticalDots} filled={!!menuAnchor} />
|
||||
</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',
|
||||
}}
|
||||
>
|
||||
<LobbyMenu
|
||||
roomId={space.roomId}
|
||||
powerLevels={powerLevels}
|
||||
requestClose={() => setMenuAnchor(undefined)}
|
||||
/>
|
||||
</FocusTrap>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</PageHeader>
|
||||
);
|
||||
}
|
||||
15
src/app/features/lobby/LobbyHero.css.tsx
Normal file
15
src/app/features/lobby/LobbyHero.css.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { style } from '@vanilla-extract/css';
|
||||
import { config } from 'folds';
|
||||
|
||||
export const LobbyHeroTopic = style({
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 3,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
|
||||
':hover': {
|
||||
cursor: 'pointer',
|
||||
opacity: config.opacity.P500,
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
});
|
||||
77
src/app/features/lobby/LobbyHero.tsx
Normal file
77
src/app/features/lobby/LobbyHero.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import React from 'react';
|
||||
import { Avatar, Overlay, OverlayBackdrop, OverlayCenter, Text } from 'folds';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { useRoomAvatar, useRoomName, useRoomTopic } from '../../hooks/useRoomMeta';
|
||||
import { useSpace } from '../../hooks/useSpace';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { RoomAvatar } from '../../components/room-avatar';
|
||||
import { nameInitials } from '../../utils/common';
|
||||
import { UseStateProvider } from '../../components/UseStateProvider';
|
||||
import { RoomTopicViewer } from '../../components/room-topic-viewer';
|
||||
import * as css from './LobbyHero.css';
|
||||
import { PageHero } from '../../components/page';
|
||||
import { onEnterOrSpace } from '../../utils/keyboard';
|
||||
|
||||
export function LobbyHero() {
|
||||
const mx = useMatrixClient();
|
||||
const space = useSpace();
|
||||
|
||||
const name = useRoomName(space);
|
||||
const topic = useRoomTopic(space);
|
||||
const avatarMxc = useRoomAvatar(space);
|
||||
const avatarUrl = avatarMxc ? mx.mxcUrlToHttp(avatarMxc, 96, 96, 'crop') ?? undefined : undefined;
|
||||
|
||||
return (
|
||||
<PageHero
|
||||
icon={
|
||||
<Avatar size="500">
|
||||
<RoomAvatar
|
||||
roomId={space.roomId}
|
||||
src={avatarUrl}
|
||||
alt={name}
|
||||
renderFallback={() => <Text size="H4">{nameInitials(name)}</Text>}
|
||||
/>
|
||||
</Avatar>
|
||||
}
|
||||
title={name}
|
||||
subTitle={
|
||||
topic && (
|
||||
<UseStateProvider initial={false}>
|
||||
{(viewTopic, setViewTopic) => (
|
||||
<>
|
||||
<Overlay open={viewTopic} backdrop={<OverlayBackdrop />}>
|
||||
<OverlayCenter>
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
clickOutsideDeactivates: true,
|
||||
onDeactivate: () => setViewTopic(false),
|
||||
}}
|
||||
>
|
||||
<RoomTopicViewer
|
||||
name={name}
|
||||
topic={topic}
|
||||
requestClose={() => setViewTopic(false)}
|
||||
/>
|
||||
</FocusTrap>
|
||||
</OverlayCenter>
|
||||
</Overlay>
|
||||
<Text
|
||||
as="span"
|
||||
onClick={() => setViewTopic(true)}
|
||||
onKeyDown={onEnterOrSpace(() => setViewTopic(true))}
|
||||
tabIndex={0}
|
||||
className={css.LobbyHeroTopic}
|
||||
size="Inherit"
|
||||
priority="300"
|
||||
>
|
||||
{topic}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</UseStateProvider>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
22
src/app/features/lobby/RoomItem.css.ts
Normal file
22
src/app/features/lobby/RoomItem.css.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { style } from '@vanilla-extract/css';
|
||||
import { config, toRem } from 'folds';
|
||||
|
||||
export const RoomItemCard = style({
|
||||
padding: config.space.S400,
|
||||
borderRadius: 0,
|
||||
position: 'relative',
|
||||
selectors: {
|
||||
'&[data-dragging=true]': {
|
||||
opacity: config.opacity.Disabled,
|
||||
},
|
||||
},
|
||||
});
|
||||
export const RoomProfileTopic = style({
|
||||
cursor: 'pointer',
|
||||
':hover': {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
});
|
||||
export const ErrorNameContainer = style({
|
||||
gap: toRem(2),
|
||||
});
|
||||
441
src/app/features/lobby/RoomItem.tsx
Normal file
441
src/app/features/lobby/RoomItem.tsx
Normal file
|
|
@ -0,0 +1,441 @@
|
|||
import React, { MouseEventHandler, ReactNode, useCallback, useEffect, useRef } from 'react';
|
||||
import {
|
||||
Avatar,
|
||||
Badge,
|
||||
Box,
|
||||
Chip,
|
||||
Icon,
|
||||
Icons,
|
||||
Line,
|
||||
Overlay,
|
||||
OverlayBackdrop,
|
||||
OverlayCenter,
|
||||
Spinner,
|
||||
Text,
|
||||
Tooltip,
|
||||
TooltipProvider,
|
||||
as,
|
||||
color,
|
||||
toRem,
|
||||
} from 'folds';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { JoinRule, MatrixError, Room } from 'matrix-js-sdk';
|
||||
import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
|
||||
import { SequenceCard } from '../../components/sequence-card';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { HierarchyItem } from '../../hooks/useSpaceHierarchy';
|
||||
import { millify } from '../../plugins/millify';
|
||||
import {
|
||||
HierarchyRoomSummaryLoader,
|
||||
LocalRoomSummaryLoader,
|
||||
} from '../../components/RoomSummaryLoader';
|
||||
import { UseStateProvider } from '../../components/UseStateProvider';
|
||||
import { RoomTopicViewer } from '../../components/room-topic-viewer';
|
||||
import { onEnterOrSpace } from '../../utils/keyboard';
|
||||
import { Membership, RoomType } from '../../../types/matrix/room';
|
||||
import * as css from './RoomItem.css';
|
||||
import * as styleCss from './style.css';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||
import { ErrorCode } from '../../cs-errorcode';
|
||||
import { getDirectRoomAvatarUrl, getRoomAvatarUrl } from '../../utils/room';
|
||||
import { ItemDraggableTarget, useDraggableItem } from './DnD';
|
||||
|
||||
type RoomJoinButtonProps = {
|
||||
roomId: string;
|
||||
via?: string[];
|
||||
};
|
||||
function RoomJoinButton({ roomId, via }: RoomJoinButtonProps) {
|
||||
const mx = useMatrixClient();
|
||||
|
||||
const [joinState, join] = useAsyncCallback<Room, MatrixError, []>(
|
||||
useCallback(() => mx.joinRoom(roomId, { viaServers: via }), [mx, roomId, via])
|
||||
);
|
||||
|
||||
const canJoin = joinState.status === AsyncStatus.Idle || joinState.status === AsyncStatus.Error;
|
||||
|
||||
return (
|
||||
<Box shrink="No" gap="200" alignItems="Center">
|
||||
{joinState.status === AsyncStatus.Error && (
|
||||
<TooltipProvider
|
||||
tooltip={
|
||||
<Tooltip variant="Critical" style={{ maxWidth: toRem(200) }}>
|
||||
<Box direction="Column" gap="100">
|
||||
<Text style={{ wordBreak: 'break-word' }} size="T400">
|
||||
{joinState.error.data?.error || joinState.error.message}
|
||||
</Text>
|
||||
<Text size="T200">{joinState.error.name}</Text>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
{(triggerRef) => (
|
||||
<Icon
|
||||
ref={triggerRef}
|
||||
style={{ color: color.Critical.Main, cursor: 'pointer' }}
|
||||
src={Icons.Warning}
|
||||
size="400"
|
||||
filled
|
||||
tabIndex={0}
|
||||
aria-label={joinState.error.data?.error || joinState.error.message}
|
||||
/>
|
||||
)}
|
||||
</TooltipProvider>
|
||||
)}
|
||||
<Chip
|
||||
variant="Secondary"
|
||||
fill="Soft"
|
||||
size="400"
|
||||
radii="Pill"
|
||||
before={
|
||||
canJoin ? <Icon src={Icons.Plus} size="50" /> : <Spinner variant="Secondary" size="100" />
|
||||
}
|
||||
onClick={join}
|
||||
disabled={!canJoin}
|
||||
>
|
||||
<Text size="B300">Join</Text>
|
||||
</Chip>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function RoomProfileLoading() {
|
||||
return (
|
||||
<Box grow="Yes" gap="300">
|
||||
<Avatar className={styleCss.AvatarPlaceholder} />
|
||||
<Box grow="Yes" direction="Column" gap="100">
|
||||
<Box gap="200" alignItems="Center">
|
||||
<Box className={styleCss.LinePlaceholder} shrink="No" style={{ maxWidth: toRem(80) }} />
|
||||
</Box>
|
||||
<Box gap="200" alignItems="Center">
|
||||
<Box className={styleCss.LinePlaceholder} shrink="No" style={{ maxWidth: toRem(40) }} />
|
||||
<Box
|
||||
className={styleCss.LinePlaceholder}
|
||||
shrink="No"
|
||||
style={{
|
||||
maxWidth: toRem(120),
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
type RoomProfileErrorProps = {
|
||||
roomId: string;
|
||||
error: Error;
|
||||
suggested?: boolean;
|
||||
via?: string[];
|
||||
};
|
||||
function RoomProfileError({ roomId, suggested, error, via }: RoomProfileErrorProps) {
|
||||
const privateRoom = error.name === ErrorCode.M_FORBIDDEN;
|
||||
|
||||
return (
|
||||
<Box grow="Yes" gap="300">
|
||||
<Avatar>
|
||||
<RoomAvatar
|
||||
roomId={roomId}
|
||||
src={undefined}
|
||||
alt={roomId}
|
||||
renderFallback={() => (
|
||||
<RoomIcon
|
||||
size="300"
|
||||
joinRule={privateRoom ? JoinRule.Invite : JoinRule.Restricted}
|
||||
filled
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Avatar>
|
||||
<Box grow="Yes" direction="Column" className={css.ErrorNameContainer}>
|
||||
<Box gap="200" alignItems="Center">
|
||||
<Text size="H5" truncate>
|
||||
Unknown
|
||||
</Text>
|
||||
{suggested && (
|
||||
<Box shrink="No" alignItems="Center">
|
||||
<Badge variant="Success" fill="Soft" radii="Pill" outlined>
|
||||
<Text size="L400">Suggested</Text>
|
||||
</Badge>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Box gap="200" alignItems="Center">
|
||||
{privateRoom && (
|
||||
<>
|
||||
<Badge variant="Secondary" fill="Soft" radii="Pill" outlined>
|
||||
<Text size="L400">Private Room</Text>
|
||||
</Badge>
|
||||
<Line
|
||||
variant="SurfaceVariant"
|
||||
style={{ height: toRem(12) }}
|
||||
direction="Vertical"
|
||||
size="400"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Text size="T200" truncate>
|
||||
{roomId}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
{!privateRoom && <RoomJoinButton roomId={roomId} via={via} />}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
type RoomProfileProps = {
|
||||
roomId: string;
|
||||
name: string;
|
||||
topic?: string;
|
||||
avatarUrl?: string;
|
||||
suggested?: boolean;
|
||||
memberCount?: number;
|
||||
joinRule?: JoinRule;
|
||||
options?: ReactNode;
|
||||
};
|
||||
function RoomProfile({
|
||||
roomId,
|
||||
name,
|
||||
topic,
|
||||
avatarUrl,
|
||||
suggested,
|
||||
memberCount,
|
||||
joinRule,
|
||||
options,
|
||||
}: RoomProfileProps) {
|
||||
return (
|
||||
<Box grow="Yes" gap="300">
|
||||
<Avatar>
|
||||
<RoomAvatar
|
||||
roomId={roomId}
|
||||
src={avatarUrl}
|
||||
alt={name}
|
||||
renderFallback={() => (
|
||||
<RoomIcon size="300" joinRule={joinRule ?? JoinRule.Restricted} filled />
|
||||
)}
|
||||
/>
|
||||
</Avatar>
|
||||
<Box grow="Yes" direction="Column">
|
||||
<Box gap="200" alignItems="Center">
|
||||
<Text size="H5" truncate>
|
||||
{name}
|
||||
</Text>
|
||||
{suggested && (
|
||||
<Box shrink="No" alignItems="Center">
|
||||
<Badge variant="Success" fill="Soft" radii="Pill" outlined>
|
||||
<Text size="L400">Suggested</Text>
|
||||
</Badge>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Box gap="200" alignItems="Center">
|
||||
{memberCount && (
|
||||
<Box shrink="No" gap="200">
|
||||
<Text size="T200" priority="300">{`${millify(memberCount)} Members`}</Text>
|
||||
</Box>
|
||||
)}
|
||||
{memberCount && topic && (
|
||||
<Line
|
||||
variant="SurfaceVariant"
|
||||
style={{ height: toRem(12) }}
|
||||
direction="Vertical"
|
||||
size="400"
|
||||
/>
|
||||
)}
|
||||
{topic && (
|
||||
<UseStateProvider initial={false}>
|
||||
{(view, setView) => (
|
||||
<>
|
||||
<Text
|
||||
className={css.RoomProfileTopic}
|
||||
size="T200"
|
||||
priority="300"
|
||||
truncate
|
||||
onClick={() => setView(true)}
|
||||
onKeyDown={onEnterOrSpace(() => setView(true))}
|
||||
tabIndex={0}
|
||||
>
|
||||
{topic}
|
||||
</Text>
|
||||
<Overlay open={view} backdrop={<OverlayBackdrop />}>
|
||||
<OverlayCenter>
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
clickOutsideDeactivates: true,
|
||||
onDeactivate: () => setView(false),
|
||||
}}
|
||||
>
|
||||
<RoomTopicViewer
|
||||
name={name}
|
||||
topic={topic}
|
||||
requestClose={() => setView(false)}
|
||||
/>
|
||||
</FocusTrap>
|
||||
</OverlayCenter>
|
||||
</Overlay>
|
||||
</>
|
||||
)}
|
||||
</UseStateProvider>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
{options}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function CallbackOnFoundSpace({
|
||||
roomId,
|
||||
onSpaceFound,
|
||||
}: {
|
||||
roomId: string;
|
||||
onSpaceFound: (roomId: string) => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
onSpaceFound(roomId);
|
||||
}, [roomId, onSpaceFound]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
type RoomItemCardProps = {
|
||||
item: HierarchyItem;
|
||||
onSpaceFound: (roomId: string) => void;
|
||||
dm?: boolean;
|
||||
firstChild?: boolean;
|
||||
lastChild?: boolean;
|
||||
onOpen: MouseEventHandler<HTMLButtonElement>;
|
||||
options?: ReactNode;
|
||||
before?: ReactNode;
|
||||
after?: ReactNode;
|
||||
onDragging: (item?: HierarchyItem) => void;
|
||||
canReorder: boolean;
|
||||
getRoom: (roomId: string) => Room | undefined;
|
||||
};
|
||||
export const RoomItemCard = as<'div', RoomItemCardProps>(
|
||||
(
|
||||
{
|
||||
item,
|
||||
onSpaceFound,
|
||||
dm,
|
||||
firstChild,
|
||||
lastChild,
|
||||
onOpen,
|
||||
options,
|
||||
before,
|
||||
after,
|
||||
onDragging,
|
||||
canReorder,
|
||||
getRoom,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const mx = useMatrixClient();
|
||||
const { roomId, content } = item;
|
||||
const room = getRoom(roomId);
|
||||
const targetRef = useRef<HTMLDivElement>(null);
|
||||
const targetHandleRef = useRef<HTMLDivElement>(null);
|
||||
useDraggableItem(item, targetRef, onDragging, targetHandleRef);
|
||||
|
||||
const joined = room?.getMyMembership() === Membership.Join;
|
||||
|
||||
return (
|
||||
<SequenceCard
|
||||
className={css.RoomItemCard}
|
||||
firstChild={firstChild}
|
||||
lastChild={lastChild}
|
||||
variant="SurfaceVariant"
|
||||
gap="300"
|
||||
alignItems="Center"
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
{before}
|
||||
<Box ref={canReorder ? targetRef : null} grow="Yes">
|
||||
{canReorder && <ItemDraggableTarget ref={targetHandleRef} />}
|
||||
{room ? (
|
||||
<LocalRoomSummaryLoader room={room}>
|
||||
{(localSummary) => (
|
||||
<RoomProfile
|
||||
roomId={roomId}
|
||||
name={localSummary.name}
|
||||
topic={localSummary.topic}
|
||||
avatarUrl={
|
||||
dm ? getDirectRoomAvatarUrl(mx, room, 96) : getRoomAvatarUrl(mx, room, 96)
|
||||
}
|
||||
memberCount={localSummary.memberCount}
|
||||
suggested={content.suggested}
|
||||
joinRule={localSummary.joinRule}
|
||||
options={
|
||||
joined ? (
|
||||
<Box shrink="No" gap="100" alignItems="Center">
|
||||
<Chip
|
||||
data-room-id={roomId}
|
||||
onClick={onOpen}
|
||||
variant="Secondary"
|
||||
fill="None"
|
||||
size="400"
|
||||
radii="Pill"
|
||||
aria-label="Open Room"
|
||||
>
|
||||
<Icon size="50" src={Icons.ArrowRight} />
|
||||
</Chip>
|
||||
</Box>
|
||||
) : (
|
||||
<RoomJoinButton roomId={roomId} via={content.via} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</LocalRoomSummaryLoader>
|
||||
) : (
|
||||
<HierarchyRoomSummaryLoader roomId={roomId}>
|
||||
{(summaryState) => (
|
||||
<>
|
||||
{summaryState.status === AsyncStatus.Loading && <RoomProfileLoading />}
|
||||
{summaryState.status === AsyncStatus.Error && (
|
||||
<RoomProfileError
|
||||
roomId={roomId}
|
||||
error={summaryState.error}
|
||||
suggested={content.suggested}
|
||||
via={content.via}
|
||||
/>
|
||||
)}
|
||||
{summaryState.status === AsyncStatus.Success && (
|
||||
<>
|
||||
{summaryState.data.room_type === RoomType.Space && (
|
||||
<CallbackOnFoundSpace
|
||||
roomId={summaryState.data.room_id}
|
||||
onSpaceFound={onSpaceFound}
|
||||
/>
|
||||
)}
|
||||
<RoomProfile
|
||||
roomId={roomId}
|
||||
name={summaryState.data.name || summaryState.data.canonical_alias || roomId}
|
||||
topic={summaryState.data.topic}
|
||||
avatarUrl={
|
||||
summaryState.data?.avatar_url
|
||||
? mx.mxcUrlToHttp(summaryState.data.avatar_url, 96, 96, 'crop') ??
|
||||
undefined
|
||||
: undefined
|
||||
}
|
||||
memberCount={summaryState.data.num_joined_members}
|
||||
suggested={content.suggested}
|
||||
joinRule={summaryState.data.join_rule}
|
||||
options={<RoomJoinButton roomId={roomId} via={content.via} />}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</HierarchyRoomSummaryLoader>
|
||||
)}
|
||||
</Box>
|
||||
{options}
|
||||
{after}
|
||||
</SequenceCard>
|
||||
);
|
||||
}
|
||||
);
|
||||
39
src/app/features/lobby/SpaceItem.css.ts
Normal file
39
src/app/features/lobby/SpaceItem.css.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { style } from '@vanilla-extract/css';
|
||||
import { color, config, toRem } from 'folds';
|
||||
import { recipe } from '@vanilla-extract/recipes';
|
||||
|
||||
export const SpaceItemCard = recipe({
|
||||
base: {
|
||||
paddingBottom: config.space.S100,
|
||||
borderBottom: `${config.borderWidth.B300} solid transparent`,
|
||||
position: 'relative',
|
||||
selectors: {
|
||||
'&[data-dragging=true]': {
|
||||
opacity: config.opacity.Disabled,
|
||||
},
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
outlined: {
|
||||
true: {
|
||||
borderBottomColor: color.Surface.ContainerLine,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
export const HeaderChip = style({
|
||||
paddingLeft: config.space.S200,
|
||||
selectors: {
|
||||
[`&[data-ui-before="true"]`]: {
|
||||
paddingLeft: config.space.S100,
|
||||
},
|
||||
},
|
||||
});
|
||||
export const HeaderChipPlaceholder = style([
|
||||
{
|
||||
borderRadius: config.radii.R400,
|
||||
paddingLeft: config.space.S100,
|
||||
paddingRight: config.space.S300,
|
||||
height: toRem(32),
|
||||
},
|
||||
]);
|
||||
493
src/app/features/lobby/SpaceItem.tsx
Normal file
493
src/app/features/lobby/SpaceItem.tsx
Normal file
|
|
@ -0,0 +1,493 @@
|
|||
import React, { MouseEventHandler, ReactNode, useCallback, useRef, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Avatar,
|
||||
Text,
|
||||
Chip,
|
||||
Icon,
|
||||
Icons,
|
||||
as,
|
||||
Badge,
|
||||
toRem,
|
||||
Spinner,
|
||||
PopOut,
|
||||
Menu,
|
||||
MenuItem,
|
||||
RectCords,
|
||||
config,
|
||||
} from 'folds';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import classNames from 'classnames';
|
||||
import { MatrixError, Room } from 'matrix-js-sdk';
|
||||
import { HierarchyItem } from '../../hooks/useSpaceHierarchy';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { RoomAvatar } from '../../components/room-avatar';
|
||||
import { nameInitials } from '../../utils/common';
|
||||
import {
|
||||
HierarchyRoomSummaryLoader,
|
||||
LocalRoomSummaryLoader,
|
||||
} from '../../components/RoomSummaryLoader';
|
||||
import { getRoomAvatarUrl } from '../../utils/room';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||
import * as css from './SpaceItem.css';
|
||||
import * as styleCss from './style.css';
|
||||
import { ErrorCode } from '../../cs-errorcode';
|
||||
import { useDraggableItem } from './DnD';
|
||||
import { openCreateRoom, openSpaceAddExisting } from '../../../client/action/navigation';
|
||||
|
||||
function SpaceProfileLoading() {
|
||||
return (
|
||||
<Box gap="200" alignItems="Center">
|
||||
<Box grow="Yes" gap="200" alignItems="Center" className={css.HeaderChipPlaceholder}>
|
||||
<Avatar className={styleCss.AvatarPlaceholder} size="200" radii="300" />
|
||||
<Box
|
||||
className={styleCss.LinePlaceholder}
|
||||
shrink="No"
|
||||
style={{ width: '100vw', maxWidth: toRem(120) }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
type UnknownPrivateSpaceProfileProps = {
|
||||
roomId: string;
|
||||
name?: string;
|
||||
avatarUrl?: string;
|
||||
suggested?: boolean;
|
||||
};
|
||||
function UnknownPrivateSpaceProfile({
|
||||
roomId,
|
||||
name,
|
||||
avatarUrl,
|
||||
suggested,
|
||||
}: UnknownPrivateSpaceProfileProps) {
|
||||
return (
|
||||
<Chip
|
||||
as="span"
|
||||
className={css.HeaderChip}
|
||||
variant="Surface"
|
||||
size="500"
|
||||
before={
|
||||
<Avatar size="200" radii="300">
|
||||
<RoomAvatar
|
||||
roomId={roomId}
|
||||
src={avatarUrl}
|
||||
alt={name}
|
||||
renderFallback={() => (
|
||||
<Text as="span" size="H6">
|
||||
{nameInitials(name)}
|
||||
</Text>
|
||||
)}
|
||||
/>
|
||||
</Avatar>
|
||||
}
|
||||
>
|
||||
<Box alignItems="Center" gap="200">
|
||||
<Text size="H4" truncate>
|
||||
{name || 'Unknown'}
|
||||
</Text>
|
||||
|
||||
<Badge variant="Secondary" fill="Soft" radii="Pill" outlined>
|
||||
<Text size="L400">Private Space</Text>
|
||||
</Badge>
|
||||
{suggested && (
|
||||
<Badge variant="Success" fill="Soft" radii="Pill" outlined>
|
||||
<Text size="L400">Suggested</Text>
|
||||
</Badge>
|
||||
)}
|
||||
</Box>
|
||||
</Chip>
|
||||
);
|
||||
}
|
||||
|
||||
type UnknownSpaceProfileProps = {
|
||||
roomId: string;
|
||||
via?: string[];
|
||||
name?: string;
|
||||
avatarUrl?: string;
|
||||
suggested?: boolean;
|
||||
};
|
||||
function UnknownSpaceProfile({
|
||||
roomId,
|
||||
via,
|
||||
name,
|
||||
avatarUrl,
|
||||
suggested,
|
||||
}: UnknownSpaceProfileProps) {
|
||||
const mx = useMatrixClient();
|
||||
|
||||
const [joinState, join] = useAsyncCallback<Room, MatrixError, []>(
|
||||
useCallback(() => mx.joinRoom(roomId, { viaServers: via }), [mx, roomId, via])
|
||||
);
|
||||
|
||||
const canJoin = joinState.status === AsyncStatus.Idle || joinState.status === AsyncStatus.Error;
|
||||
return (
|
||||
<Chip
|
||||
className={css.HeaderChip}
|
||||
variant="Surface"
|
||||
size="500"
|
||||
onClick={join}
|
||||
disabled={!canJoin}
|
||||
before={
|
||||
<Avatar size="200" radii="300">
|
||||
<RoomAvatar
|
||||
roomId={roomId}
|
||||
src={avatarUrl}
|
||||
alt={name}
|
||||
renderFallback={() => (
|
||||
<Text as="span" size="H6">
|
||||
{nameInitials(name)}
|
||||
</Text>
|
||||
)}
|
||||
/>
|
||||
</Avatar>
|
||||
}
|
||||
after={
|
||||
canJoin ? <Icon src={Icons.Plus} size="50" /> : <Spinner variant="Secondary" size="200" />
|
||||
}
|
||||
>
|
||||
<Box alignItems="Center" gap="200">
|
||||
<Text size="H4" truncate>
|
||||
{name || 'Unknown'}
|
||||
</Text>
|
||||
{suggested && (
|
||||
<Badge variant="Success" fill="Soft" radii="Pill" outlined>
|
||||
<Text size="L400">Suggested</Text>
|
||||
</Badge>
|
||||
)}
|
||||
{joinState.status === AsyncStatus.Error && (
|
||||
<Badge variant="Critical" fill="Soft" radii="Pill" outlined>
|
||||
<Text size="L400" truncate>
|
||||
{joinState.error.name}
|
||||
</Text>
|
||||
</Badge>
|
||||
)}
|
||||
</Box>
|
||||
</Chip>
|
||||
);
|
||||
}
|
||||
|
||||
type SpaceProfileProps = {
|
||||
roomId: string;
|
||||
name: string;
|
||||
avatarUrl?: string;
|
||||
suggested?: boolean;
|
||||
closed: boolean;
|
||||
categoryId: string;
|
||||
handleClose?: MouseEventHandler<HTMLButtonElement>;
|
||||
};
|
||||
function SpaceProfile({
|
||||
roomId,
|
||||
name,
|
||||
avatarUrl,
|
||||
suggested,
|
||||
closed,
|
||||
categoryId,
|
||||
handleClose,
|
||||
}: SpaceProfileProps) {
|
||||
return (
|
||||
<Chip
|
||||
data-category-id={categoryId}
|
||||
onClick={handleClose}
|
||||
className={css.HeaderChip}
|
||||
variant="Surface"
|
||||
size="500"
|
||||
before={
|
||||
<Avatar size="200" radii="300">
|
||||
<RoomAvatar
|
||||
roomId={roomId}
|
||||
src={avatarUrl}
|
||||
alt={name}
|
||||
renderFallback={() => (
|
||||
<Text as="span" size="H6">
|
||||
{nameInitials(name)}
|
||||
</Text>
|
||||
)}
|
||||
/>
|
||||
</Avatar>
|
||||
}
|
||||
after={<Icon src={closed ? Icons.ChevronRight : Icons.ChevronBottom} size="50" />}
|
||||
>
|
||||
<Box alignItems="Center" gap="200">
|
||||
<Text size="H4" truncate>
|
||||
{name}
|
||||
</Text>
|
||||
{suggested && (
|
||||
<Badge variant="Success" fill="Soft" radii="Pill" outlined>
|
||||
<Text size="L400">Suggested</Text>
|
||||
</Badge>
|
||||
)}
|
||||
</Box>
|
||||
</Chip>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
function AddRoomButton({ item }: { item: HierarchyItem }) {
|
||||
const [cords, setCords] = useState<RectCords>();
|
||||
|
||||
const handleAddRoom: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
setCords(evt.currentTarget.getBoundingClientRect());
|
||||
};
|
||||
|
||||
const handleCreateRoom = () => {
|
||||
openCreateRoom(false, item.roomId as any);
|
||||
setCords(undefined);
|
||||
};
|
||||
|
||||
const handleAddExisting = () => {
|
||||
openSpaceAddExisting(item.roomId);
|
||||
setCords(undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
<PopOut
|
||||
anchor={cords}
|
||||
position="Bottom"
|
||||
align="End"
|
||||
content={
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
onDeactivate: () => setCords(undefined),
|
||||
clickOutsideDeactivates: true,
|
||||
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
||||
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
||||
}}
|
||||
>
|
||||
<Menu style={{ padding: config.space.S100 }}>
|
||||
<MenuItem
|
||||
size="300"
|
||||
radii="300"
|
||||
variant="Primary"
|
||||
fill="None"
|
||||
onClick={handleCreateRoom}
|
||||
>
|
||||
<Text size="T300">New Room</Text>
|
||||
</MenuItem>
|
||||
<MenuItem size="300" radii="300" fill="None" onClick={handleAddExisting}>
|
||||
<Text size="T300">Existing Room</Text>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</FocusTrap>
|
||||
}
|
||||
>
|
||||
<Chip
|
||||
variant="Primary"
|
||||
radii="Pill"
|
||||
before={<Icon src={Icons.Plus} size="50" />}
|
||||
onClick={handleAddRoom}
|
||||
aria-pressed={!!cords}
|
||||
>
|
||||
<Text size="B300">Add Room</Text>
|
||||
</Chip>
|
||||
</PopOut>
|
||||
);
|
||||
}
|
||||
|
||||
function AddSpaceButton({ item }: { item: HierarchyItem }) {
|
||||
const [cords, setCords] = useState<RectCords>();
|
||||
|
||||
const handleAddSpace: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
setCords(evt.currentTarget.getBoundingClientRect());
|
||||
};
|
||||
|
||||
const handleCreateSpace = () => {
|
||||
openCreateRoom(true, item.roomId as any);
|
||||
setCords(undefined);
|
||||
};
|
||||
|
||||
const handleAddExisting = () => {
|
||||
openSpaceAddExisting(item.roomId, true);
|
||||
setCords(undefined);
|
||||
};
|
||||
return (
|
||||
<PopOut
|
||||
anchor={cords}
|
||||
position="Bottom"
|
||||
align="End"
|
||||
content={
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
initialFocus: false,
|
||||
onDeactivate: () => setCords(undefined),
|
||||
clickOutsideDeactivates: true,
|
||||
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
||||
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
||||
}}
|
||||
>
|
||||
<Menu style={{ padding: config.space.S100 }}>
|
||||
<MenuItem
|
||||
size="300"
|
||||
radii="300"
|
||||
variant="Primary"
|
||||
fill="None"
|
||||
onClick={handleCreateSpace}
|
||||
>
|
||||
<Text size="T300">New Space</Text>
|
||||
</MenuItem>
|
||||
<MenuItem size="300" radii="300" fill="None" onClick={handleAddExisting}>
|
||||
<Text size="T300">Existing Space</Text>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</FocusTrap>
|
||||
}
|
||||
>
|
||||
<Chip
|
||||
variant="SurfaceVariant"
|
||||
radii="Pill"
|
||||
before={<Icon src={Icons.Plus} size="50" />}
|
||||
onClick={handleAddSpace}
|
||||
aria-pressed={!!cords}
|
||||
>
|
||||
<Text size="B300">Add Space</Text>
|
||||
</Chip>
|
||||
</PopOut>
|
||||
);
|
||||
}
|
||||
|
||||
type SpaceItemCardProps = {
|
||||
item: HierarchyItem;
|
||||
joined?: boolean;
|
||||
categoryId: string;
|
||||
closed: boolean;
|
||||
handleClose?: MouseEventHandler<HTMLButtonElement>;
|
||||
options?: ReactNode;
|
||||
before?: ReactNode;
|
||||
after?: ReactNode;
|
||||
canEditChild: boolean;
|
||||
canReorder: boolean;
|
||||
onDragging: (item?: HierarchyItem) => void;
|
||||
getRoom: (roomId: string) => Room | undefined;
|
||||
};
|
||||
export const SpaceItemCard = as<'div', SpaceItemCardProps>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
joined,
|
||||
closed,
|
||||
categoryId,
|
||||
item,
|
||||
handleClose,
|
||||
options,
|
||||
before,
|
||||
after,
|
||||
canEditChild,
|
||||
canReorder,
|
||||
onDragging,
|
||||
getRoom,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const mx = useMatrixClient();
|
||||
const { roomId, content } = item;
|
||||
const space = getRoom(roomId);
|
||||
const targetRef = useRef<HTMLDivElement>(null);
|
||||
useDraggableItem(item, targetRef, onDragging);
|
||||
|
||||
return (
|
||||
<Box
|
||||
shrink="No"
|
||||
alignItems="Center"
|
||||
gap="200"
|
||||
className={classNames(css.SpaceItemCard({ outlined: !joined || closed }), className)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
{before}
|
||||
<Box grow="Yes" gap="100" alignItems="Inherit" justifyContent="SpaceBetween">
|
||||
<Box ref={canReorder ? targetRef : null}>
|
||||
{space ? (
|
||||
<LocalRoomSummaryLoader room={space}>
|
||||
{(localSummary) =>
|
||||
item.parentId ? (
|
||||
<SpaceProfile
|
||||
roomId={roomId}
|
||||
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 roomId={roomId} 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 || summaryState.data.canonical_alias || roomId}
|
||||
avatarUrl={
|
||||
summaryState.data?.avatar_url
|
||||
? mx.mxcUrlToHttp(summaryState.data.avatar_url, 96, 96, 'crop') ??
|
||||
undefined
|
||||
: undefined
|
||||
}
|
||||
suggested={content.suggested}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</HierarchyRoomSummaryLoader>
|
||||
)}
|
||||
</Box>
|
||||
{canEditChild && (
|
||||
<Box alignItems="Inherit" gap="200">
|
||||
<AddRoomButton item={item} />
|
||||
{item.parentId === undefined && <AddSpaceButton item={item} />}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
{options}
|
||||
{after}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
);
|
||||
1
src/app/features/lobby/index.ts
Normal file
1
src/app/features/lobby/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './Lobby';
|
||||
15
src/app/features/lobby/style.css.ts
Normal file
15
src/app/features/lobby/style.css.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { style } from '@vanilla-extract/css';
|
||||
import { DefaultReset, color, config } from 'folds';
|
||||
|
||||
export const AvatarPlaceholder = style({
|
||||
backgroundColor: color.Secondary.Container,
|
||||
});
|
||||
export const LinePlaceholder = style([
|
||||
DefaultReset,
|
||||
{
|
||||
width: '100%',
|
||||
height: config.lineHeight.T200,
|
||||
borderRadius: config.radii.R300,
|
||||
backgroundColor: color.Secondary.Container,
|
||||
},
|
||||
]);
|
||||
Loading…
Add table
Add a link
Reference in a new issue