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/UserSettingsStore.js b/src/UserSettingsStore.js index 68a1ba229f..1d1924cd23 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -33,11 +33,17 @@ export default { // XXX: Always use default, ignore localStorage and remove from labs override: true, }, + { + name: "-", + id: 'feature_flair', + default: false, + }, ], // horrible but it works. The locality makes this somewhat more palatable. doTranslations: function() { this.LABS_FEATURES[0].name = _t("Matrix Apps"); + this.LABS_FEATURES[1].name = _t("Flair"); }, loadProfileInfo: function() { 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")} +
+ + {_t("Leave")} + +
+
; + } + + 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( + {_t("Cancel")}/ - - ; + , + ); roomBody =