From 715db8920496e58912a528862f7e7a5789b2961b Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Mon, 20 Jul 2015 13:19:47 +0100 Subject: [PATCH 1/7] Move all the calls to createClient inside the MatrixClientPeg --- src/MatrixClientPeg.js | 40 ++++++++++++++++++++------- src/controllers/templates/Login.js | 28 ++++--------------- src/controllers/templates/Register.js | 17 ++---------- 3 files changed, 39 insertions(+), 46 deletions(-) diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 0b6c26496f..87e6af08ce 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -22,18 +22,25 @@ var Matrix = require("matrix-js-sdk"); var matrixClient = null; var localStorage = window.localStorage; + +function createClient(hs_url, is_url, user_id, access_token) { + var opts = { + baseUrl: hs_url, + idBaseUrl: is_url, + accessToken: access_token, + userId: user_id + }; + + matrixClient = Matrix.createClient(opts); +} + if (localStorage) { var hs_url = localStorage.getItem("mx_hs_url"); var is_url = localStorage.getItem("mx_is_url") || 'https://matrix.org'; var access_token = localStorage.getItem("mx_access_token"); var user_id = localStorage.getItem("mx_user_id"); if (access_token && user_id && hs_url) { - matrixClient = Matrix.createClient({ - baseUrl: hs_url, - idBaseUrl: is_url, - accessToken: access_token, - userId: user_id - }); + matrixClient = createClient(hs_url, is_url, user_id, access_token); } } @@ -42,15 +49,28 @@ module.exports = { return matrixClient; }, - replace: function(cli) { - matrixClient = cli; - }, - replaceUsingUrls: function(hs_url, is_url) { matrixClient = Matrix.createClient({ baseUrl: hs_url, idBaseUrl: is_url }); } + + replaceUsingAccessToken: function(hs_url, is_url, user_id, access_token) { + matrixClient = createClient(hs_url, is_url, user_id, access_token); + if (localStorage) { + try { + localStorage.clear(); + localStorage.setItem("mx_hs_url", hs_url); + localStorage.setItem("mx_is_url", is_url); + localStorage.setItem("mx_user_id", user_id); + localStorage.setItem("mx_access_token", access_token); + } catch (e) { + console.warn("Error using local storage: can't persist session!"); + } + } else { + console.warn("No local storage available: can't persist session!"); + } + } }; diff --git a/src/controllers/templates/Login.js b/src/controllers/templates/Login.js index 276692719d..135b64337d 100644 --- a/src/controllers/templates/Login.js +++ b/src/controllers/templates/Login.js @@ -75,29 +75,13 @@ module.exports = { 'user': formVals.username, 'password': formVals.password }).done(function(data) { - // XXX: we assume this means we're logged in, but there could be a next stage - MatrixClientPeg.replace(Matrix.createClient({ - baseUrl: self.state.hs_url, - idBaseUrl: self.state.is_url, - userId: data.user_id, - accessToken: data.access_token + MatrixClientPeg.replaceUsingAccessToken( + this.state.hs_url, this.state.is_url, + data.user_id, data.access_token + ); })); - var localStorage = window.localStorage; - if (localStorage) { - try { - localStorage.clear(); - localStorage.setItem("mx_hs_url", self.state.hs_url); - localStorage.setItem("mx_is_url", self.state.is_url); - localStorage.setItem("mx_user_id", data.user_id); - localStorage.setItem("mx_access_token", data.access_token); - } catch (e) { - console.warn("Error using local storage: can't persist session!"); - } - } else { - console.warn("No local storage available: can't persist session!"); - } - if (self.props.onLoggedIn) { - self.props.onLoggedIn(); + if (that.props.onLoggedIn) { + that.props.onLoggedIn(); } }, function(error) { self.setStep("stage_m.login.password"); diff --git a/src/controllers/templates/Register.js b/src/controllers/templates/Register.js index 89a3872dce..faff4c66fc 100644 --- a/src/controllers/templates/Register.js +++ b/src/controllers/templates/Register.js @@ -259,20 +259,9 @@ module.exports = { }, onRegistered: function(user_id, access_token) { - MatrixClientPeg.replace(Matrix.createClient({ - baseUrl: this.state.hs_url, - idBaseUrl: this.state.is_url, - userId: user_id, - accessToken: access_token - })); - var localStorage = window.localStorage; - if (localStorage) { - localStorage.setItem("mx_hs_url", this.state.hs_url); - localStorage.setItem("mx_user_id", user_id); - localStorage.setItem("mx_access_token", access_token); - } else { - console.warn("No local storage available: can't persist session!"); - } + MatrixClientPeg.replaceUsingAccessToken( + this.state.hs_url, this.state.is_url, user_id, access_token + ); if (this.props.onLoggedIn) { this.props.onLoggedIn(); } From d8494ff89b2abd791fc360909239a7db8cce54b7 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Mon, 20 Jul 2015 13:48:40 +0100 Subject: [PATCH 2/7] Fix syntax --- src/MatrixClientPeg.js | 6 +++--- src/controllers/templates/Login.js | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 87e6af08ce..6b36e67e6b 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -40,7 +40,7 @@ if (localStorage) { var access_token = localStorage.getItem("mx_access_token"); var user_id = localStorage.getItem("mx_user_id"); if (access_token && user_id && hs_url) { - matrixClient = createClient(hs_url, is_url, user_id, access_token); + createClient(hs_url, is_url, user_id, access_token); } } @@ -54,10 +54,10 @@ module.exports = { baseUrl: hs_url, idBaseUrl: is_url }); - } + }, replaceUsingAccessToken: function(hs_url, is_url, user_id, access_token) { - matrixClient = createClient(hs_url, is_url, user_id, access_token); + createClient(hs_url, is_url, user_id, access_token); if (localStorage) { try { localStorage.clear(); diff --git a/src/controllers/templates/Login.js b/src/controllers/templates/Login.js index 135b64337d..1cfec18d0f 100644 --- a/src/controllers/templates/Login.js +++ b/src/controllers/templates/Login.js @@ -76,10 +76,9 @@ module.exports = { 'password': formVals.password }).done(function(data) { MatrixClientPeg.replaceUsingAccessToken( - this.state.hs_url, this.state.is_url, + that.state.hs_url, that.state.is_url, data.user_id, data.access_token ); - })); if (that.props.onLoggedIn) { that.props.onLoggedIn(); } From 28022534f70ac9e9a4d0e7496447613d0fe0fe86 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Mon, 20 Jul 2015 14:19:13 +0100 Subject: [PATCH 3/7] Fix the client-peg clean up to match the changes vector has made to the sdk --- src/MatrixClientPeg.js | 4 ++++ src/controllers/pages/MatrixChat.js | 2 +- src/controllers/templates/Login.js | 6 +++--- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 6b36e67e6b..1acd2a7015 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -49,6 +49,10 @@ module.exports = { return matrixClient; }, + unset: function() { + matrixClient = null; + }, + replaceUsingUrls: function(hs_url, is_url) { matrixClient = Matrix.createClient({ baseUrl: hs_url, diff --git a/src/controllers/pages/MatrixChat.js b/src/controllers/pages/MatrixChat.js index 48c55fe090..42aa3a5223 100644 --- a/src/controllers/pages/MatrixChat.js +++ b/src/controllers/pages/MatrixChat.js @@ -90,7 +90,7 @@ module.exports = { } Notifier.stop(); MatrixClientPeg.get().removeAllListeners(); - MatrixClientPeg.replace(null); + MatrixClientPeg.unset(); break; case 'start_registration': if (this.state.logged_in) return; diff --git a/src/controllers/templates/Login.js b/src/controllers/templates/Login.js index 1cfec18d0f..37a957850a 100644 --- a/src/controllers/templates/Login.js +++ b/src/controllers/templates/Login.js @@ -76,11 +76,11 @@ module.exports = { 'password': formVals.password }).done(function(data) { MatrixClientPeg.replaceUsingAccessToken( - that.state.hs_url, that.state.is_url, + self.state.hs_url, self.state.is_url, data.user_id, data.access_token ); - if (that.props.onLoggedIn) { - that.props.onLoggedIn(); + if (self.props.onLoggedIn) { + self.props.onLoggedIn(); } }, function(error) { self.setStep("stage_m.login.password"); From bcab2f231aed672de37677e57f24e799d454c44d Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Mon, 20 Jul 2015 16:40:00 +0100 Subject: [PATCH 4/7] Add error dialogs for invites and call timeouts. --- src/CallHandler.js | 4 ++++ src/controllers/organisms/MemberList.js | 11 +++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/CallHandler.js b/src/CallHandler.js index 671d8278ff..0915a65af2 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -116,6 +116,10 @@ function _setCallListeners(call) { _setCallState(call, call.roomId, "busy"); pause("ringbackAudio"); play("busyAudio"); + Modal.createDialog(ErrorDialog, { + title: "Call Timeout", + description: "The remote side failed to pick up." + }); } else if (oldState === "invite_sent") { _setCallState(call, call.roomId, "stop_ringback"); diff --git a/src/controllers/organisms/MemberList.js b/src/controllers/organisms/MemberList.js index 6021d0fc9a..89dd985564 100644 --- a/src/controllers/organisms/MemberList.js +++ b/src/controllers/organisms/MemberList.js @@ -18,6 +18,9 @@ limitations under the License. var React = require("react"); var MatrixClientPeg = require("../../MatrixClientPeg"); +var Modal = require("../../Modal"); +var ComponentBroker = require('../../ComponentBroker'); +var ErrorDialog = ComponentBroker.get("organisms/ErrorDialog"); var INITIAL_LOAD_NUM_MEMBERS = 50; @@ -67,6 +70,10 @@ module.exports = { inputText = inputText.trim(); // react requires es5-shim so we know trim() exists if (inputText[0] !== '@' || inputText.indexOf(":") === -1) { console.error("Bad user ID to invite: %s", inputText); + Modal.createDialog(ErrorDialog, { + title: "Invite Error", + description: "Malformed user ID. Should look like '@localpart:domain'" + }); return; } self.setState({ @@ -81,6 +88,10 @@ module.exports = { }); }, function(err) { console.error("Failed to invite: %s", JSON.stringify(err)); + Modal.createDialog(ErrorDialog, { + title: "Invite Server Error", + description: err.message + }); self.setState({ inviting: false }); From 9d110d58e5b8b1a759eaf4adf1085d6ce8410706 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Mon, 20 Jul 2015 17:14:29 +0100 Subject: [PATCH 5/7] Manage presence of the client (same semantics as angular) --- src/Presence.js | 105 ++++++++++++++++++++++++++++ src/controllers/pages/MatrixChat.js | 4 +- 2 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 src/Presence.js diff --git a/src/Presence.js b/src/Presence.js new file mode 100644 index 0000000000..ce1d5d10fc --- /dev/null +++ b/src/Presence.js @@ -0,0 +1,105 @@ +/* +Copyright 2015 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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. +*/ +"use strict"; +var MatrixClientPeg = require("./MatrixClientPeg"); + + // Time in ms after that a user is considered as unavailable/away +var UNAVAILABLE_TIME_MS = 3 * 60 * 1000; // 3 mins +var PRESENCE_STATES = ["online", "offline", "unavailable"]; + +// The current presence state +var state, timer; + +module.exports = { + + /** + * Start listening the user activity to evaluate his presence state. + * Any state change will be sent to the Home Server. + */ + start: function() { + var self = this; + this.running = true; + if (undefined === state) { + // The user is online if they move the mouse or press a key + document.onmousemove = function() { self._resetTimer(); }; + document.onkeypress = function() { self._resetTimer(); }; + this._resetTimer(); + } + }, + + /** + * Stop tracking user activity + */ + stop: function() { + this.running = false; + if (timer) { + clearTimeout(timer); + timer = undefined; + } + state = undefined; + }, + + /** + * Get the current presence state. + * @returns {string} the presence state (see PRESENCE enum) + */ + getState: function() { + return state; + }, + + /** + * Set the presence state. + * If the state has changed, the Home Server will be notified. + * @param {string} newState the new presence state (see PRESENCE enum) + */ + setState: function(newState) { + if (newState === state) { + return; + } + if (PRESENCE_STATES.indexOf(newState) === -1) { + throw new Error("Bad presence state: " + newState); + } + if (!this.running) { + return; + } + state = newState; + MatrixClientPeg.get().setPresence(state).catch(function(err) { + console.error("Failed to set presence: %s", err); + }); + }, + + /** + * Callback called when the user made no action on the page for UNAVAILABLE_TIME ms. + * @private + */ + _onUnavailableTimerFire: function() { + this.setState("unavailable"); + }, + + /** + * Callback called when the user made an action on the page + * @private + */ + _resetTimer: function() { + var self = this; + this.setState("online"); + // Re-arm the timer + clearTimeout(timer); + timer = setTimeout(function() { + self._onUnavailableTimerFire(); + }, UNAVAILABLE_TIME_MS); + } +}; \ No newline at end of file diff --git a/src/controllers/pages/MatrixChat.js b/src/controllers/pages/MatrixChat.js index 48c55fe090..91fbeaebf8 100644 --- a/src/controllers/pages/MatrixChat.js +++ b/src/controllers/pages/MatrixChat.js @@ -21,7 +21,7 @@ var Loader = require("react-loader"); var MatrixClientPeg = require("../../MatrixClientPeg"); var RoomListSorter = require("../../RoomListSorter"); - +var Presence = require("../../Presence"); var dis = require("../../dispatcher"); var ComponentBroker = require('../../ComponentBroker'); @@ -89,6 +89,7 @@ module.exports = { window.localStorage.clear(); } Notifier.stop(); + Presence.stop(); MatrixClientPeg.get().removeAllListeners(); MatrixClientPeg.replace(null); break; @@ -187,6 +188,7 @@ module.exports = { }); }); Notifier.start(); + Presence.start(); cli.startClient(); }, From 5d59a5b297a91a078f80a2a8de8abab93e77348d Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Mon, 20 Jul 2015 17:42:19 +0100 Subject: [PATCH 6/7] Change opacity of member list entries when their presence changes. This is done by attaching a single room listener at the member list level and then forceUpdate()ing the individual tiles as presence changes come in from the JS SDK. This is more efficient than having hundreds of listeners attached directly to the JS SDK (if we were to add a listener per tile). --- skins/base/css/molecules/MemberTile.css | 8 ++++++++ skins/base/views/molecules/MemberTile.js | 14 +++++++++++++- skins/base/views/organisms/MemberList.js | 2 +- src/controllers/organisms/MemberList.js | 14 +++++++++++++- 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/skins/base/css/molecules/MemberTile.css b/skins/base/css/molecules/MemberTile.css index ddd3962b33..2f735f624b 100644 --- a/skins/base/css/molecules/MemberTile.css +++ b/skins/base/css/molecules/MemberTile.css @@ -51,3 +51,11 @@ limitations under the License. overflow: hidden; text-overflow: ellipsis; } + +.mx_MemberTile_unavailable { + opacity: 0.75; +} + +.mx_MemberTile_offline { + opacity: 0.5; +} \ No newline at end of file diff --git a/skins/base/views/molecules/MemberTile.js b/skins/base/views/molecules/MemberTile.js index 4e00f2af23..1286173d38 100644 --- a/skins/base/views/molecules/MemberTile.js +++ b/skins/base/views/molecules/MemberTile.js @@ -46,9 +46,21 @@ module.exports = React.createClass({ var img = "img/p/p" + Math.floor(20 * this.props.member.powerLevelNorm / 100) + ".png"; power = ; } + var presenceClass = "mx_MemberTile_offline"; + var mainClassName = "mx_MemberTile "; + if (this.props.member.user) { + if (this.props.member.user.presence === "online") { + presenceClass = "mx_MemberTile_online"; + } + else if (this.props.member.user.presence === "unavailable") { + presenceClass = "mx_MemberTile_unavailable"; + } + } + mainClassName += presenceClass; return ( -
+
+ ); }); }, diff --git a/src/controllers/organisms/MemberList.js b/src/controllers/organisms/MemberList.js index 89dd985564..0695270066 100644 --- a/src/controllers/organisms/MemberList.js +++ b/src/controllers/organisms/MemberList.js @@ -40,6 +40,7 @@ module.exports = { componentWillUnmount: function() { if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember); + MatrixClientPeg.get().removeListener("User.presence", this.userPresenceFn); } }, @@ -51,8 +52,19 @@ module.exports = { memberDict: self.roomMembers() }); }, 50); - }, + // Attach a SINGLE listener for global presence changes then locate the + // member tile and re-render it. This is more efficient than every tile + // evar attaching their own listener. + function updateUserState(event, user) { + var tile = self.refs[user.userId]; + if (tile) { + tile.forceUpdate(); + } + } + MatrixClientPeg.get().on("User.presence", updateUserState); + this.userPresenceFn = updateUserState; + }, // Remember to set 'key' on a MemberList to the ID of the room it's for /*componentWillReceiveProps: function(newProps) { },*/ From e9b2cd136440e04f391cd9f8fbb288d9a18791a4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 20 Jul 2015 10:05:53 -0700 Subject: [PATCH 7/7] Allow input history to go forwards again to the empty field --- src/controllers/molecules/MessageComposer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/molecules/MessageComposer.js b/src/controllers/molecules/MessageComposer.js index 0fa8066595..a9de008d6f 100644 --- a/src/controllers/molecules/MessageComposer.js +++ b/src/controllers/molecules/MessageComposer.js @@ -106,7 +106,7 @@ module.exports = { // show the message this.element.value = this.data[this.position]; } - else if (this.originalText) { + else if (this.originalText !== undefined) { // restore the original text the user was typing. this.element.value = this.originalText; }