Files
cinny/src/app/features/space/SpaceOptionsMenu.tsx

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)}
/>
)}
</>
);
}
);