diff --git a/src/GroupInvite.js b/src/GroupInvite.js
new file mode 100644
index 0000000000..c526ace188
--- /dev/null
+++ b/src/GroupInvite.js
@@ -0,0 +1,67 @@
+/*
+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.
+*/
+
+import Modal from './Modal';
+import sdk from './';
+import MultiInviter from './utils/MultiInviter';
+import { _t } from './languageHandler';
+
+export function showGroupInviteDialog(groupId) {
+ const UserPickerDialog = sdk.getComponent("dialogs.UserPickerDialog");
+ Modal.createTrackedDialog('Group Invite', '', UserPickerDialog, {
+ title: _t('Invite new group members'),
+ description: _t("Who would you like to add to this group?"),
+ placeholder: _t("Name or matrix ID"),
+ button: _t("Invite to Group"),
+ validAddressTypes: ['mx'],
+ onFinished: (success, addrs) => {
+ if (!success) return;
+
+ _onGroupInviteFinished(groupId, addrs);
+ },
+ });
+}
+
+function _onGroupInviteFinished(groupId, addrs) {
+ const multiInviter = new MultiInviter(groupId);
+
+ const addrTexts = addrs.map((addr) => addr.address);
+
+ multiInviter.invite(addrTexts).then((completionStates) => {
+ // Show user any errors
+ const errorList = [];
+ for (const addr of Object.keys(completionStates)) {
+ if (addrs[addr] === "error") {
+ errorList.push(addr);
+ }
+ }
+
+ if (errorList.length > 0) {
+ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ Modal.createTrackedDialog('Failed to invite the following users to the group', '', ErrorDialog, {
+ title: _t("Failed to invite the following users to %(groupId)s:", {groupId: groupId}),
+ description: errorList.join(", "),
+ });
+ }
+ }).catch((err) => {
+ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ Modal.createTrackedDialog('Failed to invite users to group', '', ErrorDialog, {
+ title: _t("Failed to invite users group"),
+ description: _t("Failed to invite users to %(groupId)s", {groupId: groupId}),
+ });
+ });
+}
+
diff --git a/src/Invite.js b/src/RoomInvite.js
similarity index 100%
rename from src/Invite.js
rename to src/RoomInvite.js
diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js
index 20fc4841ba..2c24c398e0 100644
--- a/src/components/structures/GroupView.js
+++ b/src/components/structures/GroupView.js
@@ -1,5 +1,6 @@
/*
Copyright 2017 Vector Creations Ltd.
+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.
@@ -183,12 +184,19 @@ export default React.createClass({
editing: false,
saving: false,
uploadingAvatar: false,
+ membershipBusy: false,
};
},
componentWillMount: function() {
this._changeAvatarComponent = null;
this._loadGroupFromServer(this.props.groupId);
+
+ MatrixClientPeg.get().on("Group.myMembership", this._onGroupMyMembership);
+ },
+
+ componentWillUnmount: function() {
+ MatrixClientPeg.get().removeListener("Group.myMembership", this._onGroupMyMembership);
},
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) {
MatrixClientPeg.get().getGroupSummary(groupId).done((res) => {
this.setState({
@@ -216,6 +230,10 @@ export default React.createClass({
});
},
+ _onShowRhsClick: function(ev) {
+ dis.dispatch({ action: 'show_right_panel' });
+ },
+
_onEditClick: function() {
this.setState({
editing: true,
@@ -295,6 +313,59 @@ export default React.createClass({
}).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() {
const summary = this.state.summary;
@@ -371,6 +442,50 @@ export default React.createClass({
;
},
+ _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
+
+
;
+ }
+
+ return
+ {_t("%(inviter)s has invited you to join this group", {inviter: group.inviter.userId})}
+
+
+ {_t("Accept")}
+
+
+ {_t("Decline")}
+
+
+
;
+ } else if (group.myMembership === 'join') {
+ return
+ {_t("You are a member of this group")}
+
+
;
+ }
+
+ return null;
+ },
+
render: function() {
const GroupAvatar = sdk.getComponent("avatars.GroupAvatar");
const Loader = sdk.getComponent("elements.Spinner");
@@ -384,8 +499,8 @@ export default React.createClass({
let avatarNode;
let nameNode;
let shortDescNode;
- let rightButtons;
let roomBody;
+ const rightButtons = [];
const headerClasses = {
mx_GroupView_header: true,
};
@@ -428,15 +543,19 @@ export default React.createClass({
placeholder={_t('Description')}
tabIndex="2"
/>;
- rightButtons =
-
+ rightButtons.push(
+
{_t('Save')}
-
-
+ ,
+ );
+ rightButtons.push(
+
-
- ;
+ ,
+ );
roomBody = ;
- // disabled until editing works
- rightButtons =
-
- ;
+ rightButtons.push(
+
+
+ ,
+ );
+ if (this.props.collapsedRhs) {
+ rightButtons.push(
+
+
+ ,
+ );
+ }
headerClasses.mx_GroupView_header_view = true;
}
diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js
index 6adea56a23..c6ac8e58cd 100644
--- a/src/components/structures/LoggedInView.js
+++ b/src/components/structures/LoggedInView.js
@@ -241,10 +241,10 @@ export default React.createClass({
eventPixelOffset={this.props.initialEventPixelOffset}
key={this.props.currentRoomId || 'roomview'}
opacity={this.props.middleOpacity}
- collapsedRhs={this.props.collapse_rhs}
+ collapsedRhs={this.props.collapseRhs}
ConferenceHandler={this.props.ConferenceHandler}
/>;
- if (!this.props.collapse_rhs) right_panel = ;
+ if (!this.props.collapseRhs) right_panel = ;
break;
case PageTypes.UserSettings:
@@ -255,7 +255,7 @@ export default React.createClass({
referralBaseUrl={this.props.config.referralBaseUrl}
teamToken={this.props.teamToken}
/>;
- if (!this.props.collapse_rhs) right_panel = ;
+ if (!this.props.collapseRhs) right_panel = ;
break;
case PageTypes.MyGroups:
@@ -265,9 +265,9 @@ export default React.createClass({
case PageTypes.CreateRoom:
page_element = ;
- if (!this.props.collapse_rhs) right_panel = ;
+ if (!this.props.collapseRhs) right_panel = ;
break;
case PageTypes.RoomDirectory:
@@ -300,8 +300,9 @@ export default React.createClass({
case PageTypes.GroupView:
page_element = ;
- //right_panel = ;
+ if (!this.props.collapseRhs) right_panel = ;
break;
}
@@ -333,7 +334,7 @@ export default React.createClass({
diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js
index 1bdfb3e5f9..4787d76be1 100644
--- a/src/components/structures/MatrixChat.js
+++ b/src/components/structures/MatrixChat.js
@@ -32,7 +32,7 @@ import dis from "../../dispatcher";
import Modal from "../../Modal";
import Tinter from "../../Tinter";
import sdk from '../../index';
-import { showStartChatInviteDialog, showRoomInviteDialog } from '../../Invite';
+import { showStartChatInviteDialog, showRoomInviteDialog } from '../../RoomInvite';
import * as Rooms from '../../Rooms';
import linkifyMatrix from "../../linkify-matrix";
import * as Lifecycle from '../../Lifecycle';
@@ -143,8 +143,8 @@ module.exports = React.createClass({
// If we're trying to just view a user ID (i.e. /user URL), this is it
viewUserId: null,
- collapse_lhs: false,
- collapse_rhs: false,
+ collapseLhs: false,
+ collapseRhs: false,
leftOpacity: 1.0,
middleOpacity: 1.0,
rightOpacity: 1.0,
@@ -434,7 +434,7 @@ module.exports = React.createClass({
break;
case 'view_user':
// FIXME: ugly hack to expand the RightPanel and then re-dispatch.
- if (this.state.collapse_rhs) {
+ if (this.state.collapseRhs) {
setTimeout(()=>{
dis.dispatch({
action: 'show_right_panel',
@@ -516,22 +516,22 @@ module.exports = React.createClass({
break;
case 'hide_left_panel':
this.setState({
- collapse_lhs: true,
+ collapseLhs: true,
});
break;
case 'show_left_panel':
this.setState({
- collapse_lhs: false,
+ collapseLhs: false,
});
break;
case 'hide_right_panel':
this.setState({
- collapse_rhs: true,
+ collapseRhs: true,
});
break;
case 'show_right_panel':
this.setState({
- collapse_rhs: false,
+ collapseRhs: false,
});
break;
case 'ui_opacity': {
@@ -993,8 +993,8 @@ module.exports = React.createClass({
this.setStateForNewView({
view: VIEWS.LOGIN,
ready: false,
- collapse_lhs: false,
- collapse_rhs: false,
+ collapseLhs: false,
+ collapseRhs: false,
currentRoomId: null,
page_type: PageTypes.RoomDirectory,
});
diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js
index a4443430f4..e88fc869fa 100644
--- a/src/components/views/avatars/BaseAvatar.js
+++ b/src/components/views/avatars/BaseAvatar.js
@@ -14,10 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-'use strict';
-
-var React = require('react');
-var AvatarLogic = require("../../../Avatar");
+import React from 'react';
+import AvatarLogic from '../../../Avatar';
import sdk from '../../../index';
import AccessibleButton from '../elements/AccessibleButton';
diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.js b/src/components/views/dialogs/ConfirmUserActionDialog.js
index b10df3ccef..11fba1a322 100644
--- a/src/components/views/dialogs/ConfirmUserActionDialog.js
+++ b/src/components/views/dialogs/ConfirmUserActionDialog.js
@@ -18,6 +18,7 @@ import React from 'react';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import classnames from 'classnames';
+import { GroupMemberType } from '../../../groups';
/*
* A dialog for confirming an operation on another user.
@@ -30,7 +31,10 @@ import classnames from 'classnames';
export default React.createClass({
displayName: 'ConfirmUserActionDialog',
propTypes: {
- member: React.PropTypes.object.isRequired, // matrix-js-sdk member object
+ // matrix-js-sdk (room) member object. Supply either this or 'groupMember'
+ member: React.PropTypes.object,
+ // group member object. Supply either this or 'member'
+ groupMember: GroupMemberType,
action: React.PropTypes.string.isRequired, // eg. 'Ban'
// Whether to display a text field for a reason
@@ -69,6 +73,7 @@ export default React.createClass({
render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
+ const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
const title = _t("%(actionVerb)s this person?", { actionVerb: this.props.action});
const confirmButtonClass = classnames({
@@ -91,6 +96,20 @@ export default React.createClass({
);
}
+ let avatar;
+ let name;
+ let userId;
+ if (this.props.member) {
+ avatar = ;
+ name = this.props.member.name;
+ userId = this.props.member.userId;
+ } else {
+ // we don't get this info from the API yet
+ avatar = ;
+ name = this.props.groupMember.userId;
+ userId = this.props.groupMember.userId;
+ }
+
return (
-
+ {avatar}
-
{this.props.member.name}
-
{this.props.member.userId}
+
{name}
+
{userId}
{reasonBox}
diff --git a/src/components/views/dialogs/QuestionDialog.js b/src/components/views/dialogs/QuestionDialog.js
index ec9b95d7f7..22e68e3ac3 100644
--- a/src/components/views/dialogs/QuestionDialog.js
+++ b/src/components/views/dialogs/QuestionDialog.js
@@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
+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.
@@ -17,6 +18,7 @@ limitations under the License.
import React from 'react';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
+import classnames from 'classnames';
export default React.createClass({
displayName: 'QuestionDialog',
@@ -25,6 +27,7 @@ export default React.createClass({
description: React.PropTypes.node,
extraButtons: React.PropTypes.node,
button: React.PropTypes.string,
+ danger: React.PropTypes.bool,
focus: React.PropTypes.bool,
onFinished: React.PropTypes.func.isRequired,
},
@@ -36,6 +39,7 @@ export default React.createClass({
extraButtons: null,
focus: true,
hasCancelButton: true,
+ danger: false,
};
},
@@ -54,6 +58,10 @@ export default React.createClass({
{_t("Cancel")}
) : null;
+ const buttonClasses = classnames({
+ mx_Dialog_primary: true,
+ danger: this.props.danger,
+ });
return (
-