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/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/MatrixClientPeg.js b/src/MatrixClientPeg.js index 0b6c26496f..1acd2a7015 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 - }); + createClient(hs_url, is_url, user_id, access_token); } } @@ -42,8 +49,8 @@ module.exports = { return matrixClient; }, - replace: function(cli) { - matrixClient = cli; + unset: function() { + matrixClient = null; }, replaceUsingUrls: function(hs_url, is_url) { @@ -51,6 +58,23 @@ module.exports = { baseUrl: hs_url, idBaseUrl: is_url }); + }, + + replaceUsingAccessToken: function(hs_url, is_url, user_id, access_token) { + 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/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/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; } diff --git a/src/controllers/organisms/MemberList.js b/src/controllers/organisms/MemberList.js index 6021d0fc9a..0695270066 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; @@ -37,6 +40,7 @@ module.exports = { componentWillUnmount: function() { if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember); + MatrixClientPeg.get().removeListener("User.presence", this.userPresenceFn); } }, @@ -48,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) { },*/ @@ -67,6 +82,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 +100,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 }); diff --git a/src/controllers/pages/MatrixChat.js b/src/controllers/pages/MatrixChat.js index 48c55fe090..1e38a2150c 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,8 +89,9 @@ module.exports = { window.localStorage.clear(); } Notifier.stop(); + Presence.stop(); MatrixClientPeg.get().removeAllListeners(); - MatrixClientPeg.replace(null); + MatrixClientPeg.unset(); break; case 'start_registration': if (this.state.logged_in) return; @@ -187,6 +188,7 @@ module.exports = { }); }); Notifier.start(); + Presence.start(); cli.startClient(); }, diff --git a/src/controllers/templates/Login.js b/src/controllers/templates/Login.js index 276692719d..37a957850a 100644 --- a/src/controllers/templates/Login.js +++ b/src/controllers/templates/Login.js @@ -75,27 +75,10 @@ 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 - })); - 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!"); - } + MatrixClientPeg.replaceUsingAccessToken( + self.state.hs_url, self.state.is_url, + data.user_id, data.access_token + ); if (self.props.onLoggedIn) { self.props.onLoggedIn(); } 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(); }