@@ -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 , toggle Encrypted] = useState ( true ) ;
const [ isValidAddress , updateIsValidAddress ] = useState ( null ) ;
const [ isC reatingRoom , updateIs CreatingRoom ] = useState ( false ) ;
const [ creatingError , updateCreatingError ] = useState ( null ) ;
function CreateRoomContent ( { isSpace , parentId , onRequestClose } ) {
const [ joinRule , setJoinRule ] = useState ( parentId ? 'restricted' : 'invite' ) ;
const [ isEncrypted , setIs Encrypted] = useState ( true ) ;
const [ isCreatingRoom , setIsCreatingRoom ] = useState ( false ) ;
const [ c reatingError , set CreatingError ] = useState ( null ) ;
const [ titleValue , updateTitleValue ] = useState ( undefined ) ;
const [ topic Value, updateTopic Value] = useState ( undefined ) ;
const [ addressValue , updateAddressValue ] = useState ( undefined ) ;
const [ isValidAddress , setIsValidAddress ] = useState ( null ) ;
const [ address Value, setAddress Value] = 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 ( u serId. 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 . getU serId( ) ) ;
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 ;
update IsCreatingRoom( true ) ;
update CreatingError( null ) ;
const name = nameRef . current . value ;
let topic = topicRef . current . value ;
set IsCreatingRoom( true ) ;
set CreatingError( null ) ;
const name = target . name . value ;
let topic = target . topic . value ;
if ( topic . trim ( ) === '' ) topic = undefined ;
let roomAlias ;
if ( isP ublic) {
if ( joinRule === 'p ublic' ) {
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' ) {
update CreatingError( 'ERROR: Invalid characters in room address' ) ;
update IsValidAddress( false ) ;
set CreatingError( 'ERROR: Invalid characters in address' ) ;
set IsValidAddress( false ) ;
} else if ( e . message === 'M_ROOM_IN_USE: Room alias already taken' ) {
update CreatingError( 'ERROR: Room address is already in use' ) ;
update IsValidAddress( false ) ;
} else update CreatingError( e . message ) ;
update IsCreatingRoom( false ) ;
set CreatingError( 'ERROR: This address is already in use' ) ;
set IsValidAddress( false ) ;
} else set CreatingError( e . message ) ;
set IsCreatingRoom( false ) ;
}
}
} ;
function validateAddress ( e ) {
const validateAddress = ( e ) => {
const myAddress = e . target . value ;
update IsValidAddress( null ) ;
update AddressValue( e . target . value ) ;
update CreatingError( null ) ;
set IsValidAddress( null ) ;
set AddressValue( e . target . value ) ;
set CreatingError( 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 ) ) {
update IsValidAddress( true ) ;
set IsValidAddress( true ) ;
} else {
update IsValidAddress( false ) ;
set IsValidAddress( 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 ) => (
< >
< MenuHeader > Visibility ( who can join ) < / MenuHeader >
{
joinRules . map ( ( rule ) => (
< MenuItem
key = { rule }
variant = { rule === joinRule ? 'positive' : 'surface' }
iconSrc = {
isSpace
? jrSpaceIC [ joinRules . indexOf ( rule ) ]
: jrRoomIC [ joinRules . indexOf ( rule ) ]
}
onClick = { ( ) => { closeMenu ( ) ; setJoinRule ( rule ) ; } }
disabled = { ! parentId && rule === 'restricted' }
>
{ joinRuleText [ joinRules . indexOf ( rule ) ] }
< / MenuItem >
) )
}
< / >
) ,
) ;
} ;
return (
< PopupWindow
isOpen = { isOpen }
title = "Create room"
contentOptions = { < IconButton src = { CrossIC } onClick = { onRequestClose } tooltip = "Close" / > }
onRequestClose = { onRequestClose }
>
< div className = "create-room" >
< form className = "create-room__form" onSubmit = { ( e ) => { e . preventDefault ( ) ; createRoom ( ) ; } } >
< SettingTile
title = "Make room public"
options = { < Toggle isActive = { isPublic } onToggle = { togglePublic } / > }
content = { < Text variant = "b3" > Public room can be joined by anyone . < / Text > }
/ >
{ isPublic && (
< div >
< Text className = "create-room__address__label" variant = "b2" > Room address < / Text >
< div className = "create-room__address" >
< Text variant = "b1" > # < / Text >
< Input value = { addressValue } onChange = { validateAddress } state = { ( isValidAddress === false ) ? 'error' : 'normal' } forwardRef = { addressRef } placeholder = "my_room" required / >
< Text variant = "b1" > { hsString } < / Text >
< / div >
{ isValidAddress === false && < Text className = "create-room__address__tip" variant = "b3" > < span style = { { color : 'var(--bg-danger)' } } > { ` # ${ addressValue } ${ hsString } is already in use ` } < / span > < / Text > }
< / div >
< div className = "create-room" >
< form className = "create-room__form" onSubmit = { handleSubmit } >
< SettingTile
title = "Visibility"
options = { (
< Button onClick = { handleJoinRule } iconSrc = { ChevronBottomIC } >
{ joinRuleShortText [ joinRules . indexOf ( joinRule ) ] }
< / Button >
) }
{ ! isPublic && (
< SettingTile
tit le ="Enable end-to-end encryption"
options = { < Toggle isActive = { isEncrypted } onToggle = { toggleEncrypted } / > }
content = { < Text variant = "b3 " > You can ’ t disable this later . Bridges & most bots won ’ t work yet . < / Text > }
content = { < Text variant = "b3" > { ` Select who can join this ${ isSpace ? 'space' : 'room' } . ` } < / Text > }
/ >
{ joinRu le === 'public' && (
< div >
< Text className = "create-room__address__label" variant = "b2 " > { isSpace ? 'Space address' : 'Room address' } < / Text >
< div className = "create-room__address" >
< Text variant = "b1" > # < / Text >
< Input
value = { addressValue }
onChange = { validateAddress }
state = { ( isValidAddress === false ) ? 'error' : 'normal' }
forwardRef = { addressRef }
placeholder = "my_address"
required
/ >
< Text variant = "b1" > { ` : ${ userHs } ` } < / Text >
< / div >
{ isValidAddress === false && < Text className = "create-room__address__tip" variant = "b3" > < span style = { { color : 'var(--bg-danger)' } } > { ` # ${ addressValue } : ${ userHs } is already in use ` } < / span > < / Text > }
< / div >
) }
{ ! isSpace && joinRule !== 'public' && (
< SettingTile
title = "Enable end-to-end encryption"
options = { < Toggle isActive = { isEncrypted } onToggle = { setIsEncrypted } / > }
content = { < Text variant = "b3" > You can ’ t disable this later . Bridges & most bots won ’ t work yet . < / Text > }
/ >
) }
< SettingTile
title = "Select your role"
options = { (
< SegmentControl
selected = { roleIndex }
segments = { [ { text : 'Admin' } , { text : 'Founder' } ] }
onSelect = { setRoleIndex }
/ >
) }
< SettingTile
title = "Select your role"
options = { (
< SegmentControl
selected = { roleIndex }
segments = { [ { text : 'Admin' } , { text : 'Founder' } ] }
onSelect = { setRoleIndex }
/ >
) }
content = { (
< Text variant = "b3" > Override the default ( 100 ) power level . < / Text >
) }
/ >
< Input value = { topicValue } onChange = { handleTopicChange } forwardRef = { topicRef } minHeight = { 174 } resizable label = "Topic (optional)" / >
< div className = "create-room__name-wrapper" >
< Input value = { titleValue } onChange = { handleTitleChange } forwardRef = { nameRef } label = "Room name" required / >
< Button disabled = { isValidAddress === false || isCreatingRoom } iconSrc = { HashPlusIC } type = "submit" variant = "primary" > Create < / Button >
< / div >
{ isCreatingRoom && (
< div className = "create-room__loading" >
< Spinner size = "small" / >
< Text > Creating room ... < / Text >
< / div >
content = { (
< Text variant = "b3" > Override the default ( 100 ) power level . < / Text >
) }
{ typeof creatingError === 'string' && < Text className = "create-room__error" variant = "b3" > { creatingError } < / Text > }
< /form >
< / div >
< / PopupWindow >
/ >
< Input name = "topic" minHeight = { 174 } resizable label = "Topic (optional)" / >
< div className = "create-room__name-wrapper" >
< Input name = "name" label = { ` ${ isSpace ? 'Space' : 'Room' } name ` } required / >
< Button
disabled = { isValidAddress === false || isCreatingRoom }
iconSrc = { isSpace ? SpacePlusIC : HashPlusIC }
type = "submit"
variant = "primary"
>
Create
< / Button >
< / div >
{ isCreatingRoom && (
< div className = "create-room__loading" >
< Spinner size = "small" / >
< Text > { ` Creating ${ isSpace ? 'space' : 'room' } ... ` } < / Text >
< / div >
) }
{ typeof creatingError === 'string' && < Text className = "create-room__error" variant = "b3" > { creatingError } < / Text > }
< / form >
< / div >
) ;
}
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 (
< Dialog
isOpen = { create !== null }
title = { (
< Text variant = "s1" weight = "medium" primary >
{ parentId ? twemojify ( room . name ) : 'Home' }
< span style = { { color : 'var(--tc-surface-low)' } } >
{ ` — create ${ isSpace ? 'space' : 'room' } ` }
< / span >
< / Text >
) }
contentOptions = { < IconButton src = { CrossIC } onClick = { onRequestClose } tooltip = "Close" / > }
onRequestClose = { onRequestClose }
>
{
create
? (
< CreateRoomContent
isSpace = { isSpace }
parentId = { parentId }
onRequestClose = { onRequestClose }
/ >
) : < div / >
}
< / Dialog >
) ;
}
export default CreateRoom ;