From 318e7c74586a5b3bfe8b35791400e1ac6b8b473f Mon Sep 17 00:00:00 2001 From: Ajay Bura Date: Sun, 20 Feb 2022 20:17:13 +0530 Subject: [PATCH] Make dialog to add existing rooms to space Signed-off-by: Ajay Bura --- .../space-add-existing/SpaceAddExisting.jsx | 202 ++++++++++++++++++ .../space-add-existing/SpaceAddExisting.scss | 74 +++++++ src/app/organisms/navigation/DrawerHeader.jsx | 124 +++++++---- src/app/organisms/pw/Dialogs.jsx | 2 + src/client/action/navigation.js | 7 + src/client/state/cons.js | 2 + src/client/state/navigation.js | 3 + 7 files changed, 370 insertions(+), 44 deletions(-) create mode 100644 src/app/molecules/space-add-existing/SpaceAddExisting.jsx create mode 100644 src/app/molecules/space-add-existing/SpaceAddExisting.scss diff --git a/src/app/molecules/space-add-existing/SpaceAddExisting.jsx b/src/app/molecules/space-add-existing/SpaceAddExisting.jsx new file mode 100644 index 0000000..5bbb1da --- /dev/null +++ b/src/app/molecules/space-add-existing/SpaceAddExisting.jsx @@ -0,0 +1,202 @@ +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 ( + <> +
{ + ev.preventDefault(); + const { target } = ev; + target.searchInput.value = ''; + setSearchIds(null); + }} + > + + + + + {searchIds?.length === 0 && No result found} + { + (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 ( + + )} + /> + ); + }) + } + {selected.length !== 0 && ( +
+ {process && } + {process || `${selected.length} item selected`} + { !process && ( + + )} +
+ )} + + ); +} +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 ( + + {roomId && twemojify(room.name)} + — add existing rooms + + )} + contentOptions={} + onRequestClose={requestClose} + > + { + roomId + ? + :
+ } +
+ ); +} + +export default SpaceAddExisting; diff --git a/src/app/molecules/space-add-existing/SpaceAddExisting.scss b/src/app/molecules/space-add-existing/SpaceAddExisting.scss new file mode 100644 index 0000000..792cdc0 --- /dev/null +++ b/src/app/molecules/space-add-existing/SpaceAddExisting.scss @@ -0,0 +1,74 @@ +@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 diff --git a/src/app/organisms/navigation/DrawerHeader.jsx b/src/app/organisms/navigation/DrawerHeader.jsx index 198e61e..5052865 100644 --- a/src/app/organisms/navigation/DrawerHeader.jsx +++ b/src/app/organisms/navigation/DrawerHeader.jsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import './DrawerHeader.scss'; @@ -7,9 +7,9 @@ import { twemojify } from '../../../util/twemojify'; 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'; @@ -18,24 +18,83 @@ import Text from '../../atoms/text/Text'; 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 ( + <> + Add rooms or spaces + { afterOptionSelect(); openCreateRoom(); }} + disabled={!canManage} + > + Create new room + + { afterOptionSelect(); }} + disabled={!canManage} + > + Create new space + + { !spaceId && ( + { afterOptionSelect(); openPublicRooms(); }} + > + Join public room + + )} + { spaceId && ( + { afterOptionSelect(); openSpaceAddExisting(spaceId); }} + disabled={!canManage} + > + Add existing + + )} + { spaceId && ( + { afterOptionSelect(); openSpaceManage(spaceId); }} + iconSrc={HashSearchIC} + > + Manage rooms + + )} + + ); +} +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(); @@ -46,6 +105,15 @@ function DrawerHeader({ selectedTab, spaceId }) { ); }; + const openHomeSpaceOptions = (e) => { + e.preventDefault(); + openReusableContextMenu( + 'right', + getEventCords(e, '.ic-btn'), + (closeMenu) => , + ); + }; + return (
{spaceName ? ( @@ -65,41 +133,9 @@ function DrawerHeader({ selectedTab, spaceId }) { {tabName} )} - {spaceName && ( - { - if (spaceShortcut.has(spaceId)) deleteSpaceShortcut(spaceId); - else createSpaceShortcut(spaceId); - forceUpdate({}); - }} - /> - )} - { selectedTab === cons.tabs.DIRECTS && openInviteUser()} tooltip="Start DM" src={PlusIC} size="normal" /> } - { selectedTab !== cons.tabs.DIRECTS && !spaceName && ( - ( - <> - Add room - { hideMenu(); openCreateRoom(); }} - > - Create new room - - { hideMenu(); openPublicRooms(); }} - > - Join public room - - - )} - render={(toggleMenu) => ()} - /> - )} + + { isDMTab && openInviteUser()} tooltip="Start DM" src={PlusIC} size="small" /> } + { !isDMTab && }
); } diff --git a/src/app/organisms/pw/Dialogs.jsx b/src/app/organisms/pw/Dialogs.jsx index 0fa89cb..b5038e5 100644 --- a/src/app/organisms/pw/Dialogs.jsx +++ b/src/app/organisms/pw/Dialogs.jsx @@ -2,6 +2,7 @@ import React from 'react'; 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() { @@ -9,6 +10,7 @@ function Dialogs() { <> + ); diff --git a/src/client/action/navigation.js b/src/client/action/navigation.js index 6f82a42..964a3f5 100644 --- a/src/client/action/navigation.js +++ b/src/client/action/navigation.js @@ -38,6 +38,13 @@ export function openSpaceManage(roomId) { }); } +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, diff --git a/src/client/state/cons.js b/src/client/state/cons.js index 2ac4cbd..2d20dbe 100644 --- a/src/client/state/cons.js +++ b/src/client/state/cons.js @@ -32,6 +32,7 @@ const cons = { 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', @@ -71,6 +72,7 @@ const cons = { 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', diff --git a/src/client/state/navigation.js b/src/client/state/navigation.js index a7044f5..52d5f15 100644 --- a/src/client/state/navigation.js +++ b/src/client/state/navigation.js @@ -95,6 +95,9 @@ class Navigation extends EventEmitter { [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(