Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into set_default_federate_by_settings

This commit is contained in:
Michael Telatynski 2017-10-04 22:35:29 +01:00
commit 7492f2dffa
No known key found for this signature in database
GPG key ID: 0435A1D4BBD34D64
149 changed files with 7702 additions and 5253 deletions

View file

@ -28,6 +28,8 @@ import ScalarMessaging from '../../../ScalarMessaging';
import { _t } from '../../../languageHandler';
import WidgetUtils from '../../../WidgetUtils';
// The maximum number of widgets that can be added in a room
const MAX_WIDGETS = 2;
module.exports = React.createClass({
displayName: 'AppsDrawer',
@ -51,19 +53,18 @@ module.exports = React.createClass({
this.scalarClient = null;
if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) {
this.scalarClient = new ScalarAuthClient();
this.scalarClient.connect().done(() => {
this.scalarClient.connect().then(() => {
this.forceUpdate();
if (this.state.apps && this.state.apps.length < 1) {
this.onClickAddWidget();
}
// TODO -- Handle Scalar errors
// },
// (err) => {
// this.setState({
// scalar_error: err,
// });
}).catch((e) => {
console.log("Failed to connect to integrations server");
// TODO -- Handle Scalar errors
// this.setState({
// scalar_error: err,
// });
});
}
this.dispatcherRef = dis.register(this.onAction);
},
componentWillUnmount: function() {
@ -71,6 +72,27 @@ module.exports = React.createClass({
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
}
dis.unregister(this.dispatcherRef);
},
componentWillReceiveProps(newProps) {
// Room has changed probably, update apps
this._updateApps();
},
onAction: function(action) {
switch (action.action) {
case 'appsDrawer':
// When opening the app draw when there aren't any apps, auto-launch the
// integrations manager to skip the awkward click on "Add widget"
if (action.show) {
const apps = this._getApps();
if (apps.length === 0) {
this._launchManageIntegrations();
}
}
break;
}
},
/**
@ -93,7 +115,7 @@ module.exports = React.createClass({
return pathTemplate;
},
_initAppConfig: function(appId, app) {
_initAppConfig: function(appId, app, sender) {
const user = MatrixClientPeg.get().getUser(this.props.userId);
const params = {
'$matrix_user_id': this.props.userId,
@ -111,6 +133,7 @@ module.exports = React.createClass({
app.id = appId;
app.name = app.name || app.type;
app.url = this.encodeUri(app.url, params);
app.creatorUserId = (sender && sender.userId) ? sender.userId : null;
return app;
},
@ -131,18 +154,12 @@ module.exports = React.createClass({
return appsStateEvents.filter((ev) => {
return ev.getContent().type && ev.getContent().url;
}).map((ev) => {
return this._initAppConfig(ev.getStateKey(), ev.getContent());
return this._initAppConfig(ev.getStateKey(), ev.getContent(), ev.sender);
});
},
_updateApps: function() {
const apps = this._getApps();
if (apps.length < 1) {
dis.dispatch({
action: 'appsDrawer',
show: false,
});
}
this.setState({
apps: apps,
});
@ -157,11 +174,7 @@ module.exports = React.createClass({
}
},
onClickAddWidget: function(e) {
if (e) {
e.preventDefault();
}
_launchManageIntegrations: function() {
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId, 'add_integ') :
@ -171,6 +184,23 @@ module.exports = React.createClass({
}, "mx_IntegrationsManager");
},
onClickAddWidget: function(e) {
e.preventDefault();
// Display a warning dialog if the max number of widgets have already been added to the room
const apps = this._getApps();
if (apps && apps.length >= MAX_WIDGETS) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const errorMsg = `The maximum number of ${MAX_WIDGETS} widgets have already been added to this room.`;
console.error(errorMsg);
Modal.createDialog(ErrorDialog, {
title: _t("Cannot add any more widgets"),
description: _t("The maximum permitted number of widgets have already been added to this room."),
});
return;
}
this._launchManageIntegrations();
},
render: function() {
const apps = this.state.apps.map(
(app, index, arr) => {
@ -183,24 +213,34 @@ module.exports = React.createClass({
fullWidth={arr.length<2 ? true : false}
room={this.props.room}
userId={this.props.userId}
show={this.props.showApps}
creatorUserId={app.creatorUserId}
/>);
});
const addWidget = this.state.apps && this.state.apps.length < 2 && this._canUserModify() &&
(<div onClick={this.onClickAddWidget}
role="button"
tabIndex="0"
className="mx_AddWidget_button"
title={_t('Add a widget')}>
[+] {_t('Add a widget')}
</div>);
let addWidget;
if (this.props.showApps &&
this._canUserModify()
) {
addWidget = <div
onClick={this.onClickAddWidget}
role="button"
tabIndex="0"
className={this.state.apps.length<2 ?
"mx_AddWidget_button mx_AddWidget_button_full_width" :
"mx_AddWidget_button"
}
title={_t('Add a widget')}>
[+] { _t('Add a widget') }
</div>;
}
return (
<div className="mx_AppsDrawer">
<div id="apps" className="mx_AppsContainer">
{apps}
{ apps }
</div>
{addWidget}
{ this._canUserModify() && addWidget }
</div>
);
},

View file

@ -143,7 +143,6 @@ export default class Autocomplete extends React.Component {
return null;
}
this.setSelection(selectionOffset);
return selectionOffset === COMPOSER_SELECTED ? null : this.state.completionList[selectionOffset - 1];
}
// called from MessageComposerInput
@ -155,7 +154,6 @@ export default class Autocomplete extends React.Component {
return null;
}
this.setSelection(selectionOffset);
return selectionOffset === COMPOSER_SELECTED ? null : this.state.completionList[selectionOffset - 1];
}
onEscape(e): boolean {
@ -201,6 +199,9 @@ export default class Autocomplete extends React.Component {
setSelection(selectionOffset: number) {
this.setState({selectionOffset, hide: false});
if (this.props.onSelectionChange) {
this.props.onSelectionChange(this.state.completionList[selectionOffset - 1]);
}
}
componentDidUpdate() {

View file

@ -129,11 +129,13 @@ module.exports = React.createClass({
);
let appsDrawer = null;
if(UserSettingsStore.isFeatureEnabled('matrix_apps') && this.props.showApps) {
if(UserSettingsStore.isFeatureEnabled('matrix_apps')) {
appsDrawer = <AppsDrawer ref="appsDrawer"
room={this.props.room}
userId={this.props.userId}
maxHeight={this.props.maxHeight}/>;
maxHeight={this.props.maxHeight}
showApps={this.props.showApps}
/>;
}
return (

View file

@ -44,6 +44,8 @@ var eventTileTypes = {
'm.room.history_visibility' : 'messages.TextualEvent',
'm.room.encryption' : 'messages.TextualEvent',
'm.room.power_levels' : 'messages.TextualEvent',
'im.vector.modular.widgets': 'messages.TextualEvent',
};
var MAX_READ_AVATARS = 5;
@ -506,10 +508,10 @@ module.exports = withMatrixClient(React.createClass({
if (msgtype === 'm.image') aux = _t('sent an image');
else if (msgtype === 'm.video') aux = _t('sent a video');
else if (msgtype === 'm.file') aux = _t('uploaded a file');
sender = <SenderProfile onClick={ this.onSenderProfileClick } mxEvent={this.props.mxEvent} aux={aux} />;
sender = <SenderProfile onClick={ this.onSenderProfileClick } mxEvent={this.props.mxEvent} enableFlair={!aux} aux={aux} />;
}
else {
sender = <SenderProfile mxEvent={this.props.mxEvent} />;
sender = <SenderProfile mxEvent={this.props.mxEvent} enableFlair={true} />;
}
}

View file

@ -61,7 +61,7 @@ module.exports = React.createClass({
render: function() {
return (
<div className="mx_ForwardMessage">
<h1>{_t('Please select the destination room for this message')}</h1>
<h1>{ _t('Please select the destination room for this message') }</h1>
</div>
);
},

View file

@ -62,6 +62,7 @@ module.exports = withMatrixClient(React.createClass({
updating: 0,
devicesLoading: true,
devices: null,
isIgnoring: false,
};
},
@ -81,6 +82,8 @@ module.exports = withMatrixClient(React.createClass({
cli.on("RoomState.events", this.onRoomStateEvents);
cli.on("RoomMember.name", this.onRoomMemberName);
cli.on("accountData", this.onAccountData);
this._checkIgnoreState();
},
componentDidMount: function() {
@ -111,6 +114,11 @@ module.exports = withMatrixClient(React.createClass({
}
},
_checkIgnoreState: function() {
const isIgnoring = this.props.matrixClient.isUserIgnored(this.props.member.userId);
this.setState({isIgnoring: isIgnoring});
},
_disambiguateDevices: function(devices) {
var names = Object.create(null);
for (var i = 0; i < devices.length; i++) {
@ -225,6 +233,18 @@ module.exports = withMatrixClient(React.createClass({
});
},
onIgnoreToggle: function() {
const ignoredUsers = this.props.matrixClient.getIgnoredUsers();
if (this.state.isIgnoring) {
const index = ignoredUsers.indexOf(this.props.member.userId);
if (index !== -1) ignoredUsers.splice(index, 1);
} else {
ignoredUsers.push(this.props.member.userId);
}
this.props.matrixClient.setIgnoredUsers(ignoredUsers).then(() => this.setState({isIgnoring: !this.state.isIgnoring}));
},
onKick: function() {
const membership = this.props.member.membership;
const kickLabel = membership === "invite" ? _t("Disinvite") : _t("Kick");
@ -607,6 +627,29 @@ module.exports = withMatrixClient(React.createClass({
);
},
_renderUserOptions: function() {
// Only allow the user to ignore the user if its not ourselves
let ignoreButton = null;
if (this.props.member.userId !== this.props.matrixClient.getUserId()) {
ignoreButton = (
<AccessibleButton onClick={this.onIgnoreToggle} className="mx_MemberInfo_field">
{this.state.isIgnoring ? _t("Unignore") : _t("Ignore")}
</AccessibleButton>
);
}
if (!ignoreButton) return null;
return (
<div>
<h3>{ _t("User Options") }</h3>
<div className="mx_MemberInfo_buttons">
{ignoreButton}
</div>
</div>
);
},
render: function() {
var startChat, kickButton, banButton, muteButton, giveModButton, spinner;
if (this.props.member.userId !== this.props.matrixClient.credentials.userId) {
@ -708,7 +751,7 @@ module.exports = withMatrixClient(React.createClass({
if (kickButton || banButton || muteButton || giveModButton) {
adminTools =
<div>
<h3>{_t("Admin tools")}</h3>
<h3>{_t("Admin Tools")}</h3>
<div className="mx_MemberInfo_buttons">
{muteButton}
@ -756,6 +799,8 @@ module.exports = withMatrixClient(React.createClass({
</div>
</div>
{ this._renderUserOptions() }
{ adminTools }
{ startChat }

View file

@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
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.
@ -14,42 +15,37 @@ 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.
*/
var React = require('react');
import React from 'react';
import { _t } from '../../../languageHandler';
var classNames = require('classnames');
var Matrix = require("matrix-js-sdk");
import Promise from 'bluebird';
var MatrixClientPeg = require("../../../MatrixClientPeg");
var Modal = require("../../../Modal");
var Entities = require("../../../Entities");
var sdk = require('../../../index');
var GeminiScrollbar = require('react-gemini-scrollbar');
var rate_limited_func = require('../../../ratelimitedfunc');
var CallHandler = require("../../../CallHandler");
var Invite = require("../../../Invite");
var INITIAL_LOAD_NUM_MEMBERS = 30;
const INITIAL_LOAD_NUM_MEMBERS = 30;
const INITIAL_LOAD_NUM_INVITED = 5;
const SHOW_MORE_INCREMENT = 100;
module.exports = React.createClass({
displayName: 'MemberList',
getInitialState: function() {
var state = {
members: [],
this.memberDict = this.getMemberDict();
const members = this.roomMembers();
return {
members: members,
filteredJoinedMembers: this._filterMembers(members, 'join'),
filteredInvitedMembers: this._filterMembers(members, 'invite'),
// ideally we'd size this to the page height, but
// in practice I find that a little constraining
truncateAt: INITIAL_LOAD_NUM_MEMBERS,
truncateAtJoined: INITIAL_LOAD_NUM_MEMBERS,
truncateAtInvited: INITIAL_LOAD_NUM_INVITED,
searchQuery: "",
};
if (!this.props.roomId) return state;
var cli = MatrixClientPeg.get();
var room = cli.getRoom(this.props.roomId);
if (!room) return state;
this.memberDict = this.getMemberDict();
state.members = this.roomMembers();
return state;
},
componentWillMount: function() {
@ -147,10 +143,12 @@ module.exports = React.createClass({
// console.log("Updating memberlist");
this.memberDict = this.getMemberDict();
var self = this;
this.setState({
members: self.roomMembers()
});
const newState = {
members: this.roomMembers(),
};
newState.filteredJoinedMembers = this._filterMembers(newState.members, 'join');
newState.filteredInvitedMembers = this._filterMembers(newState.members, 'invite');
this.setState(newState);
}, 500),
getMemberDict: function() {
@ -199,7 +197,15 @@ module.exports = React.createClass({
return to_display;
},
_createOverflowTile: function(overflowCount, totalCount) {
_createOverflowTileJoined: function(overflowCount, totalCount) {
return this._createOverflowTile(overflowCount, totalCount, this._showMoreJoinedMemberList);
},
_createOverflowTileInvited: function(overflowCount, totalCount) {
return this._createOverflowTile(overflowCount, totalCount, this._showMoreInvitedMemberList);
},
_createOverflowTile: function(overflowCount, totalCount, onClick) {
// For now we'll pretend this is any entity. It should probably be a separate tile.
const EntityTile = sdk.getComponent("rooms.EntityTile");
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
@ -208,13 +214,19 @@ module.exports = React.createClass({
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
<BaseAvatar url="img/ellipsis.svg" name="..." width={36} height={36} />
} name={text} presenceState="online" suppressOnHover={true}
onClick={this._showFullMemberList} />
onClick={onClick} />
);
},
_showFullMemberList: function() {
_showMoreJoinedMemberList: function() {
this.setState({
truncateAt: -1
truncateAtJoined: this.state.truncateAtJoined + SHOW_MORE_INCREMENT,
});
},
_showMoreInvitedMemberList: function() {
this.setState({
truncateAtInvited: this.state.truncateAtInvited + SHOW_MORE_INCREMENT,
});
},
@ -280,17 +292,17 @@ module.exports = React.createClass({
},
onSearchQueryChanged: function(ev) {
this.setState({ searchQuery: ev.target.value });
const q = ev.target.value;
this.setState({
searchQuery: q,
filteredJoinedMembers: this._filterMembers(this.state.members, 'join', q),
filteredInvitedMembers: this._filterMembers(this.state.members, 'invite', q),
});
},
makeMemberTiles: function(membership, query) {
var MemberTile = sdk.getComponent("rooms.MemberTile");
query = (query || "").toLowerCase();
var self = this;
var memberList = self.state.members.filter(function(userId) {
var m = self.memberDict[userId];
_filterMembers: function(members, membership, query) {
return members.filter((userId) => {
const m = this.memberDict[userId];
if (query) {
const matchesName = m.name.toLowerCase().indexOf(query) !== -1;
@ -302,14 +314,23 @@ module.exports = React.createClass({
}
return m.membership == membership;
}).map(function(userId) {
var m = self.memberDict[userId];
});
},
_makeMemberTiles: function(members, membership) {
const MemberTile = sdk.getComponent("rooms.MemberTile");
const memberList = members.map((userId) => {
const m = this.memberDict[userId];
return (
<MemberTile key={userId} member={m} ref={userId} />
);
});
// XXX: surely this is not the right home for this logic.
// Double XXX: Now it's really, really not the right home for this logic:
// we shouldn't even be passing in the 'membership' param to this function.
// Ew, ew, and ew.
if (membership === "invite") {
// include 3pid invites (m.room.third_party_invite) state events.
// The HS may have already converted these into m.room.member invites so
@ -333,7 +354,7 @@ module.exports = React.createClass({
return;
}
memberList.push(
<EntityTile key={e.getStateKey()} name={e.getContent().display_name} />
<EntityTile key={e.getStateKey()} name={e.getContent().display_name} suppressOnHover={true} />
);
});
}
@ -342,21 +363,42 @@ module.exports = React.createClass({
return memberList;
},
_getChildrenJoined: function(start, end) {
return this._makeMemberTiles(this.state.filteredJoinedMembers.slice(start, end));
},
_getChildCountJoined: function() {
return this.state.filteredJoinedMembers.length;
},
_getChildrenInvited: function(start, end) {
return this._makeMemberTiles(this.state.filteredInvitedMembers.slice(start, end), 'invite');
},
_getChildCountInvited: function() {
return this.state.filteredInvitedMembers.length;
},
render: function() {
var invitedSection = null;
var invitedMemberTiles = this.makeMemberTiles('invite', this.state.searchQuery);
if (invitedMemberTiles.length > 0) {
const TruncatedList = sdk.getComponent("elements.TruncatedList");
let invitedSection = null;
if (this._getChildCountInvited() > 0) {
invitedSection = (
<div className="mx_MemberList_invited">
<h2>{ _t("Invited") }</h2>
<div className="mx_MemberList_wrapper">
{invitedMemberTiles}
<TruncatedList className="mx_MemberList_wrapper" truncateAt={this.state.truncateAtInvited}
createOverflowElement={this._createOverflowTileInvited}
getChildren={this._getChildrenInvited}
getChildCount={this._getChildCountInvited}
/>
</div>
</div>
);
}
var inputBox = (
const inputBox = (
<form autoComplete="off">
<input className="mx_MemberList_query" id="mx_MemberList_query" type="text"
onChange={this.onSearchQueryChanged} value={this.state.searchQuery}
@ -364,15 +406,15 @@ module.exports = React.createClass({
</form>
);
var TruncatedList = sdk.getComponent("elements.TruncatedList");
return (
<div className="mx_MemberList">
{ inputBox }
<GeminiScrollbar autoshow={true} className="mx_MemberList_joined mx_MemberList_outerWrapper">
<TruncatedList className="mx_MemberList_wrapper" truncateAt={this.state.truncateAt}
createOverflowElement={this._createOverflowTile}>
{this.makeMemberTiles('join', this.state.searchQuery)}
</TruncatedList>
<TruncatedList className="mx_MemberList_wrapper" truncateAt={this.state.truncateAtJoined}
createOverflowElement={this._createOverflowTileJoined}
getChildren={this._getChildrenJoined}
getChildCount={this._getChildCountJoined}
/>
{invitedSection}
</GeminiScrollbar>
</div>

View file

@ -289,12 +289,12 @@ export default class MessageComposer extends React.Component {
if (this.props.showApps) {
hideAppsButton =
<div key="controls_hide_apps" className="mx_MessageComposer_apps" onClick={this.onHideAppsClick} title={_t("Hide Apps")}>
<TintableSvg src="img/icons-apps-active.svg" width="35" height="35"/>
<TintableSvg src="img/icons-hide-apps.svg" width="35" height="35"/>
</div>;
} else {
showAppsButton =
<div key="show_apps" className="mx_MessageComposer_apps" onClick={this.onShowAppsClick} title={_t("Show Apps")}>
<TintableSvg src="img/icons-apps.svg" width="35" height="35"/>
<TintableSvg src="img/icons-show-apps.svg" width="35" height="35"/>
</div>;
}
}

View file

@ -30,7 +30,7 @@ import SlashCommands from '../../../SlashCommands';
import KeyCode from '../../../KeyCode';
import Modal from '../../../Modal';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import { _t, _td } from '../../../languageHandler';
import Analytics from '../../../Analytics';
import dis from '../../../dispatcher';
@ -949,8 +949,7 @@ export default class MessageComposerInput extends React.Component {
};
moveAutocompleteSelection = (up) => {
const completion = up ? this.autocomplete.onUpArrow() : this.autocomplete.onDownArrow();
return this.setDisplayedCompletion(completion);
up ? this.autocomplete.onUpArrow() : this.autocomplete.onDownArrow();
};
onEscape = async (e) => {
@ -1033,10 +1032,10 @@ export default class MessageComposerInput extends React.Component {
buttons. */
getSelectionInfo(editorState: EditorState) {
const styleName = {
BOLD: 'bold',
ITALIC: 'italic',
STRIKETHROUGH: 'strike',
UNDERLINE: 'underline',
BOLD: _td('bold'),
ITALIC: _td('italic'),
STRIKETHROUGH: _td('strike'),
UNDERLINE: _td('underline'),
};
const originalStyle = editorState.getCurrentInlineStyle().toArray();
@ -1045,10 +1044,10 @@ export default class MessageComposerInput extends React.Component {
.filter((styleName) => !!styleName);
const blockName = {
'code-block': 'code',
'blockquote': 'quote',
'unordered-list-item': 'bullet',
'ordered-list-item': 'numbullet',
'code-block': _td('code'),
'blockquote': _td('quote'),
'unordered-list-item': _td('bullet'),
'ordered-list-item': _td('numbullet'),
};
const originalBlockType = editorState.getCurrentContent()
.getBlockForKey(editorState.getSelection().getStartKey())
@ -1133,6 +1132,7 @@ export default class MessageComposerInput extends React.Component {
<Autocomplete
ref={(e) => this.autocomplete = e}
onConfirm={this.setDisplayedCompletion}
onSelectionChange={this.setDisplayedCompletion}
query={this.getAutocompleteQuery(content)}
selection={selection}/>
</div>

View file

@ -70,7 +70,7 @@ module.exports = React.createClass({
if (presence === "online") return _t("Online");
if (presence === "unavailable") return _t("Idle"); // XXX: is this actually right?
if (presence === "offline") return _t("Offline");
return "Unknown";
return _t("Unknown");
},
render: function() {

View file

@ -123,7 +123,19 @@ module.exports = React.createClass({
}
var newElement = ReactDOM.findDOMNode(this);
var startTopOffset = oldTop - newElement.offsetParent.getBoundingClientRect().top;
let startTopOffset;
if (!newElement.offsetParent) {
// this seems to happen sometimes for reasons I don't understand
// the docs for `offsetParent` say it may be null if `display` is
// `none`, but I can't see why that would happen.
console.warn(
`ReadReceiptMarker for ${this.props.member.userId} in ` +
`${this.props.member.roomId} has no offsetParent`,
);
startTopOffset = 0;
} else {
startTopOffset = oldTop - newElement.offsetParent.getBoundingClientRect().top;
}
var startStyles = [];
var enterTransitionOpts = [];
@ -131,13 +143,12 @@ module.exports = React.createClass({
if (oldInfo && oldInfo.left) {
// start at the old height and in the old h pos
var leftOffset = oldInfo.left;
startStyles.push({ top: startTopOffset+"px",
left: oldInfo.left+"px" });
var reorderTransitionOpts = {
duration: 100,
easing: 'easeOut'
easing: 'easeOut',
};
enterTransitionOpts.push(reorderTransitionOpts);
@ -175,7 +186,7 @@ module.exports = React.createClass({
if (this.props.timestamp) {
title = _t(
"Seen by %(userName)s at %(dateTime)s",
{userName: this.props.member.userId, dateTime: DateUtils.formatDate(new Date(this.props.timestamp), this.props.showTwelveHour)}
{userName: this.props.member.userId, dateTime: DateUtils.formatDate(new Date(this.props.timestamp), this.props.showTwelveHour)},
);
}

View file

@ -29,6 +29,7 @@ import * as linkify from 'linkifyjs';
import linkifyElement from 'linkifyjs/element';
import linkifyMatrix from '../../../linkify-matrix';
import AccessibleButton from '../elements/AccessibleButton';
import ManageIntegsButton from '../elements/ManageIntegsButton';
import {CancelButton} from './SimpleRoomHeader';
linkifyMatrix(linkify);
@ -47,6 +48,7 @@ module.exports = React.createClass({
onSaveClick: React.PropTypes.func,
onSearchClick: React.PropTypes.func,
onLeaveClick: React.PropTypes.func,
onCancelClick: React.PropTypes.func,
},
getDefaultProps: function() {
@ -54,6 +56,7 @@ module.exports = React.createClass({
editing: false,
inRoom: false,
onSaveClick: function() {},
onCancelClick: null,
};
},
@ -183,18 +186,18 @@ module.exports = React.createClass({
saveButton = (
<AccessibleButton className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>
{_t("Save")}
{ _t("Save") }
</AccessibleButton>
);
}
if (this.props.onCancelClick) {
cancelButton = <CancelButton onClick={this.props.onCancelClick}/>;
cancelButton = <CancelButton onClick={this.props.onCancelClick} />;
}
if (this.props.saving) {
const Spinner = sdk.getComponent("elements.Spinner");
spinner = <div className="mx_RoomHeader_spinner"><Spinner/></div>;
spinner = <div className="mx_RoomHeader_spinner"><Spinner /></div>;
}
if (canSetRoomName) {
@ -251,7 +254,7 @@ module.exports = React.createClass({
}
if (topic) {
topicElement =
<div className="mx_RoomHeader_topic" ref="topic" title={ topic } dir="auto">{ topic }</div>;
<div className="mx_RoomHeader_topic" ref="topic" title={topic} dir="auto">{ topic }</div>;
}
}
@ -259,16 +262,16 @@ module.exports = React.createClass({
if (canSetRoomAvatar) {
roomAvatar = (
<div className="mx_RoomHeader_avatarPicker">
<div onClick={ this.onAvatarPickerClick }>
<div onClick={this.onAvatarPickerClick}>
<ChangeAvatar ref="changeAvatar" room={this.props.room} showUploadSection={false} width={48} height={48} />
</div>
<div className="mx_RoomHeader_avatarPicker_edit">
<label htmlFor="avatarInput" ref="file_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" type="file" onChange={ this.onAvatarSelected }/>
<input id="avatarInput" type="file" onChange={this.onAvatarSelected} />
</div>
</div>
);
@ -283,7 +286,7 @@ module.exports = React.createClass({
if (this.props.onSettingsClick) {
settingsButton =
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSettingsClick} title={_t("Settings")}>
<TintableSvg src="img/icons-settings-room.svg" width="16" height="16"/>
<TintableSvg src="img/icons-settings-room.svg" width="16" height="16" />
</AccessibleButton>;
}
@ -298,32 +301,40 @@ module.exports = React.createClass({
let forgetButton;
if (this.props.onForgetClick) {
forgetButton =
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onForgetClick} title={ _t("Forget room") }>
<TintableSvg src="img/leave.svg" width="26" height="20"/>
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onForgetClick} title={_t("Forget room")}>
<TintableSvg src="img/leave.svg" width="26" height="20" />
</AccessibleButton>;
}
let searchButton;
if (this.props.onSearchClick && this.props.inRoom) {
searchButton =
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title={ _t("Search") }>
<TintableSvg src="img/icons-search.svg" width="35" height="35"/>
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onSearchClick} title={_t("Search")}>
<TintableSvg src="img/icons-search.svg" width="35" height="35" />
</AccessibleButton>;
}
let rightPanelButtons;
if (this.props.collapsedRhs) {
rightPanelButtons =
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShowRhsClick} title={ _t('Show panel') }>
<TintableSvg src="img/maximise.svg" width="10" height="16"/>
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShowRhsClick} title={_t('Show panel')}>
<TintableSvg src="img/maximise.svg" width="10" height="16" />
</AccessibleButton>;
}
let rightRow;
let manageIntegsButton;
if(this.props.room && this.props.room.roomId && this.props.inRoom) {
manageIntegsButton = <ManageIntegsButton
roomId={this.props.room.roomId}
/>;
}
if (!this.props.editing) {
rightRow =
<div className="mx_RoomHeader_rightRow">
{ settingsButton }
{ manageIntegsButton }
{ forgetButton }
{ searchButton }
{ rightPanelButtons }
@ -331,7 +342,7 @@ module.exports = React.createClass({
}
return (
<div className={ "mx_RoomHeader " + (this.props.editing ? "mx_RoomHeader_editing" : "") }>
<div className={"mx_RoomHeader " + (this.props.editing ? "mx_RoomHeader_editing" : "")}>
<div className="mx_RoomHeader_wrapper">
<div className="mx_RoomHeader_leftRow">
<div className="mx_RoomHeader_avatar">
@ -342,10 +353,10 @@ module.exports = React.createClass({
{ topicElement }
</div>
</div>
{spinner}
{saveButton}
{cancelButton}
{rightRow}
{ spinner }
{ saveButton }
{ cancelButton }
{ rightRow }
</div>
</div>
);

View file

@ -63,7 +63,6 @@ module.exports = React.createClass({
propTypes: {
ConferenceHandler: React.PropTypes.any,
collapsed: React.PropTypes.bool.isRequired,
currentRoom: React.PropTypes.string,
searchFilter: React.PropTypes.string,
},
@ -88,7 +87,9 @@ module.exports = React.createClass({
cli.on("Room.receipt", this.onRoomReceipt);
cli.on("RoomState.events", this.onRoomStateEvents);
cli.on("RoomMember.name", this.onRoomMemberName);
cli.on("Event.decrypted", this.onEventDecrypted);
cli.on("accountData", this.onAccountData);
cli.on("Group.myMembership", this._onGroupMyMembership);
this.refreshRoomList();
@ -155,7 +156,9 @@ module.exports = React.createClass({
MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt);
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName);
MatrixClientPeg.get().removeListener("Event.decrypted", this.onEventDecrypted);
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
MatrixClientPeg.get().removeListener("Group.myMembership", this._onGroupMyMembership);
}
// cancel any pending calls to the rate_limited_funcs
this._delayedRefreshRoomList.cancelPendingCall();
@ -224,12 +227,21 @@ module.exports = React.createClass({
this._delayedRefreshRoomList();
},
onEventDecrypted: function(ev) {
// An event being decrypted may mean we need to re-order the room list
this._delayedRefreshRoomList();
},
onAccountData: function(ev) {
if (ev.getType() == 'm.direct') {
this._delayedRefreshRoomList();
}
},
_onGroupMyMembership: function(group) {
this.forceUpdate();
},
_delayedRefreshRoomList: new rate_limited_func(function() {
this.refreshRoomList();
}, 500),
@ -544,8 +556,24 @@ module.exports = React.createClass({
}
},
_makeGroupInviteTiles() {
const ret = [];
const GroupInviteTile = sdk.getComponent('groups.GroupInviteTile');
for (const group of MatrixClientPeg.get().getGroups()) {
if (group.myMembership !== 'invite') continue;
ret.push(<GroupInviteTile key={group.groupId} group={group} />);
}
return ret;
},
render: function() {
var RoomSubList = sdk.getComponent('structures.RoomSubList');
const RoomSubList = sdk.getComponent('structures.RoomSubList');
const inviteSectionExtraTiles = this._makeGroupInviteTiles();
var self = this;
return (
<GeminiScrollbar className="mx_RoomList_scrollbar"
@ -555,12 +583,15 @@ module.exports = React.createClass({
label={ _t('Invites') }
editable={ false }
order="recent"
isInvite={true}
selectedRoom={ self.props.selectedRoom }
incomingCall={ self.state.incomingCall }
collapsed={ self.props.collapsed }
searchFilter={ self.props.searchFilter }
onHeaderClick={ self.onSubListHeaderClick }
onShowMoreRooms={ self.onShowMoreRooms } />
onShowMoreRooms={ self.onShowMoreRooms }
extraTiles={ inviteSectionExtraTiles }
/>
<RoomSubList list={ self.state.lists['m.favourite'] }
label={ _t('Favourites') }

View file

@ -24,8 +24,6 @@ import sdk from '../../../index';
import Modal from '../../../Modal';
import ObjectUtils from '../../../ObjectUtils';
import dis from '../../../dispatcher';
import ScalarAuthClient from '../../../ScalarAuthClient';
import ScalarMessaging from '../../../ScalarMessaging';
import UserSettingsStore from '../../../UserSettingsStore';
import AccessibleButton from '../elements/AccessibleButton';
@ -92,7 +90,6 @@ module.exports = React.createClass({
propTypes: {
room: React.PropTypes.object.isRequired,
onSaveClick: React.PropTypes.func,
onCancelClick: React.PropTypes.func,
},
getInitialState: function() {
@ -118,14 +115,10 @@ module.exports = React.createClass({
// Default to false if it's undefined, otherwise react complains about changing
// components from uncontrolled to controlled
isRoomPublished: this._originalIsRoomPublished || false,
scalar_error: null,
showIntegrationsError: false,
};
},
componentWillMount: function() {
ScalarMessaging.startListening();
MatrixClientPeg.get().on("RoomMember.membership", this._onRoomMemberMembership);
MatrixClientPeg.get().getRoomDirectoryVisibility(
@ -137,18 +130,6 @@ module.exports = React.createClass({
console.error("Failed to get room visibility: " + err);
});
this.scalarClient = null;
if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) {
this.scalarClient = new ScalarAuthClient();
this.scalarClient.connect().done(() => {
this.forceUpdate();
}, (err) => {
this.setState({
scalar_error: err
});
});
}
dis.dispatch({
action: 'ui_opacity',
sideOpacity: 0.3,
@ -157,8 +138,6 @@ module.exports = React.createClass({
},
componentWillUnmount: function() {
ScalarMessaging.stopListening();
const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener("RoomMember.membership", this._onRoomMemberMembership);
@ -308,6 +287,9 @@ module.exports = React.createClass({
promises.push(ps);
}
// related groups
promises.push(this.saveRelatedGroups());
// encryption
p = this.saveEnableEncryption();
if (!p.isFulfilled()) {
@ -325,6 +307,11 @@ module.exports = React.createClass({
return this.refs.alias_settings.saveSettings();
},
saveRelatedGroups: function() {
if (!this.refs.related_groups) { return Promise.resolve(); }
return this.refs.related_groups.saveSettings();
},
saveColor: function() {
if (!this.refs.color_settings) { return Promise.resolve(); }
return this.refs.color_settings.saveSettings();
@ -514,28 +501,6 @@ module.exports = React.createClass({
roomState.mayClientSendStateEvent("m.room.guest_access", cli));
},
onManageIntegrations(ev) {
ev.preventDefault();
var IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
Modal.createTrackedDialog('Integrations Manager', 'onManageIntegrations', IntegrationsManager, {
src: (this.scalarClient !== null && this.scalarClient.hasCredentials()) ?
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId) :
null,
onFinished: ()=>{
if (this._calcSavePromises().length === 0) {
this.props.onCancelClick(ev);
}
},
}, "mx_IntegrationsManager");
},
onShowIntegrationsError(ev) {
ev.preventDefault();
this.setState({
showIntegrationsError: !this.state.showIntegrationsError,
});
},
onLeaveClick() {
dis.dispatch({
action: 'leave_room',
@ -634,6 +599,7 @@ module.exports = React.createClass({
var AliasSettings = sdk.getComponent("room_settings.AliasSettings");
var ColorSettings = sdk.getComponent("room_settings.ColorSettings");
var UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings");
var RelatedGroupSettings = sdk.getComponent("room_settings.RelatedGroupSettings");
var EditableText = sdk.getComponent('elements.EditableText');
var PowerSelector = sdk.getComponent('elements.PowerSelector');
var Loader = sdk.getComponent("elements.Spinner");
@ -666,6 +632,14 @@ module.exports = React.createClass({
var self = this;
let relatedGroupsSection;
if (UserSettingsStore.isFeatureEnabled('feature_groups')) {
relatedGroupsSection = <RelatedGroupSettings ref="related_groups"
roomId={this.props.room.roomId}
canSetRelatedGroups={roomState.mayClientSendStateEvent("m.room.related_groups", cli)}
relatedGroupsEvent={this.props.room.currentState.getStateEvents('m.room.related_groups', '')} />;
}
var userLevelsSection;
if (Object.keys(user_levels).length) {
userLevelsSection =
@ -797,46 +771,10 @@ module.exports = React.createClass({
</div>;
}
let integrationsButton;
let integrationsError;
if (this.scalarClient !== null) {
if (this.state.showIntegrationsError && this.state.scalar_error) {
console.error(this.state.scalar_error);
integrationsError = (
<span className="mx_RoomSettings_integrationsButton_errorPopup">
{ _t('Could not connect to the integration server') }
</span>
);
}
if (this.scalarClient.hasCredentials()) {
integrationsButton = (
<div className="mx_RoomSettings_integrationsButton" onClick={ this.onManageIntegrations }>
{ _t('Manage Integrations') }
</div>
);
} else if (this.state.scalar_error) {
integrationsButton = (
<div className="mx_RoomSettings_integrationsButton_error" onClick={ this.onShowIntegrationsError }>
Integrations Error <img src="img/warning.svg" width="17"/>
{ integrationsError }
</div>
);
} else {
integrationsButton = (
<div className="mx_RoomSettings_integrationsButton" style={{opacity: 0.5}}>
{ _t('Manage Integrations') }
</div>
);
}
}
return (
<div className="mx_RoomSettings">
{ leaveButton }
{ integrationsButton }
{ tagsSection }
@ -872,7 +810,7 @@ module.exports = React.createClass({
<input type="checkbox" disabled={ !roomState.mayClientSendStateEvent("m.room.aliases", cli) }
onChange={ this._onToggle.bind(this, "isRoomPublished", true, false)}
checked={this.state.isRoomPublished}/>
{_t("List this room in %(domain)s's room directory?", { domain: MatrixClientPeg.get().getDomain() })}
{_t("Publish this room to the public in %(domain)s's room directory?", { domain: MatrixClientPeg.get().getDomain() })}
</label>
</div>
<div className="mx_RoomSettings_settings">
@ -926,6 +864,8 @@ module.exports = React.createClass({
canonicalAliasEvent={this.props.room.currentState.getStateEvents('m.room.canonical_alias', '')}
aliasEvents={this.props.room.currentState.getStateEvents('m.room.aliases')} />
{ relatedGroupsSection }
<UrlPreviewSettings ref="url_preview_settings" room={this.props.room} />
<h3>{ _t('Permissions') }</h3>

View file

@ -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.
@ -27,6 +28,8 @@ var RoomNotifs = require('../../../RoomNotifs');
var FormattingUtils = require('../../../utils/FormattingUtils');
import AccessibleButton from '../elements/AccessibleButton';
var UserSettingsStore = require('../../../UserSettingsStore');
import ActiveRoomObserver from '../../../ActiveRoomObserver';
import RoomViewStore from '../../../stores/RoomViewStore';
module.exports = React.createClass({
displayName: 'RoomTile',
@ -39,7 +42,6 @@ module.exports = React.createClass({
room: React.PropTypes.object.isRequired,
collapsed: React.PropTypes.bool.isRequired,
selected: React.PropTypes.bool.isRequired,
unread: React.PropTypes.bool.isRequired,
highlight: React.PropTypes.bool.isRequired,
isInvite: React.PropTypes.bool.isRequired,
@ -58,6 +60,7 @@ module.exports = React.createClass({
badgeHover : false,
menuDisplayed: false,
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
selected: this.props.room.roomId === RoomViewStore.getRoomId(),
});
},
@ -87,8 +90,15 @@ module.exports = React.createClass({
}
},
_onActiveRoomChange: function() {
this.setState({
selected: this.props.room.roomId === RoomViewStore.getRoomId(),
});
},
componentWillMount: function() {
MatrixClientPeg.get().on("accountData", this.onAccountData);
ActiveRoomObserver.addListener(this.props.room.roomId, this._onActiveRoomChange);
},
componentWillUnmount: function() {
@ -96,6 +106,7 @@ module.exports = React.createClass({
if (cli) {
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
}
ActiveRoomObserver.removeListener(this.props.room.roomId, this._onActiveRoomChange);
},
onClick: function(ev) {
@ -174,7 +185,7 @@ module.exports = React.createClass({
var classes = classNames({
'mx_RoomTile': true,
'mx_RoomTile_selected': this.props.selected,
'mx_RoomTile_selected': this.state.selected,
'mx_RoomTile_unread': this.props.unread,
'mx_RoomTile_unreadNotify': notifBadges,
'mx_RoomTile_highlight': mentionBadges,
@ -221,7 +232,7 @@ module.exports = React.createClass({
'mx_RoomTile_badgeShown': badges || this.state.badgeHover || this.state.menuDisplayed,
});
if (this.props.selected) {
if (this.state.selected) {
let nameSelected = <EmojiText>{name}</EmojiText>;
label = <div title={ name } className={ nameClasses } dir="auto">{ nameSelected }</div>;

View file

@ -26,7 +26,7 @@ export function CancelButton(props) {
return (
<AccessibleButton className='mx_RoomHeader_cancelButton' onClick={onClick}>
<img src="img/cancel.svg" className='mx_filterFlipColor'
width="18" height="18" alt={_t("Cancel")}/>
width="18" height="18" alt={_t("Cancel")} />
</AccessibleButton>
);
}