Implement "Add room to group" feature

pull/21833/head
Luke Barnard 2017-09-26 14:49:13 +01:00
parent b42cf74216
commit ddab8d7b5c
7 changed files with 328 additions and 4 deletions

View File

@ -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(", "),
});
});
}

View File

@ -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,

View File

@ -131,7 +131,7 @@ export default withMatrixClient(React.createClass({
const inputBox = (
<form autoComplete="off">
<input className="mx_MemberList_query" id="mx_MemberList_query" type="text"
<input className="mx_GroupMemberList_query" id="mx_GroupMemberList_query" type="text"
onChange={this.onSearchQueryChanged} value={this.state.searchQuery}
placeholder={ _t('Filter group members') } />
</form>

View File

@ -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 (
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
<BaseAvatar url="img/ellipsis.svg" name="..." width={36} height={36} />
} 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 (
<GroupRoomTile
key={index}
groupId={this.props.groupId}
groupRoom={groupRoom} />
);
});
return roomList;
},
render: function() {
if (this.state.fetching) {
const Spinner = sdk.getComponent("elements.Spinner");
return (<div className="mx_GroupRoomList">
<Spinner />
</div>);
} else if (this.state.rooms === null) {
return null;
}
const inputBox = (
<form autoComplete="off">
<input className="mx_GroupRoomList_query" id="mx_GroupRoomList_query" type="text"
onChange={this.onSearchQueryChanged} value={this.state.searchQuery}
placeholder={ _t('Filter group rooms') } />
</form>
);
const TruncatedList = sdk.getComponent("elements.TruncatedList");
return (
<div className="mx_GroupRoomList">
{ inputBox }
<GeminiScrollbar autoshow={true} className="mx_GroupRoomList_joined mx_GroupRoomList_outerWrapper">
<TruncatedList className="mx_GroupRoomList_wrapper" truncateAt={this.state.truncateAt}
createOverflowElement={this._createOverflowTile}>
{this.makeGroupRoomTiles(this.state.searchQuery)}
</TruncatedList>
</GeminiScrollbar>
</div>
);
},
});

View File

@ -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 = (
<BaseAvatar name={name}
width={36} height={36}
url={avatarUrl}
/>
);
return (
<AccessibleButton className="mx_GroupRoomTile" onClick={this.onClick}>
<div className="mx_GroupRoomTile_avatar">
{av}
</div>
<div className="mx_GroupRoomTile_name">
{name}
</div>
</AccessibleButton>
);
},
});
GroupRoomTile.contextTypes = {
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
};
export default GroupRoomTile;

View File

@ -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,
};
}

View File

@ -846,6 +846,7 @@
"Robot check is currently unavailable on desktop - please use a <a>web browser</a>": "Robot check is currently unavailable on desktop - please use a <a>web browser</a>",
"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:"
}