element-web/src/components/structures/GroupView.js

662 lines
24 KiB
JavaScript
Raw Normal View History

2017-06-05 15:51:50 +00:00
/*
Copyright 2017 Vector Creations Ltd.
Copyright 2017 New Vector Ltd.
2017-06-05 15:51:50 +00:00
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.
*/
2017-06-27 08:58:29 +00:00
import React from 'react';
import PropTypes from 'prop-types';
2017-06-05 15:51:50 +00:00
import MatrixClientPeg from '../../MatrixClientPeg';
import sdk from '../../index';
import dis from '../../dispatcher';
import { sanitizedHtmlNode } from '../../HtmlUtils';
import { _t } from '../../languageHandler';
import AccessibleButton from '../views/elements/AccessibleButton';
2017-07-21 13:03:10 +00:00
import Modal from '../../Modal';
import classnames from 'classnames';
2017-06-05 15:51:50 +00:00
const RoomSummaryType = PropTypes.shape({
room_id: PropTypes.string.isRequired,
profile: PropTypes.shape({
name: PropTypes.string,
avatar_url: PropTypes.string,
canonical_alias: PropTypes.string,
}).isRequired,
});
2017-06-05 15:51:50 +00:00
const UserSummaryType = PropTypes.shape({
summaryInfo: PropTypes.shape({
user_id: PropTypes.string.isRequired,
}).isRequired,
});
const CategoryRoomList = React.createClass({
displayName: 'CategoryRoomList',
props: {
rooms: PropTypes.arrayOf(RoomSummaryType).isRequired,
category: PropTypes.shape({
profile: PropTypes.shape({
name: PropTypes.string,
}).isRequired,
}),
},
render: function() {
const roomNodes = this.props.rooms.map((r) => {
return <FeaturedRoom key={r.room_id} summaryInfo={r} />;
});
let catHeader = null;
if (this.props.category && this.props.category.profile) {
catHeader = <div className="mx_GroupView_featuredThings_category">{this.props.category.profile.name}</div>;
}
return <div>
{catHeader}
{roomNodes}
</div>;
},
});
const FeaturedRoom = React.createClass({
displayName: 'FeaturedRoom',
props: {
summaryInfo: RoomSummaryType.isRequired,
},
onClick: function(e) {
e.preventDefault();
2017-07-10 18:32:02 +00:00
e.stopPropagation();
dis.dispatch({
action: 'view_room',
room_alias: this.props.summaryInfo.profile.canonical_alias,
room_id: this.props.summaryInfo.room_id,
});
},
render: function() {
const RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
const oobData = {
roomId: this.props.summaryInfo.room_id,
avatarUrl: this.props.summaryInfo.profile.avatar_url,
name: this.props.summaryInfo.profile.name,
};
let permalink = null;
if (this.props.summaryInfo.profile && this.props.summaryInfo.profile.canonical_alias) {
permalink = 'https://matrix.to/#/' + this.props.summaryInfo.profile.canonical_alias;
}
let roomNameNode = null;
if (permalink) {
roomNameNode = <a href={permalink} onClick={this.onClick} >{this.props.summaryInfo.profile.name}</a>;
} else {
roomNameNode = <span>{this.props.summaryInfo.profile.name}</span>;
}
2017-07-10 18:32:02 +00:00
return <AccessibleButton className="mx_GroupView_featuredThing" onClick={this.onClick}>
<RoomAvatar oobData={oobData} width={64} height={64} />
2017-07-10 18:32:02 +00:00
<div className="mx_GroupView_featuredThing_name">{roomNameNode}</div>
</AccessibleButton>;
},
});
const RoleUserList = React.createClass({
displayName: 'RoleUserList',
props: {
users: PropTypes.arrayOf(UserSummaryType).isRequired,
role: PropTypes.shape({
profile: PropTypes.shape({
name: PropTypes.string,
}).isRequired,
}),
},
render: function() {
const userNodes = this.props.users.map((u) => {
return <FeaturedUser key={u.user_id} summaryInfo={u} />;
});
let roleHeader = null;
if (this.props.role && this.props.role.profile) {
roleHeader = <div className="mx_GroupView_featuredThings_category">{this.props.role.profile.name}</div>;
}
return <div>
{roleHeader}
{userNodes}
</div>;
},
});
2017-07-10 18:32:02 +00:00
const FeaturedUser = React.createClass({
displayName: 'FeaturedUser',
props: {
summaryInfo: UserSummaryType.isRequired,
2017-07-10 18:32:02 +00:00
},
onClick: function(e) {
e.preventDefault();
e.stopPropagation();
dis.dispatch({
action: 'view_start_chat_or_reuse',
user_id: this.props.summaryInfo.user_id,
go_home_on_cancel: false,
});
},
render: function() {
// Add avatar once we get profile info inline in the summary response
//const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
const permalink = 'https://matrix.to/#/' + this.props.summaryInfo.user_id;
const userNameNode = <a href={permalink} onClick={this.onClick} >{this.props.summaryInfo.user_id}</a>;
return <AccessibleButton className="mx_GroupView_featuredThing" onClick={this.onClick}>
<div className="mx_GroupView_featuredThing_name">{userNameNode}</div>
</AccessibleButton>;
2017-07-07 12:46:05 +00:00
},
});
export default React.createClass({
2017-06-05 15:51:50 +00:00
displayName: 'GroupView',
propTypes: {
groupId: PropTypes.string.isRequired,
2017-06-05 15:51:50 +00:00
},
getInitialState: function() {
return {
summary: null,
error: null,
editing: false,
saving: false,
2017-07-21 13:03:10 +00:00
uploadingAvatar: false,
membershipBusy: false,
2017-06-05 15:51:50 +00:00
};
},
componentWillMount: function() {
this._changeAvatarComponent = null;
2017-06-27 09:28:46 +00:00
this._loadGroupFromServer(this.props.groupId);
MatrixClientPeg.get().on("Group.myMembership", this._onGroupMyMembership);
},
componentWillUnmount: function() {
MatrixClientPeg.get().removeListener("Group.myMembership", this._onGroupMyMembership);
2017-06-05 15:51:50 +00:00
},
2017-06-27 12:13:00 +00:00
componentWillReceiveProps: function(newProps) {
if (this.props.groupId != newProps.groupId) {
2017-06-05 15:51:50 +00:00
this.setState({
summary: null,
2017-06-27 09:28:46 +00:00
error: null,
2017-07-07 09:08:29 +00:00
}, () => {
this._loadGroupFromServer(newProps.groupId);
2017-06-27 12:13:00 +00:00
});
2017-06-05 15:51:50 +00:00
}
},
_onGroupMyMembership: function(group) {
if (group.groupId !== this.props.groupId) return;
this.setState({membershipBusy: false});
},
2017-06-05 15:51:50 +00:00
_loadGroupFromServer: function(groupId) {
MatrixClientPeg.get().getGroupSummary(groupId).done((res) => {
this.setState({
2017-06-05 15:51:50 +00:00
summary: res,
2017-06-27 09:28:46 +00:00
error: null,
2017-06-05 15:51:50 +00:00
});
}, (err) => {
this.setState({
2017-06-05 15:51:50 +00:00
summary: null,
error: err,
2017-06-05 15:51:50 +00:00
});
});
},
_onShowRhsClick: function(ev) {
dis.dispatch({ action: 'show_right_panel' });
},
2017-07-17 16:17:18 +00:00
_onEditClick: function() {
2017-07-13 17:41:51 +00:00
this.setState({
editing: true,
profileForm: Object.assign({}, this.state.summary.profile),
});
},
_onCancelClick: function() {
this.setState({
editing: false,
profileForm: null,
});
},
2017-07-17 13:40:38 +00:00
_onNameChange: function(e) {
const newProfileForm = Object.assign(this.state.profileForm, { name: e.target.value });
this.setState({
profileForm: newProfileForm,
});
},
_onShortDescChange: function(e) {
const newProfileForm = Object.assign(this.state.profileForm, { short_description: e.target.value });
this.setState({
profileForm: newProfileForm,
});
},
_onLongDescChange: function(e) {
const newProfileForm = Object.assign(this.state.profileForm, { long_description: e.target.value });
this.setState({
profileForm: newProfileForm,
});
},
2017-07-21 13:03:10 +00:00
_onAvatarSelected: function(ev) {
const file = ev.target.files[0];
if (!file) return;
2017-07-21 13:03:10 +00:00
this.setState({uploadingAvatar: true});
MatrixClientPeg.get().uploadContent(file).then((url) => {
const newProfileForm = Object.assign(this.state.profileForm, { avatar_url: url });
this.setState({
uploadingAvatar: false,
profileForm: newProfileForm,
});
}).catch((e) => {
this.setState({uploadingAvatar: false});
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to upload avatar image", e);
Modal.createTrackedDialog('Failed to upload image', '', ErrorDialog, {
2017-07-21 13:03:10 +00:00
title: _t('Error'),
description: _t('Failed to upload image'),
});
}).done();
},
2017-07-13 17:41:51 +00:00
_onSaveClick: function() {
this.setState({saving: true});
MatrixClientPeg.get().setGroupProfile(this.props.groupId, this.state.profileForm).then((result) => {
this.setState({
saving: false,
editing: false,
2017-07-21 13:18:28 +00:00
summary: null,
});
this._loadGroupFromServer(this.props.groupId);
}).catch((e) => {
this.setState({
saving: false,
});
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to save group profile", e);
Modal.createTrackedDialog('Failed to update group', '', ErrorDialog, {
title: _t('Error'),
description: _t('Failed to update group'),
});
}).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"),
});
});
2017-08-21 18:34:07 +00:00
},
});
},
2017-07-10 18:32:02 +00:00
_getFeaturedRoomsNode() {
const summary = this.state.summary;
if (summary.rooms_section.rooms.length == 0) return null;
const defaultCategoryRooms = [];
const categoryRooms = {};
summary.rooms_section.rooms.forEach((r) => {
if (r.category_id === null) {
defaultCategoryRooms.push(r);
} else {
let list = categoryRooms[r.category_id];
if (list === undefined) {
list = [];
categoryRooms[r.category_id] = list;
}
list.push(r);
}
});
let defaultCategoryNode = null;
if (defaultCategoryRooms.length > 0) {
defaultCategoryNode = <CategoryRoomList rooms={defaultCategoryRooms} />;
2017-07-10 18:32:02 +00:00
}
const categoryRoomNodes = Object.keys(categoryRooms).map((catId) => {
const cat = summary.rooms_section.categories[catId];
return <CategoryRoomList key={catId} rooms={categoryRooms[catId]} category={cat} />;
2017-07-10 18:32:02 +00:00
});
return <div className="mx_GroupView_featuredThings">
<div className="mx_GroupView_featuredThings_header">
{_t('Featured Rooms:')}
</div>
{defaultCategoryNode}
{categoryRoomNodes}
</div>;
},
_getFeaturedUsersNode() {
const summary = this.state.summary;
if (summary.users_section.users.length == 0) return null;
const noRoleUsers = [];
const roleUsers = {};
summary.users_section.users.forEach((u) => {
if (u.role_id === null) {
noRoleUsers.push(u);
} else {
let list = roleUsers[u.role_id];
if (list === undefined) {
list = [];
roleUsers[u.role_id] = list;
}
list.push(u);
}
});
let noRoleNode = null;
if (noRoleUsers.length > 0) {
noRoleNode = <RoleUserList users={noRoleUsers} />;
2017-07-10 18:32:02 +00:00
}
const roleUserNodes = Object.keys(roleUsers).map((roleId) => {
const role = summary.users_section.roles[roleId];
return <RoleUserList key={roleId} users={roleUsers[roleId]} role={role} />;
2017-07-10 18:32:02 +00:00
});
return <div className="mx_GroupView_featuredThings">
<div className="mx_GroupView_featuredThings_header">
{_t('Featured Users:')}
</div>
{noRoleNode}
{roleUserNodes}
</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;
},
2017-06-05 15:51:50 +00:00
render: function() {
const GroupAvatar = sdk.getComponent("avatars.GroupAvatar");
const Loader = sdk.getComponent("elements.Spinner");
const TintableSvg = sdk.getComponent("elements.TintableSvg");
2017-06-05 15:51:50 +00:00
if (this.state.summary === null && this.state.error === null || this.state.saving) {
return <Loader />;
2017-07-13 17:41:51 +00:00
} else if (this.state.summary) {
const summary = this.state.summary;
2017-07-13 17:41:51 +00:00
let avatarNode;
let nameNode;
2017-07-13 17:41:51 +00:00
let shortDescNode;
let roomBody;
2017-08-21 18:34:07 +00:00
const rightButtons = [];
2017-07-21 13:30:09 +00:00
const headerClasses = {
mx_GroupView_header: true,
};
2017-07-13 17:41:51 +00:00
if (this.state.editing) {
2017-07-21 13:03:10 +00:00
let avatarImage;
if (this.state.uploadingAvatar) {
avatarImage = <Loader />;
} else {
const GroupAvatar = sdk.getComponent('avatars.GroupAvatar');
avatarImage = <GroupAvatar groupId={this.props.groupId}
groupAvatarUrl={this.state.profileForm.avatar_url}
width={48} height={48} resizeMethod='crop'
/>;
}
2017-07-13 17:41:51 +00:00
avatarNode = (
<div className="mx_GroupView_avatarPicker">
<label htmlFor="avatarInput" className="mx_GroupView_avatarPicker_label">
2017-07-21 13:03:10 +00:00
{avatarImage}
</label>
2017-07-13 17:41:51 +00:00
<div className="mx_GroupView_avatarPicker_edit">
<label htmlFor="avatarInput" className="mx_GroupView_avatarPicker_label">
2017-07-13 17:41:51 +00:00
<img src="img/camera.svg"
alt={ _t("Upload avatar") } title={ _t("Upload avatar") }
width="17" height="15" />
</label>
<input id="avatarInput" className="mx_GroupView_uploadInput" type="file" onChange={this._onAvatarSelected}/>
</div>
</div>
);
nameNode = <input type="text"
value={this.state.profileForm.name}
onChange={this._onNameChange}
placeholder={_t('Group Name')}
2017-07-17 13:40:38 +00:00
tabIndex="1"
2017-07-17 17:13:20 +00:00
/>;
2017-07-13 17:41:51 +00:00
shortDescNode = <input type="text"
value={this.state.profileForm.short_description}
onChange={this._onShortDescChange}
placeholder={_t('Description')}
2017-07-17 13:40:38 +00:00
tabIndex="2"
2017-07-17 17:13:20 +00:00
/>;
rightButtons.push(
<AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
onClick={this._onSaveClick} key="_saveButton"
>
2017-07-13 17:41:51 +00:00
{_t('Save')}
2017-08-21 18:34:07 +00:00
</AccessibleButton>,
);
rightButtons.push(
<AccessibleButton className='mx_GroupView_textButton' onClick={this._onCancelClick} key="_cancelButton">
2017-07-13 17:41:51 +00:00
<img src="img/cancel.svg" className='mx_filterFlipColor'
width="18" height="18" alt={_t("Cancel")}/>
2017-08-21 18:34:07 +00:00
</AccessibleButton>,
);
roomBody = <div>
<textarea className="mx_GroupView_editLongDesc" value={this.state.profileForm.long_description}
onChange={this._onLongDescChange}
2017-07-17 13:40:38 +00:00
tabIndex="3"
/>
</div>;
} else {
2017-07-13 17:41:51 +00:00
const groupAvatarUrl = summary.profile ? summary.profile.avatar_url : null;
avatarNode = <GroupAvatar
groupId={this.props.groupId}
groupAvatarUrl={groupAvatarUrl}
width={48} height={48}
/>;
if (summary.profile && summary.profile.name) {
nameNode = <div>
<span>{summary.profile.name}</span>
<span className="mx_GroupView_header_groupid">
({this.props.groupId})
</span>
</div>;
} else {
nameNode = <span>{this.props.groupId}</span>;
}
shortDescNode = <span>{summary.profile.short_description}</span>;
2017-07-13 17:41:51 +00:00
let description = null;
if (summary.profile && summary.profile.long_description) {
description = sanitizedHtmlNode(summary.profile.long_description);
}
roomBody = <div>
{this._getMembershipSection()}
2017-07-13 17:41:51 +00:00
<div className="mx_GroupView_groupDesc">{description}</div>
{this._getFeaturedRoomsNode()}
{this._getFeaturedUsersNode()}
</div>;
rightButtons.push(
<AccessibleButton className="mx_GroupHeader_button"
onClick={this._onEditClick} title={_t("Edit Group")} key="_editButton"
>
<TintableSvg src="img/icons-settings-room.svg" width="16" height="16"/>
2017-08-21 18:34:07 +00:00
</AccessibleButton>,
);
if (this.props.collapsedRhs) {
rightButtons.push(
<AccessibleButton className="mx_GroupHeader_button"
onClick={this._onShowRhsClick} title={ _t('Show panel') } key="_maximiseButton"
>
<TintableSvg src="img/maximise.svg" width="10" height="16"/>
2017-08-21 18:34:07 +00:00
</AccessibleButton>,
);
}
headerClasses.mx_GroupView_header_view = true;
2017-07-13 17:41:51 +00:00
}
2017-07-07 17:34:40 +00:00
2017-06-05 15:51:50 +00:00
return (
2017-06-27 10:52:23 +00:00
<div className="mx_GroupView">
<div className={classnames(headerClasses)}>
2017-07-13 17:41:51 +00:00
<div className="mx_GroupView_header_leftCol">
<div className="mx_GroupView_header_avatar">
{avatarNode}
2017-06-05 15:51:50 +00:00
</div>
2017-07-13 17:41:51 +00:00
<div className="mx_GroupView_header_info">
<div className="mx_GroupView_header_name">
{nameNode}
</div>
<div className="mx_GroupView_header_shortDesc">
{shortDescNode}
2017-06-05 15:51:50 +00:00
</div>
</div>
2017-07-13 17:41:51 +00:00
</div>
<div className="mx_GroupView_header_rightCol">
{rightButtons}
2017-06-05 15:51:50 +00:00
</div>
</div>
{roomBody}
2017-06-05 15:51:50 +00:00
</div>
);
2017-06-27 09:28:46 +00:00
} else if (this.state.error) {
if (this.state.error.httpStatus === 404) {
return (
2017-06-27 10:52:23 +00:00
<div className="mx_GroupView_error">
2017-06-27 09:28:46 +00:00
Group {this.props.groupId} not found
</div>
);
} else {
let extraText;
if (this.state.error.errcode === 'M_UNRECOGNIZED') {
extraText = <div>{_t('This Home server does not support groups')}</div>;
}
return (
2017-06-27 10:52:23 +00:00
<div className="mx_GroupView_error">
2017-06-27 09:28:46 +00:00
Failed to load {this.props.groupId}
{extraText}
</div>
);
}
2017-06-27 12:41:43 +00:00
} else {
console.error("Invalid state for GroupView");
return <div />;
2017-06-05 15:51:50 +00:00
}
},
});