diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss index f7c3e902bc..8b36517207 100644 --- a/res/css/views/rooms/_RoomHeader.scss +++ b/res/css/views/rooms/_RoomHeader.scss @@ -20,7 +20,6 @@ limitations under the License. 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; diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 5d23194702..d83d8e2cf1 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -24,6 +24,7 @@ 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 Modal from '../../Modal'; import classnames from 'classnames'; @@ -1305,6 +1306,7 @@ export default React.createClass({
{ rightButtons }
+ { this._getMembershipSection() } diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index 226b4a4ba4..574631a8ae 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -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 - - ; - } -} - -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,96 +159,40 @@ 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 = [ - , - , - , - ]; - } else if (this.props.groupId) { - headerButtons = [ - , - , - ]; - } - - 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( - - - , - ); - } - let panel =
; - if (!this.props.collapsed) { - if (this.props.roomId && this.state.phase === this.Phase.RoomMemberList) { - panel = ; - } else if (this.props.groupId && this.state.phase === this.Phase.GroupMemberList) { - panel = ; - } else if (this.state.phase === this.Phase.GroupRoomList) { - panel = ; - } else if (this.state.phase === this.Phase.RoomMemberInfo) { - panel = ; - } else if (this.state.phase === this.Phase.GroupMemberInfo) { - panel = ; - } else if (this.state.phase === this.Phase.GroupRoomInfo) { - panel = ; - } else if (this.state.phase === this.Phase.NotificationPanel) { - panel = ; - } else if (this.state.phase === this.Phase.FilePanel) { - panel = ; - } - } - - if (!panel) { - panel =
; + + if (this.props.roomId && this.state.phase === RightPanel.Phase.RoomMemberList) { + panel = ; + } else if (this.props.groupId && this.state.phase === RightPanel.Phase.GroupMemberList) { + panel = ; + } else if (this.state.phase === RightPanel.Phase.GroupRoomList) { + panel = ; + } else if (this.state.phase === RightPanel.Phase.RoomMemberInfo) { + panel = ; + } else if (this.state.phase === RightPanel.Phase.GroupMemberInfo) { + panel = ; + } else if (this.state.phase === RightPanel.Phase.GroupRoomInfo) { + panel = ; + } else if (this.state.phase === RightPanel.Phase.NotificationPanel) { + panel = ; + } else if (this.state.phase === RightPanel.Phase.FilePanel) { + panel = ; } + // TODO: either include this in the DOM again, or move it to other component if (this.props.groupId && this.state.isUserPrivilegedInGroup) { inviteGroup = isPhaseGroup ? ( @@ -372,13 +218,8 @@ module.exports = React.createClass({ return ( ); - }, -}); + } +} diff --git a/src/components/views/right_panel/GroupHeaderButtons.js b/src/components/views/right_panel/GroupHeaderButtons.js new file mode 100644 index 0000000000..6d7d9da3a7 --- /dev/null +++ b/src/components/views/right_panel/GroupHeaderButtons.js @@ -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 [ + , + + ]; + } +} diff --git a/src/components/views/right_panel/HeaderButton.js b/src/components/views/right_panel/HeaderButton.js new file mode 100644 index 0000000000..a01d3444f1 --- /dev/null +++ b/src/components/views/right_panel/HeaderButton.js @@ -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 + + ; + } +} + +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, +}; diff --git a/src/components/views/right_panel/HeaderButtons.js b/src/components/views/right_panel/HeaderButtons.js new file mode 100644 index 0000000000..d47e923806 --- /dev/null +++ b/src/components/views/right_panel/HeaderButtons.js @@ -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
+ { this.renderButtons() } +
; + } +} diff --git a/src/components/views/right_panel/RoomHeaderButtons.js b/src/components/views/right_panel/RoomHeaderButtons.js new file mode 100644 index 0000000000..1d33afd9a8 --- /dev/null +++ b/src/components/views/right_panel/RoomHeaderButtons.js @@ -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 [ + , + , + + ]; + } +} diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 8a98f2a8bc..1cc04b1168 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -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 } +
);