1
0
mirror of https://github.com/matrix-org/matrix-react-sdk.git synced 2025-11-19 05:22:13 +03:00

Merge branch 'develop' into luke/feature-sticky-date-separators

This commit is contained in:
Luke Barnard
2017-08-30 14:06:06 +01:00
45 changed files with 735 additions and 249 deletions

View File

@@ -1068,10 +1068,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");

View File

@@ -62,7 +62,7 @@ module.exports = React.createClass({
ourUserId: React.PropTypes.string,
// 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
@@ -488,7 +488,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(

View File

@@ -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';
@@ -143,6 +145,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);
@@ -497,8 +501,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};
});
@@ -1716,7 +1719,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}

View File

@@ -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);
}
},
@@ -503,6 +506,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});
},
@@ -1125,8 +1140,8 @@ var TimelinePanel = React.createClass({
highlightedEventId={ this.props.highlightedEventId }
readMarkerEventId={ this.state.readMarkerEventId }
readMarkerVisible={ this.state.readMarkerVisible }
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 }

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.
@@ -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&#39;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>

View File

@@ -38,7 +38,7 @@ module.exports = React.createClass({
roomId: PropTypes.string,
button: PropTypes.string,
focus: PropTypes.bool,
validAddressTypes: PropTypes.arrayOf(PropTypes.oneOfType(addressTypes)),
validAddressTypes: PropTypes.arrayOf(PropTypes.oneOf(addressTypes)),
onFinished: PropTypes.func.isRequired,
},

View File

@@ -47,13 +47,19 @@ export default class AppPermission extends React.Component {
}
render() {
let e2eWarningText;
if (this.props.isRoomEncrypted) {
e2eWarningText =
<span className='mx_AppPermissionWarningTextLabel'>{_t('NOTE: Apps are not end-to-end encrypted')}</span>;
}
return (
<div className='mx_AppPermissionWarning'>
<div className='mx_AppPermissionWarningImage'>
<img src='img/warning.svg' alt={_t('Warning!')}/>
</div>
<div className='mx_AppPermissionWarningText'>
<span className='mx_AppPermissionWarningTextLabel'>Do you want to load widget from URL:</span> <span className='mx_AppPermissionWarningTextURL'>{this.state.curlBase}</span>
<span className='mx_AppPermissionWarningTextLabel'>{_t('Do you want to load widget from URL:')}</span> <span className='mx_AppPermissionWarningTextURL'>{this.state.curlBase}</span>
{e2eWarningText}
</div>
<input
className='mx_AppPermissionButton'
@@ -67,9 +73,11 @@ export default class AppPermission extends React.Component {
}
AppPermission.propTypes = {
isRoomEncrypted: PropTypes.bool,
url: PropTypes.string.isRequired,
onPermissionGranted: PropTypes.func.isRequired,
};
AppPermission.defaultProps = {
isRoomEncrypted: false,
onPermissionGranted: function() {},
};

View File

@@ -31,7 +31,6 @@ import WidgetUtils from '../../../WidgetUtils';
import dis from '../../../dispatcher';
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
const betaHelpMsg = 'This feature is currently experimental and is intended for beta testing only';
export default React.createClass({
displayName: 'AppTile',
@@ -73,8 +72,17 @@ export default React.createClass({
// Returns true if props.url is a scalar URL, typically https://scalar.vector.im/api
isScalarUrl: function() {
const scalarUrl = SdkConfig.get().integrations_rest_url;
return scalarUrl && this.props.url.startsWith(scalarUrl);
let scalarUrls = SdkConfig.get().integrations_widgets_urls;
if (!scalarUrls || scalarUrls.length == 0) {
scalarUrls = [SdkConfig.get().integrations_rest_url];
}
for (let i = 0; i < scalarUrls.length; i++) {
if (this.props.url.startsWith(scalarUrls[i])) {
return true;
}
}
return false;
},
isMixedContent: function() {
@@ -128,7 +136,8 @@ export default React.createClass({
_onEditClick: function(e) {
console.log("Edit widget ID ", this.props.id);
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
const src = this._scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId, 'type_' + this.props.type);
const src = this._scalarClient.getScalarInterfaceUrlForRoom(
this.props.room.roomId, 'type_' + this.props.type, this.props.id);
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, {
src: src,
}, "mx_IntegrationsManager");
@@ -223,42 +232,46 @@ export default React.createClass({
safeWidgetUrl = url.format(parsedWidgetUrl);
}
if (this.state.loading) {
appTileBody = (
<div className='mx_AppTileBody mx_AppLoading'>
<MessageSpinner msg='Loading...'/>
</div>
);
} else if (this.state.hasPermissionToLoad == true) {
if (this.isMixedContent()) {
if (this.props.show) {
if (this.state.loading) {
appTileBody = (
<div className='mx_AppTileBody mx_AppLoading'>
<MessageSpinner msg='Loading...'/>
</div>
);
} else if (this.state.hasPermissionToLoad == true) {
if (this.isMixedContent()) {
appTileBody = (
<div className="mx_AppTileBody">
<AppWarning
errorMsg="Error - Mixed content"
/>
</div>
);
} else {
appTileBody = (
<div className="mx_AppTileBody">
<iframe
ref="appFrame"
src={safeWidgetUrl}
allowFullScreen="true"
sandbox={sandboxFlags}
></iframe>
</div>
);
}
} else {
const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
appTileBody = (
<div className="mx_AppTileBody">
<AppWarning
errorMsg="Error - Mixed content"
<AppPermission
isRoomEncrypted={isRoomEncrypted}
url={this.state.widgetUrl}
onPermissionGranted={this._grantWidgetPermission}
/>
</div>
);
} else if (this.props.show) {
appTileBody = (
<div className="mx_AppTileBody">
<iframe
ref="appFrame"
src={safeWidgetUrl}
allowFullScreen="true"
sandbox={sandboxFlags}
></iframe>
</div>
);
}
} else {
appTileBody = (
<div className="mx_AppTileBody">
<AppPermission
url={this.state.widgetUrl}
onPermissionGranted={this._grantWidgetPermission}
/>
</div>
);
}
// editing is done in scalar
@@ -276,7 +289,6 @@ export default React.createClass({
<div ref="menu_bar" className="mx_AppTileMenuBar" onClick={this.onClickMenuBar}>
{this.formatAppTileName()}
<span className="mx_AppTileMenuBarWidgets">
<span className="mx_Beta" alt={betaHelpMsg} title={betaHelpMsg}>&#946;</span>
{/* Edit widget */}
{showEditButton && <img
src="img/edit.svg"

View File

@@ -75,7 +75,7 @@ export default class ManageIntegsButton extends React.Component {
}
render() {
let integrationsButton;
let integrationsButton = <div />;
let integrationsError;
if (this.scalarClient !== null) {
if (this.state.showIntegrationsError && this.state.scalarError) {

View File

@@ -17,6 +17,7 @@ limitations under the License.
'use strict';
import React from 'react';
import ReactDOM from 'react-dom';
import { _t, _tJsx } from '../../../languageHandler';
var DIV_ID = 'mx_recaptcha';
@@ -66,11 +67,10 @@ module.exports = React.createClass({
// * jumping straight to a hosted captcha page (but we don't support that yet)
// * embedding the captcha in an iframe (if that works)
// * using a better captcha lib
warning.innerHTML = _tJsx(
"Robot check is currently unavailable on desktop - please use a <a>web browser</a>",
/<a>(.*?)<\/a>/,
(sub) => { return "<a href='https://riot.im/app'>{ sub }</a>"; }
);
ReactDOM.render(_tJsx(
"Robot check is currently unavailable on desktop - please use a <a>web browser</a>",
/<a>(.*?)<\/a>/,
(sub) => { return <a href='https://riot.im/app'>{ sub }</a>; }), warning);
this.refs.recaptchaContainer.appendChild(warning);
}
else {

View File

@@ -53,18 +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();
// 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.bind(this));
this.dispatcherRef = dis.register(this.onAction);
},
componentWillUnmount: function() {
@@ -226,7 +226,10 @@ module.exports = React.createClass({
onClick={this.onClickAddWidget}
role="button"
tabIndex="0"
className="mx_AddWidget_button"
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>;

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

@@ -139,7 +139,7 @@ module.exports = React.createClass({
}
return (
<div className={ appsDrawer ? "mx_RoomView_auxPanel mx_RoomView_auxPanel_apps" : "mx_RoomView_auxPanel" } style={{maxHeight: this.props.maxHeight}} >
<div className="mx_RoomView_auxPanel" style={{maxHeight: this.props.maxHeight}} >
{ appsDrawer }
{ fileDropTarget }
{ callView }

View File

@@ -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) => {
@@ -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

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

@@ -555,6 +555,7 @@ 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 }