Merge branch 'develop' into wmwragg/one-to-one-chat

This commit is contained in:
wmwragg 2016-09-05 12:06:31 +01:00
commit 9c0f51fb82
15 changed files with 285 additions and 112 deletions

View file

@ -1,3 +1,23 @@
Changes in [0.6.5-r3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.6.5-r3) (2016-09-02)
=========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.6.5-r2...v0.6.5-r3)
* revert accidental debug logging >:(
Changes in [0.6.5-r2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.6.5-r2) (2016-09-02)
=========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.6.5-r1...v0.6.5-r2)
* Workaround vector-im/vector-web#2020 where floods of joins could crash the browser
(as seen in #matrix-dev right now)
Changes in [0.6.5-r1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.6.5-r1) (2016-09-01)
=========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.6.5...v0.6.5-r1)
* Fix guest access
Changes in [0.6.5](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.6.5) (2016-08-28) Changes in [0.6.5](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.6.5) (2016-08-28)
=================================================================================================== ===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.6.4-r1...v0.6.5) [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.6.4-r1...v0.6.5)

View file

@ -1,6 +1,6 @@
{ {
"name": "matrix-react-sdk", "name": "matrix-react-sdk",
"version": "0.6.5", "version": "0.6.5-r3",
"description": "SDK for matrix.org using React", "description": "SDK for matrix.org using React",
"author": "matrix.org", "author": "matrix.org",
"repository": { "repository": {

View file

@ -197,15 +197,31 @@ function _restoreFromLocalStorage() {
if (access_token && user_id && hs_url) { if (access_token && user_id && hs_url) {
console.log("Restoring session for %s", user_id); console.log("Restoring session for %s", user_id);
setLoggedIn({ try {
userId: user_id, setLoggedIn({
deviceId: device_id, userId: user_id,
accessToken: access_token, deviceId: device_id,
homeserverUrl: hs_url, accessToken: access_token,
identityServerUrl: is_url, homeserverUrl: hs_url,
guest: is_guest, identityServerUrl: is_url,
}); guest: is_guest,
return true; });
return true;
} catch (e) {
console.log("Unable to restore session", e);
var msg = e.message;
if (msg == "OLM.BAD_LEGACY_ACCOUNT_PICKLE") {
msg = "You need to log back in to generate end-to-end encryption keys "
+ "for this device and submit the public key to your homeserver. "
+ "This is a once off; sorry for the inconvenience.";
}
// don't leak things into the new session
_clearLocalStorage();
throw new Error("Unable to restore previous session: " + msg);
}
} else { } else {
console.log("No previous session found."); console.log("No previous session found.");
return false; return false;
@ -305,22 +321,27 @@ export function startMatrixClient() {
* a session has been logged out / ended. * a session has been logged out / ended.
*/ */
export function onLoggedOut() { export function onLoggedOut() {
if (window.localStorage) { _clearLocalStorage();
const hsUrl = window.localStorage.getItem("mx_hs_url");
const isUrl = window.localStorage.getItem("mx_is_url");
window.localStorage.clear();
// preserve our HS & IS URLs for convenience
// N.B. we cache them in hsUrl/isUrl and can't really inline them
// as getCurrentHsUrl() may call through to localStorage.
// NB. We do clear the device ID (as well as all the settings)
if (hsUrl) window.localStorage.setItem("mx_hs_url", hsUrl);
if (isUrl) window.localStorage.setItem("mx_is_url", isUrl);
}
stopMatrixClient(); stopMatrixClient();
dis.dispatch({action: 'on_logged_out'}); dis.dispatch({action: 'on_logged_out'});
} }
function _clearLocalStorage() {
if (!window.localStorage) {
return;
}
const hsUrl = window.localStorage.getItem("mx_hs_url");
const isUrl = window.localStorage.getItem("mx_is_url");
window.localStorage.clear();
// preserve our HS & IS URLs for convenience
// N.B. we cache them in hsUrl/isUrl and can't really inline them
// as getCurrentHsUrl() may call through to localStorage.
// NB. We do clear the device ID (as well as all the settings)
if (hsUrl) window.localStorage.setItem("mx_hs_url", hsUrl);
if (isUrl) window.localStorage.setItem("mx_is_url", isUrl);
}
/** /**
* Stop all the background processes related to the current client * Stop all the background processes related to the current client
*/ */

View file

@ -24,7 +24,7 @@ export const MENTIONS_ONLY = 'mentions_only';
export const MUTE = 'mute'; export const MUTE = 'mute';
export function getRoomNotifsState(roomId) { export function getRoomNotifsState(roomId) {
if (MatrixClientPeg.get().isGuest()) return RoomNotifs.ALL_MESSAGES; if (MatrixClientPeg.get().isGuest()) return ALL_MESSAGES;
// look through the override rules for a rule affecting this room: // look through the override rules for a rule affecting this room:
// if one exists, it will take precedence. // if one exists, it will take precedence.

View file

@ -18,9 +18,41 @@ var q = require("q");
var request = require('browser-request'); var request = require('browser-request');
var SdkConfig = require('./SdkConfig'); var SdkConfig = require('./SdkConfig');
var MatrixClientPeg = require('./MatrixClientPeg');
class ScalarAuthClient { class ScalarAuthClient {
getScalarToken(openid_token_object) {
constructor() {
this.scalarToken = null;
}
connect() {
return this.getScalarToken().then((tok) => {
this.scalarToken = tok;
});
}
hasCredentials() {
return this.scalarToken != null; // undef or null
}
// Returns a scalar_token string
getScalarToken() {
var tok = window.localStorage.getItem("mx_scalar_token");
if (tok) return q(tok);
// No saved token, so do the dance to get one. First, we
// need an openid bearer token from the HS.
return MatrixClientPeg.get().getOpenIdToken().then((token_object) => {
// Now we can send that to scalar and exchange it for a scalar token
return this.exchangeForScalarToken(token_object);
}).then((token_object) => {
window.localStorage.setItem("mx_scalar_token", token_object);
return token_object;
});
}
exchangeForScalarToken(openid_token_object) {
var defer = q.defer(); var defer = q.defer();
var scalar_rest_url = SdkConfig.get().integrations_rest_url; var scalar_rest_url = SdkConfig.get().integrations_rest_url;
@ -43,6 +75,17 @@ class ScalarAuthClient {
return defer.promise; return defer.promise;
} }
getScalarInterfaceUrlForRoom(roomId) {
var url = SdkConfig.get().integrations_ui_url;
url += "?scalar_token=" + encodeURIComponent(this.scalarToken);
url += "&room_id=" + encodeURIComponent(roomId);
return url;
}
getStarterLink(starterLinkUrl) {
return starterLinkUrl + "?scalar_token=" + encodeURIComponent(this.scalarToken);
}
} }
module.exports = ScalarAuthClient; module.exports = ScalarAuthClient;

View file

@ -62,6 +62,9 @@ function textForMemberEvent(ev) {
return senderName + " changed their profile picture"; return senderName + " changed their profile picture";
} else if (!ev.getPrevContent().avatar_url && ev.getContent().avatar_url) { } else if (!ev.getPrevContent().avatar_url && ev.getContent().avatar_url) {
return senderName + " set a profile picture"; return senderName + " set a profile picture";
} else {
// hacky hack for https://github.com/vector-im/vector-web/issues/2020
return senderName + " rejoined the room.";
} }
} else { } else {
if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key); if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);

View file

@ -13,6 +13,9 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import q from 'q';
var React = require('react'); var React = require('react');
var Matrix = require("matrix-js-sdk"); var Matrix = require("matrix-js-sdk");
var Favico = require('favico.js'); var Favico = require('favico.js');
@ -164,6 +167,9 @@ module.exports = React.createClass({
// their mind & log back in) // their mind & log back in)
this.guestCreds = null; this.guestCreds = null;
// if the automatic session load failed, the error
this.sessionLoadError = null;
if (this.props.config.sync_timeline_limit) { if (this.props.config.sync_timeline_limit) {
MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit; MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit;
} }
@ -191,13 +197,20 @@ module.exports = React.createClass({
window.addEventListener('resize', this.handleResize); window.addEventListener('resize', this.handleResize);
this.handleResize(); this.handleResize();
Lifecycle.loadSession({ // the extra q() ensures that synchronous exceptions hit the same codepath as
realQueryParams: this.props.realQueryParams, // asynchronous ones.
fragmentQueryParams: this.props.startingFragmentQueryParams, q().then(() => {
enableGuest: this.props.enableGuest, return Lifecycle.loadSession({
guestHsUrl: this.getCurrentHsUrl(), realQueryParams: this.props.realQueryParams,
guestIsUrl: this.getCurrentIsUrl(), fragmentQueryParams: this.props.startingFragmentQueryParams,
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName, enableGuest: this.props.enableGuest,
guestHsUrl: this.getCurrentHsUrl(),
guestIsUrl: this.getCurrentIsUrl(),
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
});
}).catch((e) => {
console.error("Unable to load session", e);
this.sessionLoadError = e.message;
}).done(()=>{ }).done(()=>{
// stuff this through the dispatcher so that it happens // stuff this through the dispatcher so that it happens
// after the on_logged_in action. // after the on_logged_in action.
@ -1097,7 +1110,7 @@ module.exports = React.createClass({
onLoginClick={this.onLoginClick} /> onLoginClick={this.onLoginClick} />
); );
} else { } else {
return ( var r = (
<Login <Login
onLoggedIn={Lifecycle.setLoggedIn} onLoggedIn={Lifecycle.setLoggedIn}
onRegisterClick={this.onRegisterClick} onRegisterClick={this.onRegisterClick}
@ -1110,8 +1123,16 @@ module.exports = React.createClass({
onForgotPasswordClick={this.onForgotPasswordClick} onForgotPasswordClick={this.onForgotPasswordClick}
enableGuest={this.props.enableGuest} enableGuest={this.props.enableGuest}
onCancelClick={this.guestCreds ? this.onReturnToGuestClick : null} onCancelClick={this.guestCreds ? this.onReturnToGuestClick : null}
/> initialErrorText={this.sessionLoadError}
/>
); );
// we only want to show the session load error the first time the
// Login component is rendered. This is pretty hacky but I can't
// think of another way to achieve it.
this.sessionLoadError = null;
return r;
} }
} }
}); });

View file

@ -52,12 +52,14 @@ module.exports = React.createClass({
// login shouldn't care how password recovery is done. // login shouldn't care how password recovery is done.
onForgotPasswordClick: React.PropTypes.func, onForgotPasswordClick: React.PropTypes.func,
onCancelClick: React.PropTypes.func, onCancelClick: React.PropTypes.func,
initialErrorText: React.PropTypes.string,
}, },
getInitialState: function() { getInitialState: function() {
return { return {
busy: false, busy: false,
errorText: null, errorText: this.props.initialErrorText,
loginIncorrect: false, loginIncorrect: false,
enteredHomeserverUrl: this.props.customHsUrl || this.props.defaultHsUrl, enteredHomeserverUrl: this.props.customHsUrl || this.props.defaultHsUrl,
enteredIdentityServerUrl: this.props.customIsUrl || this.props.defaultIsUrl, enteredIdentityServerUrl: this.props.customIsUrl || this.props.defaultIsUrl,
@ -116,7 +118,8 @@ module.exports = React.createClass({
onHsUrlChanged: function(newHsUrl) { onHsUrlChanged: function(newHsUrl) {
var self = this; var self = this;
this.setState({ this.setState({
enteredHomeserverUrl: newHsUrl enteredHomeserverUrl: newHsUrl,
errorText: null, // reset err messages
}, function() { }, function() {
self._initLoginLogic(newHsUrl); self._initLoginLogic(newHsUrl);
}); });
@ -125,7 +128,8 @@ module.exports = React.createClass({
onIsUrlChanged: function(newIsUrl) { onIsUrlChanged: function(newIsUrl) {
var self = this; var self = this;
this.setState({ this.setState({
enteredIdentityServerUrl: newIsUrl enteredIdentityServerUrl: newIsUrl,
errorText: null, // reset err messages
}, function() { }, function() {
self._initLoginLogic(null, newIsUrl); self._initLoginLogic(null, newIsUrl);
}); });
@ -160,7 +164,6 @@ module.exports = React.createClass({
enteredHomeserverUrl: hsUrl, enteredHomeserverUrl: hsUrl,
enteredIdentityServerUrl: isUrl, enteredIdentityServerUrl: isUrl,
busy: true, busy: true,
errorText: null, // reset err messages
loginIncorrect: false, loginIncorrect: false,
}); });
}, },

View file

@ -23,6 +23,10 @@ var linkify = require('linkifyjs');
var linkifyElement = require('linkifyjs/element'); var linkifyElement = require('linkifyjs/element');
var linkifyMatrix = require('../../../linkify-matrix'); var linkifyMatrix = require('../../../linkify-matrix');
var sdk = require('../../../index'); var sdk = require('../../../index');
var ScalarAuthClient = require("../../../ScalarAuthClient");
var Modal = require("../../../Modal");
var SdkConfig = require('../../../SdkConfig');
var UserSettingsStore = require('../../../UserSettingsStore');
linkifyMatrix(linkify); linkifyMatrix(linkify);
@ -176,15 +180,66 @@ module.exports = React.createClass({
} }
}, },
onStarterLinkClick: function(starterLink, ev) {
ev.preventDefault();
// We need to add on our scalar token to the starter link, but we may not have one!
// In addition, we can't fetch one on click and then go to it immediately as that
// is then treated as a popup!
// We can get around this by fetching one now and showing a "confirmation dialog" (hurr hurr)
// which requires the user to click through and THEN we can open the link in a new tab because
// the window.open command occurs in the same stack frame as the onClick callback.
let integrationsEnabled = UserSettingsStore.isFeatureEnabled("integration_management");
if (!integrationsEnabled) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Integrations disabled",
description: "You need to enable the Labs option 'Integrations Management' in your Vector user settings first.",
});
return;
}
// Go fetch a scalar token
let scalarClient = new ScalarAuthClient();
scalarClient.connect().then(() => {
let completeUrl = scalarClient.getStarterLink(starterLink);
let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
let integrationsUrl = SdkConfig.get().integrations_ui_url;
Modal.createDialog(QuestionDialog, {
title: "Add an Integration",
description:
<div>
You are about to taken to a third-party site so you can authenticate your account for use with {integrationsUrl}.<br/>
Do you wish to continue?
</div>,
button: "Continue",
onFinished: function(confirmed) {
if (!confirmed) {
return;
}
let width = window.screen.width > 1024 ? 1024 : window.screen.width;
let height = window.screen.height > 800 ? 800 : window.screen.height;
let left = (window.screen.width - width) / 2;
let top = (window.screen.height - height) / 2;
window.open(completeUrl, '_blank', `height=${height}, width=${width}, top=${top}, left=${left},`);
},
});
});
},
render: function() { render: function() {
const EmojiText = sdk.getComponent('elements.EmojiText'); const EmojiText = sdk.getComponent('elements.EmojiText');
var mxEvent = this.props.mxEvent; var mxEvent = this.props.mxEvent;
var content = mxEvent.getContent(); var content = mxEvent.getContent();
var body = HtmlUtils.bodyToHtml(content, this.props.highlights, {}); var body = HtmlUtils.bodyToHtml(content, this.props.highlights, {});
if (this.props.highlightLink) { if (this.props.highlightLink) {
body = <a href={ this.props.highlightLink }>{ body }</a>; body = <a href={ this.props.highlightLink }>{ body }</a>;
} }
else if (content.data && typeof content.data["org.matrix.neb.starter_link"] === "string") {
body = <a href="#" onClick={ this.onStarterLinkClick.bind(this, content.data["org.matrix.neb.starter_link"]) }>{ body }</a>;
}
var widgets; var widgets;
if (this.state.links.length && !this.state.widgetHidden && this.props.showUrlPreview) { if (this.state.links.length && !this.state.widgetHidden && this.props.showUrlPreview) {

View file

@ -57,12 +57,14 @@ module.exports = React.createClass({
} }
}, },
onConferenceNotificationClick: function() { onConferenceNotificationClick: function(ev, type) {
dis.dispatch({ dis.dispatch({
action: 'place_call', action: 'place_call',
type: "video", type: type,
room_id: this.props.room.roomId, room_id: this.props.room.roomId,
}); });
ev.stopPropagation();
ev.preventDefault();
}, },
render: function() { render: function() {
@ -85,14 +87,20 @@ module.exports = React.createClass({
var conferenceCallNotification = null; var conferenceCallNotification = null;
if (this.props.displayConfCallNotification) { if (this.props.displayConfCallNotification) {
var supportedText; var supportedText, joinText;
if (!MatrixClientPeg.get().supportsVoip()) { if (!MatrixClientPeg.get().supportsVoip()) {
supportedText = " (unsupported)"; supportedText = " (unsupported)";
} }
else {
joinText = (<span>
Join as <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'voice')}} href="#">voice</a>&nbsp;
or <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'video') }} href="#">video</a>.
</span>);
}
conferenceCallNotification = ( conferenceCallNotification = (
<div className="mx_RoomView_ongoingConfCallNotification" <div className="mx_RoomView_ongoingConfCallNotification">
onClick={this.onConferenceNotificationClick}> Ongoing conference call{ supportedText }. { joinText }
Ongoing conference call {supportedText}
</div> </div>
); );
} }

View file

@ -14,43 +14,45 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
var React = require('react'); import React from 'react';
var MatrixClientPeg = require("../../../MatrixClientPeg"); import MatrixClientPeg from '../../../MatrixClientPeg';
module.exports = React.createClass({ export default class MemberDeviceInfo extends React.Component {
displayName: 'MemberDeviceInfo', constructor(props) {
propTypes: { super(props);
userId: React.PropTypes.string.isRequired, this.onVerifyClick = this.onVerifyClick.bind(this);
device: React.PropTypes.object.isRequired, this.onUnverifyClick = this.onUnverifyClick.bind(this);
}, this.onBlockClick = this.onBlockClick.bind(this);
this.onUnblockClick = this.onUnblockClick.bind(this);
}
onVerifyClick: function() { onVerifyClick() {
MatrixClientPeg.get().setDeviceVerified( MatrixClientPeg.get().setDeviceVerified(
this.props.userId, this.props.device.id, true this.props.userId, this.props.device.deviceId, true
); );
}, }
onUnverifyClick: function() { onUnverifyClick() {
MatrixClientPeg.get().setDeviceVerified( MatrixClientPeg.get().setDeviceVerified(
this.props.userId, this.props.device.id, false this.props.userId, this.props.device.deviceId, false
); );
}, }
onBlockClick: function() { onBlockClick() {
MatrixClientPeg.get().setDeviceBlocked( MatrixClientPeg.get().setDeviceBlocked(
this.props.userId, this.props.device.id, true this.props.userId, this.props.device.deviceId, true
); );
}, }
onUnblockClick: function() { onUnblockClick() {
MatrixClientPeg.get().setDeviceBlocked( MatrixClientPeg.get().setDeviceBlocked(
this.props.userId, this.props.device.id, false this.props.userId, this.props.device.deviceId, false
); );
}, }
render: function() { render() {
var indicator = null, blockButton = null, verifyButton = null; var indicator = null, blockButton = null, verifyButton = null;
if (this.props.device.blocked) { if (this.props.device.isBlocked()) {
blockButton = ( blockButton = (
<div className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unblock" <div className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unblock"
onClick={this.onUnblockClick}> onClick={this.onUnblockClick}>
@ -66,7 +68,7 @@ module.exports = React.createClass({
); );
} }
if (this.props.device.verified) { if (this.props.device.isVerified()) {
verifyButton = ( verifyButton = (
<div className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unverify" <div className="mx_MemberDeviceInfo_textButton mx_MemberDeviceInfo_unverify"
onClick={this.onUnverifyClick}> onClick={this.onUnverifyClick}>
@ -82,22 +84,22 @@ module.exports = React.createClass({
); );
} }
if (this.props.device.blocked) { if (this.props.device.isBlocked()) {
indicator = ( indicator = (
<div className="mx_MemberDeviceInfo_blocked">&#x2716;</div> <div className="mx_MemberDeviceInfo_blocked">Blocked</div>
); );
} else if (this.props.device.verified) { } else if (this.props.device.isVerified()) {
indicator = ( indicator = (
<div className="mx_MemberDeviceInfo_verified">&#x2714;</div> <div className="mx_MemberDeviceInfo_verified">Verified</div>
); );
} else { } else {
indicator = ( indicator = (
<div className="mx_MemberDeviceInfo_unverified">?</div> <div className="mx_MemberDeviceInfo_unverified">Unverified</div>
); );
} }
var deviceName = this.props.device.display_name || this.props.device.id; var deviceName = this.props.device.display_name || this.props.device.deviceId;
return ( return (
<div className="mx_MemberDeviceInfo"> <div className="mx_MemberDeviceInfo">
@ -107,5 +109,11 @@ module.exports = React.createClass({
{blockButton} {blockButton}
</div> </div>
); );
}, }
}); };
MemberDeviceInfo.displayName = 'MemberDeviceInfo';
MemberDeviceInfo.propTypes = {
userId: React.PropTypes.string.isRequired,
device: React.PropTypes.object.isRequired,
};

View file

@ -159,7 +159,7 @@ module.exports = React.createClass({
if (userId == this.props.member.userId) { if (userId == this.props.member.userId) {
// no need to re-download the whole thing; just update our copy of // no need to re-download the whole thing; just update our copy of
// the list. // the list.
var devices = MatrixClientPeg.get().listDeviceKeys(userId); var devices = MatrixClientPeg.get().getStoredDevicesForUser(userId);
this.setState({devices: devices}); this.setState({devices: devices});
} }
}, },
@ -195,7 +195,7 @@ module.exports = React.createClass({
// we got cancelled - presumably a different user now // we got cancelled - presumably a different user now
return; return;
} }
var devices = client.listDeviceKeys(member.userId); var devices = client.getStoredDevicesForUser(member.userId);
self.setState({devicesLoading: false, devices: devices}); self.setState({devicesLoading: false, devices: devices});
}, function(err) { }, function(err) {
console.log("Error downloading devices", err); console.log("Error downloading devices", err);

View file

@ -65,7 +65,6 @@ module.exports = React.createClass({
// Default to false if it's undefined, otherwise react complains about changing // Default to false if it's undefined, otherwise react complains about changing
// components from uncontrolled to controlled // components from uncontrolled to controlled
isRoomPublished: this._originalIsRoomPublished || false, isRoomPublished: this._originalIsRoomPublished || false,
scalar_token: null,
scalar_error: null, scalar_error: null,
}; };
}, },
@ -81,11 +80,16 @@ module.exports = React.createClass({
console.error("Failed to get room visibility: " + err); console.error("Failed to get room visibility: " + err);
}); });
this.getScalarToken().done((token) => { if (UserSettingsStore.isFeatureEnabled("integration_management")) {
this.setState({scalar_token: token}); this.scalarClient = new ScalarAuthClient();
}, (err) => { this.scalarClient.connect().done(() => {
this.setState({scalar_error: err}); this.forceUpdate();
}); }, (err) => {
this.setState({
scalar_error: err
});
})
}
dis.dispatch({ dis.dispatch({
action: 'ui_opacity', action: 'ui_opacity',
@ -249,7 +253,7 @@ module.exports = React.createClass({
var roomId = this.props.room.roomId; var roomId = this.props.room.roomId;
return MatrixClientPeg.get().sendStateEvent( return MatrixClientPeg.get().sendStateEvent(
roomId, "m.room.encryption", roomId, "m.room.encryption",
{ algorithm: "m.olm.v1.curve25519-aes-sha2" } { algorithm: "m.megolm.v1.aes-sha2" }
); );
}, },
@ -395,34 +399,13 @@ module.exports = React.createClass({
roomState.mayClientSendStateEvent("m.room.guest_access", cli)) roomState.mayClientSendStateEvent("m.room.guest_access", cli))
}, },
getScalarInterfaceUrl: function() {
var url = SdkConfig.get().integrations_ui_url;
url += "?scalar_token=" + encodeURIComponent(this.state.scalar_token);
url += "&room_id=" + encodeURIComponent(this.props.room.roomId);
return url;
},
getScalarToken() {
var tok = window.localStorage.getItem("mx_scalar_token");
if (tok) return q(tok);
// No saved token, so do the dance to get one. First, we
// need an openid bearer token from the HS.
return MatrixClientPeg.get().getOpenIdToken().then((token_object) => {
// Now we can send that to scalar and exchange it for a scalar token
var scalar_auth_client = new ScalarAuthClient();
return scalar_auth_client.getScalarToken(token_object);
}).then((token_object) => {
window.localStorage.setItem("mx_scalar_token", token_object);
return token_object;
});
},
onManageIntegrations(ev) { onManageIntegrations(ev) {
ev.preventDefault(); ev.preventDefault();
var IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); var IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
Modal.createDialog(IntegrationsManager, { Modal.createDialog(IntegrationsManager, {
src: this.state.scalar_token ? this.getScalarInterfaceUrl() : null src: this.scalarClient.hasCredentials() ?
this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId) :
null
}, ""); }, "");
}, },
@ -649,7 +632,7 @@ module.exports = React.createClass({
if (UserSettingsStore.isFeatureEnabled("integration_management")) { if (UserSettingsStore.isFeatureEnabled("integration_management")) {
let integrations_body; let integrations_body;
if (this.state.scalar_token) { if (this.scalarClient.hasCredentials()) {
integrations_body = ( integrations_body = (
<div className="mx_RoomSettings_settings"> <div className="mx_RoomSettings_settings">
<a href="#" onClick={ this.onManageIntegrations }>Manage integrations</a> <a href="#" onClick={ this.onManageIntegrations }>Manage integrations</a>

View file

@ -95,8 +95,10 @@ module.exports = React.createClass({
if (call) { if (call) {
call.setLocalVideoElement(this.getVideoView().getLocalVideoElement()); call.setLocalVideoElement(this.getVideoView().getLocalVideoElement());
call.setRemoteVideoElement(this.getVideoView().getRemoteVideoElement()); call.setRemoteVideoElement(this.getVideoView().getRemoteVideoElement());
// give a separate element for audio stream playback - both for voice calls // always use a separate element for audio stream playback.
// and for the voice stream of screen captures // this is to let us move CallView around the DOM without interrupting remote audio
// during playback, by having the audio rendered by a top-level <audio/> element.
// rather than being rendered by the main remoteVideo <video/> element.
call.setRemoteAudioElement(this.getVideoView().getRemoteAudioElement()); call.setRemoteAudioElement(this.getVideoView().getRemoteAudioElement());
} }
if (call && call.type === "video" && call.call_state !== "ended" && call.call_state !== "ringing") { if (call && call.type === "video" && call.call_state !== "ended" && call.call_state !== "ringing") {

View file

@ -50,7 +50,14 @@ module.exports = React.createClass({
}, },
getRemoteAudioElement: function() { getRemoteAudioElement: function() {
return this.refs.remoteAudio; // this needs to be somewhere at the top of the DOM which
// always exists to avoid audio interruptions.
// Might as well just use DOM.
var remoteAudioElement = document.getElementById("remoteAudio");
if (!remoteAudioElement) {
console.error("Failed to find remoteAudio element - cannot play audio! You need to add an <audio/> to the DOM.");
}
return remoteAudioElement;
}, },
getLocalVideoElement: function() { getLocalVideoElement: function() {
@ -106,7 +113,6 @@ module.exports = React.createClass({
<div className="mx_VideoView_remoteVideoFeed"> <div className="mx_VideoView_remoteVideoFeed">
<VideoFeed ref="remote" onResize={this.props.onResize} <VideoFeed ref="remote" onResize={this.props.onResize}
maxHeight={maxVideoHeight} /> maxHeight={maxVideoHeight} />
<audio ref="remoteAudio"/>
</div> </div>
<div className="mx_VideoView_localVideoFeed"> <div className="mx_VideoView_localVideoFeed">
<VideoFeed ref="local"/> <VideoFeed ref="local"/>