diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py index 049891eaf8..f0d7b34682 100644 --- a/synapse/handlers/presence.py +++ b/synapse/handlers/presence.py @@ -166,17 +166,22 @@ class PresenceHandler(BaseHandler): observed_user=target_user ) - if visible: + if visible or True: # XXX: FIXME: Bodge to unbreak matrix.org. breaks UTs. state = yield self.store.get_presence_state( target_user.localpart ) else: + # FIXME: *Surely* we shouldn't be 404ing the whole request, whatever + # it is, just because presence info isn't visible?! + # This causes client/api/v1/rooms/!cURbafjkfsMDVwdRDQ%3Amatrix.org/members/list + # to 404 currently + raise SynapseError(404, "Presence information not visible") else: # TODO(paul): Have remote server send us permissions set state = self._get_or_offline_usercache(target_user).get_state() - if "mtime" in state: + if "mtime" in state and (state["mtime"] is not None): state["mtime_age"] = int( self.clock.time_msec() - state.pop("mtime") ) diff --git a/synapse/storage/presence.py b/synapse/storage/presence.py index 23b6d1694e..a529104f4d 100644 --- a/synapse/storage/presence.py +++ b/synapse/storage/presence.py @@ -67,6 +67,7 @@ class PresenceStore(SQLBaseStore): table="presence_allow_inbound", keyvalues={"observed_user_id": observed_localpart, "observer_user_id": observer_userid}, + retcols=["observed_user_id"], allow_none=True, ) diff --git a/webclient/app-controller.js b/webclient/app-controller.js index 7fa87e30c1..96656e12c3 100644 --- a/webclient/app-controller.js +++ b/webclient/app-controller.js @@ -45,8 +45,14 @@ angular.module('MatrixWebClientController', ['matrixService']) $scope.config = matrixService.config(); } }; + + $scope.closeConfig = function() { + if ($scope.config) { + $scope.config = undefined; + } + }; - if (matrixService.config()) { + if (matrixService.isUserLoggedIn()) { eventStreamService.resume(); } @@ -69,6 +75,15 @@ angular.module('MatrixWebClientController', ['matrixService']) console.log("Invalid access token -> log user out"); $scope.logout(); }); + + $scope.requestNotifications = function() { + if (window.Notification) { + console.log("Notification.permission: " + window.Notification.permission); + window.Notification.requestPermission(function(){}); + } + }; + + }]); diff --git a/webclient/app.css b/webclient/app.css index 122f25c9ff..b9e7771ca8 100644 --- a/webclient/app.css +++ b/webclient/app.css @@ -10,7 +10,7 @@ h1 { /*** Overall page layout ***/ -.page { +#page { position: absolute; top: 80px; bottom: 100px; @@ -20,13 +20,13 @@ h1 { margin: 20px; } -.wrapper { +#wrapper { margin: auto; max-width: 1280px; height: 100%; } -.roomName { +#roomName { max-width: 1280px; width: 100%; text-align: right; @@ -36,7 +36,7 @@ h1 { margin-bottom: 10px; } -.controlPanel { +#controlPanel { position: absolute; bottom: 0px; width: 100%; @@ -44,39 +44,43 @@ h1 { border-top: #aaa 1px solid; } -.controls { +#controls { max-width: 1280px; padding: 12px; margin: auto; } -.inputBarTable { +#inputBarTable { width: 100%; } -.inputBarTable tr td { +#inputBarTable tr td { padding: 1px 4px; } -.mainInput { +#mainInput { width: 100%; } +.blink { + background-color: #faa; +} + /*** Participant list ***/ -.usersTableWrapper { +#usersTableWrapper { float: right; width: 120px; height: 100%; overflow-y: auto; } -.usersTable { +#usersTable { width: 100%; border-collapse: collapse; } -.usersTable td { +#usersTable td { padding: 0px; } @@ -90,6 +94,7 @@ h1 { .userAvatar .userAvatarImage { position: absolute; top: 0px; + object-fit: cover; } .userAvatar .userAvatarGradient { @@ -103,7 +108,7 @@ h1 { margin: 2px; bottom: 0px; font-size: 8pt; - word-wrap: break-word; + word-break: break-all; } .userPresence { @@ -124,26 +129,26 @@ h1 { /*** Message table ***/ -.messageTableWrapper { +#messageTableWrapper { height: 100%; margin-right: 140px; overflow-y: auto; width: auto; } -.messageTable { +#messageTable { margin: auto; max-width: 1280px; width: 100%; border-collapse: collapse; } -.messageTable td { +#messageTable td { padding: 0px; } .leftBlock { - width: 1px; + width: 10em; vertical-align: top; background-color: #fff; color: #888; @@ -176,14 +181,19 @@ h1 { vertical-align: top; line-height: 0; } + +.avatarImage { + object-fit: cover; +} .text { background-color: #eee; border: 1px solid #d8d8d8; - height: 32px; + height: 31px; display: inline-table; max-width: 90%; - word-wrap: break-word; + font-size: 16px; + /* word-wrap: break-word; */ word-break: break-all; } @@ -192,6 +202,11 @@ h1 { border: 0px ! important; } +.membership { + background-color: #fff ! important; + border: 0px ! important; +} + .image { display: block; max-width:320px; @@ -201,22 +216,32 @@ h1 { } .bubble { - padding: 6px; + padding-top: 7px; + padding-bottom: 5px; padding-left: 1em; padding-right: 1em; + vertical-align: middle; +} + +.differentUser td { + padding-top: 5px ! important; + margin-top: 5px ! important; } .mine { text-align: right; } -.mine .text .bubble { - text-align: left ! important; - background-color: #d8d8e8 ! important; +.mine .text { + background-color: #f8f8ff ! important; } -.mine .emote .bubble { - background-color: #fff ! important; +.mine .emote { + background-color: #fff ! important; +} + +.mine .text .bubble { + text-align: left ! important; } /*** Profile ***/ @@ -235,24 +260,24 @@ h1 { /******************************/ -.header { +#header { padding-left: 20px; padding-right: 20px; max-width: 1280px; margin: auto; } -.header-buttons { +#header-buttons { float: right; } -.config { +#config { position: absolute; z-index: 100; top: 100px; left: 50%; - width: 400px; - margin-left: -200px; + width: 500px; + margin-left: -250px; text-align: center; padding: 20px; background-color: #aaa; diff --git a/webclient/app.js b/webclient/app.js index 58d3942c65..8d64db92d3 100644 --- a/webclient/app.js +++ b/webclient/app.js @@ -63,9 +63,8 @@ matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider', }]); matrixWebClient.run(['$location', 'matrixService', 'eventStreamService', function($location, matrixService, eventStreamService) { - // If we have no persistent login information, go to the login page - var config = matrixService.config(); - if (!config || !config.access_token) { + // If user auth details are not in cache, go to the login page + if (!matrixService.isUserLoggedIn()) { eventStreamService.stop(); $location.path("login"); } @@ -94,7 +93,62 @@ matrixWebClient } }; }]) - .filter('to_trusted', ['$sce', function($sce){ + .filter('duration', function() { + return function(time) { + if (!time) return; + var t = parseInt(time / 1000); + var s = t % 60; + var m = parseInt(t / 60) % 60; + var h = parseInt(t / (60 * 60)) % 24; + var d = parseInt(t / (60 * 60 * 24)); + if (t < 60) { + return s + "s" + } + if (t < 60 * 60) { + return m + "m "; // + s + "s"; + } + if (t < 24 * 60 * 60) { + return h + "h "; // + m + "m"; + } + return d + "d "; // + h + "h"; + } + }) + .filter('orderMembersList', function($sce) { + return function(members) { + var filtered = []; + + var displayNames = {}; + angular.forEach(members, function(value, key) { + value["id"] = key; + filtered.push( value ); + if (value["displayname"]) { + if (!displayNames[value["displayname"]]) { + displayNames[value["displayname"]] = []; + } + displayNames[value["displayname"]].push(key); + } + }); + + // FIXME: we shouldn't disambiguate displayNames on every orderMembersList + // invocation but keep track of duplicates incrementally somewhere + angular.forEach(displayNames, function(value, key) { + if (value.length > 1) { + // console.log(key + ": " + value); + for (i=0; i < value.length; i++) { + var v = value[i]; + members[v].displayname += " (" + v + ")"; + // console.log(v + " " + members[v]); + }; + } + }); + + filtered.sort(function (a, b) { + return ((a["mtime_age"] || 10e10) > (b["mtime_age"] || 10e10) ? 1 : -1); + }); + return filtered; + }; + }) + .filter('unsafe', ['$sce', function($sce) { return function(text) { return $sce.trustAsHtml(text); }; diff --git a/webclient/components/matrix/matrix-service.js b/webclient/components/matrix/matrix-service.js index 0cc85db28e..c52c94c310 100644 --- a/webclient/components/matrix/matrix-service.js +++ b/webclient/components/matrix/matrix-service.js @@ -318,12 +318,21 @@ angular.module('matrixService', []) }; return doRequest("GET", path, params); }, - - // - testLogin: function() { - + + // Indicates if user authentications details are stored in cache + isUserLoggedIn: function() { + var config = this.config(); + + // User is considered logged in if his cache is not empty and contains + // an access token + if (config && config.access_token) { + return true; + } + else { + return false; + } }, - + /****** Permanent storage of user information ******/ // Returns the current config diff --git a/webclient/index.html b/webclient/index.html index 387505372a..455eff4a13 100644 --- a/webclient/index.html +++ b/webclient/index.html @@ -7,8 +7,9 @@ - - + + + @@ -24,9 +25,9 @@
-Directive | +How | +Source | +Rendered | +
ng-bind-html | +Automatically uses $sanitize | +<div ng-bind-html="snippet"> |
+ + |
ng-bind-html | +Bypass $sanitize by explicitly trusting the dangerous value | +
+ <div ng-bind-html="deliberatelyTrustDangerousSnippet()"> +</div>+ |
+ + |
ng-bind | +Automatically escapes | +<div ng-bind="snippet"> |
+ + |
an html\nclick here\nsnippet
'); + }); + + it('should inline raw snippet if bound to a trusted value', function() { + expect(using('#bind-html-with-trust').element("div").html()). + toBe("an html\n" + + "click here\n" + + "snippet
"); + }); + + it('should escape snippet without any filter', function() { + expect(using('#bind-default').element('div').html()). + toBe("<p style=\"color:blue\">an html\n" + + "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + + "snippet</p>"); + }); + + it('should update', function() { + input('snippet').enter('new text'); + expect(using('#bind-html-with-sanitize').element('div').html()).toBe('new text'); + expect(using('#bind-html-with-trust').element('div').html()).toBe( + 'new text'); + expect(using('#bind-default').element('div').html()).toBe( + "new <b onclick=\"alert(1)\">text</b>"); + }); +Filter | +Source | +Rendered | +
linky filter | +
+ <div ng-bind-html="snippet | linky">+ |
+ + + | +
linky target | +
+ <div ng-bind-html="snippetWithTarget | linky:'_blank'">+ |
+ + + | +
no filter | +<div ng-bind="snippet"> |
+ + |
- ![]() {{ member.displayname || member.id.substr(0, member.id.indexOf(':')) }}
+ {{ member.displayname ? "" : member.id.substr(member.id.indexOf(':')) }} |
+
+ {{ member.mtime_age | duration }} {{ member.mtime_age ? "ago" : "" }} |
-
{{ members[msg.user_id].displayname || msg.user_id }}
{{ msg.content.hsob_ts | date:'MMM d HH:mm:ss' }}
|
- |
- + |
- {{ msg.content.msgtype === "m.emote" ? ("* " + (members[msg.user_id].displayname || msg.user_id) + " " + msg.content.body) : "" }}
- {{ msg.content.msgtype === "m.text" ? msg.content.body : "" }}
+
+
|
- |
{{ state.user_id }} | - + | - {{ feedback }} + | ||
- + | -- + | + | -+ |