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 = ( -
- Ongoing conference call {supportedText} +
+ Ongoing conference call{ supportedText }. { joinText }
); } diff --git a/src/components/views/rooms/MemberDeviceInfo.js b/src/components/views/rooms/MemberDeviceInfo.js index 7e684c89a2..efc2cdf638 100644 --- a/src/components/views/rooms/MemberDeviceInfo.js +++ b/src/components/views/rooms/MemberDeviceInfo.js @@ -14,43 +14,45 @@ See the License for the specific language governing permissions and limitations under the License. */ -var React = require('react'); -var MatrixClientPeg = require("../../../MatrixClientPeg"); +import React from 'react'; +import MatrixClientPeg from '../../../MatrixClientPeg'; -module.exports = React.createClass({ - displayName: 'MemberDeviceInfo', - propTypes: { - userId: React.PropTypes.string.isRequired, - device: React.PropTypes.object.isRequired, - }, +export default class MemberDeviceInfo extends React.Component { + constructor(props) { + super(props); + this.onVerifyClick = this.onVerifyClick.bind(this); + this.onUnverifyClick = this.onUnverifyClick.bind(this); + this.onBlockClick = this.onBlockClick.bind(this); + this.onUnblockClick = this.onUnblockClick.bind(this); + } - onVerifyClick: function() { + onVerifyClick() { 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( - this.props.userId, this.props.device.id, false + this.props.userId, this.props.device.deviceId, false ); - }, + } - onBlockClick: function() { + onBlockClick() { 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( - 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; - if (this.props.device.blocked) { + if (this.props.device.isBlocked()) { blockButton = (
@@ -66,7 +68,7 @@ module.exports = React.createClass({ ); } - if (this.props.device.verified) { + if (this.props.device.isVerified()) { verifyButton = (
@@ -82,22 +84,22 @@ module.exports = React.createClass({ ); } - if (this.props.device.blocked) { + if (this.props.device.isBlocked()) { indicator = ( -
+
Blocked
); - } else if (this.props.device.verified) { + } else if (this.props.device.isVerified()) { indicator = ( -
+
Verified
); } else { indicator = ( -
?
+
Unverified
); } - var deviceName = this.props.device.display_name || this.props.device.id; + var deviceName = this.props.device.display_name || this.props.device.deviceId; return (
@@ -107,5 +109,11 @@ module.exports = React.createClass({ {blockButton}
); - }, -}); + } +}; + +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