Merge pull request #4830 from vector-im/dbkr/group_userlist

Group Membership UI
This commit is contained in:
Luke Barnard 2017-09-19 13:26:02 +01:00 committed by GitHub
commit fbcccd8be0
7 changed files with 245 additions and 89 deletions

View file

@ -1,5 +1,7 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,8 +16,6 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
'use strict';
import React from 'react'; import React from 'react';
import { _t } from 'matrix-react-sdk/lib/languageHandler'; import { _t } from 'matrix-react-sdk/lib/languageHandler';
import sdk from 'matrix-react-sdk'; import sdk from 'matrix-react-sdk';
@ -26,26 +26,31 @@ import Analytics from 'matrix-react-sdk/lib/Analytics';
import rate_limited_func from 'matrix-react-sdk/lib/ratelimitedfunc'; import rate_limited_func from 'matrix-react-sdk/lib/ratelimitedfunc';
import Modal from 'matrix-react-sdk/lib/Modal'; import Modal from 'matrix-react-sdk/lib/Modal';
import AccessibleButton from 'matrix-react-sdk/lib/components/views/elements/AccessibleButton'; import AccessibleButton from 'matrix-react-sdk/lib/components/views/elements/AccessibleButton';
import { showGroupInviteDialog } from 'matrix-react-sdk/lib/GroupInvite';
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'RightPanel', displayName: 'RightPanel',
propTypes: { propTypes: {
// TODO: This should not be a prop, it should be received from the RoomViewStore // 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 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 collapsed: React.PropTypes.bool, // currently unused property to request for a minimized view of the panel
}, },
Phase: { Phase: {
MemberList: 'MemberList', RoomMemberList: 'RoomMemberList',
GroupMemberList: 'GroupMemberList',
FilePanel: 'FilePanel', FilePanel: 'FilePanel',
NotificationPanel: 'NotificationPanel', NotificationPanel: 'NotificationPanel',
MemberInfo: 'MemberInfo', RoomMemberInfo: 'RoomMemberInfo',
GroupMemberInfo: 'GroupMemberInfo',
}, },
componentWillMount: function() { componentWillMount: function() {
this.dispatcherRef = dis.register(this.onAction); this.dispatcherRef = dis.register(this.onAction);
var cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
cli.on("RoomState.members", this.onRoomStateMember); cli.on("RoomState.members", this.onRoomStateMember);
}, },
@ -57,14 +62,20 @@ module.exports = React.createClass({
}, },
getInitialState: function() { getInitialState: function() {
if (this.props.groupId) {
return { return {
phase: this.Phase.MemberList phase: this.Phase.GroupMemberList,
}; };
} else {
return {
phase: this.Phase.RoomMemberList,
};
}
}, },
onMemberListButtonClick: function() { onMemberListButtonClick: function() {
Analytics.trackEvent('Right Panel', 'Member List Button', 'click'); Analytics.trackEvent('Right Panel', 'Member List Button', 'click');
this.setState({ phase: this.Phase.MemberList }); this.setState({ phase: this.Phase.RoomMemberList });
}, },
onFileListButtonClick: function() { onFileListButtonClick: function() {
@ -89,19 +100,23 @@ module.exports = React.createClass({
return; return;
} }
// call ChatInviteDialog if (this.state.phase === this.Phase.GroupMemberList) {
showGroupInviteDialog(this.props.groupId);
} else {
// call UserPickerDialog
dis.dispatch({ dis.dispatch({
action: 'view_invite', action: 'view_invite',
roomId: this.props.roomId, roomId: this.props.roomId,
}); });
}
}, },
onRoomStateMember: function(ev, state, member) { onRoomStateMember: function(ev, state, member) {
// redraw the badge on the membership list // redraw the badge on the membership list
if (this.state.phase == this.Phase.MemberList && member.roomId === this.props.roomId) { if (this.state.phase == this.Phase.RoomMemberList && member.roomId === this.props.roomId) {
this._delayedUpdate(); this._delayedUpdate();
} }
else if (this.state.phase === this.Phase.MemberInfo && member.roomId === this.props.roomId && else if (this.state.phase === this.Phase.RoomMemberInfo && member.roomId === this.props.roomId &&
member.userId === this.state.member.userId) { member.userId === this.state.member.userId) {
// refresh the member info (e.g. new power level) // refresh the member info (e.g. new power level)
this._delayedUpdate(); this._delayedUpdate();
@ -119,39 +134,55 @@ module.exports = React.createClass({
}); });
if (payload.member) { if (payload.member) {
this.setState({ this.setState({
phase: this.Phase.MemberInfo, 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,
groupId: payload.groupId,
member: payload.member, member: payload.member,
}); });
} }
else { }
} else if (payload.action === "view_group") {
this.setState({ this.setState({
phase: this.Phase.MemberList phase: this.Phase.GroupMemberList,
groupId: payload.groupId,
member: null,
}); });
} } else if (payload.action === "view_group_user") {
}
else if (payload.action === "view_room") {
if (this.state.phase === this.Phase.MemberInfo) {
this.setState({ this.setState({
phase: this.Phase.MemberList phase: this.Phase.GroupMemberInfo,
groupId: payload.groupId,
member: payload.member,
});
} else if (payload.action === "view_room") {
this.setState({
phase: this.Phase.RoomMemberList
}); });
}
} }
}, },
render: function() { render: function() {
var MemberList = sdk.getComponent('rooms.MemberList'); const MemberList = sdk.getComponent('rooms.MemberList');
var NotificationPanel = sdk.getComponent('structures.NotificationPanel'); const GroupMemberList = sdk.getComponent('groups.GroupMemberList');
var FilePanel = sdk.getComponent('structures.FilePanel'); const NotificationPanel = sdk.getComponent('structures.NotificationPanel');
var TintableSvg = sdk.getComponent("elements.TintableSvg"); const FilePanel = sdk.getComponent('structures.FilePanel');
var buttonGroup; const TintableSvg = sdk.getComponent("elements.TintableSvg");
var inviteGroup; let inviteGroup;
var panel; let panel;
var filesHighlight; let filesHighlight;
var membersHighlight; let membersHighlight;
var notificationsHighlight; let notificationsHighlight;
if (!this.props.collapsed) { if (!this.props.collapsed) {
if (this.state.phase == this.Phase.MemberList || this.state.phase === this.Phase.MemberInfo) { if (this.state.phase == this.Phase.RoomMemberList || this.state.phase === this.Phase.RoomMemberInfo) {
membersHighlight = <div className="mx_RightPanel_headerButton_highlight"></div>; membersHighlight = <div className="mx_RightPanel_headerButton_highlight"></div>;
} }
else if (this.state.phase == this.Phase.FilePanel) { else if (this.state.phase == this.Phase.FilePanel) {
@ -162,11 +193,11 @@ module.exports = React.createClass({
} }
} }
var membersBadge; let membersBadge;
if ((this.state.phase == this.Phase.MemberList || this.state.phase === this.Phase.MemberInfo) && this.props.roomId) { if ((this.state.phase == this.Phase.RoomMemberList || this.state.phase === this.Phase.RoomMemberInfo) && this.props.roomId) {
var cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
var room = cli.getRoom(this.props.roomId); const room = cli.getRoom(this.props.roomId);
var user_is_in_room; let user_is_in_room;
if (room) { if (room) {
membersBadge = room.getJoinedMembers().length; membersBadge = room.getJoinedMembers().length;
user_is_in_room = room.hasMembershipState( user_is_in_room = room.hasMembershipState(
@ -186,48 +217,72 @@ module.exports = React.createClass({
} }
let headerButtons = [];
if (this.props.roomId) { if (this.props.roomId) {
buttonGroup = headerButtons.push(
<div className="mx_RightPanel_headerButtonGroup"> <AccessibleButton className="mx_RightPanel_headerButton" key="_membersButton"
<AccessibleButton className="mx_RightPanel_headerButton"
title={ _t('Members') } onClick={ this.onMemberListButtonClick }> title={ _t('Members') } onClick={ this.onMemberListButtonClick }>
<div className="mx_RightPanel_headerButton_badge">{ membersBadge ? membersBadge : <span>&nbsp;</span>}</div> <div className="mx_RightPanel_headerButton_badge">{ membersBadge ? membersBadge : <span>&nbsp;</span>}</div>
<TintableSvg src="img/icons-people.svg" width="25" height="25"/> <TintableSvg src="img/icons-people.svg" width="25" height="25"/>
{ membersHighlight } { membersHighlight }
</AccessibleButton> </AccessibleButton>
);
headerButtons.push(
<AccessibleButton <AccessibleButton
className="mx_RightPanel_headerButton mx_RightPanel_filebutton" className="mx_RightPanel_headerButton mx_RightPanel_filebutton" key="_filesButton"
title={ _t('Files') } onClick={ this.onFileListButtonClick }> title={ _t('Files') } onClick={ this.onFileListButtonClick }>
<div className="mx_RightPanel_headerButton_badge">&nbsp;</div> <div className="mx_RightPanel_headerButton_badge">&nbsp;</div>
<TintableSvg src="img/icons-files.svg" width="25" height="25"/> <TintableSvg src="img/icons-files.svg" width="25" height="25"/>
{ filesHighlight } { filesHighlight }
</AccessibleButton> </AccessibleButton>
);
headerButtons.push(
<AccessibleButton <AccessibleButton
className="mx_RightPanel_headerButton mx_RightPanel_notificationbutton" className="mx_RightPanel_headerButton mx_RightPanel_notificationbutton" key="_notifsButton"
title={ _t('Notifications') } onClick={ this.onNotificationListButtonClick }> title={ _t('Notifications') } onClick={ this.onNotificationListButtonClick }>
<div className="mx_RightPanel_headerButton_badge">&nbsp;</div> <div className="mx_RightPanel_headerButton_badge">&nbsp;</div>
<TintableSvg src="img/icons-notifications.svg" width="25" height="25"/> <TintableSvg src="img/icons-notifications.svg" width="25" height="25"/>
{ notificationsHighlight } { notificationsHighlight }
</AccessibleButton> </AccessibleButton>
<div className="mx_RightPanel_headerButton mx_RightPanel_collapsebutton" title={ _t("Hide panel") } onClick={ this.onCollapseClick }> );
}
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(
<div className="mx_RightPanel_headerButton mx_RightPanel_collapsebutton" key="_minimizeButton"
title={ _t("Hide panel") } onClick={ this.onCollapseClick }
>
<TintableSvg src="img/minimise.svg" width="10" height="16"/> <TintableSvg src="img/minimise.svg" width="10" height="16"/>
</div> </div>
</div>; );
} }
if (!this.props.collapsed) { if (!this.props.collapsed) {
if(this.props.roomId && this.state.phase == this.Phase.MemberList) { if (this.props.roomId && this.state.phase == this.Phase.RoomMemberList) {
panel = <MemberList roomId={this.props.roomId} key={this.props.roomId} /> panel = <MemberList roomId={this.props.roomId} key={this.props.roomId} />
} } else if (this.props.groupId && this.state.phase == this.Phase.GroupMemberList) {
else if(this.state.phase == this.Phase.MemberInfo) { panel = <GroupMemberList groupId={this.props.groupId} key={this.props.groupId} />;
var MemberInfo = sdk.getComponent('rooms.MemberInfo'); inviteGroup = (
<AccessibleButton className="mx_RightPanel_invite" onClick={ this.onInviteButtonClick } >
<div className="mx_RightPanel_icon" >
<TintableSvg src="img/icon-invite-people.svg" width="35" height="35" />
</div>
<div className="mx_RightPanel_message">{ _t('Invite to this group') }</div>
</AccessibleButton>
);
} else if (this.state.phase == this.Phase.RoomMemberInfo) {
const MemberInfo = sdk.getComponent('rooms.MemberInfo');
panel = <MemberInfo member={this.state.member} key={this.props.roomId || this.state.member.userId} /> panel = <MemberInfo member={this.state.member} key={this.props.roomId || this.state.member.userId} />
} } else if (this.state.phase == this.Phase.GroupMemberInfo) {
else if (this.state.phase == this.Phase.NotificationPanel) { const GroupMemberInfo = sdk.getComponent('groups.GroupMemberInfo');
panel = <NotificationPanel /> panel = <GroupMemberInfo groupMember={this.state.member} groupId={this.props.groupId} key={this.state.member.user_id} />;
} } else if (this.state.phase == this.Phase.NotificationPanel) {
else if (this.state.phase == this.Phase.FilePanel) { panel = <NotificationPanel />;
panel = <FilePanel roomId={this.props.roomId} /> } else if (this.state.phase == this.Phase.FilePanel) {
panel = <FilePanel roomId={this.props.roomId} />;
} }
} }
@ -235,7 +290,7 @@ module.exports = React.createClass({
panel = <div className="mx_RightPanel_blank"></div>; panel = <div className="mx_RightPanel_blank"></div>;
} }
var classes = "mx_RightPanel mx_fadable"; let classes = "mx_RightPanel mx_fadable";
if (this.props.collapsed) { if (this.props.collapsed) {
classes += " collapsed"; classes += " collapsed";
} }
@ -243,7 +298,9 @@ module.exports = React.createClass({
return ( return (
<aside className={classes} style={{ opacity: this.props.opacity }}> <aside className={classes} style={{ opacity: this.props.opacity }}>
<div className="mx_RightPanel_header"> <div className="mx_RightPanel_header">
{ buttonGroup } <div className="mx_RightPanel_headerButtonGroup">
{headerButtons}
</div>
</div> </div>
{ panel } { panel }
<div className="mx_RightPanel_footer"> <div className="mx_RightPanel_footer">

View file

@ -88,6 +88,7 @@ var RoomSubList = React.createClass({
searchFilter: React.PropTypes.string, searchFilter: React.PropTypes.string,
emptyContent: React.PropTypes.node, // content shown if the list is empty emptyContent: React.PropTypes.node, // content shown if the list is empty
headerItems: React.PropTypes.node, // content shown in the sublist header headerItems: React.PropTypes.node, // content shown in the sublist header
extraTiles: React.PropTypes.arrayOf(React.PropTypes.node), // extra elements added beneath tiles
}, },
getInitialState: function() { getInitialState: function() {
@ -102,6 +103,7 @@ var RoomSubList = React.createClass({
return { return {
onHeaderClick: function() {}, // NOP onHeaderClick: function() {}, // NOP
onShowMoreRooms: function() {}, // NOP onShowMoreRooms: function() {}, // NOP
extraTiles: [],
isInvite: false, isInvite: false,
}; };
}, },
@ -534,13 +536,14 @@ var RoomSubList = React.createClass({
var label = this.props.collapsed ? null : this.props.label; var label = this.props.collapsed ? null : this.props.label;
let content; let content;
if (this.state.sortedList.length == 0 && !this.props.searchFilter) { if (this.state.sortedList.length == 0 && !this.props.searchFilter && !this.props.extraTiles) {
content = this.props.emptyContent; content = this.props.emptyContent;
} else { } else {
content = this.makeRoomTiles(); content = this.makeRoomTiles();
content.push(...this.props.extraTiles);
} }
if (this.state.sortedList.length > 0 || this.props.editable) { if (this.state.sortedList.length > 0 || this.props.extraTiles.length > 0 || this.props.editable) {
var subList; var subList;
var classes = "mx_RoomSubList"; var classes = "mx_RoomSubList";

View file

@ -217,5 +217,6 @@
"Remember, you can always set an email address in user settings if you change your mind.": "Remember, you can always set an email address in user settings if you change your mind.", "Remember, you can always set an email address in user settings if you change your mind.": "Remember, you can always set an email address in user settings if you change your mind.",
"To return to your account in future you need to <u>set a password</u>": "To return to your account in future you need to <u>set a password</u>", "To return to your account in future you need to <u>set a password</u>": "To return to your account in future you need to <u>set a password</u>",
"Set Password": "Set Password", "Set Password": "Set Password",
"Couldn't load home page": "Couldn't load home page" "Couldn't load home page": "Couldn't load home page",
"Invite to this group": "Invite to this group"
} }

View file

@ -32,6 +32,7 @@
@import "./matrix-react-sdk/views/elements/_ProgressBar.scss"; @import "./matrix-react-sdk/views/elements/_ProgressBar.scss";
@import "./matrix-react-sdk/views/elements/_RichText.scss"; @import "./matrix-react-sdk/views/elements/_RichText.scss";
@import "./matrix-react-sdk/views/elements/_RoleButton.scss"; @import "./matrix-react-sdk/views/elements/_RoleButton.scss";
@import "./matrix-react-sdk/views/groups/_GroupInviteTile.scss";
@import "./matrix-react-sdk/views/login/_InteractiveAuthEntryComponents.scss"; @import "./matrix-react-sdk/views/login/_InteractiveAuthEntryComponents.scss";
@import "./matrix-react-sdk/views/login/_ServerConfig.scss"; @import "./matrix-react-sdk/views/login/_ServerConfig.scss";
@import "./matrix-react-sdk/views/messages/_MEmoteBody.scss"; @import "./matrix-react-sdk/views/messages/_MEmoteBody.scss";

View file

@ -70,8 +70,16 @@ limitations under the License.
flex: 1; flex: 1;
} }
.mx_GroupView_saveButton, .mx_GroupView_cancelButton { .mx_GroupView_header_rightCol {
display: table-cell; display: flex;
}
.mx_GroupView_membership_buttonContainer {
margin-top: 10px;
}
.mx_GroupView_textButton {
display: inline-block;
} }
.mx_GroupView_header_groupid { .mx_GroupView_header_groupid {
@ -126,6 +134,16 @@ limitations under the License.
top: 5px; top: 5px;
} }
.mx_GroupView_invitedSection {
width: 70%;
padding: 20px;
border: 1px solid $group-alert-color;
margin-left: auto;
margin-right: auto;
margin-bottom: 20px;
text-align: center;
}
.mx_GroupView_featuredThings { .mx_GroupView_featuredThings {
margin-top: 20px; margin-top: 20px;
} }

View file

@ -0,0 +1,74 @@
/*
Copyright 2017 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.
*/
.mx_GroupInviteTile {
position: relative;
cursor: pointer;
font-size: 13px;
display: block;
height: 34px;
}
.mx_GroupInviteTile_nameContainer {
display: inline-block;
width: 180px;
height: 24px;
}
.mx_GroupInviteTile_avatarContainer {
display: inline-block;
padding-top: 5px;
padding-bottom: 5px;
padding-left: 16px;
padding-right: 6px;
width: 24px;
height: 24px;
vertical-align: middle;
}
.mx_GroupInviteTile_name {
display: inline-block;
position: relative;
width: 165px;
vertical-align: middle;
padding-left: 6px;
padding-right: 6px;
padding-top: 2px;
padding-bottom: 3px;
color: $roomtile-name-color;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mx_GroupInviteTile_badge {
display: inline-block;
min-width: 15px;
height: 15px;
position: absolute;
right: 8px; /*gutter */
top: 9px;
border-radius: 8px;
color: $accent-fg-color;
background-color: $group-alert-color;
font-weight: 600;
font-size: 10px;
text-align: center;
padding-top: 1px;
padding-left: 4px;
padding-right: 4px;
}

View file

@ -22,6 +22,8 @@ $warning-color: #ff0064;
$mention-user-pill-bg-color: #ff0064; $mention-user-pill-bg-color: #ff0064;
$other-user-pill-bg-color: rgba(0, 0, 0, 0.1); $other-user-pill-bg-color: rgba(0, 0, 0, 0.1);
$group-alert-color: #774f7e;
$preview-bar-bg-color: #f7f7f7; $preview-bar-bg-color: #f7f7f7;
// left-panel style muted accent color // left-panel style muted accent color