Support full group membership cycle
Apart from knocking, ie. Invite / accept / reject / leave
This commit is contained in:
parent
55998028b4
commit
e77ea352e4
4 changed files with 164 additions and 7 deletions
|
@ -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()}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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?"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue