Merge pull request #2260 from matrix-org/bwindels/rightpanelbelowheader

Redesign: move right panel below room/group header
This commit is contained in:
Bruno Windels 2018-11-02 14:12:24 +00:00 committed by GitHub
commit 9546df609e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 554 additions and 627 deletions

View File

@ -15,10 +15,6 @@ limitations under the License.
*/
.mx_GroupView {
max-width: 960px;
width: 100%;
margin-left: auto;
margin-right: auto;
display: flex;
flex-direction: column;
overflow: hidden;
@ -29,7 +25,6 @@ limitations under the License.
}
.mx_GroupView_header {
max-width: 960px;
min-height: 70px;
align-items: center;
display: flex;
@ -162,6 +157,11 @@ limitations under the License.
line-height: 2em;
}
.mx_GroupView > .mx_MainSplit {
flex: 1;
display: flex;
}
.mx_GroupView_body {
flex-grow: 1;
}

View File

@ -69,7 +69,8 @@ limitations under the License.
transform: translateX(-50%);
}
.mx_MatrixChat .mx_MatrixChat_middlePanel {
/* not the left panel, and not the resize handle, so the roomview/groupview/... */
.mx_MatrixChat > :not(.mx_LeftPanel_container):not(.mx_ResizeHandle) {
background-color: $primary-bg-color;
flex: 1;
@ -81,8 +82,6 @@ limitations under the License.
*/
overflow-x: auto;
display: flex;
/* To fix https://github.com/vector-im/riot-web/issues/3298 where Safari
needed height 100% all the way down to the HomePage. Height does not
have to be auto, empirically.

View File

@ -16,20 +16,10 @@ limitations under the License.
.mx_RoomView {
word-wrap: break-word;
position: relative;
display: flex;
width: 100%;
flex-direction: column;
}
.mx_RoomView .mx_RoomHeader {
order: 1;
flex: 0 0 52px;
}
.mx_RoomView_fileDropTarget {
min-width: 0px;
width: 100%;
@ -62,15 +52,11 @@ limitations under the License.
}
.mx_RoomView_auxPanel {
order: 2;
min-width: 0px;
width: 100%;
margin: 0px auto;
overflow: auto;
border-bottom: 1px solid $primary-hairline-color;
flex: 0 0 auto;
}
@ -79,13 +65,17 @@ limitations under the License.
}
.mx_RoomView_body {
order: 3;
.mx_RoomView .mx_MainSplit {
flex: 1 1 0;
flex-direction: column;
display: flex;
}
.mx_RoomView_body {
display: flex;
flex-direction: column;
flex: 1;
}
.mx_RoomView_body .mx_RoomView_topUnreadMessagesBar {
order: 1;
}

View File

@ -24,7 +24,7 @@ limitations under the License.
}
.mx_MemberList .mx_Spinner {
flex: 0 0 auto;
flex: 1 0 auto;
}
.mx_MemberList_chevron {

View File

@ -14,6 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_RoomHeader {
flex: 0 0 52px;
border-bottom: 1px solid $primary-hairline-color;
}
/* add 20px to the height of the header when editing */
.mx_RoomHeader_editing {
flex: 0 0 93px ! important;

View File

@ -20,7 +20,6 @@ export default {
HomePage: "home_page",
RoomView: "room_view",
UserSettings: "user_settings",
CreateRoom: "create_room",
RoomDirectory: "room_directory",
UserView: "user_view",
GroupView: "group_view",

View File

@ -1,284 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
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.
*/
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../languageHandler';
import sdk from '../../index';
import MatrixClientPeg from '../../MatrixClientPeg';
const PresetValues = {
PrivateChat: "private_chat",
PublicChat: "public_chat",
Custom: "custom",
};
module.exports = React.createClass({
displayName: 'CreateRoom',
propTypes: {
onRoomCreated: PropTypes.func,
collapsedRhs: PropTypes.bool,
},
phases: {
CONFIG: "CONFIG", // We're waiting for user to configure and hit create.
CREATING: "CREATING", // We're sending the request.
CREATED: "CREATED", // We successfully created the room.
ERROR: "ERROR", // There was an error while trying to create room.
},
getDefaultProps: function() {
return {
onRoomCreated: function() {},
};
},
getInitialState: function() {
return {
phase: this.phases.CONFIG,
error_string: "",
is_private: true,
share_history: false,
default_preset: PresetValues.PrivateChat,
topic: '',
room_name: '',
invited_users: [],
};
},
onCreateRoom: function() {
const options = {};
if (this.state.room_name) {
options.name = this.state.room_name;
}
if (this.state.topic) {
options.topic = this.state.topic;
}
if (this.state.preset) {
if (this.state.preset != PresetValues.Custom) {
options.preset = this.state.preset;
} else {
options.initial_state = [
{
type: "m.room.join_rules",
content: {
"join_rule": this.state.is_private ? "invite" : "public",
},
},
{
type: "m.room.history_visibility",
content: {
"history_visibility": this.state.share_history ? "shared" : "invited",
},
},
];
}
}
options.invite = this.state.invited_users;
const alias = this.getAliasLocalpart();
if (alias) {
options.room_alias_name = alias;
}
const cli = MatrixClientPeg.get();
if (!cli) {
// TODO: Error.
console.error("Cannot create room: No matrix client.");
return;
}
const deferred = cli.createRoom(options);
if (this.state.encrypt) {
// TODO
}
this.setState({
phase: this.phases.CREATING,
});
const self = this;
deferred.then(function(resp) {
self.setState({
phase: self.phases.CREATED,
});
self.props.onRoomCreated(resp.room_id);
}, function(err) {
self.setState({
phase: self.phases.ERROR,
error_string: err.toString(),
});
});
},
getPreset: function() {
return this.refs.presets.getPreset();
},
getName: function() {
return this.refs.name_textbox.getName();
},
getTopic: function() {
return this.refs.topic.getTopic();
},
getAliasLocalpart: function() {
return this.refs.alias.getAliasLocalpart();
},
getInvitedUsers: function() {
return this.refs.user_selector.getUserIds();
},
onPresetChanged: function(preset) {
switch (preset) {
case PresetValues.PrivateChat:
this.setState({
preset: preset,
is_private: true,
share_history: false,
});
break;
case PresetValues.PublicChat:
this.setState({
preset: preset,
is_private: false,
share_history: true,
});
break;
case PresetValues.Custom:
this.setState({
preset: preset,
});
break;
}
},
onPrivateChanged: function(ev) {
this.setState({
preset: PresetValues.Custom,
is_private: ev.target.checked,
});
},
onShareHistoryChanged: function(ev) {
this.setState({
preset: PresetValues.Custom,
share_history: ev.target.checked,
});
},
onTopicChange: function(ev) {
this.setState({
topic: ev.target.value,
});
},
onNameChange: function(ev) {
this.setState({
room_name: ev.target.value,
});
},
onInviteChanged: function(invited_users) {
this.setState({
invited_users: invited_users,
});
},
onAliasChanged: function(alias) {
this.setState({
alias: alias,
});
},
onEncryptChanged: function(ev) {
this.setState({
encrypt: ev.target.checked,
});
},
render: function() {
const curr_phase = this.state.phase;
if (curr_phase == this.phases.CREATING) {
const Loader = sdk.getComponent("elements.Spinner");
return (
<Loader />
);
} else {
let error_box = "";
if (curr_phase == this.phases.ERROR) {
error_box = (
<div className="mx_Error">
{ _t('An error occurred: %(error_string)s', {error_string: this.state.error_string}) }
</div>
);
}
const CreateRoomButton = sdk.getComponent("create_room.CreateRoomButton");
const RoomAlias = sdk.getComponent("create_room.RoomAlias");
const Presets = sdk.getComponent("create_room.Presets");
const UserSelector = sdk.getComponent("elements.UserSelector");
const SimpleRoomHeader = sdk.getComponent("rooms.SimpleRoomHeader");
const domain = MatrixClientPeg.get().getDomain();
return (
<div className="mx_CreateRoom">
<SimpleRoomHeader title={_t("Create Room")} collapsedRhs={this.props.collapsedRhs} />
<div className="mx_CreateRoom_body">
<input type="text" ref="room_name" value={this.state.room_name} onChange={this.onNameChange} placeholder={_t('Name')} /> <br />
<textarea className="mx_CreateRoom_description" ref="topic" value={this.state.topic} onChange={this.onTopicChange} placeholder={_t('Topic')} /> <br />
<RoomAlias ref="alias" alias={this.state.alias} homeserver={domain} onChange={this.onAliasChanged} /> <br />
<UserSelector ref="user_selector" selected_users={this.state.invited_users} onChange={this.onInviteChanged} /> <br />
<Presets ref="presets" onChange={this.onPresetChanged} preset={this.state.preset} /> <br />
<div>
<label>
<input type="checkbox" ref="is_private" checked={this.state.is_private} onChange={this.onPrivateChanged} />
{ _t('Make this room private') }
</label>
</div>
<div>
<label>
<input type="checkbox" ref="share_history" checked={this.state.share_history} onChange={this.onShareHistoryChanged} />
{ _t('Share message history with new users') }
</label>
</div>
<div className="mx_CreateRoom_encrypt">
<label>
<input type="checkbox" ref="encrypt" checked={this.state.encrypt} onChange={this.onEncryptChanged} />
{ _t('Encrypt room') }
</label>
</div>
<div>
<CreateRoomButton onCreateRoom={this.onCreateRoom} /> <br />
</div>
{ error_box }
</div>
</div>
);
}
},
});

View File

@ -24,6 +24,9 @@ import dis from '../../dispatcher';
import { sanitizedHtmlNode } from '../../HtmlUtils';
import { _t, _td } from '../../languageHandler';
import AccessibleButton from '../views/elements/AccessibleButton';
import GroupHeaderButtons from '../views/right_panel/GroupHeaderButtons';
import MainSplit from './MainSplit';
import RightPanel from './RightPanel';
import Modal from '../../Modal';
import classnames from 'classnames';
@ -1280,6 +1283,8 @@ export default React.createClass({
}
}
const rightPanel = !this.props.collapsedRhs ? <RightPanel groupId={this.props.groupId} /> : undefined;
const headerClasses = {
mx_GroupView_header: true,
mx_GroupView_header_view: !this.state.editing,
@ -1287,7 +1292,7 @@ export default React.createClass({
};
return (
<div className="mx_GroupView">
<main className="mx_GroupView">
<div className={classnames(headerClasses)}>
<div className="mx_GroupView_header_leftCol">
<div className="mx_GroupView_header_avatar">
@ -1305,12 +1310,15 @@ export default React.createClass({
<div className="mx_GroupView_header_rightCol">
{ rightButtons }
</div>
<GroupHeaderButtons />
</div>
<GeminiScrollbarWrapper className="mx_GroupView_body">
{ this._getMembershipSection() }
{ this._getGroupSection() }
</GeminiScrollbarWrapper>
</div>
<MainSplit collapsedRhs={this.props.collapsedRhs} panel={rightPanel}>
<GeminiScrollbarWrapper className="mx_GroupView_body">
{ this._getMembershipSection() }
{ this._getGroupSection() }
</GeminiScrollbarWrapper>
</MainSplit>
</main>
);
} else if (this.state.error) {
if (this.state.error.httpStatus === 404) {

View File

@ -173,11 +173,7 @@ const LoggedInView = React.createClass({
}
},
onResized: (size, item) => {
if (item.classList.contains("mx_LeftPanel_container")) {
window.localStorage.setItem("mx_lhs_size", '' + size);
} else if(item.classList.contains("mx_RightPanel")) {
window.localStorage.setItem("mx_rhs_size", '' + size);
}
window.localStorage.setItem("mx_lhs_size", '' + size);
},
};
const resizer = new Resizer(
@ -193,10 +189,6 @@ const LoggedInView = React.createClass({
if (lhsSize !== null) {
this.resizer.forHandleAt(0).resize(parseInt(lhsSize, 10));
}
const rhsSize = window.localStorage.getItem("mx_rhs_size");
if (rhsSize !== null) {
this.resizer.forHandleAt(1).resize(parseInt(rhsSize, 10));
}
},
onAccountData: function(event) {
@ -421,10 +413,8 @@ const LoggedInView = React.createClass({
render: function() {
const LeftPanel = sdk.getComponent('structures.LeftPanel');
const RightPanel = sdk.getComponent('structures.RightPanel');
const RoomView = sdk.getComponent('structures.RoomView');
const UserSettings = sdk.getComponent('structures.UserSettings');
const CreateRoom = sdk.getComponent('structures.CreateRoom');
const RoomDirectory = sdk.getComponent('structures.RoomDirectory');
const HomePage = sdk.getComponent('structures.HomePage');
const GroupView = sdk.getComponent('structures.GroupView');
@ -437,7 +427,6 @@ const LoggedInView = React.createClass({
const ServerLimitBar = sdk.getComponent('globals.ServerLimitBar');
let page_element;
let right_panel = '';
switch (this.props.page_type) {
case PageTypes.RoomView:
@ -454,9 +443,6 @@ const LoggedInView = React.createClass({
collapsedRhs={this.props.collapseRhs}
ConferenceHandler={this.props.ConferenceHandler}
/>;
if (!this.props.collapseRhs) {
right_panel = <RightPanel roomId={this.props.currentRoomId} disabled={this.props.rightDisabled} />;
}
break;
case PageTypes.UserSettings:
@ -466,21 +452,12 @@ const LoggedInView = React.createClass({
referralBaseUrl={this.props.config.referralBaseUrl}
teamToken={this.props.teamToken}
/>;
if (!this.props.collapseRhs) right_panel = <RightPanel disabled={this.props.rightDisabled} />;
break;
case PageTypes.MyGroups:
page_element = <MyGroups />;
break;
case PageTypes.CreateRoom:
page_element = <CreateRoom
onRoomCreated={this.props.onRoomCreated}
collapsedRhs={this.props.collapseRhs}
/>;
if (!this.props.collapseRhs) right_panel = <RightPanel disabled={this.props.rightDisabled} />;
break;
case PageTypes.RoomDirectory:
page_element = <RoomDirectory
ref="roomDirectory"
@ -506,7 +483,8 @@ const LoggedInView = React.createClass({
case PageTypes.UserView:
page_element = null; // deliberately null for now
right_panel = <RightPanel disabled={this.props.rightDisabled} />;
// TODO: fix/remove UserView
// right_panel = <RightPanel disabled={this.props.rightDisabled} />;
break;
case PageTypes.GroupView:
page_element = <GroupView
@ -514,7 +492,6 @@ const LoggedInView = React.createClass({
isNew={this.props.currentGroupIsNew}
collapsedRhs={this.props.collapseRhs}
/>;
if (!this.props.collapseRhs) right_panel = <RightPanel groupId={this.props.currentGroupId} disabled={this.props.rightDisabled} />;
break;
}
@ -572,11 +549,7 @@ const LoggedInView = React.createClass({
disabled={this.props.leftDisabled}
/>
<ResizeHandle/>
<main className='mx_MatrixChat_middlePanel'>
{ page_element }
</main>
<ResizeHandle reverse={true}/>
{ right_panel }
{ page_element }
</div>
</DragDropContext>
</div>

View File

@ -0,0 +1,92 @@
/*
Copyright 2018 New Vector Ltd
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 from 'react';
import ResizeHandle from '../views/elements/ResizeHandle';
import {Resizer, FixedDistributor} from '../../resizer';
export default class MainSplit extends React.Component {
constructor(props) {
super(props);
this._setResizeContainerRef = this._setResizeContainerRef.bind(this);
}
_createResizer() {
const classNames = {
handle: "mx_ResizeHandle",
vertical: "mx_ResizeHandle_vertical",
reverse: "mx_ResizeHandle_reverse",
};
const resizer = new Resizer(
this.resizeContainer,
FixedDistributor);
resizer.setClassNames(classNames);
const rhsSize = window.localStorage.getItem("mx_rhs_size");
if (rhsSize !== null) {
resizer.forHandleAt(0).resize(parseInt(rhsSize, 10));
}
resizer.attach();
this.resizer = resizer;
}
_setResizeContainerRef(div) {
this.resizeContainer = div;
}
componentDidMount() {
if (this.props.panel && !this.collapsedRhs) {
this._createResizer();
}
}
componentWillUnmount() {
if (this.resizer) {
this.resizer.detach();
this.resizer = null;
}
}
componentDidUpdate(prevProps) {
const wasExpanded = !this.props.collapsedRhs && prevProps.collapsedRhs;
const wasCollapsed = this.props.collapsedRhs && !prevProps.collapsedRhs;
const wasPanelSet = this.props.panel && !prevProps.panel;
const wasPanelCleared = !this.props.panel && prevProps.panel;
if (wasExpanded || wasPanelSet) {
this._createResizer();
} else if (wasCollapsed || wasPanelCleared) {
this.resizer.detach();
this.resizer = null;
}
}
render() {
const bodyView = React.Children.only(this.props.children);
const panelView = this.props.panel;
if (this.props.collapsedRhs || !panelView) {
return bodyView;
} else {
return <div className="mx_MainSplit" ref={this._setResizeContainerRef}>
{ bodyView }
<ResizeHandle reverse={true} />
{ panelView }
</div>;
}
}
}

View File

@ -2,6 +2,7 @@
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2017 New Vector Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -23,79 +24,27 @@ import { _t } from '../../languageHandler';
import sdk from '../../index';
import dis from '../../dispatcher';
import { MatrixClient } from 'matrix-js-sdk';
import Analytics from '../../Analytics';
import RateLimitedFunc from '../../ratelimitedfunc';
import AccessibleButton from '../../components/views/elements/AccessibleButton';
import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
import GroupStore from '../../stores/GroupStore';
class HeaderButton extends React.Component {
constructor() {
super();
this.onClick = this.onClick.bind(this);
export default class RightPanel extends React.Component {
static get propTypes() {
return {
roomId: React.PropTypes.string, // if showing panels for a given room, this is set
groupId: React.PropTypes.string, // if showing panels for a given group, this is set
};
}
onClick(ev) {
Analytics.trackEvent(...this.props.analytics);
dis.dispatch({
action: 'view_right_panel_phase',
phase: this.props.clickPhase,
});
static get contextTypes() {
return {
matrixClient: PropTypes.instanceOf(MatrixClient),
};
}
render() {
const TintableSvg = sdk.getComponent("elements.TintableSvg");
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
const classes = classNames({
mx_RightPanel_headerButton: true,
mx_RightPanel_headerButton_highlight: this.props.isHighlighted,
});
return <AccessibleButton
aria-label={this.props.title}
aria-expanded={this.props.isHighlighted}
title={this.props.title}
className={classes}
onClick={this.onClick} >
<TintableSvg src={this.props.iconSrc} width="20" height="20" />
</AccessibleButton>;
}
}
HeaderButton.propTypes = {
// Whether this button is highlighted
isHighlighted: PropTypes.bool.isRequired,
// The phase to swap to when the button is clicked
clickPhase: PropTypes.string.isRequired,
// The source file of the icon to display
iconSrc: PropTypes.string.isRequired,
// The badge to display above the icon
badge: PropTypes.node,
// The parameters to track the click event
analytics: PropTypes.arrayOf(PropTypes.string).isRequired,
// Button title
title: PropTypes.string.isRequired,
};
module.exports = React.createClass({
displayName: 'RightPanel',
propTypes: {
// TODO: We're trying to move away from these being props, but we need to know
// whether we should be displaying a room or group member list
roomId: React.PropTypes.string, // if showing panels for a given room, this is set
groupId: React.PropTypes.string, // if showing panels for a given group, this is set
collapsed: React.PropTypes.bool, // currently unused property to request for a minimized view of the panel
},
contextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
},
Phase: {
static Phase = Object.freeze({
RoomMemberList: 'RoomMemberList',
GroupMemberList: 'GroupMemberList',
GroupRoomList: 'GroupRoomList',
@ -104,147 +53,100 @@ module.exports = React.createClass({
NotificationPanel: 'NotificationPanel',
RoomMemberInfo: 'RoomMemberInfo',
GroupMemberInfo: 'GroupMemberInfo',
},
});
componentWillMount: function() {
constructor(props, context) {
super(props, context);
this.state = {
phase: this.props.groupId ? RightPanel.Phase.GroupMemberList : RightPanel.Phase.RoomMemberList,
isUserPrivilegedInGroup: null,
};
this.onAction = this.onAction.bind(this);
this.onRoomStateMember = this.onRoomStateMember.bind(this);
this.onGroupStoreUpdated = this.onGroupStoreUpdated.bind(this);
this.onInviteToGroupButtonClick = this.onInviteToGroupButtonClick.bind(this);
this.onAddRoomToGroupButtonClick = this.onAddRoomToGroupButtonClick.bind(this);
this._delayedUpdate = new RateLimitedFunc(() => {
this.forceUpdate();
}, 500);
}
componentWillMount() {
this.dispatcherRef = dis.register(this.onAction);
const cli = this.context.matrixClient;
cli.on("RoomState.members", this.onRoomStateMember);
this._initGroupStore(this.props.groupId);
},
}
componentWillUnmount: function() {
componentWillUnmount() {
dis.unregister(this.dispatcherRef);
if (this.context.matrixClient) {
this.context.matrixClient.removeListener("RoomState.members", this.onRoomStateMember);
}
this._unregisterGroupStore(this.props.groupId);
},
getInitialState: function() {
return {
phase: this.props.groupId ? this.Phase.GroupMemberList : this.Phase.RoomMemberList,
isUserPrivilegedInGroup: null,
};
},
}
componentWillReceiveProps(newProps) {
if (newProps.groupId !== this.props.groupId) {
this._unregisterGroupStore(this.props.groupId);
this._initGroupStore(newProps.groupId);
}
},
}
_initGroupStore(groupId) {
if (!groupId) return;
GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
},
}
_unregisterGroupStore() {
GroupStore.unregisterListener(this.onGroupStoreUpdated);
},
}
onGroupStoreUpdated: function() {
onGroupStoreUpdated() {
this.setState({
isUserPrivilegedInGroup: GroupStore.isUserPrivileged(this.props.groupId),
});
},
}
onCollapseClick: function() {
dis.dispatch({
action: 'hide_right_panel',
});
},
onInviteToGroupButtonClick: function() {
onInviteToGroupButtonClick() {
showGroupInviteDialog(this.props.groupId).then(() => {
this.setState({
phase: this.Phase.GroupMemberList,
phase: RightPanel.Phase.GroupMemberList,
});
});
},
}
onAddRoomToGroupButtonClick: function() {
onAddRoomToGroupButtonClick() {
showGroupAddRoomDialog(this.props.groupId).then(() => {
this.forceUpdate();
});
},
}
onRoomStateMember: function(ev, state, member) {
onRoomStateMember(ev, state, member) {
if (member.roomId !== this.props.roomId) {
return;
}
// redraw the badge on the membership list
if (this.state.phase === this.Phase.RoomMemberList && member.roomId === this.props.roomId) {
if (this.state.phase === RightPanel.Phase.RoomMemberList && member.roomId === this.props.roomId) {
this._delayedUpdate();
} else if (this.state.phase === this.Phase.RoomMemberInfo && member.roomId === this.props.roomId &&
} else if (this.state.phase === RightPanel.Phase.RoomMemberInfo && member.roomId === this.props.roomId &&
member.userId === this.state.member.userId) {
// refresh the member info (e.g. new power level)
this._delayedUpdate();
}
},
}
_delayedUpdate: new RateLimitedFunc(function() {
this.forceUpdate(); // eslint-disable-line babel/no-invalid-this
}, 500),
onAction: function(payload) {
if (payload.action === "view_user") {
dis.dispatch({
action: 'show_right_panel',
});
if (payload.member) {
this.setState({
phase: this.Phase.RoomMemberInfo,
member: payload.member,
});
} else {
if (this.props.roomId) {
this.setState({
phase: this.Phase.RoomMemberList,
});
} else if (this.props.groupId) {
this.setState({
phase: this.Phase.GroupMemberList,
member: payload.member,
});
}
}
} else if (payload.action === "view_group") {
this.setState({
phase: this.Phase.GroupMemberList,
member: null,
});
} else if (payload.action === "view_group_room") {
this.setState({
phase: this.Phase.GroupRoomInfo,
groupRoomId: payload.groupRoomId,
});
} else if (payload.action === "view_group_room_list") {
this.setState({
phase: this.Phase.GroupRoomList,
});
} else if (payload.action === "view_group_member_list") {
this.setState({
phase: this.Phase.GroupMemberList,
});
} else if (payload.action === "view_group_user") {
this.setState({
phase: this.Phase.GroupMemberInfo,
member: payload.member,
});
} else if (payload.action === "view_room") {
this.setState({
phase: this.Phase.RoomMemberList,
});
} else if (payload.action === "view_right_panel_phase") {
onAction(payload) {
if (payload.action === "view_right_panel_phase") {
this.setState({
phase: payload.phase,
member: payload.member,
});
}
},
}
render: function() {
render() {
const MemberList = sdk.getComponent('rooms.MemberList');
const MemberInfo = sdk.getComponent('rooms.MemberInfo');
const NotificationPanel = sdk.getComponent('structures.NotificationPanel');
@ -257,98 +159,41 @@ module.exports = React.createClass({
const TintableSvg = sdk.getComponent("elements.TintableSvg");
// eslint-disable-next-line no-unused-vars
let inviteGroup;
let membersBadge;
const membersTitle = _t('Members');
const isPhaseGroup = [
this.Phase.GroupMemberInfo,
this.Phase.GroupMemberList,
RightPanel.Phase.GroupMemberInfo,
RightPanel.Phase.GroupMemberList,
].includes(this.state.phase);
let headerButtons = [];
if (this.props.roomId) {
headerButtons = [
<HeaderButton key="_membersButton" title={membersTitle} iconSrc="img/icons-people.svg"
isHighlighted={[this.Phase.RoomMemberList, this.Phase.RoomMemberInfo].includes(this.state.phase)}
clickPhase={this.Phase.RoomMemberList}
badge={membersBadge}
analytics={['Right Panel', 'Member List Button', 'click']}
/>,
<HeaderButton key="_filesButton" title={_t('Files')} iconSrc="img/icons-files.svg"
isHighlighted={this.state.phase === this.Phase.FilePanel}
clickPhase={this.Phase.FilePanel}
analytics={['Right Panel', 'File List Button', 'click']}
/>,
<HeaderButton key="_notifsButton" title={_t('Notifications')} iconSrc="img/icons-notifications.svg"
isHighlighted={this.state.phase === this.Phase.NotificationPanel}
clickPhase={this.Phase.NotificationPanel}
analytics={['Right Panel', 'Notification List Button', 'click']}
/>,
];
} else if (this.props.groupId) {
headerButtons = [
<HeaderButton key="_groupMembersButton" title={_t('Members')} iconSrc="img/icons-people.svg"
isHighlighted={isPhaseGroup}
clickPhase={this.Phase.GroupMemberList}
analytics={['Right Panel', 'Group Member List Button', 'click']}
/>,
<HeaderButton key="_roomsButton" title={_t('Rooms')} iconSrc="img/icons-room.svg"
isHighlighted={[this.Phase.GroupRoomList, this.Phase.GroupRoomInfo].includes(this.state.phase)}
clickPhase={this.Phase.GroupRoomList}
analytics={['Right Panel', 'Group Room List Button', 'click']}
/>,
];
}
if (this.props.roomId || this.props.groupId) {
// Hiding the right panel hides it completely and relies on an 'expand' button
// being put in the RoomHeader or GroupView header, so only show the minimise
// button on these 2 screens or you won't be able to re-expand the panel.
headerButtons.push(
<AccessibleButton className="mx_RightPanel_headerButton mx_RightPanel_collapsebutton" key="_minimizeButton"
title={_t("Hide panel")} aria-label={_t("Hide panel")} onClick={this.onCollapseClick}
>
<TintableSvg src="img/minimise.svg" width="10" height="16" alt="" />
</AccessibleButton>,
);
}
let panel = <div />;
if (!this.props.collapsed) {
if (this.props.roomId && this.state.phase === this.Phase.RoomMemberList) {
panel = <MemberList roomId={this.props.roomId} key={this.props.roomId} />;
} else if (this.props.groupId && this.state.phase === this.Phase.GroupMemberList) {
panel = <GroupMemberList groupId={this.props.groupId} key={this.props.groupId} />;
} else if (this.state.phase === this.Phase.GroupRoomList) {
panel = <GroupRoomList groupId={this.props.groupId} key={this.props.groupId} />;
} else if (this.state.phase === this.Phase.RoomMemberInfo) {
panel = <MemberInfo member={this.state.member} key={this.props.roomId || this.state.member.userId} />;
} else if (this.state.phase === this.Phase.GroupMemberInfo) {
panel = <GroupMemberInfo
groupMember={this.state.member}
groupId={this.props.groupId}
key={this.state.member.user_id} />;
} else if (this.state.phase === this.Phase.GroupRoomInfo) {
panel = <GroupRoomInfo
groupRoomId={this.state.groupRoomId}
groupId={this.props.groupId}
key={this.state.groupRoomId} />;
} else if (this.state.phase === this.Phase.NotificationPanel) {
panel = <NotificationPanel />;
} else if (this.state.phase === this.Phase.FilePanel) {
panel = <FilePanel roomId={this.props.roomId} />;
}
}
if (!panel) {
panel = <div className="mx_RightPanel_blank" />;
if (this.props.roomId && this.state.phase === RightPanel.Phase.RoomMemberList) {
panel = <MemberList roomId={this.props.roomId} key={this.props.roomId} />;
} else if (this.props.groupId && this.state.phase === RightPanel.Phase.GroupMemberList) {
panel = <GroupMemberList groupId={this.props.groupId} key={this.props.groupId} />;
} else if (this.state.phase === RightPanel.Phase.GroupRoomList) {
panel = <GroupRoomList groupId={this.props.groupId} key={this.props.groupId} />;
} else if (this.state.phase === RightPanel.Phase.RoomMemberInfo) {
panel = <MemberInfo member={this.state.member} key={this.props.roomId || this.state.member.userId} />;
} else if (this.state.phase === RightPanel.Phase.GroupMemberInfo) {
panel = <GroupMemberInfo
groupMember={this.state.member}
groupId={this.props.groupId}
key={this.state.member.user_id} />;
} else if (this.state.phase === RightPanel.Phase.GroupRoomInfo) {
panel = <GroupRoomInfo
groupRoomId={this.state.groupRoomId}
groupId={this.props.groupId}
key={this.state.groupRoomId} />;
} else if (this.state.phase === RightPanel.Phase.NotificationPanel) {
panel = <NotificationPanel />;
} else if (this.state.phase === RightPanel.Phase.FilePanel) {
panel = <FilePanel roomId={this.props.roomId} />;
}
// TODO: either include this in the DOM again, or move it to other component
if (this.props.groupId && this.state.isUserPrivilegedInGroup) {
inviteGroup = isPhaseGroup ? (
// inviteGroup =
isPhaseGroup ? (
<AccessibleButton className="mx_RightPanel_invite" onClick={this.onInviteToGroupButtonClick}>
<div className="mx_RightPanel_icon" >
<TintableSvg src="img/icon-invite-people.svg" width="35" height="35" />
@ -372,13 +217,8 @@ module.exports = React.createClass({
return (
<aside className={classes}>
<div className="mx_RightPanel_header">
<div className="mx_RightPanel_headerButtonGroup">
{ headerButtons }
</div>
</div>
{ panel }
</aside>
);
},
});
}
}

View File

@ -43,6 +43,8 @@ const Rooms = require('../../Rooms');
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
import MainSplit from './MainSplit';
import RightPanel from './RightPanel';
import RoomViewStore from '../../stores/RoomViewStore';
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
import WidgetEchoStore from '../../stores/WidgetEchoStore';
@ -1510,18 +1512,20 @@ module.exports = React.createClass({
oobData={this.props.oobData}
collapsedRhs={this.props.collapsedRhs}
/>
<div className="mx_RoomView_auxPanel">
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
onForgetClick={this.onForgetClick}
onRejectClick={this.onRejectThreepidInviteButtonClicked}
canPreview={false} error={this.state.roomLoadError}
roomAlias={roomAlias}
spinner={this.state.joining}
spinnerState="joining"
inviterName={inviterName}
invitedEmail={invitedEmail}
room={this.state.room}
/>
<div className="mx_RoomView_body">
<div className="mx_RoomView_auxPanel">
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
onForgetClick={this.onForgetClick}
onRejectClick={this.onRejectThreepidInviteButtonClicked}
canPreview={false} error={this.state.roomLoadError}
roomAlias={roomAlias}
spinner={this.state.joining}
spinnerState="joining"
inviterName={inviterName}
invitedEmail={invitedEmail}
room={this.state.room}
/>
</div>
</div>
<div className="mx_RoomView_messagePanel"></div>
</div>
@ -1555,16 +1559,18 @@ module.exports = React.createClass({
room={this.state.room}
collapsedRhs={this.props.collapsedRhs}
/>
<div className="mx_RoomView_auxPanel">
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
onForgetClick={this.onForgetClick}
onRejectClick={this.onRejectButtonClicked}
inviterName={inviterName}
canPreview={false}
spinner={this.state.joining}
spinnerState="joining"
room={this.state.room}
/>
<div className="mx_RoomView_body">
<div className="mx_RoomView_auxPanel">
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
onForgetClick={this.onForgetClick}
onRejectClick={this.onRejectButtonClicked}
inviterName={inviterName}
canPreview={false}
spinner={this.state.joining}
spinnerState="joining"
room={this.state.room}
/>
</div>
</div>
<div className="mx_RoomView_messagePanel"></div>
</div>
@ -1811,8 +1817,10 @@ module.exports = React.createClass({
},
);
const rightPanel = this.state.room ? <RightPanel roomId={this.state.room.roomId} /> : undefined;
return (
<div className={"mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "")} ref="roomView">
<main className={"mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "")} ref="roomView">
<RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo}
oobData={this.props.oobData}
editing={this.state.editingRoomSettings}
@ -1827,20 +1835,22 @@ module.exports = React.createClass({
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
/>
{ auxPanel }
<div className={fadableSectionClasses}>
{ topUnreadMessagesBar }
{ messagePanel }
{ searchResultsPanel }
<div className={statusBarAreaClass}>
<div className="mx_RoomView_statusAreaBox">
<div className="mx_RoomView_statusAreaBox_line"></div>
{ statusBar }
<MainSplit panel={rightPanel} collapsedRhs={this.props.collapsedRhs}>
<div className={fadableSectionClasses}>
{ auxPanel }
{ topUnreadMessagesBar }
{ messagePanel }
{ searchResultsPanel }
<div className={statusBarAreaClass}>
<div className="mx_RoomView_statusAreaBox">
<div className="mx_RoomView_statusAreaBox_line"></div>
{ statusBar }
</div>
</div>
{ messageComposer }
</div>
{ messageComposer }
</div>
</div>
</MainSplit>
</main>
);
},
});

View File

@ -0,0 +1,81 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2017 New Vector Ltd
Copyright 2018 New Vector Ltd
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 from 'react';
import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher';
import HeaderButton from './HeaderButton';
import HeaderButtons from './HeaderButtons';
import RightPanel from '../../structures/RightPanel';
export default class GroupHeaderButtons extends HeaderButtons {
constructor(props) {
super(props, RightPanel.Phase.GroupMemberList);
}
onAction(payload) {
super.onAction(payload);
if (payload.action === "view_user") {
dis.dispatch({
action: 'show_right_panel',
});
if (payload.member) {
this.setPhase(RightPanel.Phase.RoomMemberInfo, {member: payload.member});
} else {
this.setPhase(RightPanel.Phase.GroupMemberList);
}
} else if (payload.action === "view_group") {
this.setPhase(RightPanel.Phase.GroupMemberList);
} else if (payload.action === "view_group_room") {
this.setPhase(RightPanel.Phase.GroupRoomInfo, {groupRoomId: payload.groupRoomId});
} else if (payload.action === "view_group_room_list") {
this.setPhase(RightPanel.Phase.GroupRoomList);
} else if (payload.action === "view_group_member_list") {
this.setPhase(RightPanel.Phase.GroupMemberList);
} else if (payload.action === "view_group_user") {
this.setPhase(RightPanel.Phase.GroupMemberInfo, {member: payload.member});
}
}
renderButtons() {
const isPhaseGroup = [
RightPanel.Phase.GroupMemberInfo,
RightPanel.Phase.GroupMemberList,
].includes(this.state.phase);
const isPhaseRoom = [
RightPanel.Phase.GroupRoomList,
RightPanel.Phase.GroupRoomInfo,
].includes(this.state.phase);
return [
<HeaderButton key="_groupMembersButton" title={_t('Members')} iconSrc="img/icons-people.svg"
isHighlighted={isPhaseGroup}
clickPhase={RightPanel.Phase.GroupMemberList}
analytics={['Right Panel', 'Group Member List Button', 'click']}
/>,
<HeaderButton key="_roomsButton" title={_t('Rooms')} iconSrc="img/icons-room.svg"
isHighlighted={isPhaseRoom}
clickPhase={RightPanel.Phase.GroupRoomList}
analytics={['Right Panel', 'Group Room List Button', 'click']}
/>,
];
}
}

View File

@ -0,0 +1,74 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2017 New Vector Ltd
Copyright 2018 New Vector Ltd
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 from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import dis from '../../../dispatcher';
import Analytics from '../../../Analytics';
import AccessibleButton from '../elements/AccessibleButton';
import TintableSvg from '../elements/TintableSvg';
export default class HeaderButton extends React.Component {
constructor() {
super();
this.onClick = this.onClick.bind(this);
}
onClick(ev) {
Analytics.trackEvent(...this.props.analytics);
dis.dispatch({
action: 'view_right_panel_phase',
phase: this.props.clickPhase,
});
}
render() {
const classes = classNames({
mx_RightPanel_headerButton: true,
mx_RightPanel_headerButton_highlight: this.props.isHighlighted,
});
return <AccessibleButton
aria-label={this.props.title}
aria-expanded={this.props.isHighlighted}
title={this.props.title}
className={classes}
onClick={this.onClick} >
<TintableSvg src={this.props.iconSrc} width="20" height="20" />
</AccessibleButton>;
}
}
HeaderButton.propTypes = {
// Whether this button is highlighted
isHighlighted: PropTypes.bool.isRequired,
// The phase to swap to when the button is clicked
clickPhase: PropTypes.string.isRequired,
// The source file of the icon to display
iconSrc: PropTypes.string.isRequired,
// The badge to display above the icon
badge: PropTypes.node,
// The parameters to track the click event
analytics: PropTypes.arrayOf(PropTypes.string).isRequired,
// Button title
title: PropTypes.string.isRequired,
};

View File

@ -0,0 +1,65 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2017 New Vector Ltd
Copyright 2018 New Vector Ltd
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 from 'react';
import dis from '../../../dispatcher';
export default class HeaderButtons extends React.Component {
constructor(props, initialPhase) {
super(props);
this.state = {
phase: initialPhase,
isUserPrivilegedInGroup: null,
};
this.onAction = this.onAction.bind(this);
}
componentWillMount() {
this.dispatcherRef = dis.register(this.onAction);
}
componentWillUnmount() {
dis.unregister(this.dispatcherRef);
}
setPhase(phase, extras) {
// TODO: delay?
dis.dispatch(Object.assign({
action: 'view_right_panel_phase',
phase: phase,
}, extras));
}
onAction(payload) {
if (payload.action === "view_right_panel_phase") {
this.setState({
phase: payload.phase,
});
}
}
render() {
// inline style as this will be swapped around in future commits
return <div style={{display: 'flex'}}>
{ this.renderButtons() }
</div>;
}
}

View File

@ -0,0 +1,73 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2017 New Vector Ltd
Copyright 2018 New Vector Ltd
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 from 'react';
import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher';
import HeaderButton from './HeaderButton';
import HeaderButtons from './HeaderButtons';
import RightPanel from '../../structures/RightPanel';
export default class RoomHeaderButtons extends HeaderButtons {
constructor(props) {
super(props, RightPanel.Phase.RoomMemberList);
}
onAction(payload) {
super.onAction(payload);
if (payload.action === "view_user") {
dis.dispatch({
action: 'show_right_panel',
});
if (payload.member) {
this.setPhase(RightPanel.Phase.RoomMemberInfo, {member: payload.member});
} else {
this.setPhase(RightPanel.Phase.RoomMemberList);
}
} else if (payload.action === "view_room") {
this.setPhase(RightPanel.Phase.RoomMemberList);
}
}
renderButtons() {
const isMembersPhase = [
RightPanel.Phase.RoomMemberList,
RightPanel.Phase.RoomMemberInfo,
].includes(this.state.phase);
return [
<HeaderButton key="_membersButton" title={_t('Members')} iconSrc="img/icons-people.svg"
isHighlighted={isMembersPhase}
clickPhase={RightPanel.Phase.RoomMemberList}
analytics={['Right Panel', 'Member List Button', 'click']}
/>,
<HeaderButton key="_filesButton" title={_t('Files')} iconSrc="img/icons-files.svg"
isHighlighted={this.state.phase === RightPanel.Phase.FilePanel}
clickPhase={RightPanel.Phase.FilePanel}
analytics={['Right Panel', 'File List Button', 'click']}
/>,
<HeaderButton key="_notifsButton" title={_t('Notifications')} iconSrc="img/icons-notifications.svg"
isHighlighted={this.state.phase === RightPanel.Phase.NotificationPanel}
clickPhase={RightPanel.Phase.NotificationPanel}
analytics={['Right Panel', 'Notification List Button', 'click']}
/>,
];
}
}

View File

@ -33,6 +33,7 @@ import AccessibleButton from '../elements/AccessibleButton';
import ManageIntegsButton from '../elements/ManageIntegsButton';
import {CancelButton} from './SimpleRoomHeader';
import SettingsStore from "../../../settings/SettingsStore";
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
linkifyMatrix(linkify);
@ -432,6 +433,7 @@ module.exports = React.createClass({
{ saveButton }
{ cancelButton }
{ rightRow }
<RoomHeaderButtons />
</div>
</div>
);