From 79afc7649de8f4dd15fd42564abc3a97cb26e1c5 Mon Sep 17 00:00:00 2001 From: Ajay Bura Date: Sat, 26 Feb 2022 21:00:52 +0530 Subject: [PATCH] Add option to create room/space Signed-off-by: Ajay Bura --- src/app/organisms/create-room/CreateRoom.jsx | 356 ++++++++++++------ src/app/organisms/invite-user/InviteUser.jsx | 7 +- src/app/organisms/navigation/DrawerHeader.jsx | 16 +- .../profile-viewer/ProfileViewer.jsx | 6 +- src/app/organisms/pw/Dialogs.jsx | 2 + src/app/organisms/pw/Windows.jsx | 11 - src/client/action/navigation.js | 4 +- src/client/action/room.js | 162 +++++--- src/client/state/cons.js | 3 - src/client/state/navigation.js | 6 +- 10 files changed, 365 insertions(+), 208 deletions(-) diff --git a/src/app/organisms/create-room/CreateRoom.jsx b/src/app/organisms/create-room/CreateRoom.jsx index 6c4968ab..20e7609f 100644 --- a/src/app/organisms/create-room/CreateRoom.jsx +++ b/src/app/organisms/create-room/CreateRoom.jsx @@ -2,79 +2,87 @@ import React, { useState, useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import './CreateRoom.scss'; +import { twemojify } from '../../../util/twemojify'; import initMatrix from '../../../client/initMatrix'; import cons from '../../../client/state/cons'; -import { isRoomAliasAvailable } from '../../../util/matrixUtil'; +import navigation from '../../../client/state/navigation'; +import { selectRoom, openReusableContextMenu } from '../../../client/action/navigation'; import * as roomActions from '../../../client/action/room'; -import { selectRoom } from '../../../client/action/navigation'; +import { isRoomAliasAvailable, getIdServer } from '../../../util/matrixUtil'; +import { getEventCords } from '../../../util/common'; import Text from '../../atoms/text/Text'; import Button from '../../atoms/button/Button'; import Toggle from '../../atoms/button/Toggle'; import IconButton from '../../atoms/button/IconButton'; +import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu'; import Input from '../../atoms/input/Input'; import Spinner from '../../atoms/spinner/Spinner'; import SegmentControl from '../../atoms/segmented-controls/SegmentedControls'; -import PopupWindow from '../../molecules/popup-window/PopupWindow'; +import Dialog from '../../molecules/dialog/Dialog'; import SettingTile from '../../molecules/setting-tile/SettingTile'; import HashPlusIC from '../../../../public/res/ic/outlined/hash-plus.svg'; +import SpacePlusIC from '../../../../public/res/ic/outlined/space-plus.svg'; +import HashIC from '../../../../public/res/ic/outlined/hash.svg'; +import HashLockIC from '../../../../public/res/ic/outlined/hash-lock.svg'; +import HashGlobeIC from '../../../../public/res/ic/outlined/hash-globe.svg'; +import SpaceIC from '../../../../public/res/ic/outlined/space.svg'; +import SpaceLockIC from '../../../../public/res/ic/outlined/space-lock.svg'; +import SpaceGlobeIC from '../../../../public/res/ic/outlined/space-globe.svg'; +import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg'; import CrossIC from '../../../../public/res/ic/outlined/cross.svg'; -function CreateRoom({ isOpen, onRequestClose }) { - const [isPublic, togglePublic] = useState(false); - const [isEncrypted, toggleEncrypted] = useState(true); - const [isValidAddress, updateIsValidAddress] = useState(null); - const [isCreatingRoom, updateIsCreatingRoom] = useState(false); - const [creatingError, updateCreatingError] = useState(null); +function CreateRoomContent({ isSpace, parentId, onRequestClose }) { + const [joinRule, setJoinRule] = useState(parentId ? 'restricted' : 'invite'); + const [isEncrypted, setIsEncrypted] = useState(true); + const [isCreatingRoom, setIsCreatingRoom] = useState(false); + const [creatingError, setCreatingError] = useState(null); - const [titleValue, updateTitleValue] = useState(undefined); - const [topicValue, updateTopicValue] = useState(undefined); - const [addressValue, updateAddressValue] = useState(undefined); + const [isValidAddress, setIsValidAddress] = useState(null); + const [addressValue, setAddressValue] = useState(undefined); const [roleIndex, setRoleIndex] = useState(0); const addressRef = useRef(null); - const topicRef = useRef(null); - const nameRef = useRef(null); - const userId = initMatrix.matrixClient.getUserId(); - const hsString = userId.slice(userId.indexOf(':')); - - function resetForm() { - togglePublic(false); - toggleEncrypted(true); - updateIsValidAddress(null); - updateIsCreatingRoom(false); - updateCreatingError(null); - updateTitleValue(undefined); - updateTopicValue(undefined); - updateAddressValue(undefined); - setRoleIndex(0); - } - - const onCreated = (roomId) => { - resetForm(); - selectRoom(roomId); - onRequestClose(); - }; + const mx = initMatrix.matrixClient; + const userHs = getIdServer(mx.getUserId()); useEffect(() => { const { roomList } = initMatrix; + const onCreated = (roomId) => { + setJoinRule(false); + setIsEncrypted(true); + setIsValidAddress(null); + setIsCreatingRoom(false); + setCreatingError(null); + setAddressValue(undefined); + setRoleIndex(0); + + if (!mx.getRoom(roomId)?.isSpaceRoom()) { + selectRoom(roomId); + } + onRequestClose(); + }; roomList.on(cons.events.roomList.ROOM_CREATED, onCreated); return () => { roomList.removeListener(cons.events.roomList.ROOM_CREATED, onCreated); }; }, []); - async function createRoom() { + const handleSubmit = async (evt) => { + evt.preventDefault(); + const { target } = evt; + if (isCreatingRoom) return; - updateIsCreatingRoom(true); - updateCreatingError(null); - const name = nameRef.current.value; - let topic = topicRef.current.value; + setIsCreatingRoom(true); + setCreatingError(null); + + const name = target.name.value; + let topic = target.topic.value; if (topic.trim() === '') topic = undefined; let roomAlias; - if (isPublic) { + if (joinRule === 'public') { roomAlias = addressRef?.current?.value; if (roomAlias.trim() === '') roomAlias = undefined; } @@ -82,115 +90,217 @@ function CreateRoom({ isOpen, onRequestClose }) { const powerLevel = roleIndex === 1 ? 101 : undefined; try { - await roomActions.create({ - name, topic, isPublic, roomAlias, isEncrypted, powerLevel, + await roomActions.createRoom({ + name, + topic, + joinRule, + alias: roomAlias, + isEncrypted: (isSpace || joinRule === 'public') ? false : isEncrypted, + powerLevel, + isSpace, + parentId, }); } catch (e) { if (e.message === 'M_UNKNOWN: Invalid characters in room alias') { - updateCreatingError('ERROR: Invalid characters in room address'); - updateIsValidAddress(false); + setCreatingError('ERROR: Invalid characters in address'); + setIsValidAddress(false); } else if (e.message === 'M_ROOM_IN_USE: Room alias already taken') { - updateCreatingError('ERROR: Room address is already in use'); - updateIsValidAddress(false); - } else updateCreatingError(e.message); - updateIsCreatingRoom(false); + setCreatingError('ERROR: This address is already in use'); + setIsValidAddress(false); + } else setCreatingError(e.message); + setIsCreatingRoom(false); } - } + }; - function validateAddress(e) { + const validateAddress = (e) => { const myAddress = e.target.value; - updateIsValidAddress(null); - updateAddressValue(e.target.value); - updateCreatingError(null); + setIsValidAddress(null); + setAddressValue(e.target.value); + setCreatingError(null); setTimeout(async () => { if (myAddress !== addressRef.current.value) return; const roomAlias = addressRef.current.value; if (roomAlias === '') return; - const roomAddress = `#${roomAlias}${hsString}`; + const roomAddress = `#${roomAlias}:${userHs}`; if (await isRoomAliasAvailable(roomAddress)) { - updateIsValidAddress(true); + setIsValidAddress(true); } else { - updateIsValidAddress(false); + setIsValidAddress(false); } }, 1000); - } - function handleTitleChange(e) { - if (e.target.value.trim() === '') updateTitleValue(undefined); - updateTitleValue(e.target.value); - } - function handleTopicChange(e) { - if (e.target.value.trim() === '') updateTopicValue(undefined); - updateTopicValue(e.target.value); - } + }; + + const joinRules = ['invite', 'restricted', 'public']; + const joinRuleShortText = ['Private', 'Restricted', 'Public']; + const joinRuleText = ['Private (invite only)', 'Restricted (space member can join)', 'Public (anyone can join)']; + const jrRoomIC = [HashLockIC, HashIC, HashGlobeIC]; + const jrSpaceIC = [SpaceLockIC, SpaceIC, SpaceGlobeIC]; + const handleJoinRule = (evt) => { + openReusableContextMenu( + 'bottom', + getEventCords(evt, '.btn-surface'), + (closeMenu) => ( + <> + Visibility (who can join) + { + joinRules.map((rule) => ( + { closeMenu(); setJoinRule(rule); }} + disabled={!parentId && rule === 'restricted'} + > + { joinRuleText[joinRules.indexOf(rule)] } + + )) + } + + ), + ); + }; return ( - } - onRequestClose={onRequestClose} - > -
-
{ e.preventDefault(); createRoom(); }}> - } - content={Public room can be joined by anyone.} - /> - {isPublic && ( -
- Room address -
- # - - {hsString} -
- {isValidAddress === false && {`#${addressValue}${hsString} is already in use`}} -
+
+ + + {joinRuleShortText[joinRules.indexOf(joinRule)]} + )} - {!isPublic && ( - } - content={You can’t disable this later. Bridges & most bots won’t work yet.} + content={{`Select who can join this ${isSpace ? 'space' : 'room'}.`}} + /> + {joinRule === 'public' && ( +
+ {isSpace ? 'Space address' : 'Room address'} +
+ # + + {`:${userHs}`} +
+ {isValidAddress === false && {`#${addressValue}:${userHs} is already in use`}} +
+ )} + {!isSpace && joinRule !== 'public' && ( + } + content={You can’t disable this later. Bridges & most bots won’t work yet.} + /> + )} + )} - - )} - content={( - Override the default (100) power level. - )} - /> - -
- - -
- {isCreatingRoom && ( -
- - Creating room... -
+ content={( + Override the default (100) power level. )} - {typeof creatingError === 'string' && {creatingError}} - -
- + /> + +
+ + +
+ {isCreatingRoom && ( +
+ + {`Creating ${isSpace ? 'space' : 'room'}...`} +
+ )} + {typeof creatingError === 'string' && {creatingError}} + +
); } - -CreateRoom.propTypes = { - isOpen: PropTypes.bool.isRequired, +CreateRoomContent.defaultProps = { + parentId: null, +}; +CreateRoomContent.propTypes = { + isSpace: PropTypes.bool.isRequired, + parentId: PropTypes.string, onRequestClose: PropTypes.func.isRequired, }; +function useWindowToggle() { + const [create, setCreate] = useState(null); + + useEffect(() => { + const handleOpen = (isSpace, parentId) => { + setCreate({ + isSpace, + parentId, + }); + }; + navigation.on(cons.events.navigation.CREATE_ROOM_OPENED, handleOpen); + return () => { + navigation.removeListener(cons.events.navigation.CREATE_ROOM_OPENED, handleOpen); + }; + }, []); + + const onRequestClose = () => setCreate(null); + + return [create, onRequestClose]; +} + +function CreateRoom() { + const [create, onRequestClose] = useWindowToggle(); + const { isSpace, parentId } = create ?? {}; + const mx = initMatrix.matrixClient; + const room = mx.getRoom(parentId); + + return ( + + {parentId ? twemojify(room.name) : 'Home'} + + {` — create ${isSpace ? 'space' : 'room'}`} + + + )} + contentOptions={} + onRequestClose={onRequestClose} + > + { + create + ? ( + + ) :
+ } +
+ ); +} + export default CreateRoom; diff --git a/src/app/organisms/invite-user/InviteUser.jsx b/src/app/organisms/invite-user/InviteUser.jsx index e7d7f510..c03d3ad9 100644 --- a/src/app/organisms/invite-user/InviteUser.jsx +++ b/src/app/organisms/invite-user/InviteUser.jsx @@ -117,12 +117,7 @@ function InviteUser({ procUserError.delete(userId); updateUserProcError(getMapCopy(procUserError)); - const result = await roomActions.create({ - isPublic: false, - isEncrypted: true, - isDirect: true, - invite: [userId], - }); + const result = await roomActions.createDM(userId); roomIdToUserId.set(result.room_id, userId); updateRoomIdToUserId(getMapCopy(roomIdToUserId)); } catch (e) { diff --git a/src/app/organisms/navigation/DrawerHeader.jsx b/src/app/organisms/navigation/DrawerHeader.jsx index 5052865c..ab8d68a0 100644 --- a/src/app/organisms/navigation/DrawerHeader.jsx +++ b/src/app/organisms/navigation/DrawerHeader.jsx @@ -38,20 +38,20 @@ function HomeSpaceOptions({ spaceId, afterOptionSelect }) { return ( <> Add rooms or spaces - { afterOptionSelect(); openCreateRoom(); }} - disabled={!canManage} - > - Create new room - { afterOptionSelect(); }} + onClick={() => { afterOptionSelect(); openCreateRoom(true, spaceId); }} disabled={!canManage} > Create new space + { afterOptionSelect(); openCreateRoom(false, spaceId); }} + disabled={!canManage} + > + Create new room + { !spaceId && ( + diff --git a/src/app/organisms/pw/Windows.jsx b/src/app/organisms/pw/Windows.jsx index b1669a14..ae2bc1a6 100644 --- a/src/app/organisms/pw/Windows.jsx +++ b/src/app/organisms/pw/Windows.jsx @@ -5,7 +5,6 @@ import navigation from '../../../client/state/navigation'; import InviteList from '../invite-list/InviteList'; import PublicRooms from '../public-rooms/PublicRooms'; -import CreateRoom from '../create-room/CreateRoom'; import InviteUser from '../invite-user/InviteUser'; import Settings from '../settings/Settings'; import SpaceSettings from '../space-settings/SpaceSettings'; @@ -16,7 +15,6 @@ function Windows() { const [publicRooms, changePublicRooms] = useState({ isOpen: false, searchTerm: undefined, }); - const [isCreateRoom, changeCreateRoom] = useState(false); const [inviteUser, changeInviteUser] = useState({ isOpen: false, roomId: undefined, term: undefined, }); @@ -31,9 +29,6 @@ function Windows() { searchTerm, }); } - function openCreateRoom() { - changeCreateRoom(true); - } function openInviteUser(roomId, searchTerm) { changeInviteUser({ isOpen: true, @@ -48,13 +43,11 @@ function Windows() { useEffect(() => { navigation.on(cons.events.navigation.INVITE_LIST_OPENED, openInviteList); navigation.on(cons.events.navigation.PUBLIC_ROOMS_OPENED, openPublicRooms); - navigation.on(cons.events.navigation.CREATE_ROOM_OPENED, openCreateRoom); navigation.on(cons.events.navigation.INVITE_USER_OPENED, openInviteUser); navigation.on(cons.events.navigation.SETTINGS_OPENED, openSettings); return () => { navigation.removeListener(cons.events.navigation.INVITE_LIST_OPENED, openInviteList); navigation.removeListener(cons.events.navigation.PUBLIC_ROOMS_OPENED, openPublicRooms); - navigation.removeListener(cons.events.navigation.CREATE_ROOM_OPENED, openCreateRoom); navigation.removeListener(cons.events.navigation.INVITE_USER_OPENED, openInviteUser); navigation.removeListener(cons.events.navigation.SETTINGS_OPENED, openSettings); }; @@ -71,10 +64,6 @@ function Windows() { searchTerm={publicRooms.searchTerm} onRequestClose={() => changePublicRooms({ isOpen: false, searchTerm: undefined })} /> - changeCreateRoom(false)} - /> errcode === e.errcode)) { - appDispatcher.dispatch({ - type: cons.actions.room.error.CREATE, - error: e, - }); - throw new Error(e); - } - throw new Error('Something went wrong!'); + const result = await create(options, true); + return result; +} + +async function createRoom(opts) { + // joinRule: 'public' | 'invite' | 'restricted' + const { name, topic, joinRule } = opts; + const alias = opts.alias ?? undefined; + const parentId = opts.parentId ?? undefined; + const isSpace = opts.isSpace ?? false; + const isEncrypted = opts.isEncrypted ?? false; + const powerLevel = opts.powerLevel ?? undefined; + const blockFederation = opts.blockFederation ?? false; + + const mx = initMatrix.matrixClient; + const visibility = joinRule === 'public' ? 'public' : 'private'; + const options = { + creation_content: undefined, + name, + topic, + visibility, + room_alias_name: alias, + initial_state: [], + power_level_content_override: undefined, + }; + if (isSpace) { + options.creation_content = { type: 'm.space' }; } + if (blockFederation) { + options.creation_content = { 'm.federate': false }; + } + if (isEncrypted) { + options.initial_state.push({ + type: 'm.room.encryption', + state_key: '', + content: { + algorithm: 'm.megolm.v1.aes-sha2', + }, + }); + } + if (powerLevel) { + options.power_level_content_override = { + users: { + [mx.getUserId()]: powerLevel, + }, + }; + } + if (parentId) { + options.initial_state.push({ + type: 'm.space.parent', + state_key: parentId, + content: { + canonical: true, + via: [getIdServer(mx.getUserId())], + }, + }); + } + if (parentId && joinRule === 'restricted') { + options.initial_state.push({ + type: 'm.room.join_rules', + content: { + join_rule: 'restricted', + allow: [{ + type: 'm.room_membership', + room_id: parentId, + }], + }, + }); + } + + const result = await create(options); + + if (parentId) { + await mx.sendStateEvent(parentId, 'm.space.child', { + auto_join: false, + suggested: false, + via: [getIdServer(mx.getUserId())], + }, result.room_id); + } + + return result; } async function invite(roomId, userId) { @@ -242,7 +303,8 @@ function deleteSpaceShortcut(roomId) { export { join, leave, - create, invite, kick, ban, unban, + createDM, createRoom, + invite, kick, ban, unban, setPowerLevel, createSpaceShortcut, deleteSpaceShortcut, }; diff --git a/src/client/state/cons.js b/src/client/state/cons.js index 8ff6eede..e9413cbb 100644 --- a/src/client/state/cons.js +++ b/src/client/state/cons.js @@ -53,9 +53,6 @@ const cons = { CREATE: 'CREATE', CREATE_SPACE_SHORTCUT: 'CREATE_SPACE_SHORTCUT', DELETE_SPACE_SHORTCUT: 'DELETE_SPACE_SHORTCUT', - error: { - CREATE: 'ERROR_CREATE', - }, }, settings: { TOGGLE_SYSTEM_THEME: 'TOGGLE_SYSTEM_THEME', diff --git a/src/client/state/navigation.js b/src/client/state/navigation.js index ca083956..ee8d06c2 100644 --- a/src/client/state/navigation.js +++ b/src/client/state/navigation.js @@ -113,7 +113,11 @@ class Navigation extends EventEmitter { this.emit(cons.events.navigation.PUBLIC_ROOMS_OPENED, action.searchTerm); }, [cons.actions.navigation.OPEN_CREATE_ROOM]: () => { - this.emit(cons.events.navigation.CREATE_ROOM_OPENED); + this.emit( + cons.events.navigation.CREATE_ROOM_OPENED, + action.isSpace, + action.parentId, + ); }, [cons.actions.navigation.OPEN_INVITE_USER]: () => { this.emit(cons.events.navigation.INVITE_USER_OPENED, action.roomId, action.searchTerm);