Merge branch 'develop' into travis/message-states
This commit is contained in:
commit
017c2af663
47 changed files with 2360 additions and 143 deletions
|
@ -223,7 +223,14 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
let size;
|
||||
let collapsed;
|
||||
const collapseConfig: ICollapseConfig = {
|
||||
toggleSize: 260 - 50,
|
||||
// TODO: the space panel currently does not have a fixed width,
|
||||
// just the headers at each level have a max-width of 150px
|
||||
// Taking 222px for the space panel for now,
|
||||
// so this will look slightly off for now,
|
||||
// depending on the depth of your space tree.
|
||||
// To fix this, we'll need to turn toggleSize
|
||||
// into a callback so it can be measured when starting the resize operation
|
||||
toggleSize: 222 + 68,
|
||||
onCollapsed: (_collapsed) => {
|
||||
collapsed = _collapsed;
|
||||
if (_collapsed) {
|
||||
|
@ -244,6 +251,9 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
if (!collapsed) window.localStorage.setItem("mx_lhs_size", '' + size);
|
||||
this.props.resizeNotifier.stopResizing();
|
||||
},
|
||||
isItemCollapsed: domNode => {
|
||||
return domNode.classList.contains("mx_LeftPanel_minimized");
|
||||
},
|
||||
};
|
||||
const resizer = new Resizer(this._resizeContainer.current, CollapseDistributor, collapseConfig);
|
||||
resizer.setClassNames({
|
||||
|
|
|
@ -82,6 +82,8 @@ import {UIFeature} from "../../settings/UIFeature";
|
|||
import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore";
|
||||
import DialPadModal from "../views/voip/DialPadModal";
|
||||
import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast';
|
||||
import SpaceStore from "../../stores/SpaceStore";
|
||||
import SpaceRoomDirectory from "./SpaceRoomDirectory";
|
||||
|
||||
/** constants for MatrixChat.state.view */
|
||||
export enum Views {
|
||||
|
@ -691,10 +693,17 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
break;
|
||||
}
|
||||
case Action.ViewRoomDirectory: {
|
||||
const RoomDirectory = sdk.getComponent("structures.RoomDirectory");
|
||||
Modal.createTrackedDialog('Room directory', '', RoomDirectory, {
|
||||
initialText: payload.initialText,
|
||||
}, 'mx_RoomDirectory_dialogWrapper', false, true);
|
||||
if (SpaceStore.instance.activeSpace) {
|
||||
Modal.createTrackedDialog("Space room directory", "", SpaceRoomDirectory, {
|
||||
space: SpaceStore.instance.activeSpace,
|
||||
initialText: payload.initialText,
|
||||
}, "mx_SpaceRoomDirectory_dialogWrapper", false, true);
|
||||
} else {
|
||||
const RoomDirectory = sdk.getComponent("structures.RoomDirectory");
|
||||
Modal.createTrackedDialog('Room directory', '', RoomDirectory, {
|
||||
initialText: payload.initialText,
|
||||
}, 'mx_RoomDirectory_dialogWrapper', false, true);
|
||||
}
|
||||
|
||||
// View the welcome or home page if we need something to look at
|
||||
this.viewSomethingBehindModal();
|
||||
|
@ -1072,6 +1081,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
|
||||
private leaveRoomWarnings(roomId: string) {
|
||||
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
|
||||
const isSpace = roomToLeave?.isSpaceRoom();
|
||||
// Show a warning if there are additional complications.
|
||||
const joinRules = roomToLeave.currentState.getStateEvents('m.room.join_rules', '');
|
||||
const warnings = [];
|
||||
|
@ -1081,7 +1091,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
warnings.push((
|
||||
<span className="warning" key="non_public_warning">
|
||||
{' '/* Whitespace, otherwise the sentences get smashed together */ }
|
||||
{ _t("This room is not public. You will not be able to rejoin without an invite.") }
|
||||
{ isSpace
|
||||
? _t("This space is not public. You will not be able to rejoin without an invite.")
|
||||
: _t("This room is not public. You will not be able to rejoin without an invite.") }
|
||||
</span>
|
||||
));
|
||||
}
|
||||
|
@ -1094,11 +1106,14 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
|
||||
const warnings = this.leaveRoomWarnings(roomId);
|
||||
|
||||
Modal.createTrackedDialog('Leave room', '', QuestionDialog, {
|
||||
title: _t("Leave room"),
|
||||
const isSpace = roomToLeave?.isSpaceRoom();
|
||||
Modal.createTrackedDialog(isSpace ? "Leave space" : "Leave room", '', QuestionDialog, {
|
||||
title: isSpace ? _t("Leave space") : _t("Leave room"),
|
||||
description: (
|
||||
<span>
|
||||
{ _t("Are you sure you want to leave the room '%(roomName)s'?", {roomName: roomToLeave.name}) }
|
||||
{ isSpace
|
||||
? _t("Are you sure you want to leave the space '%(spaceName)s'?", {spaceName: roomToLeave.name})
|
||||
: _t("Are you sure you want to leave the room '%(roomName)s'?", {roomName: roomToLeave.name}) }
|
||||
{ warnings }
|
||||
</span>
|
||||
),
|
||||
|
@ -1112,6 +1127,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
|
||||
|
||||
d.finally(() => modal.close());
|
||||
dis.dispatch({
|
||||
action: "after_leave_room",
|
||||
room_id: roomId,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -192,6 +192,7 @@ export interface IState {
|
|||
rejecting?: boolean;
|
||||
rejectError?: Error;
|
||||
hasPinnedWidgets?: boolean;
|
||||
dragCounter: number;
|
||||
}
|
||||
|
||||
export default class RoomView extends React.Component<IProps, IState> {
|
||||
|
@ -242,6 +243,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
canReply: false,
|
||||
layout: SettingsStore.getValue("layout"),
|
||||
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
|
||||
dragCounter: 0,
|
||||
};
|
||||
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
|
@ -535,8 +537,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
if (!roomView.ondrop) {
|
||||
roomView.addEventListener('drop', this.onDrop);
|
||||
roomView.addEventListener('dragover', this.onDragOver);
|
||||
roomView.addEventListener('dragleave', this.onDragLeaveOrEnd);
|
||||
roomView.addEventListener('dragend', this.onDragLeaveOrEnd);
|
||||
roomView.addEventListener('dragenter', this.onDragEnter);
|
||||
roomView.addEventListener('dragleave', this.onDragLeave);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -580,8 +582,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
const roomView = this.roomView.current;
|
||||
roomView.removeEventListener('drop', this.onDrop);
|
||||
roomView.removeEventListener('dragover', this.onDragOver);
|
||||
roomView.removeEventListener('dragleave', this.onDragLeaveOrEnd);
|
||||
roomView.removeEventListener('dragend', this.onDragLeaveOrEnd);
|
||||
roomView.removeEventListener('dragenter', this.onDragEnter);
|
||||
roomView.removeEventListener('dragleave', this.onDragLeave);
|
||||
}
|
||||
dis.unregister(this.dispatcherRef);
|
||||
if (this.context) {
|
||||
|
@ -1141,6 +1143,31 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
this.updateTopUnreadMessagesBar();
|
||||
};
|
||||
|
||||
private onDragEnter = ev => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
|
||||
this.setState({
|
||||
dragCounter: this.state.dragCounter + 1,
|
||||
draggingFile: true,
|
||||
});
|
||||
};
|
||||
|
||||
private onDragLeave = ev => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
|
||||
this.setState({
|
||||
dragCounter: this.state.dragCounter - 1,
|
||||
});
|
||||
|
||||
if (this.state.dragCounter === 0) {
|
||||
this.setState({
|
||||
draggingFile: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private onDragOver = ev => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
|
@ -1148,7 +1175,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
ev.dataTransfer.dropEffect = 'none';
|
||||
|
||||
if (ev.dataTransfer.types.includes("Files") || ev.dataTransfer.types.includes("application/x-moz-file")) {
|
||||
this.setState({ draggingFile: true });
|
||||
ev.dataTransfer.dropEffect = 'copy';
|
||||
}
|
||||
};
|
||||
|
@ -1159,14 +1185,12 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
ContentMessages.sharedInstance().sendContentListToRoom(
|
||||
ev.dataTransfer.files, this.state.room.roomId, this.context,
|
||||
);
|
||||
this.setState({ draggingFile: false });
|
||||
dis.fire(Action.FocusComposer);
|
||||
};
|
||||
|
||||
private onDragLeaveOrEnd = ev => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.setState({ draggingFile: false });
|
||||
this.setState({
|
||||
draggingFile: false,
|
||||
dragCounter: this.state.dragCounter - 1,
|
||||
});
|
||||
};
|
||||
|
||||
private injectSticker(url, info, text) {
|
||||
|
@ -1768,6 +1792,19 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
let fileDropTarget = null;
|
||||
if (this.state.draggingFile) {
|
||||
fileDropTarget = (
|
||||
<div className="mx_RoomView_fileDropTarget">
|
||||
<img
|
||||
src={require("../../../res/img/upload-big.svg")}
|
||||
className="mx_RoomView_fileDropTarget_image"
|
||||
/>
|
||||
{ _t("Drop file here to upload") }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// We have successfully loaded this room, and are not previewing.
|
||||
// Display the "normal" room view.
|
||||
|
||||
|
@ -1891,7 +1928,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
room={this.state.room}
|
||||
fullHeight={false}
|
||||
userId={this.context.credentials.userId}
|
||||
draggingFile={this.state.draggingFile}
|
||||
maxHeight={this.state.auxPanelMaxHeight}
|
||||
showApps={this.state.showApps}
|
||||
onResize={this.onResize}
|
||||
|
@ -2060,6 +2096,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
<div className="mx_RoomView_body">
|
||||
{auxPanel}
|
||||
<div className={timelineClasses}>
|
||||
{fileDropTarget}
|
||||
{topUnreadMessagesBar}
|
||||
{jumpToBottom}
|
||||
{messagePanel}
|
||||
|
|
576
src/components/structures/SpaceRoomDirectory.tsx
Normal file
576
src/components/structures/SpaceRoomDirectory.tsx
Normal file
|
@ -0,0 +1,576 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useMemo, useRef, useState} from "react";
|
||||
import Room from "matrix-js-sdk/src/models/room";
|
||||
import MatrixEvent from "matrix-js-sdk/src/models/event";
|
||||
import {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import {_t} from "../../languageHandler";
|
||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import BaseDialog from "../views/dialogs/BaseDialog";
|
||||
import FormButton from "../views/elements/FormButton";
|
||||
import SearchBox from "./SearchBox";
|
||||
import RoomAvatar from "../views/avatars/RoomAvatar";
|
||||
import RoomName from "../views/elements/RoomName";
|
||||
import {useAsyncMemo} from "../../hooks/useAsyncMemo";
|
||||
import {shouldShowSpaceSettings} from "../../utils/space";
|
||||
import {EnhancedMap} from "../../utils/maps";
|
||||
import StyledCheckbox from "../views/elements/StyledCheckbox";
|
||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||
import BaseAvatar from "../views/avatars/BaseAvatar";
|
||||
|
||||
interface IProps {
|
||||
space: Room;
|
||||
initialText?: string;
|
||||
onFinished(): void;
|
||||
}
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
export interface ISpaceSummaryRoom {
|
||||
canonical_alias?: string;
|
||||
aliases: string[];
|
||||
avatar_url?: string;
|
||||
guest_can_join: boolean;
|
||||
name?: string;
|
||||
num_joined_members: number
|
||||
room_id: string;
|
||||
topic?: string;
|
||||
world_readable: boolean;
|
||||
num_refs: number;
|
||||
room_type: string;
|
||||
}
|
||||
|
||||
export interface ISpaceSummaryEvent {
|
||||
room_id: string;
|
||||
event_id: string;
|
||||
origin_server_ts: number;
|
||||
type: string;
|
||||
state_key: string;
|
||||
content: {
|
||||
order?: string;
|
||||
auto_join?: boolean;
|
||||
via?: string;
|
||||
};
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
interface ISubspaceProps {
|
||||
space: ISpaceSummaryRoom;
|
||||
event?: MatrixEvent;
|
||||
editing?: boolean;
|
||||
onPreviewClick?(): void;
|
||||
queueAction?(action: IAction): void;
|
||||
onJoinClick?(): void;
|
||||
}
|
||||
|
||||
const SubSpace: React.FC<ISubspaceProps> = ({
|
||||
space,
|
||||
editing,
|
||||
event,
|
||||
queueAction,
|
||||
onJoinClick,
|
||||
onPreviewClick,
|
||||
children,
|
||||
}) => {
|
||||
const name = space.name || space.canonical_alias || space.aliases?.[0] || _t("Unnamed Space");
|
||||
|
||||
const evContent = event?.getContent();
|
||||
const [autoJoin, _setAutoJoin] = useState(evContent?.auto_join);
|
||||
const [removed, _setRemoved] = useState(!evContent?.via);
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
const cliRoom = cli.getRoom(space.room_id);
|
||||
const myMembership = cliRoom?.getMyMembership();
|
||||
|
||||
// TODO DRY code
|
||||
let actions;
|
||||
if (editing && queueAction) {
|
||||
if (event && cli.getRoom(event.getRoomId())?.currentState.maySendStateEvent(event.getType(), cli.getUserId())) {
|
||||
const setAutoJoin = () => {
|
||||
_setAutoJoin(v => {
|
||||
queueAction({
|
||||
event,
|
||||
removed,
|
||||
autoJoin: !v,
|
||||
});
|
||||
return !v;
|
||||
});
|
||||
};
|
||||
|
||||
const setRemoved = () => {
|
||||
_setRemoved(v => {
|
||||
queueAction({
|
||||
event,
|
||||
removed: !v,
|
||||
autoJoin,
|
||||
});
|
||||
return !v;
|
||||
});
|
||||
};
|
||||
|
||||
if (removed) {
|
||||
actions = <React.Fragment>
|
||||
<FormButton kind="danger" onClick={setRemoved} label={_t("Undo")} />
|
||||
</React.Fragment>;
|
||||
} else {
|
||||
actions = <React.Fragment>
|
||||
<FormButton kind="danger" onClick={setRemoved} label={_t("Remove from Space")} />
|
||||
<StyledCheckbox checked={autoJoin} onChange={setAutoJoin} />
|
||||
</React.Fragment>;
|
||||
}
|
||||
} else {
|
||||
actions = <span className="mx_SpaceRoomDirectory_actionsText">
|
||||
{ _t("No permissions")}
|
||||
</span>;
|
||||
}
|
||||
// TODO confirm remove from space click behaviour here
|
||||
} else {
|
||||
if (myMembership === "join") {
|
||||
actions = <span className="mx_SpaceRoomDirectory_actionsText">
|
||||
{ _t("You're in this space")}
|
||||
</span>;
|
||||
} else if (onJoinClick) {
|
||||
actions = <React.Fragment>
|
||||
<AccessibleButton onClick={onPreviewClick} kind="link">
|
||||
{ _t("Preview") }
|
||||
</AccessibleButton>
|
||||
<FormButton onClick={onJoinClick} label={_t("Join")} />
|
||||
</React.Fragment>
|
||||
}
|
||||
}
|
||||
|
||||
let url: string;
|
||||
if (space.avatar_url) {
|
||||
url = MatrixClientPeg.get().mxcUrlToHttp(
|
||||
space.avatar_url,
|
||||
Math.floor(24 * window.devicePixelRatio),
|
||||
Math.floor(24 * window.devicePixelRatio),
|
||||
"crop",
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="mx_SpaceRoomDirectory_subspace">
|
||||
<div className="mx_SpaceRoomDirectory_subspace_info">
|
||||
<BaseAvatar name={name} idName={space.room_id} url={url} width={24} height={24} />
|
||||
{ name }
|
||||
|
||||
<div className="mx_SpaceRoomDirectory_actions">
|
||||
{ actions }
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_SpaceRoomDirectory_subspace_children">
|
||||
{ children }
|
||||
</div>
|
||||
</div>
|
||||
};
|
||||
|
||||
interface IAction {
|
||||
event: MatrixEvent;
|
||||
removed: boolean;
|
||||
autoJoin: boolean;
|
||||
}
|
||||
|
||||
interface IRoomTileProps {
|
||||
room: ISpaceSummaryRoom;
|
||||
event?: MatrixEvent;
|
||||
editing?: boolean;
|
||||
onPreviewClick(): void;
|
||||
queueAction?(action: IAction): void;
|
||||
onJoinClick?(): void;
|
||||
}
|
||||
|
||||
const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinClick }: IRoomTileProps) => {
|
||||
const name = room.name || room.canonical_alias || room.aliases?.[0] || _t("Unnamed Room");
|
||||
|
||||
const evContent = event?.getContent();
|
||||
const [autoJoin, _setAutoJoin] = useState(evContent?.auto_join);
|
||||
const [removed, _setRemoved] = useState(!evContent?.via);
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
const cliRoom = cli.getRoom(room.room_id);
|
||||
const myMembership = cliRoom?.getMyMembership();
|
||||
|
||||
let actions;
|
||||
if (editing && queueAction) {
|
||||
if (event && cli.getRoom(event.getRoomId())?.currentState.maySendStateEvent(event.getType(), cli.getUserId())) {
|
||||
const setAutoJoin = () => {
|
||||
_setAutoJoin(v => {
|
||||
queueAction({
|
||||
event,
|
||||
removed,
|
||||
autoJoin: !v,
|
||||
});
|
||||
return !v;
|
||||
});
|
||||
};
|
||||
|
||||
const setRemoved = () => {
|
||||
_setRemoved(v => {
|
||||
queueAction({
|
||||
event,
|
||||
removed: !v,
|
||||
autoJoin,
|
||||
});
|
||||
return !v;
|
||||
});
|
||||
};
|
||||
|
||||
if (removed) {
|
||||
actions = <React.Fragment>
|
||||
<FormButton kind="danger" onClick={setRemoved} label={_t("Undo")} />
|
||||
</React.Fragment>;
|
||||
} else {
|
||||
actions = <React.Fragment>
|
||||
<FormButton kind="danger" onClick={setRemoved} label={_t("Remove from Space")} />
|
||||
<StyledCheckbox checked={autoJoin} onChange={setAutoJoin} />
|
||||
</React.Fragment>;
|
||||
}
|
||||
} else {
|
||||
actions = <span className="mx_SpaceRoomDirectory_actionsText">
|
||||
{ _t("No permissions")}
|
||||
</span>;
|
||||
}
|
||||
// TODO confirm remove from space click behaviour here
|
||||
} else {
|
||||
if (myMembership === "join") {
|
||||
actions = <span className="mx_SpaceRoomDirectory_actionsText">
|
||||
{ _t("You're in this room")}
|
||||
</span>;
|
||||
} else if (onJoinClick) {
|
||||
actions = <React.Fragment>
|
||||
<AccessibleButton onClick={onPreviewClick} kind="link">
|
||||
{ _t("Preview") }
|
||||
</AccessibleButton>
|
||||
<FormButton onClick={onJoinClick} label={_t("Join")} />
|
||||
</React.Fragment>
|
||||
}
|
||||
}
|
||||
|
||||
let url: string;
|
||||
if (room.avatar_url) {
|
||||
url = cli.mxcUrlToHttp(
|
||||
room.avatar_url,
|
||||
Math.floor(32 * window.devicePixelRatio),
|
||||
Math.floor(32 * window.devicePixelRatio),
|
||||
"crop",
|
||||
);
|
||||
}
|
||||
|
||||
const content = <React.Fragment>
|
||||
<BaseAvatar name={name} idName={room.room_id} url={url} width={32} height={32} />
|
||||
|
||||
<div className="mx_SpaceRoomDirectory_roomTile_info">
|
||||
<div className="mx_SpaceRoomDirectory_roomTile_name">
|
||||
{ name }
|
||||
</div>
|
||||
<div className="mx_SpaceRoomDirectory_roomTile_topic">
|
||||
{ room.topic }
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_SpaceRoomDirectory_roomTile_memberCount">
|
||||
{ room.num_joined_members }
|
||||
</div>
|
||||
|
||||
<div className="mx_SpaceRoomDirectory_actions">
|
||||
{ actions }
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
|
||||
if (editing) {
|
||||
return <div className="mx_SpaceRoomDirectory_roomTile">
|
||||
{ content }
|
||||
</div>
|
||||
}
|
||||
|
||||
return <AccessibleButton className="mx_SpaceRoomDirectory_roomTile" onClick={onPreviewClick}>
|
||||
{ content }
|
||||
</AccessibleButton>;
|
||||
};
|
||||
|
||||
export const showRoom = (room: ISpaceSummaryRoom, viaServers?: string[], autoJoin = false) => {
|
||||
// Don't let the user view a room they won't be able to either peek or join:
|
||||
// fail earlier so they don't have to click back to the directory.
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
if (!room.world_readable && !room.guest_can_join) {
|
||||
dis.dispatch({ action: "require_registration" });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const roomAlias = getDisplayAliasForRoom(room) || undefined;
|
||||
dis.dispatch({
|
||||
action: "view_room",
|
||||
auto_join: autoJoin,
|
||||
should_peek: true,
|
||||
_type: "room_directory", // instrumentation
|
||||
room_alias: roomAlias,
|
||||
room_id: room.room_id,
|
||||
via_servers: viaServers,
|
||||
oob_data: {
|
||||
avatarUrl: room.avatar_url,
|
||||
// XXX: This logic is duplicated from the JS SDK which would normally decide what the name is.
|
||||
name: room.name || roomAlias || _t("Unnamed room"),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
interface IHierarchyLevelProps {
|
||||
spaceId: string;
|
||||
rooms: Map<string, ISpaceSummaryRoom>;
|
||||
editing?: boolean;
|
||||
relations: EnhancedMap<string, string[]>;
|
||||
parents: Set<string>;
|
||||
queueAction?(action: IAction): void;
|
||||
onPreviewClick(roomId: string): void;
|
||||
onRemoveFromSpaceClick?(roomId: string): void;
|
||||
onJoinClick?(roomId: string): void;
|
||||
}
|
||||
|
||||
export const HierarchyLevel = ({
|
||||
spaceId,
|
||||
rooms,
|
||||
editing,
|
||||
relations,
|
||||
parents,
|
||||
onPreviewClick,
|
||||
onJoinClick,
|
||||
queueAction,
|
||||
}: IHierarchyLevelProps) => {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const space = cli.getRoom(spaceId);
|
||||
// TODO respect order
|
||||
const [subspaces, childRooms] = relations.get(spaceId)?.reduce((result, roomId: string) => {
|
||||
if (!rooms.has(roomId)) return result; // TODO wat
|
||||
result[rooms.get(roomId).room_type === RoomType.Space ? 0 : 1].push(roomId);
|
||||
return result;
|
||||
}, [[], []]) || [[], []];
|
||||
|
||||
// Don't render this subspace if it has no rooms we can show
|
||||
// TODO this is broken - as a space may have subspaces we still need to show
|
||||
// if (!childRooms.length) return null;
|
||||
|
||||
const userId = cli.getUserId();
|
||||
|
||||
const newParents = new Set(parents).add(spaceId);
|
||||
return <React.Fragment>
|
||||
{
|
||||
childRooms.map(roomId => (
|
||||
<RoomTile
|
||||
key={roomId}
|
||||
room={rooms.get(roomId)}
|
||||
event={space?.currentState.maySendStateEvent(EventType.SpaceChild, userId)
|
||||
? space?.currentState.getStateEvents(EventType.SpaceChild, roomId)
|
||||
: undefined}
|
||||
editing={editing}
|
||||
queueAction={queueAction}
|
||||
onPreviewClick={() => {
|
||||
onPreviewClick(roomId);
|
||||
}}
|
||||
onJoinClick={onJoinClick ? () => {
|
||||
onJoinClick(roomId);
|
||||
} : undefined}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
{
|
||||
subspaces.filter(roomId => !newParents.has(roomId)).map(roomId => (
|
||||
<SubSpace
|
||||
key={roomId}
|
||||
space={rooms.get(roomId)}
|
||||
event={space?.currentState.getStateEvents(EventType.SpaceChild, roomId)}
|
||||
editing={editing}
|
||||
queueAction={queueAction}
|
||||
onPreviewClick={() => {
|
||||
onPreviewClick(roomId);
|
||||
}}
|
||||
onJoinClick={() => {
|
||||
onJoinClick(roomId);
|
||||
}}
|
||||
>
|
||||
<HierarchyLevel
|
||||
spaceId={roomId}
|
||||
rooms={rooms}
|
||||
editing={editing}
|
||||
relations={relations}
|
||||
parents={newParents}
|
||||
onPreviewClick={onPreviewClick}
|
||||
onJoinClick={onJoinClick}
|
||||
queueAction={queueAction}
|
||||
/>
|
||||
</SubSpace>
|
||||
))
|
||||
}
|
||||
</React.Fragment>
|
||||
};
|
||||
|
||||
const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinished }) => {
|
||||
// TODO pagination
|
||||
const cli = MatrixClientPeg.get();
|
||||
const [query, setQuery] = useState(initialText);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
const onCreateRoomClick = () => {
|
||||
dis.dispatch({
|
||||
action: 'view_create_room',
|
||||
public: true,
|
||||
});
|
||||
onFinished();
|
||||
};
|
||||
|
||||
// stored within a ref as we don't need to re-render when it changes
|
||||
const pendingActions = useRef(new Map<string, IAction>());
|
||||
|
||||
let adminButton;
|
||||
if (shouldShowSpaceSettings(cli, space)) { // TODO this is an imperfect test
|
||||
const onManageButtonClicked = () => {
|
||||
setIsEditing(true);
|
||||
};
|
||||
|
||||
const onSaveButtonClicked = () => {
|
||||
// TODO setBusy
|
||||
pendingActions.current.forEach(({event, autoJoin, removed}) => {
|
||||
const content = {
|
||||
...event.getContent(),
|
||||
auto_join: autoJoin,
|
||||
};
|
||||
|
||||
if (removed) {
|
||||
delete content["via"];
|
||||
}
|
||||
|
||||
cli.sendStateEvent(event.getRoomId(), event.getType(), content, event.getStateKey());
|
||||
});
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
if (isEditing) {
|
||||
adminButton = <React.Fragment>
|
||||
<FormButton label={_t("Save changes")} onClick={onSaveButtonClicked} />
|
||||
<span>{ _t("All users join by default") }</span>
|
||||
</React.Fragment>;
|
||||
} else {
|
||||
adminButton = <FormButton label={_t("Manage rooms")} onClick={onManageButtonClicked} />;
|
||||
}
|
||||
}
|
||||
|
||||
const [rooms, relations, viaMap] = useAsyncMemo(async () => {
|
||||
try {
|
||||
const data = await cli.getSpaceSummary(space.roomId);
|
||||
|
||||
const parentChildRelations = new EnhancedMap<string, string[]>();
|
||||
const viaMap = new EnhancedMap<string, Set<string>>();
|
||||
data.events.map((ev: ISpaceSummaryEvent) => {
|
||||
if (ev.type === EventType.SpaceChild) {
|
||||
parentChildRelations.getOrCreate(ev.room_id, []).push(ev.state_key);
|
||||
}
|
||||
if (Array.isArray(ev.content["via"])) {
|
||||
const set = viaMap.getOrCreate(ev.state_key, new Set());
|
||||
ev.content["via"].forEach(via => set.add(via));
|
||||
}
|
||||
});
|
||||
|
||||
return [data.rooms, parentChildRelations, viaMap];
|
||||
} catch (e) {
|
||||
console.error(e); // TODO
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [space], []);
|
||||
|
||||
const roomsMap = useMemo(() => {
|
||||
if (!rooms) return null;
|
||||
const lcQuery = query.toLowerCase();
|
||||
|
||||
const filteredRooms = rooms.filter(r => {
|
||||
return r.room_type === RoomType.Space // always include spaces to allow filtering of sub-space rooms
|
||||
|| r.name?.toLowerCase().includes(lcQuery)
|
||||
|| r.topic?.toLowerCase().includes(lcQuery);
|
||||
});
|
||||
|
||||
return new Map<string, ISpaceSummaryRoom>(filteredRooms.map(r => [r.room_id, r]));
|
||||
// const root = rooms.get(space.roomId);
|
||||
}, [rooms, query]);
|
||||
|
||||
const title = <React.Fragment>
|
||||
<RoomAvatar room={space} height={40} width={40} />
|
||||
<div>
|
||||
<h1>{ _t("Explore rooms") }</h1>
|
||||
<div><RoomName room={space} /></div>
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
const explanation =
|
||||
_t("If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.", null,
|
||||
{a: sub => {
|
||||
return <AccessibleButton kind="link" onClick={onCreateRoomClick}>{sub}</AccessibleButton>;
|
||||
}},
|
||||
);
|
||||
|
||||
let content;
|
||||
if (roomsMap) {
|
||||
content = <AutoHideScrollbar className="mx_SpaceRoomDirectory_list">
|
||||
<HierarchyLevel
|
||||
spaceId={space.roomId}
|
||||
rooms={roomsMap}
|
||||
editing={isEditing}
|
||||
relations={relations}
|
||||
parents={new Set()}
|
||||
queueAction={action => {
|
||||
pendingActions.current.set(action.event.room_id, action);
|
||||
}}
|
||||
onPreviewClick={roomId => {
|
||||
showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), false);
|
||||
onFinished();
|
||||
}}
|
||||
onJoinClick={(roomId) => {
|
||||
showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), true);
|
||||
onFinished();
|
||||
}}
|
||||
/>
|
||||
</AutoHideScrollbar>;
|
||||
}
|
||||
|
||||
// TODO loading state/error state
|
||||
return (
|
||||
<BaseDialog className="mx_SpaceRoomDirectory" hasCancel={true} onFinished={onFinished} title={title}>
|
||||
<div className="mx_Dialog_content">
|
||||
{ explanation }
|
||||
|
||||
<SearchBox
|
||||
className="mx_textinput_icon mx_textinput_search"
|
||||
placeholder={ _t("Find a room...") }
|
||||
onSearch={setQuery}
|
||||
/>
|
||||
|
||||
<div className="mx_SpaceRoomDirectory_listHeader">
|
||||
{ adminButton }
|
||||
</div>
|
||||
{ content }
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpaceRoomDirectory;
|
||||
|
||||
// Similar to matrix-react-sdk's MatrixTools.getDisplayAliasForRoom
|
||||
// but works with the objects we get from the public room list
|
||||
function getDisplayAliasForRoom(room: ISpaceSummaryRoom) {
|
||||
return room.canonical_alias || (room.aliases ? room.aliases[0] : "");
|
||||
}
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, {RefObject, useContext, useRef, useState} from "react";
|
||||
import {EventType} from "matrix-js-sdk/src/@types/event";
|
||||
import {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
|
@ -24,8 +24,9 @@ import {_t} from "../../languageHandler";
|
|||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import RoomName from "../views/elements/RoomName";
|
||||
import RoomTopic from "../views/elements/RoomTopic";
|
||||
import InlineSpinner from "../views/elements/InlineSpinner";
|
||||
import FormButton from "../views/elements/FormButton";
|
||||
import {inviteMultipleToRoom, showSpaceInviteDialog} from "../../RoomInvite";
|
||||
import {inviteMultipleToRoom, showRoomInviteDialog} from "../../RoomInvite";
|
||||
import {useRoomMembers} from "../../hooks/useRoomMembers";
|
||||
import createRoom, {IOpts, Preset} from "../../createRoom";
|
||||
import Field from "../views/elements/Field";
|
||||
|
@ -46,8 +47,13 @@ import {RightPanelPhases} from "../../stores/RightPanelStorePhases";
|
|||
import {SetRightPanelPhasePayload} from "../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||
import {useStateArray} from "../../hooks/useStateArray";
|
||||
import SpacePublicShare from "../views/spaces/SpacePublicShare";
|
||||
import {shouldShowSpaceSettings} from "../../utils/space";
|
||||
import {showAddExistingRooms, showCreateNewRoom, shouldShowSpaceSettings, showSpaceSettings} from "../../utils/space";
|
||||
import {HierarchyLevel, ISpaceSummaryEvent, ISpaceSummaryRoom, showRoom} from "./SpaceRoomDirectory";
|
||||
import {useAsyncMemo} from "../../hooks/useAsyncMemo";
|
||||
import {EnhancedMap} from "../../utils/maps";
|
||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||
import MemberAvatar from "../views/avatars/MemberAvatar";
|
||||
import {useStateToggle} from "../../hooks/useStateToggle";
|
||||
|
||||
interface IProps {
|
||||
space: Room;
|
||||
|
@ -108,6 +114,94 @@ const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
|
|||
</div>;
|
||||
}
|
||||
|
||||
let inviteButton;
|
||||
if (myMembership === "join" && space.canInvite(userId)) {
|
||||
inviteButton = (
|
||||
<AccessibleButton className="mx_SpaceRoomView_landing_inviteButton" onClick={() => {
|
||||
showRoomInviteDialog(space.roomId);
|
||||
}}>
|
||||
{ _t("Invite people") }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
const canAddRooms = myMembership === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId);
|
||||
|
||||
const [_, forceUpdate] = useStateToggle(false); // TODO
|
||||
|
||||
let addRoomButtons;
|
||||
if (canAddRooms) {
|
||||
addRoomButtons = <React.Fragment>
|
||||
<AccessibleButton className="mx_SpaceRoomView_landing_addButton" onClick={async () => {
|
||||
const [added] = await showAddExistingRooms(cli, space);
|
||||
if (added) {
|
||||
forceUpdate();
|
||||
}
|
||||
}}>
|
||||
{ _t("Add existing rooms & spaces") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className="mx_SpaceRoomView_landing_createButton" onClick={() => {
|
||||
showCreateNewRoom(cli, space);
|
||||
}}>
|
||||
{ _t("Create a new room") }
|
||||
</AccessibleButton>
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
let settingsButton;
|
||||
if (shouldShowSpaceSettings(cli, space)) {
|
||||
settingsButton = <AccessibleButton className="mx_SpaceRoomView_landing_settingsButton" onClick={() => {
|
||||
showSpaceSettings(cli, space);
|
||||
}}>
|
||||
{ _t("Settings") }
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
const [loading, roomsMap, relations, numRooms] = useAsyncMemo(async () => {
|
||||
try {
|
||||
const data = await cli.getSpaceSummary(space.roomId, undefined, myMembership !== "join");
|
||||
|
||||
const parentChildRelations = new EnhancedMap<string, string[]>();
|
||||
data.events.map((ev: ISpaceSummaryEvent) => {
|
||||
if (ev.type === EventType.SpaceChild) {
|
||||
parentChildRelations.getOrCreate(ev.room_id, []).push(ev.state_key);
|
||||
}
|
||||
});
|
||||
|
||||
const roomsMap = new Map<string, ISpaceSummaryRoom>(data.rooms.map(r => [r.room_id, r]));
|
||||
const numRooms = data.rooms.filter(r => r.room_type !== RoomType.Space).length;
|
||||
return [false, roomsMap, parentChildRelations, numRooms];
|
||||
} catch (e) {
|
||||
console.error(e); // TODO
|
||||
}
|
||||
|
||||
return [false];
|
||||
}, [space, _], [true]);
|
||||
|
||||
let previewRooms;
|
||||
if (roomsMap) {
|
||||
previewRooms = <AutoHideScrollbar className="mx_SpaceRoomDirectory_list">
|
||||
<div className="mx_SpaceRoomDirectory_roomCount">
|
||||
<h3>{ myMembership === "join" ? _t("Rooms") : _t("Default Rooms")}</h3>
|
||||
<span>{ numRooms }</span>
|
||||
</div>
|
||||
<HierarchyLevel
|
||||
spaceId={space.roomId}
|
||||
rooms={roomsMap}
|
||||
editing={false}
|
||||
relations={relations}
|
||||
parents={new Set()}
|
||||
onPreviewClick={roomId => {
|
||||
showRoom(roomsMap.get(roomId), [], false); // TODO
|
||||
}}
|
||||
/>
|
||||
</AutoHideScrollbar>;
|
||||
} else if (loading) {
|
||||
previewRooms = <InlineSpinner />;
|
||||
} else {
|
||||
previewRooms = <p>{_t("Your server does not support showing space hierarchies.")}</p>;
|
||||
}
|
||||
|
||||
return <div className="mx_SpaceRoomView_landing">
|
||||
<RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} />
|
||||
<div className="mx_SpaceRoomView_landing_name">
|
||||
|
@ -167,6 +261,13 @@ const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
|
|||
<RoomTopic room={space} />
|
||||
</div>
|
||||
{ joinButtons }
|
||||
<div className="mx_SpaceRoomView_landing_adminButtons">
|
||||
{ inviteButton }
|
||||
{ addRoomButtons }
|
||||
{ settingsButton }
|
||||
</div>
|
||||
|
||||
{ previewRooms }
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
@ -361,7 +462,7 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
|
|||
<div className="mx_SpaceRoomView_inviteTeammates_buttons">
|
||||
<AccessibleButton
|
||||
className="mx_SpaceRoomView_inviteTeammates_inviteDialogButton"
|
||||
onClick={() => showSpaceInviteDialog(space.roomId)}
|
||||
onClick={() => showRoomInviteDialog(space.roomId)}
|
||||
>
|
||||
{ _t("Invite by username") }
|
||||
</AccessibleButton>
|
||||
|
|
208
src/components/views/dialogs/AddExistingToSpaceDialog.tsx
Normal file
208
src/components/views/dialogs/AddExistingToSpaceDialog.tsx
Normal file
|
@ -0,0 +1,208 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useState} from "react";
|
||||
import classNames from "classnames";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||
|
||||
import {_t} from '../../../languageHandler';
|
||||
import {IDialogProps} from "./IDialogProps";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import FormButton from "../elements/FormButton";
|
||||
import Dropdown from "../elements/Dropdown";
|
||||
import SearchBox from "../../structures/SearchBox";
|
||||
import SpaceStore from "../../../stores/SpaceStore";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import {getDisplayAliasForRoom} from "../../../Rooms";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
import {allSettled} from "../../../utils/promise";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import {calculateRoomVia} from "../../../utils/permalinks/Permalinks";
|
||||
import StyledCheckbox from "../elements/StyledCheckbox";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
matrixClient: MatrixClient;
|
||||
space: Room;
|
||||
onCreateRoomClick(cli: MatrixClient, space: Room): void;
|
||||
}
|
||||
|
||||
const Entry = ({ room, checked, onChange }) => {
|
||||
return <div className="mx_AddExistingToSpaceDialog_entry">
|
||||
<RoomAvatar room={room} height={32} width={32} />
|
||||
<span className="mx_AddExistingToSpaceDialog_entry_name">{ room.name }</span>
|
||||
<StyledCheckbox onChange={(e) => onChange(e.target.checked)} checked={checked} />
|
||||
</div>;
|
||||
};
|
||||
|
||||
const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => {
|
||||
const [query, setQuery] = useState("");
|
||||
const lcQuery = query.toLowerCase();
|
||||
|
||||
const [selectedSpace, setSelectedSpace] = useState(space);
|
||||
const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>());
|
||||
|
||||
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
|
||||
const existingSubspacesSet = new Set(existingSubspaces);
|
||||
const spaces = SpaceStore.instance.getSpaces().filter(s => {
|
||||
return !existingSubspacesSet.has(s) // not already in space
|
||||
&& space !== s // not the top-level space
|
||||
&& selectedSpace !== s // not the selected space
|
||||
&& s.name.toLowerCase().includes(lcQuery); // contains query
|
||||
});
|
||||
|
||||
const existingRooms = SpaceStore.instance.getChildRooms(space.roomId);
|
||||
const existingRoomsSet = new Set(existingRooms);
|
||||
const rooms = cli.getVisibleRooms().filter(room => {
|
||||
return !existingRoomsSet.has(room) // not already in space
|
||||
&& room.name.toLowerCase().includes(lcQuery) // contains query
|
||||
&& !DMRoomMap.shared().getUserIdForRoomId(room.roomId); // not a DM
|
||||
});
|
||||
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
let spaceOptionSection;
|
||||
if (existingSubspacesSet.size > 0) {
|
||||
const options = [space, ...existingSubspaces].map((space) => {
|
||||
const classes = classNames("mx_AddExistingToSpaceDialog_dropdownOption", {
|
||||
mx_AddExistingToSpaceDialog_dropdownOptionActive: space === selectedSpace,
|
||||
});
|
||||
return <div key={space.roomId} className={classes}>
|
||||
<RoomAvatar room={space} width={24} height={24} />
|
||||
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
|
||||
</div>;
|
||||
});
|
||||
|
||||
spaceOptionSection = (
|
||||
<Dropdown
|
||||
id="mx_SpaceSelectDropdown"
|
||||
onOptionChange={(key: string) => {
|
||||
setSelectedSpace(existingSubspaces.find(space => space.roomId === key) || space);
|
||||
}}
|
||||
value={selectedSpace.roomId}
|
||||
label={_t("Space selection")}
|
||||
>
|
||||
{ options }
|
||||
</Dropdown>
|
||||
);
|
||||
} else {
|
||||
spaceOptionSection = <div className="mx_AddExistingToSpaceDialog_onlySpace">
|
||||
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
|
||||
</div>;
|
||||
}
|
||||
|
||||
const title = <React.Fragment>
|
||||
<RoomAvatar room={selectedSpace} height={40} width={40} />
|
||||
<div>
|
||||
<h1>{ _t("Add existing spaces/rooms") }</h1>
|
||||
{ spaceOptionSection }
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
|
||||
return <BaseDialog
|
||||
title={title}
|
||||
className="mx_AddExistingToSpaceDialog"
|
||||
contentId="mx_AddExistingToSpaceDialog"
|
||||
onFinished={onFinished}
|
||||
fixedWidth={false}
|
||||
>
|
||||
{ error && <div className="mx_AddExistingToSpaceDialog_errorText">{ error }</div> }
|
||||
|
||||
<SearchBox
|
||||
className="mx_textinput_icon mx_textinput_search"
|
||||
placeholder={ _t("Filter your rooms and spaces") }
|
||||
onSearch={setQuery}
|
||||
/>
|
||||
<AutoHideScrollbar className="mx_AddExistingToSpaceDialog_content" id="mx_AddExistingToSpaceDialog">
|
||||
{ spaces.length > 0 ? (
|
||||
<div className="mx_AddExistingToSpaceDialog_section mx_AddExistingToSpaceDialog_section_spaces">
|
||||
<h3>{ _t("Spaces") }</h3>
|
||||
{ spaces.map(space => {
|
||||
return <Entry
|
||||
key={space.roomId}
|
||||
room={space}
|
||||
checked={selectedToAdd.has(space)}
|
||||
onChange={(checked) => {
|
||||
if (checked) {
|
||||
selectedToAdd.add(space);
|
||||
} else {
|
||||
selectedToAdd.delete(space);
|
||||
}
|
||||
setSelectedToAdd(new Set(selectedToAdd));
|
||||
}}
|
||||
/>;
|
||||
}) }
|
||||
</div>
|
||||
) : null }
|
||||
|
||||
{ rooms.length > 0 ? (
|
||||
<div className="mx_AddExistingToSpaceDialog_section">
|
||||
<h3>{ _t("Rooms") }</h3>
|
||||
{ rooms.map(room => {
|
||||
return <Entry
|
||||
key={room.roomId}
|
||||
room={room}
|
||||
checked={selectedToAdd.has(room)}
|
||||
onChange={(checked) => {
|
||||
if (checked) {
|
||||
selectedToAdd.add(room);
|
||||
} else {
|
||||
selectedToAdd.delete(room);
|
||||
}
|
||||
setSelectedToAdd(new Set(selectedToAdd));
|
||||
}}
|
||||
/>;
|
||||
}) }
|
||||
</div>
|
||||
) : undefined }
|
||||
|
||||
{ spaces.length + rooms.length < 1 ? <span className="mx_AddExistingToSpaceDialog_noResults">
|
||||
{ _t("No results") }
|
||||
</span> : undefined }
|
||||
</AutoHideScrollbar>
|
||||
|
||||
<div className="mx_AddExistingToSpaceDialog_footer">
|
||||
<span>
|
||||
<div>{ _t("Don't want to add an existing room?") }</div>
|
||||
<AccessibleButton onClick={() => onCreateRoomClick(cli, space)} kind="link">
|
||||
{ _t("Create a new room") }
|
||||
</AccessibleButton>
|
||||
</span>
|
||||
|
||||
<FormButton
|
||||
label={busy ? _t("Applying...") : _t("Apply")}
|
||||
disabled={busy || selectedToAdd.size < 1}
|
||||
onClick={async () => {
|
||||
setBusy(true);
|
||||
try {
|
||||
await allSettled(Array.from(selectedToAdd).map((room) =>
|
||||
SpaceStore.instance.addRoomToSpace(space, room.roomId, calculateRoomVia(room))));
|
||||
onFinished(true);
|
||||
} catch (e) {
|
||||
console.error("Failed to add rooms to space", e);
|
||||
setError(_t("Failed to add rooms to space"));
|
||||
}
|
||||
setBusy(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</BaseDialog>;
|
||||
};
|
||||
|
||||
export default AddExistingToSpaceDialog;
|
||||
|
|
@ -17,6 +17,8 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import withValidation from '../elements/Validation';
|
||||
|
@ -30,6 +32,7 @@ export default class CreateRoomDialog extends React.Component {
|
|||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
defaultPublic: PropTypes.bool,
|
||||
parentSpace: PropTypes.instanceOf(Room),
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
|
@ -85,6 +88,10 @@ export default class CreateRoomDialog extends React.Component {
|
|||
opts.associatedWithCommunity = CommunityPrototypeStore.instance.getSelectedCommunityId();
|
||||
}
|
||||
|
||||
if (this.props.parentSpace) {
|
||||
opts.parentSpace = this.props.parentSpace;
|
||||
}
|
||||
|
||||
return opts;
|
||||
}
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ export default class InfoDialog extends React.Component {
|
|||
className: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
description: PropTypes.node,
|
||||
button: PropTypes.string,
|
||||
button: PropTypes.oneOfType(PropTypes.string, PropTypes.bool),
|
||||
onFinished: PropTypes.func,
|
||||
hasCloseButton: PropTypes.bool,
|
||||
onKeyDown: PropTypes.func,
|
||||
|
@ -60,11 +60,11 @@ export default class InfoDialog extends React.Component {
|
|||
<div className={classNames("mx_Dialog_content", this.props.className)} id="mx_Dialog_content">
|
||||
{ this.props.description }
|
||||
</div>
|
||||
<DialogButtons primaryButton={this.props.button || _t('OK')}
|
||||
{ this.props.button !== false && <DialogButtons primaryButton={this.props.button || _t('OK')}
|
||||
onPrimaryButtonClick={this.onFinished}
|
||||
hasCancel={false}
|
||||
>
|
||||
</DialogButtons>
|
||||
</DialogButtons> }
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
|
|
162
src/components/views/dialogs/SpaceSettingsDialog.tsx
Normal file
162
src/components/views/dialogs/SpaceSettingsDialog.tsx
Normal file
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useState} from 'react';
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||
import {EventType} from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
import {_t} from '../../../languageHandler';
|
||||
import {IDialogProps} from "./IDialogProps";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DevtoolsDialog from "./DevtoolsDialog";
|
||||
import SpaceBasicSettings from '../spaces/SpaceBasicSettings';
|
||||
import {getTopic} from "../elements/RoomTopic";
|
||||
import {avatarUrlForRoom} from "../../../Avatar";
|
||||
import ToggleSwitch from "../elements/ToggleSwitch";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import FormButton from "../elements/FormButton";
|
||||
import Modal from "../../../Modal";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import {allSettled} from "../../../utils/promise";
|
||||
import {useDispatcher} from "../../../hooks/useDispatcher";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
matrixClient: MatrixClient;
|
||||
space: Room;
|
||||
}
|
||||
|
||||
const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFinished }) => {
|
||||
useDispatcher(defaultDispatcher, ({action, ...params}) => {
|
||||
if (action === "after_leave_room" && params.room_id === space.roomId) {
|
||||
onFinished(false);
|
||||
}
|
||||
});
|
||||
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const userId = cli.getUserId();
|
||||
|
||||
const [newAvatar, setNewAvatar] = useState<File>(null); // undefined means to remove avatar
|
||||
const canSetAvatar = space.currentState.maySendStateEvent(EventType.RoomAvatar, userId);
|
||||
const avatarChanged = newAvatar !== null;
|
||||
|
||||
const [name, setName] = useState<string>(space.name);
|
||||
const canSetName = space.currentState.maySendStateEvent(EventType.RoomName, userId);
|
||||
const nameChanged = name !== space.name;
|
||||
|
||||
const currentTopic = getTopic(space);
|
||||
const [topic, setTopic] = useState<string>(currentTopic);
|
||||
const canSetTopic = space.currentState.maySendStateEvent(EventType.RoomTopic, userId);
|
||||
const topicChanged = topic !== currentTopic;
|
||||
|
||||
const currentJoinRule = space.getJoinRule();
|
||||
const [joinRule, setJoinRule] = useState(currentJoinRule);
|
||||
const canSetJoinRule = space.currentState.maySendStateEvent(EventType.RoomJoinRules, userId);
|
||||
const joinRuleChanged = joinRule !== currentJoinRule;
|
||||
|
||||
const onSave = async () => {
|
||||
setBusy(true);
|
||||
const promises = [];
|
||||
|
||||
if (avatarChanged) {
|
||||
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {
|
||||
url: await cli.uploadContent(newAvatar),
|
||||
}, ""));
|
||||
}
|
||||
|
||||
if (nameChanged) {
|
||||
promises.push(cli.setRoomName(space.roomId, name));
|
||||
}
|
||||
|
||||
if (topicChanged) {
|
||||
promises.push(cli.setRoomTopic(space.roomId, topic));
|
||||
}
|
||||
|
||||
if (joinRuleChanged) {
|
||||
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomJoinRules, { join_rule: joinRule }, ""));
|
||||
}
|
||||
|
||||
const results = await allSettled(promises);
|
||||
setBusy(false);
|
||||
const failures = results.filter(r => r.status === "rejected");
|
||||
if (failures.length > 0) {
|
||||
console.error("Failed to save space settings: ", failures);
|
||||
setError(_t("Failed to save space settings."));
|
||||
}
|
||||
};
|
||||
|
||||
return <BaseDialog
|
||||
title={_t("Space settings")}
|
||||
className="mx_SpaceSettingsDialog"
|
||||
contentId="mx_SpaceSettingsDialog"
|
||||
onFinished={onFinished}
|
||||
fixedWidth={false}
|
||||
>
|
||||
<div className="mx_SpaceSettingsDialog_content" id="mx_SpaceSettingsDialog">
|
||||
<div>{ _t("Edit settings relating to your space.") }</div>
|
||||
|
||||
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
|
||||
|
||||
<SpaceBasicSettings
|
||||
avatarUrl={avatarUrlForRoom(space, 80, 80, "crop")}
|
||||
avatarDisabled={!canSetAvatar}
|
||||
setAvatar={setNewAvatar}
|
||||
name={name}
|
||||
nameDisabled={!canSetName}
|
||||
setName={setName}
|
||||
topic={topic}
|
||||
topicDisabled={!canSetTopic}
|
||||
setTopic={setTopic}
|
||||
/>
|
||||
|
||||
<div>
|
||||
{ _t("Make this space private") }
|
||||
<ToggleSwitch
|
||||
checked={joinRule === "private"}
|
||||
onChange={checked => setJoinRule(checked ? "private" : "invite")}
|
||||
disabled={!canSetJoinRule}
|
||||
aria-label={_t("Make this space private")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormButton
|
||||
kind="danger"
|
||||
label={_t("Leave Space")}
|
||||
onClick={() => {
|
||||
defaultDispatcher.dispatch({
|
||||
action: "leave_room",
|
||||
room_id: space.roomId,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="mx_SpaceSettingsDialog_buttons">
|
||||
<AccessibleButton onClick={() => Modal.createDialog(DevtoolsDialog, {roomId: space.roomId})}>
|
||||
{ _t("View dev tools") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton onClick={onFinished} disabled={busy} kind="link">
|
||||
{ _t("Cancel") }
|
||||
</AccessibleButton>
|
||||
<FormButton onClick={onSave} disabled={busy} label={busy ? _t("Saving...") : _t("Save Changes")} />
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>;
|
||||
};
|
||||
|
||||
export default SpaceSettingsDialog;
|
||||
|
|
@ -60,7 +60,9 @@ import QuestionDialog from "../dialogs/QuestionDialog";
|
|||
import ConfirmUserActionDialog from "../dialogs/ConfirmUserActionDialog";
|
||||
import InfoDialog from "../dialogs/InfoDialog";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||
import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import RoomName from "../elements/RoomName";
|
||||
|
||||
interface IDevice {
|
||||
deviceId: string;
|
||||
|
@ -302,7 +304,8 @@ const UserOptionsSection: React.FC<{
|
|||
member: RoomMember;
|
||||
isIgnored: boolean;
|
||||
canInvite: boolean;
|
||||
}> = ({member, isIgnored, canInvite}) => {
|
||||
isSpace?: boolean;
|
||||
}> = ({member, isIgnored, canInvite, isSpace}) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
let ignoreButton = null;
|
||||
|
@ -342,7 +345,7 @@ const UserOptionsSection: React.FC<{
|
|||
</AccessibleButton>
|
||||
);
|
||||
|
||||
if (member.roomId) {
|
||||
if (member.roomId && !isSpace) {
|
||||
const onReadReceiptButton = function() {
|
||||
const room = cli.getRoom(member.roomId);
|
||||
dis.dispatch({
|
||||
|
@ -434,14 +437,18 @@ const UserOptionsSection: React.FC<{
|
|||
);
|
||||
};
|
||||
|
||||
const warnSelfDemote = async () => {
|
||||
const warnSelfDemote = async (isSpace) => {
|
||||
const {finished} = Modal.createTrackedDialog('Demoting Self', '', QuestionDialog, {
|
||||
title: _t("Demote yourself?"),
|
||||
description:
|
||||
<div>
|
||||
{ _t("You will not be able to undo this change as you are demoting yourself, " +
|
||||
"if you are the last privileged user in the room it will be impossible " +
|
||||
"to regain privileges.") }
|
||||
{ isSpace
|
||||
? _t("You will not be able to undo this change as you are demoting yourself, " +
|
||||
"if you are the last privileged user in the space it will be impossible " +
|
||||
"to regain privileges.")
|
||||
: _t("You will not be able to undo this change as you are demoting yourself, " +
|
||||
"if you are the last privileged user in the room it will be impossible " +
|
||||
"to regain privileges.") }
|
||||
</div>,
|
||||
button: _t("Demote"),
|
||||
});
|
||||
|
@ -717,7 +724,7 @@ const MuteToggleButton: React.FC<IBaseRoomProps> = ({member, room, powerLevels,
|
|||
// if muting self, warn as it may be irreversible
|
||||
if (target === cli.getUserId()) {
|
||||
try {
|
||||
if (!(await warnSelfDemote())) return;
|
||||
if (!(await warnSelfDemote(room?.isSpaceRoom()))) return;
|
||||
} catch (e) {
|
||||
console.error("Failed to warn about self demotion: ", e);
|
||||
return;
|
||||
|
@ -806,7 +813,7 @@ const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({
|
|||
if (canAffectUser && me.powerLevel >= kickPowerLevel) {
|
||||
kickButton = <RoomKickButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />;
|
||||
}
|
||||
if (me.powerLevel >= redactPowerLevel) {
|
||||
if (me.powerLevel >= redactPowerLevel && !room.isSpaceRoom()) {
|
||||
redactButton = (
|
||||
<RedactMessagesButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />
|
||||
);
|
||||
|
@ -1085,7 +1092,7 @@ const PowerLevelEditor: React.FC<{
|
|||
} else if (myUserId === target) {
|
||||
// If we are changing our own PL it can only ever be decreasing, which we cannot reverse.
|
||||
try {
|
||||
if (!(await warnSelfDemote())) return;
|
||||
if (!(await warnSelfDemote(room?.isSpaceRoom()))) return;
|
||||
} catch (e) {
|
||||
console.error("Failed to warn about self demotion: ", e);
|
||||
}
|
||||
|
@ -1315,12 +1322,10 @@ const BasicUserInfo: React.FC<{
|
|||
if (!isRoomEncrypted) {
|
||||
if (!cryptoEnabled) {
|
||||
text = _t("This client does not support end-to-end encryption.");
|
||||
} else if (room) {
|
||||
} else if (room && !room.isSpaceRoom()) {
|
||||
text = _t("Messages in this room are not end-to-end encrypted.");
|
||||
} else {
|
||||
// TODO what to render for GroupMember
|
||||
}
|
||||
} else {
|
||||
} else if (!room.isSpaceRoom()) {
|
||||
text = _t("Messages in this room are end-to-end encrypted.");
|
||||
}
|
||||
|
||||
|
@ -1381,7 +1386,9 @@ const BasicUserInfo: React.FC<{
|
|||
<UserOptionsSection
|
||||
canInvite={roomPermissions.canInvite}
|
||||
isIgnored={isIgnored}
|
||||
member={member} />
|
||||
member={member}
|
||||
isSpace={room?.isSpaceRoom()}
|
||||
/>
|
||||
|
||||
{ adminToolsContainer }
|
||||
|
||||
|
@ -1498,7 +1505,7 @@ interface IProps {
|
|||
user: Member;
|
||||
groupId?: string;
|
||||
room?: Room;
|
||||
phase: RightPanelPhases.RoomMemberInfo | RightPanelPhases.GroupMemberInfo;
|
||||
phase: RightPanelPhases.RoomMemberInfo | RightPanelPhases.GroupMemberInfo | RightPanelPhases.SpaceMemberInfo;
|
||||
onClose(): void;
|
||||
}
|
||||
|
||||
|
@ -1542,7 +1549,9 @@ const UserInfo: React.FC<Props> = ({
|
|||
previousPhase = RightPanelPhases.RoomMemberInfo;
|
||||
refireParams = {member: member};
|
||||
} else if (room) {
|
||||
previousPhase = RightPanelPhases.RoomMemberList;
|
||||
previousPhase = previousPhase = room.isSpaceRoom()
|
||||
? RightPanelPhases.SpaceMemberList
|
||||
: RightPanelPhases.RoomMemberList;
|
||||
}
|
||||
|
||||
const onEncryptionPanelClose = () => {
|
||||
|
@ -1557,6 +1566,7 @@ const UserInfo: React.FC<Props> = ({
|
|||
switch (phase) {
|
||||
case RightPanelPhases.RoomMemberInfo:
|
||||
case RightPanelPhases.GroupMemberInfo:
|
||||
case RightPanelPhases.SpaceMemberInfo:
|
||||
content = (
|
||||
<BasicUserInfo
|
||||
room={room}
|
||||
|
@ -1587,7 +1597,18 @@ const UserInfo: React.FC<Props> = ({
|
|||
}
|
||||
}
|
||||
|
||||
const header = <UserInfoHeader member={member} e2eStatus={e2eStatus} />;
|
||||
let scopeHeader;
|
||||
if (room?.isSpaceRoom()) {
|
||||
scopeHeader = <div className="mx_RightPanel_scopeHeader">
|
||||
<RoomAvatar room={room} height={32} width={32} />
|
||||
<RoomName room={room} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
const header = <React.Fragment>
|
||||
{ scopeHeader }
|
||||
<UserInfoHeader member={member} e2eStatus={e2eStatus} />
|
||||
</React.Fragment>;
|
||||
return <BaseCard
|
||||
className={classes.join(" ")}
|
||||
header={header}
|
||||
|
|
|
@ -100,10 +100,11 @@ export default class RoomProfileSettings extends React.Component {
|
|||
const newState = {};
|
||||
|
||||
// TODO: What do we do about errors?
|
||||
|
||||
const displayName = this.state.displayName.trim();
|
||||
if (this.state.originalDisplayName !== this.state.displayName) {
|
||||
await client.setRoomName(this.props.roomId, this.state.displayName);
|
||||
newState.originalDisplayName = this.state.displayName;
|
||||
await client.setRoomName(this.props.roomId, displayName);
|
||||
newState.originalDisplayName = displayName;
|
||||
newState.displayName = displayName;
|
||||
}
|
||||
|
||||
if (this.state.avatarFile) {
|
||||
|
|
|
@ -17,10 +17,8 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import { Room } from 'matrix-js-sdk/src/models/room'
|
||||
import * as sdk from '../../../index';
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import AppsDrawer from './AppsDrawer';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import classNames from 'classnames';
|
||||
import RateLimitedFunc from '../../../ratelimitedfunc';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
@ -36,9 +34,6 @@ interface IProps {
|
|||
userId: string,
|
||||
showApps: boolean, // Render apps
|
||||
|
||||
// set to true to show the file drop target
|
||||
draggingFile: boolean,
|
||||
|
||||
// maxHeight attribute for the aux panel and the video
|
||||
// therein
|
||||
maxHeight: number,
|
||||
|
@ -149,21 +144,6 @@ export default class AuxPanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
let fileDropTarget = null;
|
||||
if (this.props.draggingFile) {
|
||||
fileDropTarget = (
|
||||
<div className="mx_RoomView_fileDropTarget">
|
||||
<div className="mx_RoomView_fileDropTargetLabel" title={_t("Drop File Here")}>
|
||||
<TintableSvg src={require("../../../../res/img/upload-big.svg")} width="45" height="59" />
|
||||
<br />
|
||||
{ _t("Drop file here to upload") }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const callView = (
|
||||
<CallViewForRoom
|
||||
roomId={this.props.room.roomId}
|
||||
|
@ -246,7 +226,6 @@ export default class AuxPanel extends React.Component<IProps, IState> {
|
|||
<AutoHideScrollbar className={classes} style={style} >
|
||||
{ stateViews }
|
||||
{ appsDrawer }
|
||||
{ fileDropTarget }
|
||||
{ callView }
|
||||
{ this.props.children }
|
||||
</AutoHideScrollbar>
|
||||
|
|
|
@ -27,6 +27,8 @@ import * as sdk from "../../../index";
|
|||
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
|
||||
import BaseCard from "../right_panel/BaseCard";
|
||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import RoomName from "../elements/RoomName";
|
||||
|
||||
const INITIAL_LOAD_NUM_MEMBERS = 30;
|
||||
const INITIAL_LOAD_NUM_INVITED = 5;
|
||||
|
@ -456,6 +458,8 @@ export default class MemberList extends React.Component {
|
|||
const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat();
|
||||
if (chat && chat.roomId === this.props.roomId) {
|
||||
inviteButtonText = _t("Invite to this community");
|
||||
} else if (room.isSpaceRoom()) {
|
||||
inviteButtonText = _t("Invite to this space");
|
||||
}
|
||||
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
@ -483,12 +487,26 @@ export default class MemberList extends React.Component {
|
|||
onSearch={ this.onSearchQueryChanged } />
|
||||
);
|
||||
|
||||
let previousPhase = RightPanelPhases.RoomSummary;
|
||||
// We have no previousPhase for when viewing a MemberList from a Space
|
||||
let scopeHeader;
|
||||
if (room?.isSpaceRoom()) {
|
||||
previousPhase = undefined;
|
||||
scopeHeader = <div className="mx_RightPanel_scopeHeader">
|
||||
<RoomAvatar room={room} height={32} width={32} />
|
||||
<RoomName room={room} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
return <BaseCard
|
||||
className="mx_MemberList"
|
||||
header={inviteButton}
|
||||
header={<React.Fragment>
|
||||
{ scopeHeader }
|
||||
{ inviteButton }
|
||||
</React.Fragment>}
|
||||
footer={footer}
|
||||
onClose={this.props.onClose}
|
||||
previousPhase={RightPanelPhases.RoomSummary}
|
||||
previousPhase={previousPhase}
|
||||
>
|
||||
<div className="mx_MemberList_wrapper">
|
||||
<TruncatedList className="mx_MemberList_section mx_MemberList_joined" truncateAt={this.state.truncateAtJoined}
|
||||
|
|
|
@ -47,6 +47,9 @@ import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../con
|
|||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||
import CallHandler from "../../../CallHandler";
|
||||
import SpaceStore from "../../../stores/SpaceStore";
|
||||
import { showAddExistingRooms, showCreateNewRoom } from "../../../utils/space";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
interface IProps {
|
||||
onKeyDown: (ev: React.KeyboardEvent) => void;
|
||||
|
@ -152,6 +155,50 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
|
|||
defaultHidden: false,
|
||||
addRoomLabel: _td("Add room"),
|
||||
addRoomContextMenu: (onFinished: () => void) => {
|
||||
if (SpaceStore.instance.activeSpace) {
|
||||
const canAddRooms = SpaceStore.instance.activeSpace.currentState.maySendStateEvent(EventType.SpaceChild,
|
||||
MatrixClientPeg.get().getUserId());
|
||||
|
||||
return <IconizedContextMenuOptionList first>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Create new room")}
|
||||
iconClassName="mx_RoomList_iconPlus"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onFinished();
|
||||
showCreateNewRoom(MatrixClientPeg.get(), SpaceStore.instance.activeSpace);
|
||||
}}
|
||||
disabled={!canAddRooms}
|
||||
tooltip={canAddRooms ? undefined
|
||||
: _t("You do not have permissions to create new rooms in this space")}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Add existing room")}
|
||||
iconClassName="mx_RoomList_iconHash"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onFinished();
|
||||
showAddExistingRooms(MatrixClientPeg.get(), SpaceStore.instance.activeSpace);
|
||||
}}
|
||||
disabled={!canAddRooms}
|
||||
tooltip={canAddRooms ? undefined
|
||||
: _t("You do not have permissions to add rooms to this space")}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Explore space rooms")}
|
||||
iconClassName="mx_RoomList_iconExplore"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onFinished();
|
||||
defaultDispatcher.fire(Action.ViewRoomDirectory);
|
||||
}}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>;
|
||||
}
|
||||
|
||||
return <IconizedContextMenuOptionList first>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Create new room")}
|
||||
|
|
|
@ -404,7 +404,9 @@ export default class SendMessageComposer extends React.Component {
|
|||
this._editorRef.clearUndoHistory();
|
||||
this._editorRef.focus();
|
||||
this._clearStoredEditorState();
|
||||
dis.dispatch({action: "scroll_to_bottom"});
|
||||
if (SettingsStore.getValue("scrollToBottomOnMessageSent")) {
|
||||
dis.dispatch({action: "scroll_to_bottom"});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
|
|
@ -23,6 +23,8 @@ import dis from "../../../dispatcher/dispatcher";
|
|||
import * as sdk from "../../../index";
|
||||
import Modal from "../../../Modal";
|
||||
import {isValid3pidInvite} from "../../../RoomInvite";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import RoomName from "../elements/RoomName";
|
||||
|
||||
export default class ThirdPartyMemberInfo extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -32,14 +34,14 @@ export default class ThirdPartyMemberInfo extends React.Component {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.event.getRoomId());
|
||||
const me = room.getMember(MatrixClientPeg.get().getUserId());
|
||||
const powerLevels = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
this.room = MatrixClientPeg.get().getRoom(this.props.event.getRoomId());
|
||||
const me = this.room.getMember(MatrixClientPeg.get().getUserId());
|
||||
const powerLevels = this.room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
|
||||
let kickLevel = powerLevels ? powerLevels.getContent().kick : 50;
|
||||
if (typeof(kickLevel) !== 'number') kickLevel = 50;
|
||||
|
||||
const sender = room.getMember(this.props.event.getSender());
|
||||
const sender = this.room.getMember(this.props.event.getSender());
|
||||
|
||||
this.state = {
|
||||
stateKey: this.props.event.getStateKey(),
|
||||
|
@ -119,9 +121,18 @@ export default class ThirdPartyMemberInfo extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
let scopeHeader;
|
||||
if (this.room.isSpaceRoom()) {
|
||||
scopeHeader = <div className="mx_RightPanel_scopeHeader">
|
||||
<RoomAvatar room={this.room} height={32} width={32} />
|
||||
<RoomName room={this.room} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
// We shamelessly rip off the MemberInfo styles here.
|
||||
return (
|
||||
<div className="mx_MemberInfo" role="tabpanel">
|
||||
{ scopeHeader }
|
||||
<div className="mx_MemberInfo_name">
|
||||
<AccessibleButton className="mx_MemberInfo_cancel"
|
||||
onClick={this.onCancel}
|
||||
|
|
|
@ -81,10 +81,12 @@ export default class ProfileSettings extends React.Component {
|
|||
const client = MatrixClientPeg.get();
|
||||
const newState = {};
|
||||
|
||||
const displayName = this.state.displayName.trim();
|
||||
try {
|
||||
if (this.state.originalDisplayName !== this.state.displayName) {
|
||||
await client.setDisplayName(this.state.displayName);
|
||||
newState.originalDisplayName = this.state.displayName;
|
||||
await client.setDisplayName(displayName);
|
||||
newState.originalDisplayName = displayName;
|
||||
newState.displayName = displayName;
|
||||
}
|
||||
|
||||
if (this.state.avatarFile) {
|
||||
|
|
|
@ -48,6 +48,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
|
|||
'showRedactions',
|
||||
'enableSyntaxHighlightLanguageDetection',
|
||||
'expandCodeByDefault',
|
||||
'scrollToBottomOnMessageSent',
|
||||
'showCodeLineNumbers',
|
||||
'showJoinLeaves',
|
||||
'showAvatarChanges',
|
||||
|
|
|
@ -22,7 +22,7 @@ import AccessibleButton from "../elements/AccessibleButton";
|
|||
import {copyPlaintext} from "../../../utils/strings";
|
||||
import {sleep} from "../../../utils/promise";
|
||||
import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
|
||||
import {showSpaceInviteDialog} from "../../../RoomInvite";
|
||||
import {showRoomInviteDialog} from "../../../RoomInvite";
|
||||
|
||||
interface IProps {
|
||||
space: Room;
|
||||
|
@ -53,7 +53,7 @@ const SpacePublicShare = ({ space, onFinished }: IProps) => {
|
|||
<AccessibleButton
|
||||
className="mx_SpacePublicShare_inviteButton"
|
||||
onClick={() => {
|
||||
showSpaceInviteDialog(space.roomId);
|
||||
showRoomInviteDialog(space.roomId);
|
||||
onFinished();
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -23,7 +23,27 @@ import SpaceStore from "../../../stores/SpaceStore";
|
|||
import NotificationBadge from "../rooms/NotificationBadge";
|
||||
import {RovingAccessibleButton} from "../../../accessibility/roving/RovingAccessibleButton";
|
||||
import {RovingAccessibleTooltipButton} from "../../../accessibility/roving/RovingAccessibleTooltipButton";
|
||||
import IconizedContextMenu, {
|
||||
IconizedContextMenuOption,
|
||||
IconizedContextMenuOptionList,
|
||||
} from "../context_menus/IconizedContextMenu";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {ContextMenuTooltipButton} from "../../../accessibility/context_menu/ContextMenuTooltipButton";
|
||||
import {toRightOf} from "../../structures/ContextMenu";
|
||||
import {shouldShowSpaceSettings, showCreateNewRoom, showSpaceSettings} from "../../../utils/space";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {ButtonEvent} from "../elements/AccessibleButton";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import Modal from "../../../Modal";
|
||||
import SpacePublicShare from "./SpacePublicShare";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import RoomViewStore from "../../../stores/RoomViewStore";
|
||||
import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||
import {showRoomInviteDialog} from "../../../RoomInvite";
|
||||
import InfoDialog from "../dialogs/InfoDialog";
|
||||
import {EventType} from "matrix-js-sdk/src/@types/event";
|
||||
import SpaceRoomDirectory from "../../structures/SpaceRoomDirectory";
|
||||
|
||||
interface IItemProps {
|
||||
space?: Room;
|
||||
|
@ -78,6 +98,200 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
|||
SpaceStore.instance.setActiveSpace(this.props.space);
|
||||
};
|
||||
|
||||
private onMenuOpenClick = (ev: React.MouseEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const target = ev.target as HTMLButtonElement;
|
||||
this.setState({contextMenuPosition: target.getBoundingClientRect()});
|
||||
};
|
||||
|
||||
private onMenuClose = () => {
|
||||
this.setState({contextMenuPosition: null});
|
||||
};
|
||||
|
||||
private onHomeClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
defaultDispatcher.dispatch({
|
||||
action: "view_room",
|
||||
room_id: this.props.space.roomId,
|
||||
});
|
||||
this.setState({contextMenuPosition: null}); // also close the menu
|
||||
};
|
||||
|
||||
private onInviteClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
if (this.props.space.getJoinRule() === "public") {
|
||||
const modal = Modal.createTrackedDialog("Space Invite", "User Menu", InfoDialog, {
|
||||
title: _t("Invite members"),
|
||||
description: <React.Fragment>
|
||||
<span>{ _t("Share your public space") }</span>
|
||||
<SpacePublicShare space={this.props.space} onFinished={() => modal.close()} />
|
||||
</React.Fragment>,
|
||||
fixedWidth: false,
|
||||
button: false,
|
||||
className: "mx_SpacePanel_sharePublicSpace",
|
||||
hasCloseButton: true,
|
||||
});
|
||||
} else {
|
||||
showRoomInviteDialog(this.props.space.roomId);
|
||||
}
|
||||
this.setState({contextMenuPosition: null}); // also close the menu
|
||||
};
|
||||
|
||||
private onSettingsClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
showSpaceSettings(this.context, this.props.space);
|
||||
this.setState({contextMenuPosition: null}); // also close the menu
|
||||
};
|
||||
|
||||
private onLeaveClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
defaultDispatcher.dispatch({
|
||||
action: "leave_room",
|
||||
room_id: this.props.space.roomId,
|
||||
});
|
||||
this.setState({contextMenuPosition: null}); // also close the menu
|
||||
};
|
||||
|
||||
private onNewRoomClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
showCreateNewRoom(this.context, this.props.space);
|
||||
this.setState({contextMenuPosition: null}); // also close the menu
|
||||
};
|
||||
|
||||
private onMembersClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
if (!RoomViewStore.getRoomId()) {
|
||||
defaultDispatcher.dispatch({
|
||||
action: "view_room",
|
||||
room_id: this.props.space.roomId,
|
||||
}, true);
|
||||
}
|
||||
|
||||
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
|
||||
action: Action.SetRightPanelPhase,
|
||||
phase: RightPanelPhases.SpaceMemberList,
|
||||
refireParams: { space: this.props.space },
|
||||
});
|
||||
this.setState({contextMenuPosition: null}); // also close the menu
|
||||
};
|
||||
|
||||
private onExploreRoomsClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
Modal.createTrackedDialog("Space room directory", "Space panel", SpaceRoomDirectory, {
|
||||
space: this.props.space,
|
||||
}, "mx_SpaceRoomDirectory_dialogWrapper", false, true);
|
||||
this.setState({contextMenuPosition: null}); // also close the menu
|
||||
};
|
||||
|
||||
private renderContextMenu(): React.ReactElement {
|
||||
let contextMenu = null;
|
||||
if (this.state.contextMenuPosition) {
|
||||
const userId = this.context.getUserId();
|
||||
|
||||
let inviteOption;
|
||||
if (this.props.space.canInvite(userId)) {
|
||||
inviteOption = (
|
||||
<IconizedContextMenuOption
|
||||
className="mx_SpacePanel_contextMenu_inviteButton"
|
||||
iconClassName="mx_SpacePanel_iconInvite"
|
||||
label={_t("Invite people")}
|
||||
onClick={this.onInviteClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let settingsOption;
|
||||
let leaveSection;
|
||||
if (shouldShowSpaceSettings(this.context, this.props.space)) {
|
||||
settingsOption = (
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_SpacePanel_iconSettings"
|
||||
label={_t("Settings")}
|
||||
onClick={this.onSettingsClick}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
leaveSection = <IconizedContextMenuOptionList red first>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_SpacePanel_iconLeave"
|
||||
label={_t("Leave space")}
|
||||
onClick={this.onLeaveClick}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>;
|
||||
}
|
||||
|
||||
let newRoomOption;
|
||||
if (this.props.space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) {
|
||||
newRoomOption = (
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_SpacePanel_iconPlus"
|
||||
label={_t("New room")}
|
||||
onClick={this.onNewRoomClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
contextMenu = <IconizedContextMenu
|
||||
{...toRightOf(this.state.contextMenuPosition, 0)}
|
||||
onFinished={this.onMenuClose}
|
||||
className="mx_SpacePanel_contextMenu"
|
||||
compact
|
||||
>
|
||||
<div className="mx_SpacePanel_contextMenu_header">
|
||||
{ this.props.space.name }
|
||||
</div>
|
||||
<IconizedContextMenuOptionList first>
|
||||
{ inviteOption }
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_SpacePanel_iconHome"
|
||||
label={_t("Space Home")}
|
||||
onClick={this.onHomeClick}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_SpacePanel_iconMembers"
|
||||
label={_t("Members")}
|
||||
onClick={this.onMembersClick}
|
||||
/>
|
||||
{ settingsOption }
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_SpacePanel_iconExplore"
|
||||
label={_t("Explore rooms")}
|
||||
onClick={this.onExploreRoomsClick}
|
||||
/>
|
||||
{ newRoomOption }
|
||||
</IconizedContextMenuOptionList>
|
||||
{ leaveSection }
|
||||
</IconizedContextMenu>;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ContextMenuTooltipButton
|
||||
className="mx_SpaceButton_menuButton"
|
||||
onClick={this.onMenuOpenClick}
|
||||
title={_t("Space options")}
|
||||
isExpanded={!!this.state.contextMenuPosition}
|
||||
/>
|
||||
{ contextMenu }
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {space, activeSpaces, isNested} = this.props;
|
||||
|
||||
|
@ -133,6 +347,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
|||
<div className="mx_SpaceButton_selectionWrapper">
|
||||
<RoomAvatar width={avatarSize} height={avatarSize} room={space} />
|
||||
{ notifBadge }
|
||||
{ this.renderContextMenu() }
|
||||
</div>
|
||||
</RovingAccessibleTooltipButton>
|
||||
);
|
||||
|
@ -149,6 +364,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
|||
<RoomAvatar width={avatarSize} height={avatarSize} room={space} />
|
||||
<span className="mx_SpaceButton_name">{ space.name }</span>
|
||||
{ notifBadge }
|
||||
{ this.renderContextMenu() }
|
||||
</div>
|
||||
</RovingAccessibleButton>
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue