--- /dev/null
+import React, { useState, useEffect } from 'react';
+import PropTypes from 'prop-types';
+import './SpaceAddExisting.scss';
+
+import { twemojify } from '../../../util/twemojify';
+
+import initMatrix from '../../../client/initMatrix';
+import cons from '../../../client/state/cons';
+import navigation from '../../../client/state/navigation';
+import { joinRuleToIconSrc } from '../../../util/matrixUtil';
+import { Debounce } from '../../../util/common';
+
+import Text from '../../atoms/text/Text';
+import RawIcon from '../../atoms/system-icons/RawIcon';
+import Button from '../../atoms/button/Button';
+import IconButton from '../../atoms/button/IconButton';
+import Checkbox from '../../atoms/button/Checkbox';
+import Input from '../../atoms/input/Input';
+import Spinner from '../../atoms/spinner/Spinner';
+import RoomSelector from '../room-selector/RoomSelector';
+import Dialog from '../dialog/Dialog';
+
+import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
+import SearchIC from '../../../../public/res/ic/outlined/search.svg';
+
+function SpaceAddExistingContent({ roomId }) {
+ const [debounce] = useState(new Debounce());
+ const [process, setProcess] = useState(null);
+ const [selected, setSelected] = useState([]);
+ const [searchIds, setSearchIds] = useState(null);
+ const mx = initMatrix.matrixClient;
+ const {
+ spaces, rooms, directs, roomIdToParents,
+ } = initMatrix.roomList;
+
+ let allRoomIds = [...spaces, ...rooms, ...directs];
+ allRoomIds = allRoomIds.filter((rId) => (
+ rId !== roomId
+ && !roomIdToParents.get(rId)?.has(roomId)
+ ));
+
+ const toggleSelection = (rId) => {
+ if (process !== null) return;
+ const newSelected = [...selected];
+ const selectedIndex = newSelected.indexOf(rId);
+
+ if (selectedIndex > -1) {
+ newSelected.splice(selectedIndex, 1);
+ setSelected(newSelected);
+ return;
+ }
+ newSelected.push(rId);
+ setSelected(newSelected);
+ };
+
+ const handleSearch = (ev) => {
+ const term = ev.target.value.toLocaleLowerCase().replaceAll(' ', '');
+ if (term === '') {
+ setSearchIds(null);
+ return;
+ }
+
+ debounce._(() => {
+ const searchedIds = allRoomIds.filter((rId) => {
+ let name = mx.getRoom(rId)?.name;
+ if (!name) return false;
+ name = name.normalize('NFKC')
+ .toLocaleLowerCase()
+ .replaceAll(' ', '');
+ return name.includes(term);
+ });
+ setSearchIds(searchedIds);
+ }, 400)();
+ };
+
+ const handleAdd = async () => {
+ setProcess(`Adding ${selected.length} items...`);
+ };
+
+ return (
+ <>
+ <form
+ onSubmit={(ev) => {
+ ev.preventDefault();
+ const { target } = ev;
+ target.searchInput.value = '';
+ setSearchIds(null);
+ }}
+ >
+ <RawIcon size="small" src={SearchIC} />
+ <Input
+ name="searchInput"
+ onChange={handleSearch}
+ placeholder="Search room"
+ autoFocus
+ />
+ <IconButton size="small" type="submit" src={CrossIC} />
+ </form>
+ {searchIds?.length === 0 && <Text>No result found</Text>}
+ {
+ (searchIds || allRoomIds).map((rId) => {
+ const room = mx.getRoom(rId);
+ let imageSrc = room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null;
+ if (imageSrc === null) imageSrc = room.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null;
+
+ const parentSet = roomIdToParents.get(rId);
+ const parentNames = parentSet
+ ? [...parentSet].map((parentId) => mx.getRoom(parentId).name)
+ : undefined;
+ const parents = parentNames ? parentNames.join(', ') : null;
+
+ const handleSelect = () => toggleSelection(rId);
+
+ return (
+ <RoomSelector
+ key={rId}
+ name={room.name}
+ parentName={parents}
+ roomId={rId}
+ imageSrc={directs.has(rId) ? imageSrc : null}
+ iconSrc={
+ directs.has(rId)
+ ? null
+ : joinRuleToIconSrc(room.getJoinRule(), room.isSpaceRoom())
+ }
+ isUnread={false}
+ notificationCount={0}
+ isAlert={false}
+ onClick={handleSelect}
+ options={(
+ <Checkbox
+ isActive={selected.includes(rId)}
+ variant="positive"
+ onToggle={handleSelect}
+ tabIndex={-1}
+ disabled={process !== null}
+ />
+ )}
+ />
+ );
+ })
+ }
+ {selected.length !== 0 && (
+ <div className="space-add-existing__footer">
+ {process && <Spinner size="small" />}
+ <Text weight="medium">{process || `${selected.length} item selected`}</Text>
+ { !process && (
+ <Button onClick={handleAdd} variant="primary">Add</Button>
+ )}
+ </div>
+ )}
+ </>
+ );
+}
+SpaceAddExistingContent.propTypes = {
+ roomId: PropTypes.string.isRequired,
+};
+
+function useVisibilityToggle() {
+ const [roomId, setRoomId] = useState(null);
+
+ useEffect(() => {
+ const handleOpen = (rId) => setRoomId(rId);
+ navigation.on(cons.events.navigation.SPACE_ADDEXISTING_OPENED, handleOpen);
+ return () => {
+ navigation.removeListener(cons.events.navigation.SPACE_ADDEXISTING_OPENED, handleOpen);
+ };
+ }, []);
+
+ const requestClose = () => setRoomId(null);
+
+ return [roomId, requestClose];
+}
+
+function SpaceAddExisting() {
+ const [roomId, requestClose] = useVisibilityToggle();
+ const mx = initMatrix.matrixClient;
+ const room = mx.getRoom(roomId);
+
+ return (
+ <Dialog
+ isOpen={roomId !== null}
+ className="space-add-existing"
+ title={(
+ <Text variant="s1" weight="medium" primary>
+ {roomId && twemojify(room.name)}
+ <span style={{ color: 'var(--tc-surface-low)' }}> — add existing rooms</span>
+ </Text>
+ )}
+ contentOptions={<IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />}
+ onRequestClose={requestClose}
+ >
+ {
+ roomId
+ ? <SpaceAddExistingContent roomId={roomId} />
+ : <div />
+ }
+ </Dialog>
+ );
+}
+
+export default SpaceAddExisting;
--- /dev/null
+@use '../../partials/dir';
+@use '../../partials/flex';
+
+.space-add-existing {
+ height: 100%;
+
+ .dialog__content-container {
+ padding: 0;
+ padding-bottom: 80px;
+ @include dir.side(padding, var(--sp-extra-tight), 0);
+
+ & > .text {
+ margin: var(--sp-loose) var(--sp-normal);
+ text-align: center;
+ }
+ }
+
+ & form {
+ @extend .cp-fx__row--s-c;
+ padding: var(--sp-extra-tight);
+ padding-top: var(--sp-normal);
+
+ position: sticky;
+ top: 0;
+ z-index: 999;
+ background-color: var(--bg-surface);
+
+ & > .ic-raw,
+ & > .ic-btn {
+ position: absolute;
+ }
+ & > .ic-raw {
+ margin: 0 var(--sp-tight);
+ }
+ & > .ic-btn {
+ border-radius: calc(var(--bo-radius) / 2);
+ @include dir.prop(right, var(--sp-tight), unset);
+ @include dir.prop(left, unset, var(--sp-tight));
+ }
+ & input {
+ padding: var(--sp-tight) 40px;
+ }
+ }
+
+ .input-container {
+ @extend .cp-fx__item-one;
+ }
+
+ .room-selector__options {
+ display: flex;
+ margin: 0 10px;
+ }
+}
+
+.space-add-existing__footer {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ padding: var(--sp-normal);
+ background-color: var(--bg-surface);
+ border-top: 1px solid var(--bg-surface-border);
+ display: flex;
+ align-items: center;
+
+ & > .text {
+ @extend .cp-fx__item-one;
+ padding: 0 var(--sp-tight);
+ }
+
+ & > button {
+ @include dir.side(margin, var(--sp-normal), 0);
+ }
+}
\ No newline at end of file
-import React, { useState } from 'react';
+import React from 'react';
import PropTypes from 'prop-types';
import './DrawerHeader.scss';
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import {
- openPublicRooms, openCreateRoom, openInviteUser, openReusableContextMenu,
+ openPublicRooms, openCreateRoom, openSpaceManage,
+ openSpaceAddExisting, openInviteUser, openReusableContextMenu,
} from '../../../client/action/navigation';
-import { createSpaceShortcut, deleteSpaceShortcut } from '../../../client/action/room';
import { getEventCords } from '../../../util/common';
import { blurOnBubbling } from '../../atoms/button/script';
import RawIcon from '../../atoms/system-icons/RawIcon';
import Header, { TitleWrapper } from '../../atoms/header/Header';
import IconButton from '../../atoms/button/IconButton';
-import ContextMenu, { MenuItem, MenuHeader } from '../../atoms/context-menu/ContextMenu';
+import { MenuItem, MenuHeader } from '../../atoms/context-menu/ContextMenu';
import SpaceOptions from '../../molecules/space-options/SpaceOptions';
import PlusIC from '../../../../public/res/ic/outlined/plus.svg';
import HashPlusIC from '../../../../public/res/ic/outlined/hash-plus.svg';
import HashGlobeIC from '../../../../public/res/ic/outlined/hash-globe.svg';
+import HashSearchIC from '../../../../public/res/ic/outlined/hash-search.svg';
+import SpacePlusIC from '../../../../public/res/ic/outlined/space-plus.svg';
import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
-import PinIC from '../../../../public/res/ic/outlined/pin.svg';
-import PinFilledIC from '../../../../public/res/ic/filled/pin.svg';
+
+function HomeSpaceOptions({ spaceId, afterOptionSelect }) {
+ const mx = initMatrix.matrixClient;
+ const room = mx.getRoom(spaceId);
+ const canManage = room
+ ? room.currentState.maySendStateEvent('m.space.child', mx.getUserId())
+ : true;
+
+ return (
+ <>
+ <MenuHeader>Add rooms or spaces</MenuHeader>
+ <MenuItem
+ iconSrc={HashPlusIC}
+ onClick={() => { afterOptionSelect(); openCreateRoom(); }}
+ disabled={!canManage}
+ >
+ Create new room
+ </MenuItem>
+ <MenuItem
+ iconSrc={SpacePlusIC}
+ onClick={() => { afterOptionSelect(); }}
+ disabled={!canManage}
+ >
+ Create new space
+ </MenuItem>
+ { !spaceId && (
+ <MenuItem
+ iconSrc={HashGlobeIC}
+ onClick={() => { afterOptionSelect(); openPublicRooms(); }}
+ >
+ Join public room
+ </MenuItem>
+ )}
+ { spaceId && (
+ <MenuItem
+ iconSrc={PlusIC}
+ onClick={() => { afterOptionSelect(); openSpaceAddExisting(spaceId); }}
+ disabled={!canManage}
+ >
+ Add existing
+ </MenuItem>
+ )}
+ { spaceId && (
+ <MenuItem
+ onClick={() => { afterOptionSelect(); openSpaceManage(spaceId); }}
+ iconSrc={HashSearchIC}
+ >
+ Manage rooms
+ </MenuItem>
+ )}
+ </>
+ );
+}
+HomeSpaceOptions.defaultProps = {
+ spaceId: null,
+};
+HomeSpaceOptions.propTypes = {
+ spaceId: PropTypes.string,
+ afterOptionSelect: PropTypes.func.isRequired,
+};
function DrawerHeader({ selectedTab, spaceId }) {
- const [, forceUpdate] = useState({});
const mx = initMatrix.matrixClient;
- const { spaceShortcut } = initMatrix.roomList;
const tabName = selectedTab !== cons.tabs.DIRECTS ? 'Home' : 'Direct messages';
+ const isDMTab = selectedTab === cons.tabs.DIRECTS;
const room = mx.getRoom(spaceId);
- const spaceName = selectedTab === cons.tabs.DIRECTS ? null : (room?.name || null);
+ const spaceName = isDMTab ? null : (room?.name || null);
const openSpaceOptions = (e) => {
e.preventDefault();
);
};
+ const openHomeSpaceOptions = (e) => {
+ e.preventDefault();
+ openReusableContextMenu(
+ 'right',
+ getEventCords(e, '.ic-btn'),
+ (closeMenu) => <HomeSpaceOptions spaceId={spaceId} afterOptionSelect={closeMenu} />,
+ );
+ };
+
return (
<Header>
{spaceName ? (
<Text variant="s1" weight="medium" primary>{tabName}</Text>
</TitleWrapper>
)}
- {spaceName && (
- <IconButton
- size="extra-small"
- tooltip={spaceShortcut.has(spaceId) ? 'Unpin' : 'Pin to sidebar'}
- src={spaceShortcut.has(spaceId) ? PinFilledIC : PinIC}
- onClick={() => {
- if (spaceShortcut.has(spaceId)) deleteSpaceShortcut(spaceId);
- else createSpaceShortcut(spaceId);
- forceUpdate({});
- }}
- />
- )}
- { selectedTab === cons.tabs.DIRECTS && <IconButton onClick={() => openInviteUser()} tooltip="Start DM" src={PlusIC} size="normal" /> }
- { selectedTab !== cons.tabs.DIRECTS && !spaceName && (
- <ContextMenu
- content={(hideMenu) => (
- <>
- <MenuHeader>Add room</MenuHeader>
- <MenuItem
- iconSrc={HashPlusIC}
- onClick={() => { hideMenu(); openCreateRoom(); }}
- >
- Create new room
- </MenuItem>
- <MenuItem
- iconSrc={HashGlobeIC}
- onClick={() => { hideMenu(); openPublicRooms(); }}
- >
- Join public room
- </MenuItem>
- </>
- )}
- render={(toggleMenu) => (<IconButton onClick={toggleMenu} tooltip="Add room" src={PlusIC} size="normal" />)}
- />
- )}
+
+ { isDMTab && <IconButton onClick={() => openInviteUser()} tooltip="Start DM" src={PlusIC} size="small" /> }
+ { !isDMTab && <IconButton onClick={openHomeSpaceOptions} tooltip="Add rooms/spaces" src={PlusIC} size="small" /> }
</Header>
);
}
import ReadReceipts from '../read-receipts/ReadReceipts';
import ProfileViewer from '../profile-viewer/ProfileViewer';
+import SpaceAddExisting from '../../molecules/space-add-existing/SpaceAddExisting';
import Search from '../search/Search';
function Dialogs() {
<>
<ReadReceipts />
<ProfileViewer />
+ <SpaceAddExisting />
<Search />
</>
);
});
}
+export function openSpaceAddExisting(roomId) {
+ appDispatcher.dispatch({
+ type: cons.actions.navigation.OPEN_SPACE_ADDEXISTING,
+ roomId,
+ });
+}
+
export function toggleRoomSettings(tabText) {
appDispatcher.dispatch({
type: cons.actions.navigation.TOGGLE_ROOM_SETTINGS,
SELECT_ROOM: 'SELECT_ROOM',
OPEN_SPACE_SETTINGS: 'OPEN_SPACE_SETTINGS',
OPEN_SPACE_MANAGE: 'OPEN_SPACE_MANAGE',
+ OPEN_SPACE_ADDEXISTING: 'OPEN_SPACE_ADDEXISTING',
TOGGLE_ROOM_SETTINGS: 'TOGGLE_ROOM_SETTINGS',
OPEN_INVITE_LIST: 'OPEN_INVITE_LIST',
OPEN_PUBLIC_ROOMS: 'OPEN_PUBLIC_ROOMS',
ROOM_SELECTED: 'ROOM_SELECTED',
SPACE_SETTINGS_OPENED: 'SPACE_SETTINGS_OPENED',
SPACE_MANAGE_OPENED: 'SPACE_MANAGE_OPENED',
+ SPACE_ADDEXISTING_OPENED: 'SPACE_ADDEXISTING_OPENED',
ROOM_SETTINGS_TOGGLED: 'ROOM_SETTINGS_TOGGLED',
INVITE_LIST_OPENED: 'INVITE_LIST_OPENED',
PUBLIC_ROOMS_OPENED: 'PUBLIC_ROOMS_OPENED',
[cons.actions.navigation.OPEN_SPACE_MANAGE]: () => {
this.emit(cons.events.navigation.SPACE_MANAGE_OPENED, action.roomId);
},
+ [cons.actions.navigation.OPEN_SPACE_ADDEXISTING]: () => {
+ this.emit(cons.events.navigation.SPACE_ADDEXISTING_OPENED, action.roomId);
+ },
[cons.actions.navigation.TOGGLE_ROOM_SETTINGS]: () => {
this.isRoomSettings = !this.isRoomSettings;
this.emit(