From ddab8d7b5c70d5d3ab2490a656693e08ade3acb8 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Tue, 26 Sep 2017 14:49:13 +0100 Subject: [PATCH] Implement "Add room to group" feature --- src/{GroupInvite.js => GroupAddressPicker.js} | 46 +++++- .../views/dialogs/AddressPickerDialog.js | 29 +++- .../views/groups/GroupMemberList.js | 2 +- src/components/views/groups/GroupRoomList.js | 143 ++++++++++++++++++ src/components/views/groups/GroupRoomTile.js | 89 +++++++++++ src/groups.js | 16 ++ src/i18n/strings/en_EN.json | 7 +- 7 files changed, 328 insertions(+), 4 deletions(-) rename src/{GroupInvite.js => GroupAddressPicker.js} (59%) create mode 100644 src/components/views/groups/GroupRoomList.js create mode 100644 src/components/views/groups/GroupRoomTile.js diff --git a/src/GroupInvite.js b/src/GroupAddressPicker.js similarity index 59% rename from src/GroupInvite.js rename to src/GroupAddressPicker.js index e04e90d751..c8cff0ab3e 100644 --- a/src/GroupInvite.js +++ b/src/GroupAddressPicker.js @@ -18,11 +18,12 @@ import Modal from './Modal'; import sdk from './'; import MultiInviter from './utils/MultiInviter'; import { _t } from './languageHandler'; +import MatrixClientPeg from './MatrixClientPeg'; export function showGroupInviteDialog(groupId) { const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog"); Modal.createTrackedDialog('Group Invite', '', AddressPickerDialog, { - title: _t('Invite new group members'), + 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"), @@ -35,6 +36,25 @@ export function showGroupInviteDialog(groupId) { }); } +export function showGroupAddRoomDialog(groupId) { + return new Promise((resolve, reject) => { + const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog"); + Modal.createTrackedDialog('Add Rooms to Group', '', AddressPickerDialog, { + title: _t("Add rooms to the group"), + description: _t("Which rooms would you like to add to this group?"), + placeholder: _t("Room name or alias"), + button: _t("Add to group"), + pickerType: 'room', + validAddressTypes: ['mx'], + onFinished: (success, addrs) => { + if (!success) return; + + _onGroupAddRoomFinished(groupId, addrs).then(resolve, reject); + }, + }); + }); +} + function _onGroupInviteFinished(groupId, addrs) { const multiInviter = new MultiInviter(groupId); @@ -65,3 +85,27 @@ function _onGroupInviteFinished(groupId, addrs) { }); } +function _onGroupAddRoomFinished(groupId, addrs) { + const errorList = []; + return Promise.all(addrs.map((addr) => { + return MatrixClientPeg.get() + .addRoomToGroup(groupId, addr.address) + .catch(() => { errorList.push(addr.address); }) + .reflect(); + })).then(() => { + if (errorList.length === 0) { + return; + } + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog( + 'Failed to add the following room to the group', + '', ErrorDialog, + { + title: _t( + "Failed to add the following rooms to %(groupId)s:", + {groupId}, + ), + description: errorList.join(", "), + }); + }); +} diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js index 741ff1fe8c..523dd9ad0d 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.js @@ -155,7 +155,7 @@ module.exports = React.createClass({ if (this.props.groupId) { this._doNaiveGroupRoomSearch(query); } else { - console.error('Room searching only implemented for groups'); + this._doRoomSearch(query); } } else { console.error('Unknown pickerType', this.props.pickerType); @@ -264,6 +264,33 @@ module.exports = React.createClass({ }); }, + _doRoomSearch: function(query) { + MatrixClientPeg.get().publicRooms({ + filter: { + generic_search_term: query, + }, + }).then((resp) => { + const results = []; + resp.chunk.forEach((r) => { + results.push({ + room_id: r.room_id, + avatar_url: r.avatar_url, + name: r.name, + }); + }); + this._processResults(results, query); + }).catch((err) => { + console.error('Error whilst searching public rooms: ', err); + this.setState({ + searchError: err.errcode ? err.message : _t('Something went wrong!'), + }); + }).done(() => { + this.setState({ + busy: false, + }); + }); + }, + _doUserDirectorySearch: function(query) { this.setState({ busy: true, diff --git a/src/components/views/groups/GroupMemberList.js b/src/components/views/groups/GroupMemberList.js index 273a04da20..809dac93d0 100644 --- a/src/components/views/groups/GroupMemberList.js +++ b/src/components/views/groups/GroupMemberList.js @@ -131,7 +131,7 @@ export default withMatrixClient(React.createClass({ const inputBox = (
-
diff --git a/src/components/views/groups/GroupRoomList.js b/src/components/views/groups/GroupRoomList.js new file mode 100644 index 0000000000..0a9dbb4d76 --- /dev/null +++ b/src/components/views/groups/GroupRoomList.js @@ -0,0 +1,143 @@ +/* +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 React from 'react'; +import { _t } from '../../../languageHandler'; +import sdk from '../../../index'; +import { groupRoomFromApiObject } from '../../../groups'; +import GeminiScrollbar from 'react-gemini-scrollbar'; +import PropTypes from 'prop-types'; +import {MatrixClient} from 'matrix-js-sdk'; + +const INITIAL_LOAD_NUM_ROOMS = 30; + +export default React.createClass({ + contextTypes: { + matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired, + }, + + propTypes: { + groupId: PropTypes.string.isRequired, + }, + + getInitialState: function() { + return { + fetching: false, + rooms: null, + truncateAt: INITIAL_LOAD_NUM_ROOMS, + searchQuery: "", + }; + }, + + componentWillMount: function() { + this._unmounted = false; + this._fetchRooms(); + }, + + _fetchRooms: function() { + this.setState({fetching: true}); + this.context.matrixClient.getGroupRooms(this.props.groupId).then((result) => { + this.setState({ + rooms: result.chunk.map((apiRoom) => { + return groupRoomFromApiObject(apiRoom); + }), + fetching: false, + }); + }).catch((e) => { + this.setState({fetching: false}); + console.error("Failed to get group room list: ", e); + }); + }, + + _createOverflowTile: function(overflowCount, totalCount) { + // For now we'll pretend this is any entity. It should probably be a separate tile. + const EntityTile = sdk.getComponent("rooms.EntityTile"); + const BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); + const text = _t("and %(count)s others...", { count: overflowCount }); + return ( + + } name={text} presenceState="online" suppressOnHover={true} + onClick={this._showFullRoomList} /> + ); + }, + + _showFullRoomList: function() { + this.setState({ + truncateAt: -1, + }); + }, + + onSearchQueryChanged: function(ev) { + this.setState({ searchQuery: ev.target.value }); + }, + + makeGroupRoomTiles: function(query) { + const GroupRoomTile = sdk.getComponent("groups.GroupRoomTile"); + query = (query || "").toLowerCase(); + + let roomList = this.state.rooms; + if (query) { + roomList = roomList.filter((room) => { + const matchesName = (room.name || "").toLowerCase().include(query); + const matchesAlias = (room.canonicalAlias || "").toLowerCase().includes(query); + return matchesName || matchesAlias; + }); + } + + roomList = roomList.map((groupRoom, index) => { + return ( + + ); + }); + + return roomList; + }, + + render: function() { + if (this.state.fetching) { + const Spinner = sdk.getComponent("elements.Spinner"); + return (
+ +
); + } else if (this.state.rooms === null) { + return null; + } + + const inputBox = ( +
+ +
+ ); + + const TruncatedList = sdk.getComponent("elements.TruncatedList"); + return ( +
+ { inputBox } + + + {this.makeGroupRoomTiles(this.state.searchQuery)} + + +
+ ); + }, +}); diff --git a/src/components/views/groups/GroupRoomTile.js b/src/components/views/groups/GroupRoomTile.js new file mode 100644 index 0000000000..771e3d9c4f --- /dev/null +++ b/src/components/views/groups/GroupRoomTile.js @@ -0,0 +1,89 @@ +/* +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 React from 'react'; +import {MatrixClient} from 'matrix-js-sdk'; +import { _t } from '../../../languageHandler'; +import PropTypes from 'prop-types'; +import sdk from '../../../index'; +import dis from '../../../dispatcher'; +import { GroupRoomType } from '../../../groups'; + +const GroupRoomTile = React.createClass({ + displayName: 'GroupRoomTile', + + propTypes: { + groupId: PropTypes.string.isRequired, + groupRoom: GroupRoomType.isRequired, + }, + + getInitialState: function() { + return {}; + }, + + onClick: function(e) { + let roomId; + let roomAlias; + if (this.props.groupRoom.canonicalAlias) { + roomAlias = this.props.groupRoom.canonicalAlias; + } else { + roomId = this.props.groupRoom.roomId; + } + dis.dispatch({ + action: 'view_room', + room_id: roomId, + room_alias: roomAlias, + }); + }, + + render: function() { + const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + + const name = this.props.groupRoom.name || + this.props.groupRoom.canonicalAlias || + _t("Unnamed Room"); + const avatarUrl = this.context.matrixClient.mxcUrlToHttp( + this.props.groupRoom.avatarUrl, + 36, 36, 'crop', + ); + + const av = ( + + ); + + return ( + +
+ {av} +
+
+ {name} +
+
+ ); + }, +}); + +GroupRoomTile.contextTypes = { + matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired, +}; + + +export default GroupRoomTile; diff --git a/src/groups.js b/src/groups.js index 24c0562bde..2ff95f7d65 100644 --- a/src/groups.js +++ b/src/groups.js @@ -22,6 +22,14 @@ export const GroupMemberType = PropTypes.shape({ avatarUrl: PropTypes.string, }); +export const GroupRoomType = PropTypes.shape({ + name: PropTypes.string, + // TODO: API doesn't return this yet + // roomId: PropTypes.string.isRequired, + canonicalAlias: PropTypes.string, + avatarUrl: PropTypes.string, +}); + export function groupMemberFromApiObject(apiObject) { return { userId: apiObject.user_id, @@ -29,3 +37,11 @@ export function groupMemberFromApiObject(apiObject) { avatarUrl: apiObject.avatar_url, }; } + +export function groupRoomFromApiObject(apiObject) { + return { + name: apiObject.name, + canonicalAlias: apiObject.canonical_alias, + avatarUrl: apiObject.avatar_url, + }; +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9dd479c789..2e7c6d513c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -846,6 +846,7 @@ "Robot check is currently unavailable on desktop - please use a web browser": "Robot check is currently unavailable on desktop - please use a web browser", "Description": "Description", "Filter group members": "Filter group members", + "Filter group rooms": "Filter group rooms", "Remove from group": "Remove from group", "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?", @@ -886,5 +887,9 @@ "Your membership of this group is public": "Your membership of this group is public", "Your membership of this group is private": "Your membership of this group is private", "Make private": "Make private", - "Make public": "Make public" + "Make public": "Make public", + "Add rooms to the group": "Add rooms to the group", + "Which rooms would you like to add to this group?": "Which rooms would you like to add to this group?", + "Add to group": "Add to group", + "Failed to add the following rooms to %(groupId)s:": "Failed to add the following rooms to %(groupId)s:" }