380 lines
13 KiB
TypeScript
380 lines
13 KiB
TypeScript
import React, { forwardRef, useState } from 'react';
|
|
import { Room } from 'matrix-js-sdk';
|
|
import { useAtomValue } from 'jotai';
|
|
import { Box, Line, Menu, MenuItem, Text, config, toRem } from 'folds';
|
|
import { Icon, Icons } from '../../components/icons';
|
|
import type { HierarchyItem } from '../../hooks/useSpaceHierarchy';
|
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
|
import { useOpenCreateRoomModal } from '../../state/hooks/createRoomModal';
|
|
import { useOpenCreateSpaceModal } from '../../state/hooks/createSpaceModal';
|
|
import { AddExistingModal } from '../add-existing';
|
|
import { useSetting } from '../../state/hooks/settings';
|
|
import { settingsAtom } from '../../state/settings';
|
|
import { usePowerLevels } from '../../hooks/usePowerLevels';
|
|
import { useRoomCreators } from '../../hooks/useRoomCreators';
|
|
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
|
|
import { useOpenSpaceSettings } from '../../state/hooks/spaceSettings';
|
|
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
|
|
import { useMarkRoomsAsRead } from '../../hooks/useMarkAsRead';
|
|
import { useRecursiveChildScopeFactory, useSpaceChildren } from '../../state/hooks/roomList';
|
|
import { allRoomsAtom } from '../../state/room-list/roomList';
|
|
import { roomToParentsAtom } from '../../state/room/roomToParents';
|
|
import { useRoomsUnread } from '../../state/hooks/unread';
|
|
import { roomToUnreadAtom } from '../../state/room/roomToUnread';
|
|
import { UseStateProvider } from '../../components/UseStateProvider';
|
|
import { LeaveSpacePrompt } from '../../components/leave-space-prompt';
|
|
import { RemoveInaccessiblePrompt } from '../../components/remove-inaccessible-prompt';
|
|
import { InviteUserPrompt } from '../../components/invite-user-prompt';
|
|
import { copyToClipboard } from '../../utils/dom';
|
|
import { getCanonicalAliasOrRoomId, isRoomAlias } from '../../utils/matrix';
|
|
import { getMatrixToRoom } from '../../plugins/matrix-to';
|
|
import { getViaServers } from '../../plugins/via-servers';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { getSpaceFeedPath, getSpaceLobbyPath } from '../../pages/pathUtils';
|
|
import { shouldShowForumLobby } from '../../utils/room';
|
|
import { useSpaceFeedSelected } from '../../hooks/router/useSelectedSpace';
|
|
import { StateEvent } from '../../../types/matrix/room';
|
|
|
|
type ForumAddOptions = {
|
|
item: HierarchyItem;
|
|
showAddSpace: boolean;
|
|
};
|
|
|
|
export type OpenAddExistingOptions = {
|
|
parentId: string;
|
|
space: boolean;
|
|
};
|
|
|
|
type SpaceOptionsMenuProps = {
|
|
room: Room;
|
|
requestClose: () => void;
|
|
forumAdd?: ForumAddOptions;
|
|
/** Render Add Existing outside the popout (forum kebab). */
|
|
onOpenAddExisting?: (options: OpenAddExistingOptions) => void;
|
|
};
|
|
|
|
export const SpaceOptionsMenu = forwardRef<HTMLDivElement, SpaceOptionsMenuProps>(
|
|
({ room, requestClose, forumAdd, onOpenAddExisting }, ref) => {
|
|
const mx = useMatrixClient();
|
|
const navigate = useNavigate();
|
|
const onFeed = useSpaceFeedSelected();
|
|
const isForum = shouldShowForumLobby(room);
|
|
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
|
const [developerTools] = useSetting(settingsAtom, 'developerTools');
|
|
const roomToParents = useAtomValue(roomToParentsAtom);
|
|
const powerLevels = usePowerLevels(room);
|
|
const creators = useRoomCreators(room);
|
|
|
|
const permissions = useRoomPermissions(creators, powerLevels);
|
|
const canInvite = permissions.action('invite', mx.getSafeUserId());
|
|
const canEditChildren = permissions.stateEvent(StateEvent.SpaceChild, mx.getSafeUserId());
|
|
const openSpaceSettings = useOpenSpaceSettings();
|
|
const { navigateRoom } = useRoomNavigate();
|
|
const markRoomsAsRead = useMarkRoomsAsRead(mx);
|
|
|
|
const [invitePrompt, setInvitePrompt] = useState(false);
|
|
const [addExistingRoom, setAddExistingRoom] = useState(false);
|
|
const [addExistingSpace, setAddExistingSpace] = useState(false);
|
|
const openCreateRoomModal = useOpenCreateRoomModal();
|
|
const openCreateSpaceModal = useOpenCreateSpaceModal();
|
|
|
|
const allChild = useSpaceChildren(
|
|
allRoomsAtom,
|
|
room.roomId,
|
|
useRecursiveChildScopeFactory(mx, roomToParents)
|
|
);
|
|
const unread = useRoomsUnread(allChild, roomToUnreadAtom);
|
|
|
|
const handleMarkAsRead = () => {
|
|
markRoomsAsRead(allChild, hideActivity);
|
|
requestClose();
|
|
};
|
|
|
|
const handleCopyLink = () => {
|
|
const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId);
|
|
const viaServers = isRoomAlias(roomIdOrAlias) ? undefined : getViaServers(room);
|
|
copyToClipboard(getMatrixToRoom(roomIdOrAlias, viaServers));
|
|
requestClose();
|
|
};
|
|
|
|
const handleInvite = () => {
|
|
setInvitePrompt(true);
|
|
};
|
|
|
|
const handleRoomSettings = () => {
|
|
openSpaceSettings(room.roomId);
|
|
requestClose();
|
|
};
|
|
|
|
const handleOpenTimeline = () => {
|
|
navigateRoom(room.roomId);
|
|
requestClose();
|
|
};
|
|
|
|
const handleOpenFeed = () => {
|
|
navigate(getSpaceFeedPath(getCanonicalAliasOrRoomId(mx, room.roomId)));
|
|
requestClose();
|
|
};
|
|
|
|
const handleOpenManage = () => {
|
|
navigate(getSpaceLobbyPath(getCanonicalAliasOrRoomId(mx, room.roomId)));
|
|
requestClose();
|
|
};
|
|
|
|
const handleAddTopic = () => {
|
|
if (!forumAdd) return;
|
|
openCreateRoomModal(forumAdd.item.roomId);
|
|
requestClose();
|
|
};
|
|
|
|
const handleAddCategory = () => {
|
|
if (!forumAdd) return;
|
|
openCreateSpaceModal(forumAdd.item.roomId);
|
|
requestClose();
|
|
};
|
|
|
|
const handleAddExistingTopic = () => {
|
|
if (!forumAdd) return;
|
|
if (onOpenAddExisting) {
|
|
onOpenAddExisting({ parentId: forumAdd.item.roomId, space: false });
|
|
requestClose();
|
|
return;
|
|
}
|
|
setAddExistingRoom(true);
|
|
};
|
|
|
|
const handleAddExistingCategory = () => {
|
|
if (!forumAdd) return;
|
|
if (onOpenAddExisting) {
|
|
onOpenAddExisting({ parentId: forumAdd.item.roomId, space: true });
|
|
requestClose();
|
|
return;
|
|
}
|
|
setAddExistingSpace(true);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Menu ref={ref} style={{ maxWidth: toRem(160), width: '100vw' }}>
|
|
{forumAdd && (
|
|
<>
|
|
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
|
<MenuItem
|
|
onClick={handleAddTopic}
|
|
variant="Primary"
|
|
fill="None"
|
|
size="300"
|
|
after={<Icon size="100" src={Icons.Plus} />}
|
|
radii="300"
|
|
>
|
|
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
|
Add Topic
|
|
</Text>
|
|
</MenuItem>
|
|
<MenuItem
|
|
onClick={handleAddExistingTopic}
|
|
size="300"
|
|
radii="300"
|
|
fill="None"
|
|
>
|
|
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
|
Add Existing Topic
|
|
</Text>
|
|
</MenuItem>
|
|
{forumAdd.showAddSpace && (
|
|
<>
|
|
<MenuItem
|
|
onClick={handleAddCategory}
|
|
size="300"
|
|
after={<Icon size="100" src={Icons.Plus} />}
|
|
radii="300"
|
|
fill="None"
|
|
>
|
|
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
|
Add Section
|
|
</Text>
|
|
</MenuItem>
|
|
<MenuItem
|
|
onClick={handleAddExistingCategory}
|
|
size="300"
|
|
radii="300"
|
|
fill="None"
|
|
>
|
|
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
|
Add Existing Section
|
|
</Text>
|
|
</MenuItem>
|
|
</>
|
|
)}
|
|
</Box>
|
|
<Line variant="Surface" size="300" />
|
|
</>
|
|
)}
|
|
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
|
{invitePrompt && (
|
|
<InviteUserPrompt
|
|
room={room}
|
|
requestClose={() => {
|
|
setInvitePrompt(false);
|
|
requestClose();
|
|
}}
|
|
/>
|
|
)}
|
|
{isForum && !onFeed && (
|
|
<MenuItem
|
|
onClick={handleOpenFeed}
|
|
size="300"
|
|
after={<Icon size="100" src={Icons.Thread} />}
|
|
radii="300"
|
|
>
|
|
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
|
Feed
|
|
</Text>
|
|
</MenuItem>
|
|
)}
|
|
{isForum && onFeed && (
|
|
<MenuItem
|
|
onClick={handleOpenManage}
|
|
size="300"
|
|
after={<Icon size="100" src={Icons.Flag} />}
|
|
radii="300"
|
|
>
|
|
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
|
Manage
|
|
</Text>
|
|
</MenuItem>
|
|
)}
|
|
<MenuItem
|
|
onClick={handleMarkAsRead}
|
|
size="300"
|
|
after={<Icon size="100" src={Icons.CheckTwice} />}
|
|
radii="300"
|
|
disabled={!unread}
|
|
>
|
|
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
|
Mark as Read
|
|
</Text>
|
|
</MenuItem>
|
|
</Box>
|
|
<Line variant="Surface" size="300" />
|
|
<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"
|
|
aria-pressed={invitePrompt}
|
|
disabled={!canInvite}
|
|
>
|
|
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
|
Invite
|
|
</Text>
|
|
</MenuItem>
|
|
<MenuItem
|
|
onClick={handleCopyLink}
|
|
size="300"
|
|
after={<Icon size="100" src={Icons.Link} />}
|
|
radii="300"
|
|
>
|
|
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
|
Copy Link
|
|
</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>
|
|
{developerTools && (
|
|
<MenuItem
|
|
onClick={handleOpenTimeline}
|
|
size="300"
|
|
after={<Icon size="100" src={Icons.Terminal} />}
|
|
radii="300"
|
|
>
|
|
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
|
Event Timeline
|
|
</Text>
|
|
</MenuItem>
|
|
)}
|
|
</Box>
|
|
<Line variant="Surface" size="300" />
|
|
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
|
|
{canEditChildren && (
|
|
<UseStateProvider initial={false}>
|
|
{(promptRemove, setPromptRemove) => (
|
|
<>
|
|
<MenuItem
|
|
onClick={() => setPromptRemove(true)}
|
|
variant="Critical"
|
|
fill="None"
|
|
size="300"
|
|
after={<Icon size="100" src={Icons.Delete} />}
|
|
radii="300"
|
|
aria-pressed={promptRemove}
|
|
>
|
|
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
|
Remove Inaccessible
|
|
</Text>
|
|
</MenuItem>
|
|
{promptRemove && (
|
|
<RemoveInaccessiblePrompt
|
|
roomId={room.roomId}
|
|
onDone={requestClose}
|
|
onCancel={() => setPromptRemove(false)}
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
</UseStateProvider>
|
|
)}
|
|
<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={room.roomId}
|
|
onDone={requestClose}
|
|
onCancel={() => setPromptLeave(false)}
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
</UseStateProvider>
|
|
</Box>
|
|
</Menu>
|
|
{forumAdd && addExistingRoom && (
|
|
<AddExistingModal parentId={forumAdd.item.roomId} requestClose={() => setAddExistingRoom(false)} />
|
|
)}
|
|
{forumAdd && addExistingSpace && (
|
|
<AddExistingModal
|
|
space
|
|
parentId={forumAdd.item.roomId}
|
|
requestClose={() => setAddExistingSpace(false)}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
);
|