Support full group membership cycle

Apart from knocking, ie. Invite / accept / reject / leave
This commit is contained in:
David Baker 2017-08-21 19:18:32 +01:00
parent 55998028b4
commit e77ea352e4
4 changed files with 164 additions and 7 deletions

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2017 Vector Creations 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.
@ -183,12 +184,19 @@ export default React.createClass({
editing: false, editing: false,
saving: false, saving: false,
uploadingAvatar: false, uploadingAvatar: false,
membershipBusy: false,
}; };
}, },
componentWillMount: function() { componentWillMount: function() {
this._changeAvatarComponent = null; this._changeAvatarComponent = null;
this._loadGroupFromServer(this.props.groupId); this._loadGroupFromServer(this.props.groupId);
MatrixClientPeg.get().on("Group.myMembership", this._onGroupMyMembership);
},
componentWillUnmount: function() {
MatrixClientPeg.get().removeListener("Group.myMembership", this._onGroupMyMembership);
}, },
componentWillReceiveProps: function(newProps) { componentWillReceiveProps: function(newProps) {
@ -202,6 +210,12 @@ export default React.createClass({
} }
}, },
_onGroupMyMembership: function(group) {
if (group.groupId !== this.props.groupId) return;
this.setState({membershipBusy: false});
},
_loadGroupFromServer: function(groupId) { _loadGroupFromServer: function(groupId) {
MatrixClientPeg.get().getGroupSummary(groupId).done((res) => { MatrixClientPeg.get().getGroupSummary(groupId).done((res) => {
this.setState({ this.setState({
@ -299,6 +313,59 @@ export default React.createClass({
}).done(); }).done();
}, },
_onAcceptInviteClick: function() {
this.setState({membershipBusy: true});
MatrixClientPeg.get().acceptGroupInvite(this.props.groupId).then(() => {
// don't reset membershipBusy here: wait for the membership change to come down the sync
}).catch((e) => {
this.setState({membershipBusy: false});
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Error accepting invite', '', ErrorDialog, {
title: _t("Error"),
description: _t("Unable to accept invite"),
});
});
},
_onRejectInviteClick: function() {
this.setState({membershipBusy: true});
MatrixClientPeg.get().leaveGroup(this.props.groupId).then(() => {
// don't reset membershipBusy here: wait for the membership change to come down the sync
}).catch((e) => {
this.setState({membershipBusy: false});
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Error rejecting invite', '', ErrorDialog, {
title: _t("Error"),
description: _t("Unable to reject invite"),
});
});
},
_onLeaveClick: function() {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Leave Group', '', QuestionDialog, {
title: _t("Leave Group"),
description: _t("Leave %(groupName)s?", {groupName: this.props.groupId}),
button: _t("Leave"),
danger: true,
onFinished: (confirmed) => {
if (!confirmed) return;
this.setState({membershipBusy: true});
MatrixClientPeg.get().leaveGroup(this.props.groupId).then(() => {
// don't reset membershipBusy here: wait for the membership change to come down the sync
}).catch((e) => {
this.setState({membershipBusy: false});
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Error leaving room', '', ErrorDialog, {
title: _t("Error"),
description: _t("Unable to leave room"),
});
});
}
});
},
_getFeaturedRoomsNode() { _getFeaturedRoomsNode() {
const summary = this.state.summary; const summary = this.state.summary;
@ -375,6 +442,50 @@ export default React.createClass({
</div>; </div>;
}, },
_getMembershipSection: function() {
const group = MatrixClientPeg.get().getGroup(this.props.groupId);
if (!group) return null;
if (group.myMembership === 'invite') {
const Spinner = sdk.getComponent("elements.Spinner");
if (this.state.membershipBusy) {
return <div className="mx_GroupView_invitedSection">
<Spinner />
</div>;
}
return <div className="mx_GroupView_invitedSection">
{_t("%(inviter)s has invited you to join this group", {inviter: group.inviter.userId})}
<div className="mx_GroupView_membership_buttonContainer">
<AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
onClick={this._onAcceptInviteClick}
>
{_t('Accept')}
</AccessibleButton>
<AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
onClick={this._onRejectInviteClick}
>
{_t('Decline')}
</AccessibleButton>
</div>
</div>;
} else if (group.myMembership === 'join') {
return <div className="mx_GroupView_invitedSection">
{_t("You are a member of this group")}
<div className="mx_GroupView_membership_buttonContainer">
<AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
onClick={this._onLeaveClick}
>
{_t('Leave')}
</AccessibleButton>
</div>
</div>;
}
return null;
},
render: function() { render: function() {
const GroupAvatar = sdk.getComponent("avatars.GroupAvatar"); const GroupAvatar = sdk.getComponent("avatars.GroupAvatar");
const Loader = sdk.getComponent("elements.Spinner"); const Loader = sdk.getComponent("elements.Spinner");
@ -433,14 +544,14 @@ export default React.createClass({
tabIndex="2" tabIndex="2"
/>; />;
rightButtons.push( rightButtons.push(
<AccessibleButton className="mx_GroupView_saveButton mx_RoomHeader_textButton" <AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
onClick={this._onSaveClick} key="_saveButton" onClick={this._onSaveClick} key="_saveButton"
> >
{_t('Save')} {_t('Save')}
</AccessibleButton> </AccessibleButton>
); );
rightButtons.push( rightButtons.push(
<AccessibleButton className='mx_GroupView_cancelButton' onClick={this._onCancelClick} key="_cancelButton"> <AccessibleButton className='mx_GroupView_textButton' onClick={this._onCancelClick} key="_cancelButton">
<img src="img/cancel.svg" className='mx_filterFlipColor' <img src="img/cancel.svg" className='mx_filterFlipColor'
width="18" height="18" alt={_t("Cancel")}/> width="18" height="18" alt={_t("Cancel")}/>
</AccessibleButton> </AccessibleButton>
@ -475,6 +586,7 @@ export default React.createClass({
description = sanitizedHtmlNode(summary.profile.long_description); description = sanitizedHtmlNode(summary.profile.long_description);
} }
roomBody = <div> roomBody = <div>
{this._getMembershipSection()}
<div className="mx_GroupView_groupDesc">{description}</div> <div className="mx_GroupView_groupDesc">{description}</div>
{this._getFeaturedRoomsNode()} {this._getFeaturedRoomsNode()}
{this._getFeaturedUsersNode()} {this._getFeaturedUsersNode()}

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket 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.
@ -17,6 +18,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import classnames from 'classnames';
export default React.createClass({ export default React.createClass({
displayName: 'QuestionDialog', displayName: 'QuestionDialog',
@ -25,6 +27,7 @@ export default React.createClass({
description: React.PropTypes.node, description: React.PropTypes.node,
extraButtons: React.PropTypes.node, extraButtons: React.PropTypes.node,
button: React.PropTypes.string, button: React.PropTypes.string,
danger: React.PropTypes.bool,
focus: React.PropTypes.bool, focus: React.PropTypes.bool,
onFinished: React.PropTypes.func.isRequired, onFinished: React.PropTypes.func.isRequired,
}, },
@ -36,6 +39,7 @@ export default React.createClass({
extraButtons: null, extraButtons: null,
focus: true, focus: true,
hasCancelButton: true, hasCancelButton: true,
danger: false,
}; };
}, },
@ -54,6 +58,10 @@ export default React.createClass({
{_t("Cancel")} {_t("Cancel")}
</button> </button>
) : null; ) : null;
const buttonClasses = classnames({
mx_Dialog_primary: true,
danger: this.props.danger,
});
return ( return (
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished} <BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
onEnterPressed={ this.onOk } onEnterPressed={ this.onOk }
@ -63,7 +71,7 @@ export default React.createClass({
{this.props.description} {this.props.description}
</div> </div>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" onClick={this.onOk} autoFocus={this.props.focus}> <button className={buttonClasses} onClick={this.onOk} autoFocus={this.props.focus}>
{this.props.button || _t('OK')} {this.props.button || _t('OK')}
</button> </button>
{this.props.extraButtons} {this.props.extraButtons}

View file

@ -41,6 +41,14 @@ module.exports = withMatrixClient(React.createClass({
member: GroupMemberType, member: GroupMemberType,
}, },
getInitialState: function() {
return {
fetching: false,
removingUser: false,
members: null,
}
},
componentWillMount: function() { componentWillMount: function() {
this._fetchMembers(); this._fetchMembers();
}, },
@ -67,11 +75,28 @@ module.exports = withMatrixClient(React.createClass({
action: _t('Remove from group'), action: _t('Remove from group'),
danger: true, danger: true,
onFinished: (proceed) => { onFinished: (proceed) => {
if (!proceed) return;
this.setState({removingUser: true});
this.props.matrixClient.removeUserFromGroup(this.props.groupId, this.props.member.userId).then(() => {
dis.dispatch({
action: "view_user",
member: null
});
}).catch((e) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to remove user from group', '', ErrorDialog, {
title: _t('Error'),
description: _t('Failed to remove user from group'),
});
}).finally(() => {
this.setState({removingUser: false});
});
}, },
}); });
}, },
onCancel: function(e) { _onCancel: function(e) {
dis.dispatch({ dis.dispatch({
action: "view_user", action: "view_user",
member: null member: null
@ -86,7 +111,7 @@ module.exports = withMatrixClient(React.createClass({
}, },
render: function() { render: function() {
if (this.state.fetching) { if (this.state.fetching || this.state.removingUser) {
const Loader = sdk.getComponent("elements.Spinner"); const Loader = sdk.getComponent("elements.Spinner");
return <Loader />; return <Loader />;
} }
@ -140,7 +165,7 @@ module.exports = withMatrixClient(React.createClass({
return ( return (
<div className="mx_MemberInfo"> <div className="mx_MemberInfo">
<GeminiScrollbar autoshow={true}> <GeminiScrollbar autoshow={true}>
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this.onCancel}> <img src="img/cancel.svg" width="18" height="18"/></AccessibleButton> <AccessibleButton className="mx_MemberInfo_cancel" onClick={this._onCancel}> <img src="img/cancel.svg" width="18" height="18"/></AccessibleButton>
<div className="mx_MemberInfo_avatar"> <div className="mx_MemberInfo_avatar">
{avatar} {avatar}
</div> </div>

View file

@ -976,5 +976,17 @@
"Invite new group members": "Invite new group members", "Invite new group members": "Invite new group members",
"Who would you like to add to this group?": "Who would you like to add to this group?", "Who would you like to add to this group?": "Who would you like to add to this group?",
"Name or matrix ID": "Name or matrix ID", "Name or matrix ID": "Name or matrix ID",
"Invite to Group": "Invite to Group" "Invite to Group": "Invite to Group",
"Unable to accept invite": "Unable to accept invite",
"Unable to leave room": "Unable to leave room",
"%(inviter)s has invited you to join this group": "%(inviter)s has invited you to join this group",
"You are a member of this group": "You are a member of this group",
"Leave": "Leave",
"Failed to remove user from group": "Failed to remove user from group",
"Failed to invite the following users to %(groupId)s:": "Failed to invite the following users to %(groupId)s:",
"Failed to invite users group": "Failed to invite users group",
"Failed to invite users to %(groupId)s": "Failed to invite users to %(groupId)s",
"Unable to reject invite": "Unable to reject invite",
"Leave Group": "Leave Group",
"Leave %(groupName)s?": "Leave %(groupName)s?"
} }