Add option to create room/space

Signed-off-by: Ajay Bura <ajbura@gmail.com>
This commit is contained in:
Ajay Bura 2022-02-26 21:00:52 +05:30
parent 2eee3736df
commit 79afc7649d
10 changed files with 365 additions and 208 deletions

View file

@ -2,79 +2,87 @@ import React, { useState, useEffect, useRef } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import './CreateRoom.scss'; import './CreateRoom.scss';
import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix'; import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons'; 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 * 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 Text from '../../atoms/text/Text';
import Button from '../../atoms/button/Button'; import Button from '../../atoms/button/Button';
import Toggle from '../../atoms/button/Toggle'; import Toggle from '../../atoms/button/Toggle';
import IconButton from '../../atoms/button/IconButton'; import IconButton from '../../atoms/button/IconButton';
import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
import Input from '../../atoms/input/Input'; import Input from '../../atoms/input/Input';
import Spinner from '../../atoms/spinner/Spinner'; import Spinner from '../../atoms/spinner/Spinner';
import SegmentControl from '../../atoms/segmented-controls/SegmentedControls'; 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 SettingTile from '../../molecules/setting-tile/SettingTile';
import HashPlusIC from '../../../../public/res/ic/outlined/hash-plus.svg'; 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'; import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
function CreateRoom({ isOpen, onRequestClose }) { function CreateRoomContent({ isSpace, parentId, onRequestClose }) {
const [isPublic, togglePublic] = useState(false); const [joinRule, setJoinRule] = useState(parentId ? 'restricted' : 'invite');
const [isEncrypted, toggleEncrypted] = useState(true); const [isEncrypted, setIsEncrypted] = useState(true);
const [isValidAddress, updateIsValidAddress] = useState(null); const [isCreatingRoom, setIsCreatingRoom] = useState(false);
const [isCreatingRoom, updateIsCreatingRoom] = useState(false); const [creatingError, setCreatingError] = useState(null);
const [creatingError, updateCreatingError] = useState(null);
const [titleValue, updateTitleValue] = useState(undefined); const [isValidAddress, setIsValidAddress] = useState(null);
const [topicValue, updateTopicValue] = useState(undefined); const [addressValue, setAddressValue] = useState(undefined);
const [addressValue, updateAddressValue] = useState(undefined);
const [roleIndex, setRoleIndex] = useState(0); const [roleIndex, setRoleIndex] = useState(0);
const addressRef = useRef(null); const addressRef = useRef(null);
const topicRef = useRef(null);
const nameRef = useRef(null);
const userId = initMatrix.matrixClient.getUserId(); const mx = initMatrix.matrixClient;
const hsString = userId.slice(userId.indexOf(':')); const userHs = getIdServer(mx.getUserId());
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();
};
useEffect(() => { useEffect(() => {
const { roomList } = initMatrix; 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); roomList.on(cons.events.roomList.ROOM_CREATED, onCreated);
return () => { return () => {
roomList.removeListener(cons.events.roomList.ROOM_CREATED, onCreated); roomList.removeListener(cons.events.roomList.ROOM_CREATED, onCreated);
}; };
}, []); }, []);
async function createRoom() { const handleSubmit = async (evt) => {
evt.preventDefault();
const { target } = evt;
if (isCreatingRoom) return; if (isCreatingRoom) return;
updateIsCreatingRoom(true); setIsCreatingRoom(true);
updateCreatingError(null); setCreatingError(null);
const name = nameRef.current.value;
let topic = topicRef.current.value; const name = target.name.value;
let topic = target.topic.value;
if (topic.trim() === '') topic = undefined; if (topic.trim() === '') topic = undefined;
let roomAlias; let roomAlias;
if (isPublic) { if (joinRule === 'public') {
roomAlias = addressRef?.current?.value; roomAlias = addressRef?.current?.value;
if (roomAlias.trim() === '') roomAlias = undefined; if (roomAlias.trim() === '') roomAlias = undefined;
} }
@ -82,78 +90,116 @@ function CreateRoom({ isOpen, onRequestClose }) {
const powerLevel = roleIndex === 1 ? 101 : undefined; const powerLevel = roleIndex === 1 ? 101 : undefined;
try { try {
await roomActions.create({ await roomActions.createRoom({
name, topic, isPublic, roomAlias, isEncrypted, powerLevel, name,
topic,
joinRule,
alias: roomAlias,
isEncrypted: (isSpace || joinRule === 'public') ? false : isEncrypted,
powerLevel,
isSpace,
parentId,
}); });
} catch (e) { } catch (e) {
if (e.message === 'M_UNKNOWN: Invalid characters in room alias') { if (e.message === 'M_UNKNOWN: Invalid characters in room alias') {
updateCreatingError('ERROR: Invalid characters in room address'); setCreatingError('ERROR: Invalid characters in address');
updateIsValidAddress(false); setIsValidAddress(false);
} else if (e.message === 'M_ROOM_IN_USE: Room alias already taken') { } else if (e.message === 'M_ROOM_IN_USE: Room alias already taken') {
updateCreatingError('ERROR: Room address is already in use'); setCreatingError('ERROR: This address is already in use');
updateIsValidAddress(false); setIsValidAddress(false);
} else updateCreatingError(e.message); } else setCreatingError(e.message);
updateIsCreatingRoom(false); setIsCreatingRoom(false);
}
} }
};
function validateAddress(e) { const validateAddress = (e) => {
const myAddress = e.target.value; const myAddress = e.target.value;
updateIsValidAddress(null); setIsValidAddress(null);
updateAddressValue(e.target.value); setAddressValue(e.target.value);
updateCreatingError(null); setCreatingError(null);
setTimeout(async () => { setTimeout(async () => {
if (myAddress !== addressRef.current.value) return; if (myAddress !== addressRef.current.value) return;
const roomAlias = addressRef.current.value; const roomAlias = addressRef.current.value;
if (roomAlias === '') return; if (roomAlias === '') return;
const roomAddress = `#${roomAlias}${hsString}`; const roomAddress = `#${roomAlias}:${userHs}`;
if (await isRoomAliasAvailable(roomAddress)) { if (await isRoomAliasAvailable(roomAddress)) {
updateIsValidAddress(true); setIsValidAddress(true);
} else { } else {
updateIsValidAddress(false); setIsValidAddress(false);
} }
}, 1000); }, 1000);
};
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)]
} }
function handleTitleChange(e) { onClick={() => { closeMenu(); setJoinRule(rule); }}
if (e.target.value.trim() === '') updateTitleValue(undefined); disabled={!parentId && rule === 'restricted'}
updateTitleValue(e.target.value); >
} { joinRuleText[joinRules.indexOf(rule)] }
function handleTopicChange(e) { </MenuItem>
if (e.target.value.trim() === '') updateTopicValue(undefined); ))
updateTopicValue(e.target.value);
} }
</>
),
);
};
return ( return (
<PopupWindow
isOpen={isOpen}
title="Create room"
contentOptions={<IconButton src={CrossIC} onClick={onRequestClose} tooltip="Close" />}
onRequestClose={onRequestClose}
>
<div className="create-room"> <div className="create-room">
<form className="create-room__form" onSubmit={(e) => { e.preventDefault(); createRoom(); }}> <form className="create-room__form" onSubmit={handleSubmit}>
<SettingTile <SettingTile
title="Make room public" title="Visibility"
options={<Toggle isActive={isPublic} onToggle={togglePublic} />} options={(
content={<Text variant="b3">Public room can be joined by anyone.</Text>} <Button onClick={handleJoinRule} iconSrc={ChevronBottomIC}>
{joinRuleShortText[joinRules.indexOf(joinRule)]}
</Button>
)}
content={<Text variant="b3">{`Select who can join this ${isSpace ? 'space' : 'room'}.`}</Text>}
/> />
{isPublic && ( {joinRule === 'public' && (
<div> <div>
<Text className="create-room__address__label" variant="b2">Room address</Text> <Text className="create-room__address__label" variant="b2">{isSpace ? 'Space address' : 'Room address'}</Text>
<div className="create-room__address"> <div className="create-room__address">
<Text variant="b1">#</Text> <Text variant="b1">#</Text>
<Input value={addressValue} onChange={validateAddress} state={(isValidAddress === false) ? 'error' : 'normal'} forwardRef={addressRef} placeholder="my_room" required /> <Input
<Text variant="b1">{hsString}</Text> value={addressValue}
onChange={validateAddress}
state={(isValidAddress === false) ? 'error' : 'normal'}
forwardRef={addressRef}
placeholder="my_address"
required
/>
<Text variant="b1">{`:${userHs}`}</Text>
</div> </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>} {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> </div>
)} )}
{!isPublic && ( {!isSpace && joinRule !== 'public' && (
<SettingTile <SettingTile
title="Enable end-to-end encryption" title="Enable end-to-end encryption"
options={<Toggle isActive={isEncrypted} onToggle={toggleEncrypted} />} options={<Toggle isActive={isEncrypted} onToggle={setIsEncrypted} />}
content={<Text variant="b3">You cant disable this later. Bridges & most bots wont work yet.</Text>} content={<Text variant="b3">You cant disable this later. Bridges & most bots wont work yet.</Text>}
/> />
)} )}
@ -170,27 +216,91 @@ function CreateRoom({ isOpen, onRequestClose }) {
<Text variant="b3">Override the default (100) power level.</Text> <Text variant="b3">Override the default (100) power level.</Text>
)} )}
/> />
<Input value={topicValue} onChange={handleTopicChange} forwardRef={topicRef} minHeight={174} resizable label="Topic (optional)" /> <Input name="topic" minHeight={174} resizable label="Topic (optional)" />
<div className="create-room__name-wrapper"> <div className="create-room__name-wrapper">
<Input value={titleValue} onChange={handleTitleChange} forwardRef={nameRef} label="Room name" required /> <Input name="name" label={`${isSpace ? 'Space' : 'Room'} name`} required />
<Button disabled={isValidAddress === false || isCreatingRoom} iconSrc={HashPlusIC} type="submit" variant="primary">Create</Button> <Button
disabled={isValidAddress === false || isCreatingRoom}
iconSrc={isSpace ? SpacePlusIC : HashPlusIC}
type="submit"
variant="primary"
>
Create
</Button>
</div> </div>
{isCreatingRoom && ( {isCreatingRoom && (
<div className="create-room__loading"> <div className="create-room__loading">
<Spinner size="small" /> <Spinner size="small" />
<Text>Creating room...</Text> <Text>{`Creating ${isSpace ? 'space' : 'room'}...`}</Text>
</div> </div>
)} )}
{typeof creatingError === 'string' && <Text className="create-room__error" variant="b3">{creatingError}</Text>} {typeof creatingError === 'string' && <Text className="create-room__error" variant="b3">{creatingError}</Text>}
</form> </form>
</div> </div>
</PopupWindow>
); );
} }
CreateRoomContent.defaultProps = {
CreateRoom.propTypes = { parentId: null,
isOpen: PropTypes.bool.isRequired, };
CreateRoomContent.propTypes = {
isSpace: PropTypes.bool.isRequired,
parentId: PropTypes.string,
onRequestClose: PropTypes.func.isRequired, 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; export default CreateRoom;

View file

@ -117,12 +117,7 @@ function InviteUser({
procUserError.delete(userId); procUserError.delete(userId);
updateUserProcError(getMapCopy(procUserError)); updateUserProcError(getMapCopy(procUserError));
const result = await roomActions.create({ const result = await roomActions.createDM(userId);
isPublic: false,
isEncrypted: true,
isDirect: true,
invite: [userId],
});
roomIdToUserId.set(result.room_id, userId); roomIdToUserId.set(result.room_id, userId);
updateRoomIdToUserId(getMapCopy(roomIdToUserId)); updateRoomIdToUserId(getMapCopy(roomIdToUserId));
} catch (e) { } catch (e) {

View file

@ -38,20 +38,20 @@ function HomeSpaceOptions({ spaceId, afterOptionSelect }) {
return ( return (
<> <>
<MenuHeader>Add rooms or spaces</MenuHeader> <MenuHeader>Add rooms or spaces</MenuHeader>
<MenuItem
iconSrc={HashPlusIC}
onClick={() => { afterOptionSelect(); openCreateRoom(); }}
disabled={!canManage}
>
Create new room
</MenuItem>
<MenuItem <MenuItem
iconSrc={SpacePlusIC} iconSrc={SpacePlusIC}
onClick={() => { afterOptionSelect(); }} onClick={() => { afterOptionSelect(); openCreateRoom(true, spaceId); }}
disabled={!canManage} disabled={!canManage}
> >
Create new space Create new space
</MenuItem> </MenuItem>
<MenuItem
iconSrc={HashPlusIC}
onClick={() => { afterOptionSelect(); openCreateRoom(false, spaceId); }}
disabled={!canManage}
>
Create new room
</MenuItem>
{ !spaceId && ( { !spaceId && (
<MenuItem <MenuItem
iconSrc={HashGlobeIC} iconSrc={HashGlobeIC}

View file

@ -200,11 +200,7 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
// Create new DM // Create new DM
try { try {
setIsCreatingDM(true); setIsCreatingDM(true);
await roomActions.create({ await roomActions.createDM(userId);
isEncrypted: true,
isDirect: true,
invite: [userId],
});
} catch { } catch {
if (isMountedRef.current === false) return; if (isMountedRef.current === false) return;
setIsCreatingDM(false); setIsCreatingDM(false);

View file

@ -5,6 +5,7 @@ import ProfileViewer from '../profile-viewer/ProfileViewer';
import SpaceAddExisting from '../../molecules/space-add-existing/SpaceAddExisting'; import SpaceAddExisting from '../../molecules/space-add-existing/SpaceAddExisting';
import Search from '../search/Search'; import Search from '../search/Search';
import ViewSource from '../view-source/ViewSource'; import ViewSource from '../view-source/ViewSource';
import CreateRoom from '../create-room/CreateRoom';
function Dialogs() { function Dialogs() {
return ( return (
@ -12,6 +13,7 @@ function Dialogs() {
<ReadReceipts /> <ReadReceipts />
<ViewSource /> <ViewSource />
<ProfileViewer /> <ProfileViewer />
<CreateRoom />
<SpaceAddExisting /> <SpaceAddExisting />
<Search /> <Search />
</> </>

View file

@ -5,7 +5,6 @@ import navigation from '../../../client/state/navigation';
import InviteList from '../invite-list/InviteList'; import InviteList from '../invite-list/InviteList';
import PublicRooms from '../public-rooms/PublicRooms'; import PublicRooms from '../public-rooms/PublicRooms';
import CreateRoom from '../create-room/CreateRoom';
import InviteUser from '../invite-user/InviteUser'; import InviteUser from '../invite-user/InviteUser';
import Settings from '../settings/Settings'; import Settings from '../settings/Settings';
import SpaceSettings from '../space-settings/SpaceSettings'; import SpaceSettings from '../space-settings/SpaceSettings';
@ -16,7 +15,6 @@ function Windows() {
const [publicRooms, changePublicRooms] = useState({ const [publicRooms, changePublicRooms] = useState({
isOpen: false, searchTerm: undefined, isOpen: false, searchTerm: undefined,
}); });
const [isCreateRoom, changeCreateRoom] = useState(false);
const [inviteUser, changeInviteUser] = useState({ const [inviteUser, changeInviteUser] = useState({
isOpen: false, roomId: undefined, term: undefined, isOpen: false, roomId: undefined, term: undefined,
}); });
@ -31,9 +29,6 @@ function Windows() {
searchTerm, searchTerm,
}); });
} }
function openCreateRoom() {
changeCreateRoom(true);
}
function openInviteUser(roomId, searchTerm) { function openInviteUser(roomId, searchTerm) {
changeInviteUser({ changeInviteUser({
isOpen: true, isOpen: true,
@ -48,13 +43,11 @@ function Windows() {
useEffect(() => { useEffect(() => {
navigation.on(cons.events.navigation.INVITE_LIST_OPENED, openInviteList); navigation.on(cons.events.navigation.INVITE_LIST_OPENED, openInviteList);
navigation.on(cons.events.navigation.PUBLIC_ROOMS_OPENED, openPublicRooms); 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.INVITE_USER_OPENED, openInviteUser);
navigation.on(cons.events.navigation.SETTINGS_OPENED, openSettings); navigation.on(cons.events.navigation.SETTINGS_OPENED, openSettings);
return () => { return () => {
navigation.removeListener(cons.events.navigation.INVITE_LIST_OPENED, openInviteList); navigation.removeListener(cons.events.navigation.INVITE_LIST_OPENED, openInviteList);
navigation.removeListener(cons.events.navigation.PUBLIC_ROOMS_OPENED, openPublicRooms); 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.INVITE_USER_OPENED, openInviteUser);
navigation.removeListener(cons.events.navigation.SETTINGS_OPENED, openSettings); navigation.removeListener(cons.events.navigation.SETTINGS_OPENED, openSettings);
}; };
@ -71,10 +64,6 @@ function Windows() {
searchTerm={publicRooms.searchTerm} searchTerm={publicRooms.searchTerm}
onRequestClose={() => changePublicRooms({ isOpen: false, searchTerm: undefined })} onRequestClose={() => changePublicRooms({ isOpen: false, searchTerm: undefined })}
/> />
<CreateRoom
isOpen={isCreateRoom}
onRequestClose={() => changeCreateRoom(false)}
/>
<InviteUser <InviteUser
isOpen={inviteUser.isOpen} isOpen={inviteUser.isOpen}
roomId={inviteUser.roomId} roomId={inviteUser.roomId}

View file

@ -65,9 +65,11 @@ export function openPublicRooms(searchTerm) {
}); });
} }
export function openCreateRoom() { export function openCreateRoom(isSpace = false, parentId = null) {
appDispatcher.dispatch({ appDispatcher.dispatch({
type: cons.actions.navigation.OPEN_CREATE_ROOM, type: cons.actions.navigation.OPEN_CREATE_ROOM,
isSpace,
parentId,
}); });
} }

View file

@ -1,6 +1,7 @@
import initMatrix from '../initMatrix'; import initMatrix from '../initMatrix';
import appDispatcher from '../dispatcher'; import appDispatcher from '../dispatcher';
import cons from '../state/cons'; import cons from '../state/cons';
import { getIdServer } from '../../util/matrixUtil';
/** /**
* https://github.com/matrix-org/matrix-react-sdk/blob/1e6c6e9d800890c732d60429449bc280de01a647/src/Rooms.js#L73 * https://github.com/matrix-org/matrix-react-sdk/blob/1e6c6e9d800890c732d60429449bc280de01a647/src/Rooms.js#L73
@ -125,36 +126,37 @@ function leave(roomId) {
}).catch(); }).catch();
} }
/** async function create(options, isDM = false) {
* Create a room.
* @param {Object} opts
* @param {string} [opts.name]
* @param {string} [opts.topic]
* @param {boolean} [opts.isPublic=false] Sets room visibility to public
* @param {string} [opts.roomAlias] Sets the room address
* @param {boolean} [opts.isEncrypted=false] Makes room encrypted
* @param {boolean} [opts.isDirect=false] Makes room as direct message
* @param {string[]} [opts.invite=[]] An array of userId's to invite
* @param{number} [opts.powerLevel=100] My power level
*/
async function create(opts) {
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const customPowerLevels = [101]; try {
const options = { const result = await mx.createRoom(options);
name: opts.name, if (isDM && typeof options.invite?.[0] === 'string') {
topic: opts.topic, await addRoomToMDirect(result.room_id, options.invite[0]);
visibility: opts.isPublic === true ? 'public' : 'private', }
room_alias_name: opts.roomAlias, appDispatcher.dispatch({
is_direct: opts.isDirect === true, type: cons.actions.room.CREATE,
invite: opts.invite || [], roomId: result.room_id,
initial_state: [], isDM,
preset: opts.isDirect === true ? 'trusted_private_chat' : undefined, });
power_level_content_override: customPowerLevels.indexOf(opts.powerLevel) === -1 ? undefined : { return result;
users: { [initMatrix.matrixClient.getUserId()]: opts.powerLevel }, } catch (e) {
}, const errcodes = ['M_UNKNOWN', 'M_BAD_JSON', 'M_ROOM_IN_USE', 'M_INVALID_ROOM_STATE', 'M_UNSUPPORTED_ROOM_VERSION'];
}; if (errcodes.includes(e.errcode)) {
throw new Error(e);
}
throw new Error('Something went wrong!');
}
}
if (opts.isPublic !== true && opts.isEncrypted === true) { async function createDM(userId, isEncrypted = true) {
const options = {
is_direct: true,
invite: [userId],
visibility: 'private',
preset: 'trusted_private_chat',
initial_state: [],
};
if (isEncrypted) {
options.initial_state.push({ options.initial_state.push({
type: 'm.room.encryption', type: 'm.room.encryption',
state_key: '', state_key: '',
@ -164,28 +166,87 @@ async function create(opts) {
}); });
} }
try { const result = await create(options, true);
const result = await mx.createRoom(options);
if (opts.isDirect === true && typeof opts.invite[0] !== 'undefined') {
await addRoomToMDirect(result.room_id, opts.invite[0]);
}
appDispatcher.dispatch({
type: cons.actions.room.CREATE,
roomId: result.room_id,
isDM: opts.isDirect === true,
});
return result; return result;
} catch (e) { }
const errcodes = ['M_UNKNOWN', 'M_BAD_JSON', 'M_ROOM_IN_USE', 'M_INVALID_ROOM_STATE', 'M_UNSUPPORTED_ROOM_VERSION'];
if (errcodes.find((errcode) => errcode === e.errcode)) { async function createRoom(opts) {
appDispatcher.dispatch({ // joinRule: 'public' | 'invite' | 'restricted'
type: cons.actions.room.error.CREATE, const { name, topic, joinRule } = opts;
error: e, 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',
},
}); });
throw new Error(e);
} }
throw new Error('Something went wrong!'); 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) { async function invite(roomId, userId) {
@ -242,7 +303,8 @@ function deleteSpaceShortcut(roomId) {
export { export {
join, leave, join, leave,
create, invite, kick, ban, unban, createDM, createRoom,
invite, kick, ban, unban,
setPowerLevel, setPowerLevel,
createSpaceShortcut, deleteSpaceShortcut, createSpaceShortcut, deleteSpaceShortcut,
}; };

View file

@ -53,9 +53,6 @@ const cons = {
CREATE: 'CREATE', CREATE: 'CREATE',
CREATE_SPACE_SHORTCUT: 'CREATE_SPACE_SHORTCUT', CREATE_SPACE_SHORTCUT: 'CREATE_SPACE_SHORTCUT',
DELETE_SPACE_SHORTCUT: 'DELETE_SPACE_SHORTCUT', DELETE_SPACE_SHORTCUT: 'DELETE_SPACE_SHORTCUT',
error: {
CREATE: 'ERROR_CREATE',
},
}, },
settings: { settings: {
TOGGLE_SYSTEM_THEME: 'TOGGLE_SYSTEM_THEME', TOGGLE_SYSTEM_THEME: 'TOGGLE_SYSTEM_THEME',

View file

@ -113,7 +113,11 @@ class Navigation extends EventEmitter {
this.emit(cons.events.navigation.PUBLIC_ROOMS_OPENED, action.searchTerm); this.emit(cons.events.navigation.PUBLIC_ROOMS_OPENED, action.searchTerm);
}, },
[cons.actions.navigation.OPEN_CREATE_ROOM]: () => { [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]: () => { [cons.actions.navigation.OPEN_INVITE_USER]: () => {
this.emit(cons.events.navigation.INVITE_USER_OPENED, action.roomId, action.searchTerm); this.emit(cons.events.navigation.INVITE_USER_OPENED, action.roomId, action.searchTerm);