Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into set_default_federate_by_settings
This commit is contained in:
commit
7492f2dffa
149 changed files with 7702 additions and 5253 deletions
|
@ -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.
|
||||
|
@ -16,6 +17,7 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Promise from 'bluebird';
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
import sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
|
@ -25,6 +27,9 @@ import AccessibleButton from '../views/elements/AccessibleButton';
|
|||
import Modal from '../../Modal';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import GroupStoreCache from '../../stores/GroupStoreCache';
|
||||
import GroupStore from '../../stores/GroupStore';
|
||||
|
||||
const RoomSummaryType = PropTypes.shape({
|
||||
room_id: PropTypes.string.isRequired,
|
||||
profile: PropTypes.shape({
|
||||
|
@ -37,6 +42,9 @@ const RoomSummaryType = PropTypes.shape({
|
|||
const UserSummaryType = PropTypes.shape({
|
||||
summaryInfo: PropTypes.shape({
|
||||
user_id: PropTypes.string.isRequired,
|
||||
role_id: PropTypes.string,
|
||||
avatar_url: PropTypes.string,
|
||||
displayname: PropTypes.string,
|
||||
}).isRequired,
|
||||
});
|
||||
|
||||
|
@ -50,19 +58,79 @@ const CategoryRoomList = React.createClass({
|
|||
name: PropTypes.string,
|
||||
}).isRequired,
|
||||
}),
|
||||
groupId: PropTypes.string.isRequired,
|
||||
|
||||
// Whether the list should be editable
|
||||
editing: PropTypes.bool.isRequired,
|
||||
},
|
||||
|
||||
onAddRoomsClicked: function(ev) {
|
||||
ev.preventDefault();
|
||||
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
|
||||
Modal.createTrackedDialog('Add Rooms to Group Summary', '', AddressPickerDialog, {
|
||||
title: _t('Add rooms to the group summary'),
|
||||
description: _t("Which rooms would you like to add to this summary?"),
|
||||
placeholder: _t("Room name or alias"),
|
||||
button: _t("Add to summary"),
|
||||
pickerType: 'room',
|
||||
validAddressTypes: ['mx-room-id'],
|
||||
groupId: this.props.groupId,
|
||||
onFinished: (success, addrs) => {
|
||||
if (!success) return;
|
||||
const errorList = [];
|
||||
Promise.all(addrs.map((addr) => {
|
||||
return this.context.groupStore
|
||||
.addRoomToGroupSummary(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 summary',
|
||||
'', ErrorDialog,
|
||||
{
|
||||
title: _t(
|
||||
"Failed to add the following rooms to the summary of %(groupId)s:",
|
||||
{groupId: this.props.groupId},
|
||||
),
|
||||
description: errorList.join(", "),
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const addButton = this.props.editing ?
|
||||
(<AccessibleButton className="mx_GroupView_featuredThings_addButton" onClick={this.onAddRoomsClicked}>
|
||||
<TintableSvg src="img/icons-create-room.svg" width="64" height="64" />
|
||||
<div className="mx_GroupView_featuredThings_addButton_label">
|
||||
{ _t('Add a Room') }
|
||||
</div>
|
||||
</AccessibleButton>) : <div />;
|
||||
|
||||
const roomNodes = this.props.rooms.map((r) => {
|
||||
return <FeaturedRoom key={r.room_id} summaryInfo={r} />;
|
||||
return <FeaturedRoom
|
||||
key={r.room_id}
|
||||
groupId={this.props.groupId}
|
||||
editing={this.props.editing}
|
||||
summaryInfo={r} />;
|
||||
});
|
||||
let catHeader = null;
|
||||
|
||||
let catHeader = <div />;
|
||||
if (this.props.category && this.props.category.profile) {
|
||||
catHeader = <div className="mx_GroupView_featuredThings_category">{this.props.category.profile.name}</div>;
|
||||
catHeader = <div className="mx_GroupView_featuredThings_category">
|
||||
{ this.props.category.profile.name }
|
||||
</div>;
|
||||
}
|
||||
return <div>
|
||||
{catHeader}
|
||||
{roomNodes}
|
||||
return <div className="mx_GroupView_featuredThings_container">
|
||||
{ catHeader }
|
||||
{ roomNodes }
|
||||
{ addButton }
|
||||
</div>;
|
||||
},
|
||||
});
|
||||
|
@ -72,6 +140,8 @@ const FeaturedRoom = React.createClass({
|
|||
|
||||
props: {
|
||||
summaryInfo: RoomSummaryType.isRequired,
|
||||
editing: PropTypes.bool.isRequired,
|
||||
groupId: PropTypes.string.isRequired,
|
||||
},
|
||||
|
||||
onClick: function(e) {
|
||||
|
@ -85,28 +155,69 @@ const FeaturedRoom = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onDeleteClicked: function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.context.groupStore.removeRoomFromGroupSummary(
|
||||
this.props.summaryInfo.room_id,
|
||||
).catch((err) => {
|
||||
console.error('Error whilst removing room from group summary', err);
|
||||
const roomName = this.props.summaryInfo.name ||
|
||||
this.props.summaryInfo.canonical_alias ||
|
||||
this.props.summaryInfo.room_id;
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog(
|
||||
'Failed to remove room from group summary',
|
||||
'', ErrorDialog,
|
||||
{
|
||||
title: _t(
|
||||
"Failed to remove the room from the summary of %(groupId)s",
|
||||
{groupId: this.props.groupId},
|
||||
),
|
||||
description: _t("The room '%(roomName)s' could not be removed from the summary.", {roomName}),
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
|
||||
|
||||
const roomName = this.props.summaryInfo.profile.name ||
|
||||
this.props.summaryInfo.profile.canonical_alias ||
|
||||
_t("Unnamed Room");
|
||||
|
||||
const oobData = {
|
||||
roomId: this.props.summaryInfo.room_id,
|
||||
avatarUrl: this.props.summaryInfo.profile.avatar_url,
|
||||
name: this.props.summaryInfo.profile.name,
|
||||
name: roomName,
|
||||
};
|
||||
|
||||
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>;
|
||||
roomNameNode = <a href={permalink} onClick={this.onClick} >{ roomName }</a>;
|
||||
} else {
|
||||
roomNameNode = <span>{this.props.summaryInfo.profile.name}</span>;
|
||||
roomNameNode = <span>{ roomName }</span>;
|
||||
}
|
||||
|
||||
const deleteButton = this.props.editing ?
|
||||
<img
|
||||
className="mx_GroupView_featuredThing_deleteButton"
|
||||
src="img/cancel-small.svg"
|
||||
width="14"
|
||||
height="14"
|
||||
alt="Delete"
|
||||
onClick={this.onDeleteClicked} />
|
||||
: <div />;
|
||||
|
||||
return <AccessibleButton className="mx_GroupView_featuredThing" onClick={this.onClick}>
|
||||
<RoomAvatar oobData={oobData} width={64} height={64} />
|
||||
<div className="mx_GroupView_featuredThing_name">{roomNameNode}</div>
|
||||
<div className="mx_GroupView_featuredThing_name">{ roomNameNode }</div>
|
||||
{ deleteButton }
|
||||
</AccessibleButton>;
|
||||
},
|
||||
});
|
||||
|
@ -121,19 +232,75 @@ const RoleUserList = React.createClass({
|
|||
name: PropTypes.string,
|
||||
}).isRequired,
|
||||
}),
|
||||
groupId: PropTypes.string.isRequired,
|
||||
|
||||
// Whether the list should be editable
|
||||
editing: PropTypes.bool.isRequired,
|
||||
},
|
||||
|
||||
onAddUsersClicked: function(ev) {
|
||||
ev.preventDefault();
|
||||
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
|
||||
Modal.createTrackedDialog('Add Users to Group Summary', '', AddressPickerDialog, {
|
||||
title: _t('Add users to the group summary'),
|
||||
description: _t("Who would you like to add to this summary?"),
|
||||
placeholder: _t("Name or matrix ID"),
|
||||
button: _t("Add to summary"),
|
||||
validAddressTypes: ['mx-user-id'],
|
||||
groupId: this.props.groupId,
|
||||
shouldOmitSelf: false,
|
||||
onFinished: (success, addrs) => {
|
||||
if (!success) return;
|
||||
const errorList = [];
|
||||
Promise.all(addrs.map((addr) => {
|
||||
return this.context.groupStore
|
||||
.addUserToGroupSummary(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 users to the group summary',
|
||||
'', ErrorDialog,
|
||||
{
|
||||
title: _t(
|
||||
"Failed to add the following users to the summary of %(groupId)s:",
|
||||
{groupId: this.props.groupId},
|
||||
),
|
||||
description: errorList.join(", "),
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const addButton = this.props.editing ?
|
||||
(<AccessibleButton className="mx_GroupView_featuredThings_addButton" onClick={this.onAddUsersClicked}>
|
||||
<TintableSvg src="img/icons-create-room.svg" width="64" height="64" />
|
||||
<div className="mx_GroupView_featuredThings_addButton_label">
|
||||
{ _t('Add a User') }
|
||||
</div>
|
||||
</AccessibleButton>) : <div />;
|
||||
const userNodes = this.props.users.map((u) => {
|
||||
return <FeaturedUser key={u.user_id} summaryInfo={u} />;
|
||||
return <FeaturedUser
|
||||
key={u.user_id}
|
||||
summaryInfo={u}
|
||||
editing={this.props.editing}
|
||||
groupId={this.props.groupId} />;
|
||||
});
|
||||
let roleHeader = null;
|
||||
let roleHeader = <div />;
|
||||
if (this.props.role && this.props.role.profile) {
|
||||
roleHeader = <div className="mx_GroupView_featuredThings_category">{this.props.role.profile.name}</div>;
|
||||
roleHeader = <div className="mx_GroupView_featuredThings_category">{ this.props.role.profile.name }</div>;
|
||||
}
|
||||
return <div>
|
||||
{roleHeader}
|
||||
{userNodes}
|
||||
return <div className="mx_GroupView_featuredThings_container">
|
||||
{ roleHeader }
|
||||
{ userNodes }
|
||||
{ addButton }
|
||||
</div>;
|
||||
},
|
||||
});
|
||||
|
@ -143,6 +310,8 @@ const FeaturedUser = React.createClass({
|
|||
|
||||
props: {
|
||||
summaryInfo: UserSummaryType.isRequired,
|
||||
editing: PropTypes.bool.isRequired,
|
||||
groupId: PropTypes.string.isRequired,
|
||||
},
|
||||
|
||||
onClick: function(e) {
|
||||
|
@ -156,19 +325,64 @@ const FeaturedUser = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onDeleteClicked: function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.context.groupStore.removeUserFromGroupSummary(
|
||||
this.props.summaryInfo.user_id,
|
||||
).catch((err) => {
|
||||
console.error('Error whilst removing user from group summary', err);
|
||||
const displayName = this.props.summaryInfo.displayname || this.props.summaryInfo.user_id;
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog(
|
||||
'Failed to remove user from group summary',
|
||||
'', ErrorDialog,
|
||||
{
|
||||
title: _t(
|
||||
"Failed to remove a user from the summary of %(groupId)s",
|
||||
{groupId: this.props.groupId},
|
||||
),
|
||||
description: _t("The user '%(displayName)s' could not be removed from the summary.", {displayName}),
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
// Add avatar once we get profile info inline in the summary response
|
||||
//const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
const name = this.props.summaryInfo.displayname || this.props.summaryInfo.user_id;
|
||||
|
||||
const permalink = 'https://matrix.to/#/' + this.props.summaryInfo.user_id;
|
||||
const userNameNode = <a href={permalink} onClick={this.onClick} >{this.props.summaryInfo.user_id}</a>;
|
||||
const userNameNode = <a href={permalink} onClick={this.onClick}>{ name }</a>;
|
||||
const httpUrl = MatrixClientPeg.get()
|
||||
.mxcUrlToHttp(this.props.summaryInfo.avatar_url, 64, 64);
|
||||
|
||||
const deleteButton = this.props.editing ?
|
||||
<img
|
||||
className="mx_GroupView_featuredThing_deleteButton"
|
||||
src="img/cancel-small.svg"
|
||||
width="14"
|
||||
height="14"
|
||||
alt="Delete"
|
||||
onClick={this.onDeleteClicked} />
|
||||
: <div />;
|
||||
|
||||
return <AccessibleButton className="mx_GroupView_featuredThing" onClick={this.onClick}>
|
||||
<div className="mx_GroupView_featuredThing_name">{userNameNode}</div>
|
||||
<BaseAvatar name={name} url={httpUrl} width={64} height={64} />
|
||||
<div className="mx_GroupView_featuredThing_name">{ userNameNode }</div>
|
||||
{ deleteButton }
|
||||
</AccessibleButton>;
|
||||
},
|
||||
});
|
||||
|
||||
const GroupContext = {
|
||||
groupStore: React.PropTypes.instanceOf(GroupStore).isRequired,
|
||||
};
|
||||
|
||||
CategoryRoomList.contextTypes = GroupContext;
|
||||
FeaturedRoom.contextTypes = GroupContext;
|
||||
RoleUserList.contextTypes = GroupContext;
|
||||
FeaturedUser.contextTypes = GroupContext;
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'GroupView',
|
||||
|
||||
|
@ -176,6 +390,16 @@ export default React.createClass({
|
|||
groupId: PropTypes.string.isRequired,
|
||||
},
|
||||
|
||||
childContextTypes: {
|
||||
groupStore: React.PropTypes.instanceOf(GroupStore),
|
||||
},
|
||||
|
||||
getChildContext: function() {
|
||||
return {
|
||||
groupStore: this._groupStore,
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
summary: null,
|
||||
|
@ -183,12 +407,21 @@ export default React.createClass({
|
|||
editing: false,
|
||||
saving: false,
|
||||
uploadingAvatar: false,
|
||||
membershipBusy: false,
|
||||
publicityBusy: false,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._changeAvatarComponent = null;
|
||||
this._loadGroupFromServer(this.props.groupId);
|
||||
this._initGroupStore(this.props.groupId);
|
||||
|
||||
MatrixClientPeg.get().on("Group.myMembership", this._onGroupMyMembership);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
MatrixClientPeg.get().removeListener("Group.myMembership", this._onGroupMyMembership);
|
||||
this._groupStore.removeAllListeners();
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
|
@ -197,18 +430,26 @@ export default React.createClass({
|
|||
summary: null,
|
||||
error: null,
|
||||
}, () => {
|
||||
this._loadGroupFromServer(newProps.groupId);
|
||||
this._initGroupStore(newProps.groupId);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_loadGroupFromServer: function(groupId) {
|
||||
MatrixClientPeg.get().getGroupSummary(groupId).done((res) => {
|
||||
_onGroupMyMembership: function(group) {
|
||||
if (group.groupId !== this.props.groupId) return;
|
||||
|
||||
this.setState({membershipBusy: false});
|
||||
},
|
||||
|
||||
_initGroupStore: function(groupId) {
|
||||
this._groupStore = GroupStoreCache.getGroupStore(MatrixClientPeg.get(), groupId);
|
||||
this._groupStore.on('update', () => {
|
||||
this.setState({
|
||||
summary: res,
|
||||
summary: this._groupStore.getSummary(),
|
||||
error: null,
|
||||
});
|
||||
}, (err) => {
|
||||
});
|
||||
this._groupStore.on('error', (err) => {
|
||||
this.setState({
|
||||
summary: null,
|
||||
error: err,
|
||||
|
@ -216,6 +457,10 @@ export default React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_onShowRhsClick: function(ev) {
|
||||
dis.dispatch({ action: 'show_right_panel' });
|
||||
},
|
||||
|
||||
_onEditClick: function() {
|
||||
this.setState({
|
||||
editing: true,
|
||||
|
@ -281,7 +526,7 @@ export default React.createClass({
|
|||
editing: false,
|
||||
summary: null,
|
||||
});
|
||||
this._loadGroupFromServer(this.props.groupId);
|
||||
this._initGroupStore(this.props.groupId);
|
||||
}).catch((e) => {
|
||||
this.setState({
|
||||
saving: false,
|
||||
|
@ -295,10 +540,80 @@ export default React.createClass({
|
|||
}).done();
|
||||
},
|
||||
|
||||
_getFeaturedRoomsNode() {
|
||||
const summary = this.state.summary;
|
||||
_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"),
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
if (summary.rooms_section.rooms.length == 0) return null;
|
||||
_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"),
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
_onPubliciseOffClick: function() {
|
||||
this._setPublicity(false);
|
||||
},
|
||||
|
||||
_onPubliciseOnClick: function() {
|
||||
this._setPublicity(true);
|
||||
},
|
||||
|
||||
_setPublicity: function(publicity) {
|
||||
this.setState({
|
||||
publicityBusy: true,
|
||||
});
|
||||
this._groupStore.setGroupPublicity(publicity).then(() => {
|
||||
this.setState({
|
||||
publicityBusy: false,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_getFeaturedRoomsNode: function() {
|
||||
const summary = this.state.summary;
|
||||
|
||||
const defaultCategoryRooms = [];
|
||||
const categoryRooms = {};
|
||||
|
@ -315,29 +630,32 @@ export default React.createClass({
|
|||
}
|
||||
});
|
||||
|
||||
let defaultCategoryNode = null;
|
||||
if (defaultCategoryRooms.length > 0) {
|
||||
defaultCategoryNode = <CategoryRoomList rooms={defaultCategoryRooms} />;
|
||||
}
|
||||
const defaultCategoryNode = <CategoryRoomList
|
||||
rooms={defaultCategoryRooms}
|
||||
groupId={this.props.groupId}
|
||||
editing={this.state.editing} />;
|
||||
const categoryRoomNodes = Object.keys(categoryRooms).map((catId) => {
|
||||
const cat = summary.rooms_section.categories[catId];
|
||||
return <CategoryRoomList key={catId} rooms={categoryRooms[catId]} category={cat} />;
|
||||
return <CategoryRoomList
|
||||
key={catId}
|
||||
rooms={categoryRooms[catId]}
|
||||
category={cat}
|
||||
groupId={this.props.groupId}
|
||||
editing={this.state.editing} />;
|
||||
});
|
||||
|
||||
return <div className="mx_GroupView_featuredThings">
|
||||
<div className="mx_GroupView_featuredThings_header">
|
||||
{_t('Featured Rooms:')}
|
||||
{ _t('Featured Rooms:') }
|
||||
</div>
|
||||
{defaultCategoryNode}
|
||||
{categoryRoomNodes}
|
||||
{ defaultCategoryNode }
|
||||
{ categoryRoomNodes }
|
||||
</div>;
|
||||
},
|
||||
|
||||
_getFeaturedUsersNode() {
|
||||
_getFeaturedUsersNode: function() {
|
||||
const summary = this.state.summary;
|
||||
|
||||
if (summary.users_section.users.length == 0) return null;
|
||||
|
||||
const noRoleUsers = [];
|
||||
const roleUsers = {};
|
||||
summary.users_section.users.forEach((u) => {
|
||||
|
@ -353,24 +671,121 @@ export default React.createClass({
|
|||
}
|
||||
});
|
||||
|
||||
let noRoleNode = null;
|
||||
if (noRoleUsers.length > 0) {
|
||||
noRoleNode = <RoleUserList users={noRoleUsers} />;
|
||||
}
|
||||
const noRoleNode = <RoleUserList
|
||||
users={noRoleUsers}
|
||||
groupId={this.props.groupId}
|
||||
editing={this.state.editing} />;
|
||||
const roleUserNodes = Object.keys(roleUsers).map((roleId) => {
|
||||
const role = summary.users_section.roles[roleId];
|
||||
return <RoleUserList key={roleId} users={roleUsers[roleId]} role={role} />;
|
||||
return <RoleUserList
|
||||
key={roleId}
|
||||
users={roleUsers[roleId]}
|
||||
role={role}
|
||||
groupId={this.props.groupId}
|
||||
editing={this.state.editing} />;
|
||||
});
|
||||
|
||||
return <div className="mx_GroupView_featuredThings">
|
||||
<div className="mx_GroupView_featuredThings_header">
|
||||
{_t('Featured Users:')}
|
||||
{ _t('Featured Users:') }
|
||||
</div>
|
||||
{noRoleNode}
|
||||
{roleUserNodes}
|
||||
{ noRoleNode }
|
||||
{ roleUserNodes }
|
||||
</div>;
|
||||
},
|
||||
|
||||
_getMembershipSection: function() {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
|
||||
const group = MatrixClientPeg.get().getGroup(this.props.groupId);
|
||||
if (!group) return null;
|
||||
|
||||
if (group.myMembership === 'invite') {
|
||||
if (this.state.membershipBusy) {
|
||||
return <div className="mx_GroupView_membershipSection">
|
||||
<Spinner />
|
||||
</div>;
|
||||
}
|
||||
|
||||
return <div className="mx_GroupView_membershipSection mx_GroupView_membershipSection_invited">
|
||||
<div className="mx_GroupView_membershipSection_description">
|
||||
{ _t("%(inviter)s has invited you to join this group", {inviter: group.inviter.userId}) }
|
||||
</div>
|
||||
<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') {
|
||||
let youAreAMemberText = _t("You are a member of this group");
|
||||
if (this.state.summary.user && this.state.summary.user.is_privileged) {
|
||||
youAreAMemberText = _t("You are an administrator of this group");
|
||||
}
|
||||
|
||||
let publicisedButton;
|
||||
if (this.state.publicityBusy) {
|
||||
publicisedButton = <Spinner />;
|
||||
}
|
||||
|
||||
let publicisedSection;
|
||||
if (this.state.summary.user && this.state.summary.user.is_publicised) {
|
||||
if (!this.state.publicityBusy) {
|
||||
publicisedButton = <AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
|
||||
onClick={this._onPubliciseOffClick}
|
||||
>
|
||||
{ _t("Unpublish") }
|
||||
</AccessibleButton>;
|
||||
}
|
||||
publicisedSection = <div className="mx_GroupView_membershipSubSection">
|
||||
{ _t("This group is published on your profile") }
|
||||
<div className="mx_GroupView_membership_buttonContainer">
|
||||
{ publicisedButton }
|
||||
</div>
|
||||
</div>;
|
||||
} else {
|
||||
if (!this.state.publicityBusy) {
|
||||
publicisedButton = <AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
|
||||
onClick={this._onPubliciseOnClick}
|
||||
>
|
||||
{ _t("Publish") }
|
||||
</AccessibleButton>;
|
||||
}
|
||||
publicisedSection = <div className="mx_GroupView_membershipSubSection">
|
||||
{ _t("This group is not published on your profile") }
|
||||
<div className="mx_GroupView_membership_buttonContainer">
|
||||
{ publicisedButton }
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
return <div className="mx_GroupView_membershipSection mx_GroupView_membershipSection_joined">
|
||||
<div className="mx_GroupView_membershipSubSection">
|
||||
<div className="mx_GroupView_membershipSection_description">
|
||||
{ youAreAMemberText }
|
||||
</div>
|
||||
<div className="mx_GroupView_membership_buttonContainer">
|
||||
<AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
|
||||
onClick={this._onLeaveClick}
|
||||
>
|
||||
{ _t("Leave") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
{ publicisedSection }
|
||||
</div>;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const GroupAvatar = sdk.getComponent("avatars.GroupAvatar");
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
|
@ -384,8 +799,8 @@ export default React.createClass({
|
|||
let avatarNode;
|
||||
let nameNode;
|
||||
let shortDescNode;
|
||||
let rightButtons;
|
||||
let roomBody;
|
||||
const rightButtons = [];
|
||||
const headerClasses = {
|
||||
mx_GroupView_header: true,
|
||||
};
|
||||
|
@ -404,15 +819,15 @@ export default React.createClass({
|
|||
avatarNode = (
|
||||
<div className="mx_GroupView_avatarPicker">
|
||||
<label htmlFor="avatarInput" className="mx_GroupView_avatarPicker_label">
|
||||
{avatarImage}
|
||||
{ avatarImage }
|
||||
</label>
|
||||
<div className="mx_GroupView_avatarPicker_edit">
|
||||
<label htmlFor="avatarInput" className="mx_GroupView_avatarPicker_label">
|
||||
<img src="img/camera.svg"
|
||||
alt={ _t("Upload avatar") } title={ _t("Upload avatar") }
|
||||
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}/>
|
||||
<input id="avatarInput" className="mx_GroupView_uploadInput" type="file" onChange={this._onAvatarSelected} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -428,20 +843,26 @@ export default React.createClass({
|
|||
placeholder={_t('Description')}
|
||||
tabIndex="2"
|
||||
/>;
|
||||
rightButtons = <span>
|
||||
<AccessibleButton className="mx_GroupView_saveButton mx_RoomHeader_textButton" onClick={this._onSaveClick}>
|
||||
{_t('Save')}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className='mx_GroupView_cancelButton' onClick={this._onCancelClick}>
|
||||
<img src="img/cancel.svg" className='mx_filterFlipColor'
|
||||
width="18" height="18" alt={_t("Cancel")}/>
|
||||
</AccessibleButton>
|
||||
</span>;
|
||||
rightButtons.push(
|
||||
<AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
|
||||
onClick={this._onSaveClick} key="_saveButton"
|
||||
>
|
||||
{ _t('Save') }
|
||||
</AccessibleButton>,
|
||||
);
|
||||
rightButtons.push(
|
||||
<AccessibleButton className="mx_RoomHeader_cancelButton" onClick={this._onCancelClick} key="_cancelButton">
|
||||
<img src="img/cancel.svg" className="mx_filterFlipColor"
|
||||
width="18" height="18" alt={_t("Cancel")} />
|
||||
</AccessibleButton>,
|
||||
);
|
||||
roomBody = <div>
|
||||
<textarea className="mx_GroupView_editLongDesc" value={this.state.profileForm.long_description}
|
||||
onChange={this._onLongDescChange}
|
||||
tabIndex="3"
|
||||
/>
|
||||
{ this._getFeaturedRoomsNode() }
|
||||
{ this._getFeaturedUsersNode() }
|
||||
</div>;
|
||||
} else {
|
||||
const groupAvatarUrl = summary.profile ? summary.profile.avatar_url : null;
|
||||
|
@ -452,31 +873,44 @@ export default React.createClass({
|
|||
/>;
|
||||
if (summary.profile && summary.profile.name) {
|
||||
nameNode = <div>
|
||||
<span>{summary.profile.name}</span>
|
||||
<span>{ summary.profile.name }</span>
|
||||
<span className="mx_GroupView_header_groupid">
|
||||
({this.props.groupId})
|
||||
({ this.props.groupId })
|
||||
</span>
|
||||
</div>;
|
||||
} else {
|
||||
nameNode = <span>{this.props.groupId}</span>;
|
||||
nameNode = <span>{ this.props.groupId }</span>;
|
||||
}
|
||||
shortDescNode = <span>{summary.profile.short_description}</span>;
|
||||
shortDescNode = <span>{ summary.profile.short_description }</span>;
|
||||
|
||||
let description = null;
|
||||
if (summary.profile && summary.profile.long_description) {
|
||||
description = sanitizedHtmlNode(summary.profile.long_description);
|
||||
}
|
||||
roomBody = <div>
|
||||
<div className="mx_GroupView_groupDesc">{description}</div>
|
||||
{this._getFeaturedRoomsNode()}
|
||||
{this._getFeaturedUsersNode()}
|
||||
{ this._getMembershipSection() }
|
||||
<div className="mx_GroupView_groupDesc">{ description }</div>
|
||||
{ this._getFeaturedRoomsNode() }
|
||||
{ this._getFeaturedUsersNode() }
|
||||
</div>;
|
||||
// disabled until editing works
|
||||
rightButtons = <AccessibleButton className="mx_GroupHeader_button"
|
||||
onClick={this._onEditClick} title={_t("Edit Group")}
|
||||
>
|
||||
<TintableSvg src="img/icons-settings-room.svg" width="16" height="16"/>
|
||||
</AccessibleButton>;
|
||||
if (summary.user && summary.user.is_privileged) {
|
||||
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" />
|
||||
</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" />
|
||||
</AccessibleButton>,
|
||||
);
|
||||
}
|
||||
|
||||
headerClasses.mx_GroupView_header_view = true;
|
||||
}
|
||||
|
@ -486,40 +920,40 @@ export default React.createClass({
|
|||
<div className={classnames(headerClasses)}>
|
||||
<div className="mx_GroupView_header_leftCol">
|
||||
<div className="mx_GroupView_header_avatar">
|
||||
{avatarNode}
|
||||
{ avatarNode }
|
||||
</div>
|
||||
<div className="mx_GroupView_header_info">
|
||||
<div className="mx_GroupView_header_name">
|
||||
{nameNode}
|
||||
{ nameNode }
|
||||
</div>
|
||||
<div className="mx_GroupView_header_shortDesc">
|
||||
{shortDescNode}
|
||||
{ shortDescNode }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_GroupView_header_rightCol">
|
||||
{rightButtons}
|
||||
{ rightButtons }
|
||||
</div>
|
||||
</div>
|
||||
{roomBody}
|
||||
{ roomBody }
|
||||
</div>
|
||||
);
|
||||
} else if (this.state.error) {
|
||||
if (this.state.error.httpStatus === 404) {
|
||||
return (
|
||||
<div className="mx_GroupView_error">
|
||||
Group {this.props.groupId} not found
|
||||
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>;
|
||||
extraText = <div>{ _t('This Home server does not support groups') }</div>;
|
||||
}
|
||||
return (
|
||||
<div className="mx_GroupView_error">
|
||||
Failed to load {this.props.groupId}
|
||||
{extraText}
|
||||
Failed to load { this.props.groupId }
|
||||
{ extraText }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -81,10 +81,6 @@ export default React.createClass({
|
|||
// stash the MatrixClient in case we log out before we are unmounted
|
||||
this._matrixClient = this.props.matrixClient;
|
||||
|
||||
// _scrollStateMap is a map from room id to the scroll state returned by
|
||||
// RoomView.getScrollState()
|
||||
this._scrollStateMap = {};
|
||||
|
||||
CallMediaHandler.loadDevices();
|
||||
|
||||
document.addEventListener('keydown', this._onKeyDown);
|
||||
|
@ -116,10 +112,6 @@ export default React.createClass({
|
|||
return Boolean(MatrixClientPeg.get());
|
||||
},
|
||||
|
||||
getScrollStateForRoom: function(roomId) {
|
||||
return this._scrollStateMap[roomId];
|
||||
},
|
||||
|
||||
canResetTimelineInRoom: function(roomId) {
|
||||
if (!this.refs.roomView) {
|
||||
return true;
|
||||
|
@ -139,6 +131,9 @@ export default React.createClass({
|
|||
useCompactLayout: event.getContent().useCompactLayout,
|
||||
});
|
||||
}
|
||||
if (event.getType() === "m.ignored_user_list") {
|
||||
dis.dispatch({action: "ignore_state_changed"});
|
||||
}
|
||||
},
|
||||
|
||||
_onKeyDown: function(ev) {
|
||||
|
@ -246,11 +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}
|
||||
scrollStateMap={this._scrollStateMap}
|
||||
/>;
|
||||
if (!this.props.collapse_rhs) right_panel = <RightPanel roomId={this.props.currentRoomId} opacity={this.props.rightOpacity} />;
|
||||
if (!this.props.collapseRhs) right_panel = <RightPanel roomId={this.props.currentRoomId} opacity={this.props.rightOpacity} />;
|
||||
break;
|
||||
|
||||
case PageTypes.UserSettings:
|
||||
|
@ -261,7 +255,7 @@ export default React.createClass({
|
|||
referralBaseUrl={this.props.config.referralBaseUrl}
|
||||
teamToken={this.props.teamToken}
|
||||
/>;
|
||||
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.rightOpacity}/>;
|
||||
if (!this.props.collapseRhs) right_panel = <RightPanel opacity={this.props.rightOpacity}/>;
|
||||
break;
|
||||
|
||||
case PageTypes.MyGroups:
|
||||
|
@ -271,9 +265,9 @@ export default React.createClass({
|
|||
case PageTypes.CreateRoom:
|
||||
page_element = <CreateRoom
|
||||
onRoomCreated={this.props.onRoomCreated}
|
||||
collapsedRhs={this.props.collapse_rhs}
|
||||
collapsedRhs={this.props.collapseRhs}
|
||||
/>;
|
||||
if (!this.props.collapse_rhs) right_panel = <RightPanel opacity={this.props.rightOpacity}/>;
|
||||
if (!this.props.collapseRhs) right_panel = <RightPanel opacity={this.props.rightOpacity}/>;
|
||||
break;
|
||||
|
||||
case PageTypes.RoomDirectory:
|
||||
|
@ -306,8 +300,9 @@ export default React.createClass({
|
|||
case PageTypes.GroupView:
|
||||
page_element = <GroupView
|
||||
groupId={this.props.currentGroupId}
|
||||
collapsedRhs={this.props.collapseRhs}
|
||||
/>;
|
||||
//right_panel = <RightPanel opacity={this.props.rightOpacity} />;
|
||||
if (!this.props.collapseRhs) right_panel = <RightPanel groupId={this.props.currentGroupId} opacity={this.props.rightOpacity} />;
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -339,7 +334,7 @@ export default React.createClass({
|
|||
<div className={bodyClasses}>
|
||||
<LeftPanel
|
||||
selectedRoom={this.props.currentRoomId}
|
||||
collapsed={this.props.collapse_lhs || false}
|
||||
collapsed={this.props.collapseLhs || false}
|
||||
opacity={this.props.leftOpacity}
|
||||
/>
|
||||
<main className='mx_MatrixChat_middlePanel'>
|
||||
|
|
|
@ -32,13 +32,12 @@ 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';
|
||||
// LifecycleStore is not used but does listen to and dispatch actions
|
||||
require('../../stores/LifecycleStore');
|
||||
import RoomViewStore from '../../stores/RoomViewStore';
|
||||
import PageTypes from '../../PageTypes';
|
||||
|
||||
import createRoom from "../../createRoom";
|
||||
|
@ -144,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,
|
||||
|
@ -214,9 +213,6 @@ module.exports = React.createClass({
|
|||
componentWillMount: function() {
|
||||
SdkConfig.put(this.props.config);
|
||||
|
||||
this._roomViewStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdated);
|
||||
this._onRoomViewStoreUpdated();
|
||||
|
||||
if (!UserSettingsStore.getLocalSetting('analyticsOptOut', false)) Analytics.enable();
|
||||
|
||||
// Used by _viewRoom before getting state from sync
|
||||
|
@ -353,7 +349,6 @@ module.exports = React.createClass({
|
|||
UDEHandler.stopListening();
|
||||
window.removeEventListener("focus", this.onFocus);
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
this._roomViewStoreToken.remove();
|
||||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
|
@ -439,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',
|
||||
|
@ -521,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': {
|
||||
|
@ -587,10 +582,6 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_onRoomViewStoreUpdated: function() {
|
||||
this.setState({ currentRoomId: RoomViewStore.getRoomId() });
|
||||
},
|
||||
|
||||
_setPage: function(pageType) {
|
||||
this.setState({
|
||||
page_type: pageType,
|
||||
|
@ -677,10 +668,10 @@ module.exports = React.createClass({
|
|||
this.focusComposer = true;
|
||||
|
||||
const newState = {
|
||||
currentRoomId: roomInfo.room_id || null,
|
||||
page_type: PageTypes.RoomView,
|
||||
thirdPartyInvite: roomInfo.third_party_invite,
|
||||
roomOobData: roomInfo.oob_data,
|
||||
autoJoin: roomInfo.auto_join,
|
||||
};
|
||||
|
||||
if (roomInfo.room_alias) {
|
||||
|
@ -860,7 +851,7 @@ module.exports = React.createClass({
|
|||
title: _t("Leave room"),
|
||||
description: (
|
||||
<span>
|
||||
{_t("Are you sure you want to leave the room '%(roomName)s'?", {roomName: roomToLeave.name})}
|
||||
{ _t("Are you sure you want to leave the room '%(roomName)s'?", {roomName: roomToLeave.name}) }
|
||||
</span>
|
||||
),
|
||||
onFinished: (shouldLeave) => {
|
||||
|
@ -1000,8 +991,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,
|
||||
});
|
||||
|
@ -1066,10 +1057,13 @@ module.exports = React.createClass({
|
|||
self.setState({ready: true});
|
||||
});
|
||||
cli.on('Call.incoming', function(call) {
|
||||
// we dispatch this synchronously to make sure that the event
|
||||
// handlers on the call are set up immediately (so that if
|
||||
// we get an immediate hangup, we don't get a stuck call)
|
||||
dis.dispatch({
|
||||
action: 'incoming_call',
|
||||
call: call,
|
||||
});
|
||||
}, true);
|
||||
});
|
||||
cli.on('Session.logged_out', function(call) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
@ -1454,7 +1448,7 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<div className="mx_MatrixChat_splash">
|
||||
<Spinner />
|
||||
<a href="#" className="mx_MatrixChat_splashButtons" onClick={ this.onLogoutClick }>
|
||||
<a href="#" className="mx_MatrixChat_splashButtons" onClick={this.onLogoutClick}>
|
||||
{ _t('Logout') }
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -65,7 +65,7 @@ module.exports = React.createClass({
|
|||
suppressFirstDateSeparator: React.PropTypes.bool,
|
||||
|
||||
// whether to show read receipts
|
||||
manageReadReceipts: React.PropTypes.bool,
|
||||
showReadReceipts: React.PropTypes.bool,
|
||||
|
||||
// true if updates to the event list should cause the scroll panel to
|
||||
// scroll down when we are at the bottom of the window. See ScrollPanel
|
||||
|
@ -241,6 +241,10 @@ module.exports = React.createClass({
|
|||
|
||||
// TODO: Implement granular (per-room) hide options
|
||||
_shouldShowEvent: function(mxEv) {
|
||||
if (mxEv.sender && MatrixClientPeg.get().isUserIgnored(mxEv.sender.userId)) {
|
||||
return false; // ignored = no show (only happens if the ignore happens after an event was received)
|
||||
}
|
||||
|
||||
const EventTile = sdk.getComponent('rooms.EventTile');
|
||||
if (!EventTile.haveTileForEvent(mxEv)) {
|
||||
return false; // no tile = no show
|
||||
|
@ -339,6 +343,15 @@ module.exports = React.createClass({
|
|||
for (;i + 1 < this.props.events.length; i++) {
|
||||
const collapsedMxEv = this.props.events[i + 1];
|
||||
|
||||
// Ignore redacted/hidden member events
|
||||
if (!this._shouldShowEvent(collapsedMxEv)) {
|
||||
// If this hidden event is the RM and in or at end of a MELS put RM after MELS.
|
||||
if (collapsedMxEv.getId() === this.props.readMarkerEventId) {
|
||||
readMarkerInMels = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isMembershipChange(collapsedMxEv) ||
|
||||
this._wantsDateSeparator(this.props.events[i], collapsedMxEv.getDate())) {
|
||||
break;
|
||||
|
@ -349,16 +362,16 @@ module.exports = React.createClass({
|
|||
readMarkerInMels = true;
|
||||
}
|
||||
|
||||
// Ignore redacted/hidden member events
|
||||
if (!this._shouldShowEvent(collapsedMxEv)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
summarisedEvents.push(collapsedMxEv);
|
||||
}
|
||||
|
||||
let highlightInMels = false;
|
||||
|
||||
// At this point, i = the index of the last event in the summary sequence
|
||||
let eventTiles = summarisedEvents.map((e) => {
|
||||
if (e.getId() === this.props.highlightedEventId) {
|
||||
highlightInMels = true;
|
||||
}
|
||||
// In order to prevent DateSeparators from appearing in the expanded form
|
||||
// of MemberEventListSummary, render each member event as if the previous
|
||||
// one was itself. This way, the timestamp of the previous event === the
|
||||
|
@ -372,15 +385,13 @@ module.exports = React.createClass({
|
|||
eventTiles = null;
|
||||
}
|
||||
|
||||
ret.push(
|
||||
<MemberEventListSummary
|
||||
key={key}
|
||||
events={summarisedEvents}
|
||||
onToggle={this._onWidgetLoad} // Update scroll state
|
||||
>
|
||||
{eventTiles}
|
||||
</MemberEventListSummary>
|
||||
);
|
||||
ret.push(<MemberEventListSummary key={key}
|
||||
events={summarisedEvents}
|
||||
onToggle={this._onWidgetLoad} // Update scroll state
|
||||
startExpanded={highlightInMels}
|
||||
>
|
||||
{eventTiles}
|
||||
</MemberEventListSummary>);
|
||||
|
||||
if (readMarkerInMels) {
|
||||
ret.push(this._getReadMarkerTile(visible));
|
||||
|
@ -487,7 +498,7 @@ module.exports = React.createClass({
|
|||
var scrollToken = mxEv.status ? undefined : eventId;
|
||||
|
||||
var readReceipts;
|
||||
if (this.props.manageReadReceipts) {
|
||||
if (this.props.showReadReceipts) {
|
||||
readReceipts = this._getReadReceiptsForEvent(mxEv);
|
||||
}
|
||||
ret.push(
|
||||
|
@ -545,6 +556,9 @@ module.exports = React.createClass({
|
|||
if (!r.userId || r.type !== "m.read" || r.userId === myUserId) {
|
||||
return; // ignore non-read receipts and receipts from self.
|
||||
}
|
||||
if (MatrixClientPeg.get().isUserIgnored(r.userId)) {
|
||||
return; // ignore ignored users
|
||||
}
|
||||
let member = room.getMember(r.userId);
|
||||
if (!member) {
|
||||
return; // ignore unknown user IDs
|
||||
|
|
|
@ -39,7 +39,7 @@ const GroupTile = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
return <a onClick={this.onClick} href="#">{this.props.groupId}</a>;
|
||||
return <a onClick={this.onClick} href="#">{ this.props.groupId }</a>;
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -90,51 +90,51 @@ export default withMatrixClient(React.createClass({
|
|||
);
|
||||
});
|
||||
content = <div>
|
||||
<div>{_t('You are a member of these groups:')}</div>
|
||||
{groupNodes}
|
||||
<div>{ _t('You are a member of these groups:') }</div>
|
||||
{ groupNodes }
|
||||
</div>;
|
||||
} else if (this.state.error) {
|
||||
content = <div className="mx_MyGroups_error">
|
||||
{_t('Error whilst fetching joined groups')}
|
||||
{ _t('Error whilst fetching joined groups') }
|
||||
</div>;
|
||||
} else {
|
||||
content = <Loader />;
|
||||
}
|
||||
|
||||
return <div className="mx_MyGroups">
|
||||
<SimpleRoomHeader title={ _t("Groups") } />
|
||||
<SimpleRoomHeader title={_t("Groups")} icon="img/icons-groups.svg" />
|
||||
<div className='mx_MyGroups_joinCreateBox'>
|
||||
<div className="mx_MyGroups_createBox">
|
||||
<div className="mx_MyGroups_joinCreateHeader">
|
||||
{_t('Create a new group')}
|
||||
{ _t('Create a new group') }
|
||||
</div>
|
||||
<AccessibleButton className='mx_MyGroups_joinCreateButton' onClick={this._onCreateGroupClick}>
|
||||
<TintableSvg src="img/icons-create-room.svg" width="50" height="50" />
|
||||
</AccessibleButton>
|
||||
{_t(
|
||||
{ _t(
|
||||
'Create a group to represent your community! '+
|
||||
'Define a set of rooms and your own custom homepage '+
|
||||
'to mark out your space in the Matrix universe.',
|
||||
)}
|
||||
) }
|
||||
</div>
|
||||
<div className="mx_MyGroups_joinBox">
|
||||
<div className="mx_MyGroups_joinCreateHeader">
|
||||
{_t('Join an existing group')}
|
||||
{ _t('Join an existing group') }
|
||||
</div>
|
||||
<AccessibleButton className='mx_MyGroups_joinCreateButton' onClick={this._onJoinGroupClick}>
|
||||
<TintableSvg src="img/icons-create-room.svg" width="50" height="50" />
|
||||
</AccessibleButton>
|
||||
{_tJsx(
|
||||
'To join an exisitng group you\'ll have to '+
|
||||
{ _tJsx(
|
||||
'To join an existing group you\'ll have to '+
|
||||
'know its group identifier; this will look '+
|
||||
'something like <i>+example:matrix.org</i>.',
|
||||
/<i>(.*)<\/i>/,
|
||||
(sub) => <i>{sub}</i>,
|
||||
)}
|
||||
(sub) => <i>{ sub }</i>,
|
||||
) }
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_MyGroups_content">
|
||||
{content}
|
||||
{ content }
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
|
|
@ -121,7 +121,7 @@ module.exports = React.createClass({
|
|||
|
||||
onRoomMemberTyping: function(ev, member) {
|
||||
this.setState({
|
||||
usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room),
|
||||
usersTyping: WhoIsTyping.usersTypingApartFromMeAndIgnored(this.props.room),
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -20,6 +20,8 @@ limitations under the License.
|
|||
// - Drag and drop
|
||||
// - File uploading - uploadFile()
|
||||
|
||||
import shouldHideEvent from "../../shouldHideEvent";
|
||||
|
||||
var React = require("react");
|
||||
var ReactDOM = require("react-dom");
|
||||
import Promise from 'bluebird';
|
||||
|
@ -45,6 +47,7 @@ import KeyCode from '../../KeyCode';
|
|||
import UserProvider from '../../autocomplete/UserProvider';
|
||||
|
||||
import RoomViewStore from '../../stores/RoomViewStore';
|
||||
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
|
||||
|
||||
let DEBUG = false;
|
||||
let debuglog = function() {};
|
||||
|
@ -120,6 +123,9 @@ module.exports = React.createClass({
|
|||
// store the error here.
|
||||
roomLoadError: null,
|
||||
|
||||
// Have we sent a request to join the room that we're waiting to complete?
|
||||
joining: false,
|
||||
|
||||
// this is true if we are fully scrolled-down, and are looking at
|
||||
// the end of the live timeline. It has the effect of hiding the
|
||||
// 'scroll to bottom' knob, among a couple of other things.
|
||||
|
@ -143,6 +149,8 @@ module.exports = React.createClass({
|
|||
MatrixClientPeg.get().on("RoomMember.membership", this.onRoomMemberMembership);
|
||||
MatrixClientPeg.get().on("accountData", this.onAccountData);
|
||||
|
||||
this._syncedSettings = UserSettingsStore.getSyncedSettings();
|
||||
|
||||
// Start listening for RoomViewStore updates
|
||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
||||
this._onRoomViewStoreUpdate(true);
|
||||
|
@ -152,6 +160,22 @@ module.exports = React.createClass({
|
|||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!initial && this.state.roomId !== RoomViewStore.getRoomId()) {
|
||||
// RoomView explicitly does not support changing what room
|
||||
// is being viewed: instead it should just be re-mounted when
|
||||
// switching rooms. Therefore, if the room ID changes, we
|
||||
// ignore this. We either need to do this or add code to handle
|
||||
// saving the scroll position (otherwise we end up saving the
|
||||
// scroll position against the wrong room).
|
||||
|
||||
// Given that doing the setState here would cause a bunch of
|
||||
// unnecessary work, we just ignore the change since we know
|
||||
// that if the current room ID has changed from what we thought
|
||||
// it was, it means we're about to be unmounted.
|
||||
return;
|
||||
}
|
||||
|
||||
const newState = {
|
||||
roomId: RoomViewStore.getRoomId(),
|
||||
roomAlias: RoomViewStore.getRoomAlias(),
|
||||
|
@ -159,16 +183,11 @@ module.exports = React.createClass({
|
|||
roomLoadError: RoomViewStore.getRoomLoadError(),
|
||||
joining: RoomViewStore.isJoining(),
|
||||
initialEventId: RoomViewStore.getInitialEventId(),
|
||||
initialEventPixelOffset: RoomViewStore.getInitialEventPixelOffset(),
|
||||
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
|
||||
forwardingEvent: RoomViewStore.getForwardingEvent(),
|
||||
shouldPeek: RoomViewStore.shouldPeek(),
|
||||
};
|
||||
|
||||
// finished joining, start waiting for a room and show a spinner. See onRoom.
|
||||
newState.waitingForRoom = this.state.joining && !newState.joining &&
|
||||
!RoomViewStore.getJoinError();
|
||||
|
||||
// Temporary logging to diagnose https://github.com/vector-im/riot-web/issues/4307
|
||||
console.log(
|
||||
'RVS update:',
|
||||
|
@ -177,7 +196,6 @@ module.exports = React.createClass({
|
|||
'loading?', newState.roomLoading,
|
||||
'joining?', newState.joining,
|
||||
'initial?', initial,
|
||||
'waiting?', newState.waitingForRoom,
|
||||
'shouldPeek?', newState.shouldPeek,
|
||||
);
|
||||
|
||||
|
@ -185,6 +203,25 @@ module.exports = React.createClass({
|
|||
// the RoomView instance
|
||||
if (initial) {
|
||||
newState.room = MatrixClientPeg.get().getRoom(newState.roomId);
|
||||
if (newState.room) {
|
||||
newState.unsentMessageError = this._getUnsentMessageError(newState.room);
|
||||
newState.showApps = this._shouldShowApps(newState.room);
|
||||
this._onRoomLoaded(newState.room);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.state.roomId === null && newState.roomId !== null) {
|
||||
// Get the scroll state for the new room
|
||||
|
||||
// If an event ID wasn't specified, default to the one saved for this room
|
||||
// in the scroll state store. Assume initialEventPixelOffset should be set.
|
||||
if (!newState.initialEventId) {
|
||||
const roomScrollState = RoomScrollStateStore.getScrollState(newState.roomId);
|
||||
if (roomScrollState) {
|
||||
newState.initialEventId = roomScrollState.focussedEvent;
|
||||
newState.initialEventPixelOffset = roomScrollState.pixelOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the search results when clicking a search result (which changes the
|
||||
|
@ -193,22 +230,20 @@ module.exports = React.createClass({
|
|||
newState.searchResults = null;
|
||||
}
|
||||
|
||||
// Store the scroll state for the previous room so that we can return to this
|
||||
// position when viewing this room in future.
|
||||
if (this.state.roomId !== newState.roomId) {
|
||||
this._updateScrollMap(this.state.roomId);
|
||||
}
|
||||
this.setState(newState);
|
||||
// At this point, newState.roomId could be null (e.g. the alias might not
|
||||
// have been resolved yet) so anything called here must handle this case.
|
||||
|
||||
this.setState(newState, () => {
|
||||
// At this point, this.state.roomId could be null (e.g. the alias might not
|
||||
// have been resolved yet) so anything called here must handle this case.
|
||||
if (initial) {
|
||||
this._onHaveRoom();
|
||||
}
|
||||
});
|
||||
// We pass the new state into this function for it to read: it needs to
|
||||
// observe the new state but we don't want to put it in the setState
|
||||
// callback because this would prevent the setStates from being batched,
|
||||
// ie. cause it to render RoomView twice rather than the once that is necessary.
|
||||
if (initial) {
|
||||
this._setupRoom(newState.room, newState.roomId, newState.joining, newState.shouldPeek);
|
||||
}
|
||||
},
|
||||
|
||||
_onHaveRoom: function() {
|
||||
_setupRoom: function(room, roomId, joining, shouldPeek) {
|
||||
// if this is an unknown room then we're in one of three states:
|
||||
// - This is a room we can peek into (search engine) (we can /peek)
|
||||
// - This is a room we can publicly join or were invited to. (we can /join)
|
||||
|
@ -224,23 +259,15 @@ module.exports = React.createClass({
|
|||
// about it). We don't peek in the historical case where we were joined but are
|
||||
// now not joined because the js-sdk peeking API will clobber our historical room,
|
||||
// making it impossible to indicate a newly joined room.
|
||||
const room = this.state.room;
|
||||
if (room) {
|
||||
this.setState({
|
||||
unsentMessageError: this._getUnsentMessageError(room),
|
||||
showApps: this._shouldShowApps(room),
|
||||
});
|
||||
this._onRoomLoaded(room);
|
||||
}
|
||||
if (!this.state.joining && this.state.roomId) {
|
||||
if (!joining && roomId) {
|
||||
if (this.props.autoJoin) {
|
||||
this.onJoinButtonClicked();
|
||||
} else if (!room && this.state.shouldPeek) {
|
||||
console.log("Attempting to peek into room %s", this.state.roomId);
|
||||
} else if (!room && shouldPeek) {
|
||||
console.log("Attempting to peek into room %s", roomId);
|
||||
this.setState({
|
||||
peekLoading: true,
|
||||
});
|
||||
MatrixClientPeg.get().peekInRoom(this.state.roomId).then((room) => {
|
||||
MatrixClientPeg.get().peekInRoom(roomId).then((room) => {
|
||||
this.setState({
|
||||
room: room,
|
||||
peekLoading: false,
|
||||
|
@ -336,7 +363,9 @@ module.exports = React.createClass({
|
|||
this.unmounted = true;
|
||||
|
||||
// update the scroll map before we get unmounted
|
||||
this._updateScrollMap(this.state.roomId);
|
||||
if (this.state.roomId) {
|
||||
RoomScrollStateStore.setScrollState(this.state.roomId, this._getScrollState());
|
||||
}
|
||||
|
||||
if (this.refs.roomView) {
|
||||
// disconnect the D&D event listeners from the room view. This
|
||||
|
@ -497,8 +526,7 @@ module.exports = React.createClass({
|
|||
// update unread count when scrolled up
|
||||
if (!this.state.searchResults && this.state.atEndOfLiveTimeline) {
|
||||
// no change
|
||||
}
|
||||
else {
|
||||
} else if (!shouldHideEvent(ev, this._syncedSettings)) {
|
||||
this.setState((state, props) => {
|
||||
return {numUnreadMessages: state.numUnreadMessages + 1};
|
||||
});
|
||||
|
@ -614,25 +642,12 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_updateScrollMap(roomId) {
|
||||
// No point updating scroll state if the room ID hasn't been resolved yet
|
||||
if (!roomId) {
|
||||
return;
|
||||
}
|
||||
dis.dispatch({
|
||||
action: 'update_scroll_state',
|
||||
room_id: roomId,
|
||||
scroll_state: this._getScrollState(),
|
||||
});
|
||||
},
|
||||
|
||||
onRoom: function(room) {
|
||||
if (!room || room.roomId !== this.state.roomId) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
room: room,
|
||||
waitingForRoom: false,
|
||||
}, () => {
|
||||
this._onRoomLoaded(room);
|
||||
});
|
||||
|
@ -688,14 +703,7 @@ module.exports = React.createClass({
|
|||
|
||||
onRoomMemberMembership: function(ev, member, oldMembership) {
|
||||
if (member.userId == MatrixClientPeg.get().credentials.userId) {
|
||||
|
||||
if (member.membership === 'join') {
|
||||
this.setState({
|
||||
waitingForRoom: false,
|
||||
});
|
||||
} else {
|
||||
this.forceUpdate();
|
||||
}
|
||||
this.forceUpdate();
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -1445,10 +1453,6 @@ module.exports = React.createClass({
|
|||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
|
||||
|
||||
// Whether the preview bar spinner should be shown. We do this when joining or
|
||||
// when waiting for a room to be returned by js-sdk when joining
|
||||
const previewBarSpinner = this.state.joining || this.state.waitingForRoom;
|
||||
|
||||
if (!this.state.room) {
|
||||
if (this.state.roomLoading || this.state.peekLoading) {
|
||||
return (
|
||||
|
@ -1482,7 +1486,7 @@ module.exports = React.createClass({
|
|||
onRejectClick={ this.onRejectThreepidInviteButtonClicked }
|
||||
canPreview={ false } error={ this.state.roomLoadError }
|
||||
roomAlias={roomAlias}
|
||||
spinner={previewBarSpinner}
|
||||
spinner={this.state.joining}
|
||||
inviterName={inviterName}
|
||||
invitedEmail={invitedEmail}
|
||||
room={this.state.room}
|
||||
|
@ -1525,7 +1529,7 @@ module.exports = React.createClass({
|
|||
onRejectClick={ this.onRejectButtonClicked }
|
||||
inviterName={ inviterName }
|
||||
canPreview={ false }
|
||||
spinner={previewBarSpinner}
|
||||
spinner={this.state.joining}
|
||||
room={this.state.room}
|
||||
/>
|
||||
</div>
|
||||
|
@ -1600,7 +1604,7 @@ module.exports = React.createClass({
|
|||
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
|
||||
onForgetClick={ this.onForgetClick }
|
||||
onRejectClick={this.onRejectThreepidInviteButtonClicked}
|
||||
spinner={previewBarSpinner}
|
||||
spinner={this.state.joining}
|
||||
inviterName={inviterName}
|
||||
invitedEmail={invitedEmail}
|
||||
canPreview={this.state.canPeek}
|
||||
|
@ -1716,7 +1720,8 @@ module.exports = React.createClass({
|
|||
var messagePanel = (
|
||||
<TimelinePanel ref={this._gatherTimelinePanelRef}
|
||||
timelineSet={this.state.room.getUnfilteredTimelineSet()}
|
||||
manageReadReceipts={!UserSettingsStore.getSyncedSetting('hideReadReceipts', false)}
|
||||
showReadReceipts={!UserSettingsStore.getSyncedSetting('hideReadReceipts', false)}
|
||||
manageReadReceipts={true}
|
||||
manageReadMarkers={true}
|
||||
hidden={hideMessagePanel}
|
||||
highlightedEventId={highlightedEventId}
|
||||
|
|
|
@ -157,7 +157,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.checkFillState();
|
||||
this.checkScroll();
|
||||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
|
|
|
@ -59,6 +59,7 @@ var TimelinePanel = React.createClass({
|
|||
// that room.
|
||||
timelineSet: React.PropTypes.object.isRequired,
|
||||
|
||||
showReadReceipts: React.PropTypes.bool,
|
||||
// Enable managing RRs and RMs. These require the timelineSet to have a room.
|
||||
manageReadReceipts: React.PropTypes.bool,
|
||||
manageReadMarkers: React.PropTypes.bool,
|
||||
|
@ -197,6 +198,7 @@ var TimelinePanel = React.createClass({
|
|||
MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt);
|
||||
MatrixClientPeg.get().on("Room.localEchoUpdated", this.onLocalEchoUpdated);
|
||||
MatrixClientPeg.get().on("Room.accountData", this.onAccountData);
|
||||
MatrixClientPeg.get().on("Event.decrypted", this.onEventDecrypted);
|
||||
MatrixClientPeg.get().on("sync", this.onSync);
|
||||
|
||||
this._initTimeline(this.props);
|
||||
|
@ -266,6 +268,7 @@ var TimelinePanel = React.createClass({
|
|||
client.removeListener("Room.receipt", this.onRoomReceipt);
|
||||
client.removeListener("Room.localEchoUpdated", this.onLocalEchoUpdated);
|
||||
client.removeListener("Room.accountData", this.onAccountData);
|
||||
client.removeListener("Event.decrypted", this.onEventDecrypted);
|
||||
client.removeListener("sync", this.onSync);
|
||||
}
|
||||
},
|
||||
|
@ -341,9 +344,16 @@ var TimelinePanel = React.createClass({
|
|||
newState[canPaginateOtherWayKey] = true;
|
||||
}
|
||||
|
||||
this.setState(newState);
|
||||
|
||||
return r;
|
||||
// Don't resolve until the setState has completed: we need to let
|
||||
// the component update before we consider the pagination completed,
|
||||
// otherwise we'll end up paginating in all the history the js-sdk
|
||||
// has in memory because we never gave the component a chance to scroll
|
||||
// itself into the right place
|
||||
return new Promise((resolve) => {
|
||||
this.setState(newState, () => {
|
||||
resolve(r);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -374,6 +384,9 @@ var TimelinePanel = React.createClass({
|
|||
this.sendReadReceipt();
|
||||
this.updateReadMarker();
|
||||
break;
|
||||
case 'ignore_state_changed':
|
||||
this.forceUpdate();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -503,6 +516,18 @@ var TimelinePanel = React.createClass({
|
|||
}, this.props.onReadMarkerUpdated);
|
||||
},
|
||||
|
||||
onEventDecrypted: function(ev) {
|
||||
// Need to update as we don't display event tiles for events that
|
||||
// haven't yet been decrypted. The event will have just been updated
|
||||
// in place so we just need to re-render.
|
||||
// TODO: We should restrict this to only events in our timeline,
|
||||
// but possibly the event tile itself should just update when this
|
||||
// happens to save us re-rendering the whole timeline.
|
||||
if (ev.getRoomId() === this.props.timelineSet.room.roomId) {
|
||||
this.forceUpdate();
|
||||
}
|
||||
},
|
||||
|
||||
onSync: function(state, prevState, data) {
|
||||
this.setState({clientSyncState: state});
|
||||
},
|
||||
|
@ -1126,8 +1151,8 @@ var TimelinePanel = React.createClass({
|
|||
readMarkerEventId={ this.state.readMarkerEventId }
|
||||
readMarkerVisible={ this.state.readMarkerVisible }
|
||||
suppressFirstDateSeparator={ this.state.canBackPaginate }
|
||||
showUrlPreview = { this.props.showUrlPreview }
|
||||
manageReadReceipts = { this.props.manageReadReceipts }
|
||||
showUrlPreview={ this.props.showUrlPreview }
|
||||
showReadReceipts={ this.props.showReadReceipts }
|
||||
ourUserId={ MatrixClientPeg.get().credentials.userId }
|
||||
stickyBottom={ stickyBottom }
|
||||
onScroll={ this.onMessageListScroll }
|
||||
|
|
|
@ -32,7 +32,7 @@ const AddThreepid = require('../../AddThreepid');
|
|||
const SdkConfig = require('../../SdkConfig');
|
||||
import Analytics from '../../Analytics';
|
||||
import AccessibleButton from '../views/elements/AccessibleButton';
|
||||
import { _t } from '../../languageHandler';
|
||||
import { _t, _td } from '../../languageHandler';
|
||||
import * as languageHandler from '../../languageHandler';
|
||||
import * as FormattingUtils from '../../utils/FormattingUtils';
|
||||
|
||||
|
@ -52,7 +52,7 @@ const gHVersionLabel = function(repo, token='') {
|
|||
} else {
|
||||
url = `https://github.com/${repo}/commit/${token.split('-')[0]}`;
|
||||
}
|
||||
return <a target="_blank" rel="noopener" href={url}>{token}</a>;
|
||||
return <a target="_blank" rel="noopener" href={url}>{ token }</a>;
|
||||
};
|
||||
|
||||
// Enumerate some simple 'flip a bit' UI settings (if any).
|
||||
|
@ -63,51 +63,55 @@ const gHVersionLabel = function(repo, token='') {
|
|||
const SETTINGS_LABELS = [
|
||||
{
|
||||
id: 'autoplayGifsAndVideos',
|
||||
label: 'Autoplay GIFs and videos',
|
||||
label: _td('Autoplay GIFs and videos'),
|
||||
},
|
||||
{
|
||||
id: 'hideReadReceipts',
|
||||
label: 'Hide read receipts',
|
||||
label: _td('Hide read receipts'),
|
||||
},
|
||||
{
|
||||
id: 'dontSendTypingNotifications',
|
||||
label: "Don't send typing notifications",
|
||||
label: _td("Don't send typing notifications"),
|
||||
},
|
||||
{
|
||||
id: 'alwaysShowTimestamps',
|
||||
label: 'Always show message timestamps',
|
||||
label: _td('Always show message timestamps'),
|
||||
},
|
||||
{
|
||||
id: 'showTwelveHourTimestamps',
|
||||
label: 'Show timestamps in 12 hour format (e.g. 2:30pm)',
|
||||
label: _td('Show timestamps in 12 hour format (e.g. 2:30pm)'),
|
||||
},
|
||||
{
|
||||
id: 'hideJoinLeaves',
|
||||
label: 'Hide join/leave messages (invites/kicks/bans unaffected)',
|
||||
label: _td('Hide join/leave messages (invites/kicks/bans unaffected)'),
|
||||
},
|
||||
{
|
||||
id: 'hideAvatarDisplaynameChanges',
|
||||
label: 'Hide avatar and display name changes',
|
||||
label: _td('Hide avatar and display name changes'),
|
||||
},
|
||||
{
|
||||
id: 'useCompactLayout',
|
||||
label: 'Use compact timeline layout',
|
||||
label: _td('Use compact timeline layout'),
|
||||
},
|
||||
{
|
||||
id: 'hideRedactions',
|
||||
label: 'Hide removed messages',
|
||||
label: _td('Hide removed messages'),
|
||||
},
|
||||
{
|
||||
id: 'enableSyntaxHighlightLanguageDetection',
|
||||
label: 'Enable automatic language detection for syntax highlighting',
|
||||
label: _td('Enable automatic language detection for syntax highlighting'),
|
||||
},
|
||||
{
|
||||
id: 'MessageComposerInput.autoReplaceEmoji',
|
||||
label: 'Automatically replace plain text Emoji',
|
||||
label: _td('Automatically replace plain text Emoji'),
|
||||
},
|
||||
{
|
||||
id: 'MessageComposerInput.dontSuggestEmoji',
|
||||
label: _td('Disable Emoji suggestions while typing'),
|
||||
},
|
||||
{
|
||||
id: 'Pill.shouldHidePillAvatar',
|
||||
label: 'Hide avatars in user and room mentions',
|
||||
label: _td('Hide avatars in user and room mentions'),
|
||||
},
|
||||
/*
|
||||
{
|
||||
|
@ -120,7 +124,7 @@ const SETTINGS_LABELS = [
|
|||
const ANALYTICS_SETTINGS_LABELS = [
|
||||
{
|
||||
id: 'analyticsOptOut',
|
||||
label: 'Opt out of analytics',
|
||||
label: _td('Opt out of analytics'),
|
||||
fn: function(checked) {
|
||||
Analytics[checked ? 'disable' : 'enable']();
|
||||
},
|
||||
|
@ -130,7 +134,7 @@ const ANALYTICS_SETTINGS_LABELS = [
|
|||
const WEBRTC_SETTINGS_LABELS = [
|
||||
{
|
||||
id: 'webRtcForceTURN',
|
||||
label: 'Disable Peer-to-Peer for 1:1 calls',
|
||||
label: _td('Disable Peer-to-Peer for 1:1 calls'),
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -139,7 +143,7 @@ const WEBRTC_SETTINGS_LABELS = [
|
|||
const CRYPTO_SETTINGS_LABELS = [
|
||||
{
|
||||
id: 'blacklistUnverifiedDevices',
|
||||
label: 'Never send encrypted messages to unverified devices from this device',
|
||||
label: _td('Never send encrypted messages to unverified devices from this device'),
|
||||
fn: function(checked) {
|
||||
MatrixClientPeg.get().setGlobalBlacklistUnverifiedDevices(checked);
|
||||
},
|
||||
|
@ -162,16 +166,44 @@ const CRYPTO_SETTINGS_LABELS = [
|
|||
const THEMES = [
|
||||
{
|
||||
id: 'theme',
|
||||
label: 'Light theme',
|
||||
label: _td('Light theme'),
|
||||
value: 'light',
|
||||
},
|
||||
{
|
||||
id: 'theme',
|
||||
label: 'Dark theme',
|
||||
label: _td('Dark theme'),
|
||||
value: 'dark',
|
||||
},
|
||||
];
|
||||
|
||||
const IgnoredUser = React.createClass({
|
||||
propTypes: {
|
||||
userId: React.PropTypes.string.isRequired,
|
||||
onUnignored: React.PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
_onUnignoreClick: function() {
|
||||
const ignoredUsers = MatrixClientPeg.get().getIgnoredUsers();
|
||||
const index = ignoredUsers.indexOf(this.props.userId);
|
||||
if (index !== -1) {
|
||||
ignoredUsers.splice(index, 1);
|
||||
MatrixClientPeg.get().setIgnoredUsers(ignoredUsers)
|
||||
.then(() => this.props.onUnignored(this.props.userId));
|
||||
} else this.props.onUnignored(this.props.userId);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<li>
|
||||
<AccessibleButton onClick={this._onUnignoreClick} className="mx_UserSettings_button mx_UserSettings_buttonSmall">
|
||||
{ _t("Unignore") }
|
||||
</AccessibleButton>
|
||||
{ this.props.userId }
|
||||
</li>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'UserSettings',
|
||||
|
||||
|
@ -207,6 +239,7 @@ module.exports = React.createClass({
|
|||
vectorVersion: undefined,
|
||||
rejectingInvites: false,
|
||||
mediaDevices: null,
|
||||
ignoredUsers: [],
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -228,6 +261,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
this._refreshMediaDevices();
|
||||
this._refreshIgnoredUsers();
|
||||
|
||||
// Bulk rejecting invites:
|
||||
// /sync won't have had time to return when UserSettings re-renders from state changes, so getRooms()
|
||||
|
@ -346,9 +380,22 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_refreshIgnoredUsers: function(userIdUnignored=null) {
|
||||
const users = MatrixClientPeg.get().getIgnoredUsers();
|
||||
if (userIdUnignored) {
|
||||
const index = users.indexOf(userIdUnignored);
|
||||
if (index !== -1) users.splice(index, 1);
|
||||
}
|
||||
this.setState({
|
||||
ignoredUsers: users,
|
||||
});
|
||||
},
|
||||
|
||||
onAction: function(payload) {
|
||||
if (payload.action === "notifier_enabled") {
|
||||
this.forceUpdate();
|
||||
} else if (payload.action === "ignore_state_changed") {
|
||||
this._refreshIgnoredUsers();
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -627,7 +674,7 @@ module.exports = React.createClass({
|
|||
<div>
|
||||
<h3>Referral</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
{_t("Refer a friend to Riot:")} <a href={href}>{href}</a>
|
||||
{ _t("Refer a friend to Riot:") } <a href={href}>{ href }</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -646,7 +693,7 @@ module.exports = React.createClass({
|
|||
_renderLanguageSetting: function() {
|
||||
const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown');
|
||||
return <div>
|
||||
<label htmlFor="languageSelector">{_t('Interface Language')}</label>
|
||||
<label htmlFor="languageSelector">{ _t('Interface Language') }</label>
|
||||
<LanguageDropdown ref="language" onOptionChange={this.onLanguageChange}
|
||||
className="mx_UserSettings_language"
|
||||
value={this.state.language}
|
||||
|
@ -669,7 +716,7 @@ module.exports = React.createClass({
|
|||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>{_t('Autocomplete Delay (ms):')}</strong></td>
|
||||
<td><strong>{ _t('Autocomplete Delay (ms):') }</strong></td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
|
@ -690,8 +737,8 @@ module.exports = React.createClass({
|
|||
return <div className="mx_UserSettings_toggle">
|
||||
<input id="urlPreviewsDisabled"
|
||||
type="checkbox"
|
||||
defaultChecked={ UserSettingsStore.getUrlPreviewsDisabled() }
|
||||
onChange={ this._onPreviewsDisabledChanged }
|
||||
defaultChecked={UserSettingsStore.getUrlPreviewsDisabled()}
|
||||
onChange={this._onPreviewsDisabledChanged}
|
||||
/>
|
||||
<label htmlFor="urlPreviewsDisabled">
|
||||
{ _t("Disable inline URL previews by default") }
|
||||
|
@ -712,13 +759,13 @@ module.exports = React.createClass({
|
|||
if (setting.fn) setting.fn(e.target.checked);
|
||||
};
|
||||
|
||||
return <div className="mx_UserSettings_toggle" key={ setting.id }>
|
||||
<input id={ setting.id }
|
||||
return <div className="mx_UserSettings_toggle" key={setting.id}>
|
||||
<input id={setting.id}
|
||||
type="checkbox"
|
||||
defaultChecked={ this._syncedSettings[setting.id] }
|
||||
onChange={ onChange }
|
||||
defaultChecked={this._syncedSettings[setting.id]}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<label htmlFor={ setting.id }>
|
||||
<label htmlFor={setting.id}>
|
||||
{ _t(setting.label) }
|
||||
</label>
|
||||
</div>;
|
||||
|
@ -729,6 +776,7 @@ module.exports = React.createClass({
|
|||
// to rebind the onChange each time we render
|
||||
const onChange = (e) => {
|
||||
if (e.target.checked) {
|
||||
this._syncedSettings[setting.id] = setting.value;
|
||||
UserSettingsStore.setSyncedSetting(setting.id, setting.value);
|
||||
}
|
||||
dis.dispatch({
|
||||
|
@ -736,16 +784,16 @@ module.exports = React.createClass({
|
|||
value: setting.value,
|
||||
});
|
||||
};
|
||||
return <div className="mx_UserSettings_toggle" key={ setting.id + "_" + setting.value }>
|
||||
<input id={ setting.id + "_" + setting.value }
|
||||
return <div className="mx_UserSettings_toggle" key={setting.id + "_" + setting.value}>
|
||||
<input id={setting.id + "_" + setting.value}
|
||||
type="radio"
|
||||
name={ setting.id }
|
||||
value={ setting.value }
|
||||
defaultChecked={ this._syncedSettings[setting.id] === setting.value }
|
||||
onChange={ onChange }
|
||||
name={setting.id}
|
||||
value={setting.value}
|
||||
checked={this._syncedSettings[setting.id] === setting.value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<label htmlFor={ setting.id + "_" + setting.value }>
|
||||
{ setting.label }
|
||||
<label htmlFor={setting.id + "_" + setting.value}>
|
||||
{ _t(setting.label) }
|
||||
</label>
|
||||
</div>;
|
||||
},
|
||||
|
@ -781,10 +829,10 @@ module.exports = React.createClass({
|
|||
<h3>{ _t("Cryptography") }</h3>
|
||||
<div className="mx_UserSettings_section mx_UserSettings_cryptoSection">
|
||||
<ul>
|
||||
<li><label>{_t("Device ID:")}</label>
|
||||
<span><code>{deviceId}</code></span></li>
|
||||
<li><label>{_t("Device key:")}</label>
|
||||
<span><code><b>{identityKey}</b></code></span></li>
|
||||
<li><label>{ _t("Device ID:") }</label>
|
||||
<span><code>{ deviceId }</code></span></li>
|
||||
<li><label>{ _t("Device key:") }</label>
|
||||
<span><code><b>{ identityKey }</b></code></span></li>
|
||||
</ul>
|
||||
{ importExportButtons }
|
||||
</div>
|
||||
|
@ -795,6 +843,26 @@ module.exports = React.createClass({
|
|||
);
|
||||
},
|
||||
|
||||
_renderIgnoredUsers: function() {
|
||||
if (this.state.ignoredUsers.length > 0) {
|
||||
const updateHandler = this._refreshIgnoredUsers;
|
||||
return (
|
||||
<div>
|
||||
<h3>{ _t("Ignored Users") }</h3>
|
||||
<div className="mx_UserSettings_section mx_UserSettings_ignoredUsersSection">
|
||||
<ul>
|
||||
{ this.state.ignoredUsers.map(function(userId) {
|
||||
return (<IgnoredUser key={userId}
|
||||
userId={userId}
|
||||
onUnignored={updateHandler}></IgnoredUser>);
|
||||
}) }
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else return (<div />);
|
||||
},
|
||||
|
||||
_renderLocalSetting: function(setting) {
|
||||
// TODO: this ought to be a separate component so that we don't need
|
||||
// to rebind the onChange each time we render
|
||||
|
@ -803,13 +871,13 @@ module.exports = React.createClass({
|
|||
if (setting.fn) setting.fn(e.target.checked);
|
||||
};
|
||||
|
||||
return <div className="mx_UserSettings_toggle" key={ setting.id }>
|
||||
<input id={ setting.id }
|
||||
return <div className="mx_UserSettings_toggle" key={setting.id}>
|
||||
<input id={setting.id}
|
||||
type="checkbox"
|
||||
defaultChecked={ this._localSettings[setting.id] }
|
||||
onChange={ onChange }
|
||||
defaultChecked={this._localSettings[setting.id]}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<label htmlFor={ setting.id }>
|
||||
<label htmlFor={setting.id}>
|
||||
{ _t(setting.label) }
|
||||
</label>
|
||||
</div>;
|
||||
|
@ -819,8 +887,8 @@ module.exports = React.createClass({
|
|||
const DevicesPanel = sdk.getComponent('settings.DevicesPanel');
|
||||
return (
|
||||
<div>
|
||||
<h3>{_t("Devices")}</h3>
|
||||
<DevicesPanel className="mx_UserSettings_section"/>
|
||||
<h3>{ _t("Devices") }</h3>
|
||||
<DevicesPanel className="mx_UserSettings_section" />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
@ -835,7 +903,7 @@ module.exports = React.createClass({
|
|||
<div className="mx_UserSettings_section">
|
||||
<p>{ _t("Found a bug?") }</p>
|
||||
<button className="mx_UserSettings_button danger"
|
||||
onClick={this._onBugReportClicked}>{_t('Report it')}
|
||||
onClick={this._onBugReportClicked}>{ _t('Report it') }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -843,13 +911,13 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_renderAnalyticsControl: function() {
|
||||
if (!SdkConfig.get().piwik) return <div/>;
|
||||
if (!SdkConfig.get().piwik) return <div />;
|
||||
|
||||
return <div>
|
||||
<h3>{ _t('Analytics') }</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
{_t('Riot collects anonymous analytics to allow us to improve the application.')}
|
||||
{ANALYTICS_SETTINGS_LABELS.map( this._renderLocalSetting )}
|
||||
{ _t('Riot collects anonymous analytics to allow us to improve the application.') }
|
||||
{ ANALYTICS_SETTINGS_LABELS.map( this._renderLocalSetting ) }
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
@ -879,10 +947,10 @@ module.exports = React.createClass({
|
|||
type="checkbox"
|
||||
id={feature.id}
|
||||
name={feature.id}
|
||||
defaultChecked={ UserSettingsStore.isFeatureEnabled(feature.id) }
|
||||
onChange={ onChange }
|
||||
defaultChecked={UserSettingsStore.isFeatureEnabled(feature.id)}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<label htmlFor={feature.id}>{feature.name}</label>
|
||||
<label htmlFor={feature.id}>{ feature.name }</label>
|
||||
</div>);
|
||||
});
|
||||
|
||||
|
@ -896,7 +964,7 @@ module.exports = React.createClass({
|
|||
<h3>{ _t("Labs") }</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
<p>{ _t("These are experimental features that may break in unexpected ways") }. { _t("Use with caution") }.</p>
|
||||
{features}
|
||||
{ features }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -929,10 +997,10 @@ module.exports = React.createClass({
|
|||
const platform = PlatformPeg.get();
|
||||
if ('canSelfUpdate' in platform && platform.canSelfUpdate() && 'startUpdateCheck' in platform) {
|
||||
return <div>
|
||||
<h3>{_t('Updates')}</h3>
|
||||
<h3>{ _t('Updates') }</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
<AccessibleButton className="mx_UserSettings_button" onClick={platform.startUpdateCheck}>
|
||||
{_t('Check for update')}
|
||||
{ _t('Check for update') }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>;
|
||||
|
@ -958,7 +1026,7 @@ module.exports = React.createClass({
|
|||
reject = (
|
||||
<AccessibleButton className="mx_UserSettings_button danger"
|
||||
onClick={onClick}>
|
||||
{_t("Reject all %(invitedRooms)s invites", {invitedRooms: invitedRooms.length})}
|
||||
{ _t("Reject all %(invitedRooms)s invites", {invitedRooms: invitedRooms.length}) }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
@ -966,7 +1034,7 @@ module.exports = React.createClass({
|
|||
return <div>
|
||||
<h3>{ _t("Bulk Options") }</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
{reject}
|
||||
{ reject }
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
@ -984,7 +1052,7 @@ module.exports = React.createClass({
|
|||
defaultChecked={settings['auto-launch']}
|
||||
onChange={this._onAutoLaunchChanged}
|
||||
/>
|
||||
<label htmlFor="auto-launch">{_t('Start automatically after system login')}</label>
|
||||
<label htmlFor="auto-launch">{ _t('Start automatically after system login') }</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
|
@ -996,7 +1064,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_mapWebRtcDevicesToSpans: function(devices) {
|
||||
return devices.map((device) => <span key={device.deviceId}>{device.label}</span>);
|
||||
return devices.map((device) => <span key={device.deviceId}>{ device.label }</span>);
|
||||
},
|
||||
|
||||
_setAudioInput: function(deviceId) {
|
||||
|
@ -1032,15 +1100,15 @@ module.exports = React.createClass({
|
|||
if (this.state.mediaDevices === false) {
|
||||
return (
|
||||
<p className="mx_UserSettings_link" onClick={this._requestMediaPermissions}>
|
||||
{_t('Missing Media Permissions, click here to request.')}
|
||||
{ _t('Missing Media Permissions, click here to request.') }
|
||||
</p>
|
||||
);
|
||||
} else if (!this.state.mediaDevices) return;
|
||||
|
||||
const Dropdown = sdk.getComponent('elements.Dropdown');
|
||||
|
||||
let microphoneDropdown = <p>{_t('No Microphones detected')}</p>;
|
||||
let webcamDropdown = <p>{_t('No Webcams detected')}</p>;
|
||||
let microphoneDropdown = <p>{ _t('No Microphones detected') }</p>;
|
||||
let webcamDropdown = <p>{ _t('No Webcams detected') }</p>;
|
||||
|
||||
const defaultOption = {
|
||||
deviceId: '',
|
||||
|
@ -1057,12 +1125,12 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
microphoneDropdown = <div>
|
||||
<h4>{_t('Microphone')}</h4>
|
||||
<h4>{ _t('Microphone') }</h4>
|
||||
<Dropdown
|
||||
className="mx_UserSettings_webRtcDevices_dropdown"
|
||||
value={this.state.activeAudioInput || defaultInput}
|
||||
onOptionChange={this._setAudioInput}>
|
||||
{this._mapWebRtcDevicesToSpans(audioInputs)}
|
||||
{ this._mapWebRtcDevicesToSpans(audioInputs) }
|
||||
</Dropdown>
|
||||
</div>;
|
||||
}
|
||||
|
@ -1077,25 +1145,25 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
webcamDropdown = <div>
|
||||
<h4>{_t('Camera')}</h4>
|
||||
<h4>{ _t('Camera') }</h4>
|
||||
<Dropdown
|
||||
className="mx_UserSettings_webRtcDevices_dropdown"
|
||||
value={this.state.activeVideoInput || defaultInput}
|
||||
onOptionChange={this._setVideoInput}>
|
||||
{this._mapWebRtcDevicesToSpans(videoInputs)}
|
||||
{ this._mapWebRtcDevicesToSpans(videoInputs) }
|
||||
</Dropdown>
|
||||
</div>;
|
||||
}
|
||||
|
||||
return <div>
|
||||
{microphoneDropdown}
|
||||
{webcamDropdown}
|
||||
{ microphoneDropdown }
|
||||
{ webcamDropdown }
|
||||
</div>;
|
||||
},
|
||||
|
||||
_renderWebRtcSettings: function() {
|
||||
return <div>
|
||||
<h3>{_t('VoIP')}</h3>
|
||||
<h3>{ _t('VoIP') }</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
{ WEBRTC_SETTINGS_LABELS.map(this._renderLocalSetting) }
|
||||
{ this._renderWebRtcDeviceSettings() }
|
||||
|
@ -1161,7 +1229,7 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<div className="mx_UserSettings_profileTableRow" key={pidIndex}>
|
||||
<div className="mx_UserSettings_profileLabelCell">
|
||||
<label htmlFor={id}>{this.nameForMedium(val.medium)}</label>
|
||||
<label htmlFor={id}>{ this.nameForMedium(val.medium) }</label>
|
||||
</div>
|
||||
<div className="mx_UserSettings_profileInputCell">
|
||||
<input type="text" key={val.address} id={id}
|
||||
|
@ -1169,7 +1237,7 @@ module.exports = React.createClass({
|
|||
/>
|
||||
</div>
|
||||
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
|
||||
<img src="img/cancel-small.svg" width="14" height="14" alt={ _t("Remove") }
|
||||
<img src="img/cancel-small.svg" width="14" height="14" alt={_t("Remove")}
|
||||
onClick={onRemoveClick} />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1182,16 +1250,16 @@ module.exports = React.createClass({
|
|||
addEmailSection = (
|
||||
<div className="mx_UserSettings_profileTableRow" key="_newEmail">
|
||||
<div className="mx_UserSettings_profileLabelCell">
|
||||
<label>{_t('Email')}</label>
|
||||
<label>{ _t('Email') }</label>
|
||||
</div>
|
||||
<div className="mx_UserSettings_profileInputCell">
|
||||
<EditableText
|
||||
ref="add_email_input"
|
||||
className="mx_UserSettings_editable"
|
||||
placeholderClassName="mx_UserSettings_threepidPlaceholder"
|
||||
placeholder={ _t("Add email address") }
|
||||
blurToCancel={ false }
|
||||
onValueChanged={ this._onAddEmailEditFinished } />
|
||||
placeholder={_t("Add email address")}
|
||||
blurToCancel={false}
|
||||
onValueChanged={this._onAddEmailEditFinished} />
|
||||
</div>
|
||||
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
|
||||
<img src="img/plus.svg" width="14" height="14" alt={_t("Add")} onClick={this._addEmail} />
|
||||
|
@ -1239,8 +1307,8 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<div className="mx_UserSettings">
|
||||
<SimpleRoomHeader
|
||||
title={ _t("Settings") }
|
||||
onCancelClick={ this.props.onClose }
|
||||
title={_t("Settings")}
|
||||
onCancelClick={this.props.onClose}
|
||||
/>
|
||||
|
||||
<GeminiScrollbar className="mx_UserSettings_body"
|
||||
|
@ -1258,21 +1326,21 @@ module.exports = React.createClass({
|
|||
<ChangeDisplayName />
|
||||
</div>
|
||||
</div>
|
||||
{threepidsSection}
|
||||
{ threepidsSection }
|
||||
</div>
|
||||
|
||||
<div className="mx_UserSettings_avatarPicker">
|
||||
<div onClick={ this.onAvatarPickerClick }>
|
||||
<div onClick={this.onAvatarPickerClick}>
|
||||
<ChangeAvatar ref="changeAvatar" initialAvatarUrl={avatarUrl}
|
||||
showUploadSection={false} className="mx_UserSettings_avatarPicker_img"/>
|
||||
showUploadSection={false} className="mx_UserSettings_avatarPicker_img" />
|
||||
</div>
|
||||
<div className="mx_UserSettings_avatarPicker_edit">
|
||||
<label htmlFor="avatarInput" ref="file_label">
|
||||
<img src="img/camera.svg" className="mx_filterFlipColor"
|
||||
alt={ _t("Upload avatar") } title={ _t("Upload avatar") }
|
||||
alt={_t("Upload avatar")} title={_t("Upload avatar")}
|
||||
width="17" height="15" />
|
||||
</label>
|
||||
<input id="avatarInput" type="file" onChange={this.onAvatarSelected}/>
|
||||
<input id="avatarInput" type="file" onChange={this.onAvatarSelected} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1289,36 +1357,37 @@ module.exports = React.createClass({
|
|||
</div> : null
|
||||
}
|
||||
|
||||
{accountJsx}
|
||||
{ accountJsx }
|
||||
</div>
|
||||
|
||||
{this._renderReferral()}
|
||||
{ this._renderReferral() }
|
||||
|
||||
{notificationArea}
|
||||
{ notificationArea }
|
||||
|
||||
{this._renderUserInterfaceSettings()}
|
||||
{this._renderLabs()}
|
||||
{this._renderWebRtcSettings()}
|
||||
{this._renderDevicesPanel()}
|
||||
{this._renderCryptoInfo()}
|
||||
{this._renderBulkOptions()}
|
||||
{this._renderBugReport()}
|
||||
{ this._renderUserInterfaceSettings() }
|
||||
{ this._renderLabs() }
|
||||
{ this._renderWebRtcSettings() }
|
||||
{ this._renderDevicesPanel() }
|
||||
{ this._renderCryptoInfo() }
|
||||
{ this._renderIgnoredUsers() }
|
||||
{ this._renderBulkOptions() }
|
||||
{ this._renderBugReport() }
|
||||
|
||||
{PlatformPeg.get().isElectron() && this._renderElectronSettings()}
|
||||
{ PlatformPeg.get().isElectron() && this._renderElectronSettings() }
|
||||
|
||||
{this._renderAnalyticsControl()}
|
||||
{ this._renderAnalyticsControl() }
|
||||
|
||||
<h3>{ _t("Advanced") }</h3>
|
||||
|
||||
<div className="mx_UserSettings_section">
|
||||
<div className="mx_UserSettings_advanced">
|
||||
{ _t("Logged in as:") } {this._me}
|
||||
{ _t("Logged in as:") } { this._me }
|
||||
</div>
|
||||
<div className="mx_UserSettings_advanced">
|
||||
{_t('Access Token:')}
|
||||
{ _t('Access Token:') }
|
||||
<span className="mx_UserSettings_advanced_spoiler"
|
||||
onClick={this._showSpoiler}
|
||||
data-spoiler={ MatrixClientPeg.get().getAccessToken() }>
|
||||
data-spoiler={MatrixClientPeg.get().getAccessToken()}>
|
||||
<{ _t("click to reveal") }>
|
||||
</span>
|
||||
</div>
|
||||
|
@ -1329,23 +1398,23 @@ module.exports = React.createClass({
|
|||
{ _t("Identity Server is") } { MatrixClientPeg.get().getIdentityServerUrl() }
|
||||
</div>
|
||||
<div className="mx_UserSettings_advanced">
|
||||
{_t('matrix-react-sdk version:')} {(REACT_SDK_VERSION !== '<local>')
|
||||
{ _t('matrix-react-sdk version:') } { (REACT_SDK_VERSION !== '<local>')
|
||||
? gHVersionLabel('matrix-org/matrix-react-sdk', REACT_SDK_VERSION)
|
||||
: REACT_SDK_VERSION
|
||||
}<br/>
|
||||
{_t('riot-web version:')} {(this.state.vectorVersion !== undefined)
|
||||
}<br />
|
||||
{ _t('riot-web version:') } { (this.state.vectorVersion !== undefined)
|
||||
? gHVersionLabel('vector-im/riot-web', this.state.vectorVersion)
|
||||
: 'unknown'
|
||||
}<br/>
|
||||
{ _t("olm version:") } {olmVersionString}<br/>
|
||||
}<br />
|
||||
{ _t("olm version:") } { olmVersionString }<br />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this._renderCheckUpdate()}
|
||||
{ this._renderCheckUpdate() }
|
||||
|
||||
{this._renderClearCache()}
|
||||
{ this._renderClearCache() }
|
||||
|
||||
{this._renderDeactivateAccount()}
|
||||
{ this._renderDeactivateAccount() }
|
||||
|
||||
</GeminiScrollbar>
|
||||
</div>
|
||||
|
|
|
@ -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.
|
||||
|
@ -136,16 +137,15 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onHsUrlChanged: function(newHsUrl) {
|
||||
this.setState({
|
||||
enteredHomeserverUrl: newHsUrl
|
||||
});
|
||||
},
|
||||
|
||||
onIsUrlChanged: function(newIsUrl) {
|
||||
this.setState({
|
||||
enteredIdentityServerUrl: newIsUrl
|
||||
});
|
||||
onServerConfigChange: function(config) {
|
||||
const newState = {};
|
||||
if (config.hsUrl !== undefined) {
|
||||
newState.enteredHomeserverUrl = config.hsUrl;
|
||||
}
|
||||
if (config.isUrl !== undefined) {
|
||||
newState.enteredIdentityServerUrl = config.isUrl;
|
||||
}
|
||||
this.setState(newState);
|
||||
},
|
||||
|
||||
showErrorDialog: function(body, title) {
|
||||
|
@ -170,7 +170,7 @@ module.exports = React.createClass({
|
|||
else if (this.state.progress === "sent_email") {
|
||||
resetPasswordJsx = (
|
||||
<div>
|
||||
{ _t('An email has been sent to') } {this.state.email}. { _t('Once you've followed the link it contains, click below') }.
|
||||
{ _t('An email has been sent to') } {this.state.email}. { _t("Once you've followed the link it contains, click below") }.
|
||||
<br />
|
||||
<input className="mx_Login_submit" type="button" onClick={this.onVerify}
|
||||
value={ _t('I have verified my email address') } />
|
||||
|
@ -221,8 +221,7 @@ module.exports = React.createClass({
|
|||
defaultIsUrl={this.props.defaultIsUrl}
|
||||
customHsUrl={this.props.customHsUrl}
|
||||
customIsUrl={this.props.customIsUrl}
|
||||
onHsUrlChanged={this.onHsUrlChanged}
|
||||
onIsUrlChanged={this.onIsUrlChanged}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
delayTimeMs={0}/>
|
||||
<div className="mx_Login_error">
|
||||
</div>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue