diff --git a/CHANGELOG.md b/CHANGELOG.md
index c18ffa24d7..af1c994ee3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.6.4-r1...v0.6.5)
diff --git a/package.json b/package.json
index cd5a2d4f26..85b68f659b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
- "version": "0.6.5",
+ "version": "0.6.5-r3",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
diff --git a/src/Lifecycle.js b/src/Lifecycle.js
index ee82a8890a..9f9b4b5389 100644
--- a/src/Lifecycle.js
+++ b/src/Lifecycle.js
@@ -197,15 +197,31 @@ function _restoreFromLocalStorage() {
if (access_token && user_id && hs_url) {
console.log("Restoring session for %s", user_id);
- setLoggedIn({
- userId: user_id,
- deviceId: device_id,
- accessToken: access_token,
- homeserverUrl: hs_url,
- identityServerUrl: is_url,
- guest: is_guest,
- });
- return true;
+ try {
+ setLoggedIn({
+ userId: user_id,
+ deviceId: device_id,
+ accessToken: access_token,
+ homeserverUrl: hs_url,
+ identityServerUrl: is_url,
+ guest: is_guest,
+ });
+ 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 {
console.log("No previous session found.");
return false;
@@ -305,22 +321,27 @@ export function startMatrixClient() {
* a session has been logged out / ended.
*/
export function onLoggedOut() {
- if (window.localStorage) {
- 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);
- }
+ _clearLocalStorage();
stopMatrixClient();
-
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
*/
diff --git a/src/RoomNotifs.js b/src/RoomNotifs.js
index 00cad23791..d0cdd6ead7 100644
--- a/src/RoomNotifs.js
+++ b/src/RoomNotifs.js
@@ -24,7 +24,7 @@ export const MENTIONS_ONLY = 'mentions_only';
export const MUTE = 'mute';
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:
// if one exists, it will take precedence.
diff --git a/src/ScalarAuthClient.js b/src/ScalarAuthClient.js
index d145cebfe0..e1928e15d4 100644
--- a/src/ScalarAuthClient.js
+++ b/src/ScalarAuthClient.js
@@ -18,9 +18,41 @@ var q = require("q");
var request = require('browser-request');
var SdkConfig = require('./SdkConfig');
+var MatrixClientPeg = require('./MatrixClientPeg');
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 scalar_rest_url = SdkConfig.get().integrations_rest_url;
@@ -43,6 +75,17 @@ class ScalarAuthClient {
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;
diff --git a/src/TextForEvent.js b/src/TextForEvent.js
index 93f5f7c4e2..f32d1a7132 100644
--- a/src/TextForEvent.js
+++ b/src/TextForEvent.js
@@ -62,6 +62,9 @@ function textForMemberEvent(ev) {
return senderName + " changed their profile picture";
} else if (!ev.getPrevContent().avatar_url && ev.getContent().avatar_url) {
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 {
if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);
diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js
index a8d8355cf1..bfd8ae8e86 100644
--- a/src/components/structures/MatrixChat.js
+++ b/src/components/structures/MatrixChat.js
@@ -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
limitations under the License.
*/
+
+import q from 'q';
+
var React = require('react');
var Matrix = require("matrix-js-sdk");
var Favico = require('favico.js');
@@ -164,6 +167,9 @@ module.exports = React.createClass({
// their mind & log back in)
this.guestCreds = null;
+ // if the automatic session load failed, the error
+ this.sessionLoadError = null;
+
if (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);
this.handleResize();
- Lifecycle.loadSession({
- realQueryParams: this.props.realQueryParams,
- fragmentQueryParams: this.props.startingFragmentQueryParams,
- enableGuest: this.props.enableGuest,
- guestHsUrl: this.getCurrentHsUrl(),
- guestIsUrl: this.getCurrentIsUrl(),
- defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
+ // the extra q() ensures that synchronous exceptions hit the same codepath as
+ // asynchronous ones.
+ q().then(() => {
+ return Lifecycle.loadSession({
+ realQueryParams: this.props.realQueryParams,
+ fragmentQueryParams: this.props.startingFragmentQueryParams,
+ 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(()=>{
// stuff this through the dispatcher so that it happens
// after the on_logged_in action.
@@ -1097,7 +1110,7 @@ module.exports = React.createClass({
onLoginClick={this.onLoginClick} />
);
} else {
- return (
+ var r = (
+ 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;
}
}
});
diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js
index 8025504857..0315a3186a 100644
--- a/src/components/structures/login/Login.js
+++ b/src/components/structures/login/Login.js
@@ -52,12 +52,14 @@ module.exports = React.createClass({
// login shouldn't care how password recovery is done.
onForgotPasswordClick: React.PropTypes.func,
onCancelClick: React.PropTypes.func,
+
+ initialErrorText: React.PropTypes.string,
},
getInitialState: function() {
return {
busy: false,
- errorText: null,
+ errorText: this.props.initialErrorText,
loginIncorrect: false,
enteredHomeserverUrl: this.props.customHsUrl || this.props.defaultHsUrl,
enteredIdentityServerUrl: this.props.customIsUrl || this.props.defaultIsUrl,
@@ -116,7 +118,8 @@ module.exports = React.createClass({
onHsUrlChanged: function(newHsUrl) {
var self = this;
this.setState({
- enteredHomeserverUrl: newHsUrl
+ enteredHomeserverUrl: newHsUrl,
+ errorText: null, // reset err messages
}, function() {
self._initLoginLogic(newHsUrl);
});
@@ -125,7 +128,8 @@ module.exports = React.createClass({
onIsUrlChanged: function(newIsUrl) {
var self = this;
this.setState({
- enteredIdentityServerUrl: newIsUrl
+ enteredIdentityServerUrl: newIsUrl,
+ errorText: null, // reset err messages
}, function() {
self._initLoginLogic(null, newIsUrl);
});
@@ -160,7 +164,6 @@ module.exports = React.createClass({
enteredHomeserverUrl: hsUrl,
enteredIdentityServerUrl: isUrl,
busy: true,
- errorText: null, // reset err messages
loginIncorrect: false,
});
},
diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js
index 19ca9657c2..6bb1a994d5 100644
--- a/src/components/views/messages/TextualBody.js
+++ b/src/components/views/messages/TextualBody.js
@@ -23,6 +23,10 @@ var linkify = require('linkifyjs');
var linkifyElement = require('linkifyjs/element');
var linkifyMatrix = require('../../../linkify-matrix');
var sdk = require('../../../index');
+var ScalarAuthClient = require("../../../ScalarAuthClient");
+var Modal = require("../../../Modal");
+var SdkConfig = require('../../../SdkConfig');
+var UserSettingsStore = require('../../../UserSettingsStore');
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:
+
+ You are about to taken to a third-party site so you can authenticate your account for use with {integrationsUrl}.
+ Do you wish to continue?
+
,
+ 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() {
const EmojiText = sdk.getComponent('elements.EmojiText');
var mxEvent = this.props.mxEvent;
var content = mxEvent.getContent();
+
var body = HtmlUtils.bodyToHtml(content, this.props.highlights, {});
if (this.props.highlightLink) {
body = { body };
}
+ else if (content.data && typeof content.data["org.matrix.neb.starter_link"] === "string") {
+ body = { body };
+ }
var widgets;
if (this.state.links.length && !this.state.widgetHidden && this.props.showUrlPreview) {
diff --git a/src/components/views/rooms/AuxPanel.js b/src/components/views/rooms/AuxPanel.js
index ec885989bc..4eaa19193e 100644
--- a/src/components/views/rooms/AuxPanel.js
+++ b/src/components/views/rooms/AuxPanel.js
@@ -57,12 +57,14 @@ module.exports = React.createClass({
}
},
- onConferenceNotificationClick: function() {
+ onConferenceNotificationClick: function(ev, type) {
dis.dispatch({
action: 'place_call',
- type: "video",
+ type: type,
room_id: this.props.room.roomId,
});
+ ev.stopPropagation();
+ ev.preventDefault();
},
render: function() {
@@ -85,14 +87,20 @@ module.exports = React.createClass({
var conferenceCallNotification = null;
if (this.props.displayConfCallNotification) {
- var supportedText;
+ var supportedText, joinText;
if (!MatrixClientPeg.get().supportsVoip()) {
supportedText = " (unsupported)";
}
+ else {
+ joinText = (
+ Join as { this.onConferenceNotificationClick(event, 'voice')}} href="#">voice
+ or { this.onConferenceNotificationClick(event, 'video') }} href="#">video.
+ );
+
+ }
conferenceCallNotification = (
-
);
- },
-});
+ }
+};
+
+MemberDeviceInfo.displayName = 'MemberDeviceInfo';
+MemberDeviceInfo.propTypes = {
+ userId: React.PropTypes.string.isRequired,
+ device: React.PropTypes.object.isRequired,
+};
diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js
index c02b009c39..be6663be67 100644
--- a/src/components/views/rooms/MemberInfo.js
+++ b/src/components/views/rooms/MemberInfo.js
@@ -159,7 +159,7 @@ module.exports = React.createClass({
if (userId == this.props.member.userId) {
// no need to re-download the whole thing; just update our copy of
// the list.
- var devices = MatrixClientPeg.get().listDeviceKeys(userId);
+ var devices = MatrixClientPeg.get().getStoredDevicesForUser(userId);
this.setState({devices: devices});
}
},
@@ -195,7 +195,7 @@ module.exports = React.createClass({
// we got cancelled - presumably a different user now
return;
}
- var devices = client.listDeviceKeys(member.userId);
+ var devices = client.getStoredDevicesForUser(member.userId);
self.setState({devicesLoading: false, devices: devices});
}, function(err) {
console.log("Error downloading devices", err);
diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js
index 145b8559b7..a5070cfa21 100644
--- a/src/components/views/rooms/RoomSettings.js
+++ b/src/components/views/rooms/RoomSettings.js
@@ -65,7 +65,6 @@ 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_token: null,
scalar_error: null,
};
},
@@ -81,11 +80,16 @@ module.exports = React.createClass({
console.error("Failed to get room visibility: " + err);
});
- this.getScalarToken().done((token) => {
- this.setState({scalar_token: token});
- }, (err) => {
- this.setState({scalar_error: err});
- });
+ if (UserSettingsStore.isFeatureEnabled("integration_management")) {
+ this.scalarClient = new ScalarAuthClient();
+ this.scalarClient.connect().done(() => {
+ this.forceUpdate();
+ }, (err) => {
+ this.setState({
+ scalar_error: err
+ });
+ })
+ }
dis.dispatch({
action: 'ui_opacity',
@@ -249,7 +253,7 @@ module.exports = React.createClass({
var roomId = this.props.room.roomId;
return MatrixClientPeg.get().sendStateEvent(
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))
},
- 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) {
ev.preventDefault();
var IntegrationsManager = sdk.getComponent("views.settings.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")) {
let integrations_body;
- if (this.state.scalar_token) {
+ if (this.scalarClient.hasCredentials()) {
integrations_body = (
Manage integrations
diff --git a/src/components/views/voip/CallView.js b/src/components/views/voip/CallView.js
index f566ec21bb..91d3904615 100644
--- a/src/components/views/voip/CallView.js
+++ b/src/components/views/voip/CallView.js
@@ -95,8 +95,10 @@ module.exports = React.createClass({
if (call) {
call.setLocalVideoElement(this.getVideoView().getLocalVideoElement());
call.setRemoteVideoElement(this.getVideoView().getRemoteVideoElement());
- // give a separate element for audio stream playback - both for voice calls
- // and for the voice stream of screen captures
+ // always use a separate element for audio stream playback.
+ // 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 element.
+ // rather than being rendered by the main remoteVideo element.
call.setRemoteAudioElement(this.getVideoView().getRemoteAudioElement());
}
if (call && call.type === "video" && call.call_state !== "ended" && call.call_state !== "ringing") {
diff --git a/src/components/views/voip/VideoView.js b/src/components/views/voip/VideoView.js
index 46c5b354f8..d018a2603c 100644
--- a/src/components/views/voip/VideoView.js
+++ b/src/components/views/voip/VideoView.js
@@ -50,7 +50,14 @@ module.exports = React.createClass({
},
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 to the DOM.");
+ }
+ return remoteAudioElement;
},
getLocalVideoElement: function() {
@@ -106,7 +113,6 @@ module.exports = React.createClass({