summary refs log tree commit diff
diff options
context:
space:
mode:
authorMatthew Hodgson <matthew@matrix.org>2014-11-11 04:39:16 +0000
committerMatthew Hodgson <matthew@matrix.org>2014-11-11 04:39:30 +0000
commitb765dc005ba22cb41e01439f1b07d567d2de68da (patch)
tree6615a1d5e02337ec31204dcd023976561e5e9bdf
parenttrivial spacing fix (diff)
downloadsynapse-b765dc005ba22cb41e01439f1b07d567d2de68da.tar.xz
major CSS overhaul to try to make things look a bit cleaner
-rwxr-xr-xsyweb/webclient/app.css150
-rw-r--r--syweb/webclient/img/attach.pngbin0 -> 473 bytes
-rw-r--r--syweb/webclient/img/settings.pngbin0 -> 864 bytes
-rw-r--r--syweb/webclient/img/video.pngbin0 -> 604 bytes
-rw-r--r--syweb/webclient/img/voice.pngbin0 -> 659 bytes
-rw-r--r--syweb/webclient/index.html2
-rw-r--r--syweb/webclient/js/angular-peity.js69
-rw-r--r--syweb/webclient/js/jquery.peity.min.js13
-rw-r--r--syweb/webclient/mobile.css18
-rw-r--r--syweb/webclient/room/room-controller.js19
-rw-r--r--syweb/webclient/room/room.html131
11 files changed, 300 insertions, 102 deletions
diff --git a/syweb/webclient/app.css b/syweb/webclient/app.css
index be2a73872d..e2f99f3975 100755
--- a/syweb/webclient/app.css
+++ b/syweb/webclient/app.css
@@ -318,7 +318,7 @@ textarea, input {
     position: absolute;
     bottom: 0px;
     width: 100%;
-    height: 100px;
+    height: 70px;
     background-color: #f8f8f8;
     border-top: #aaa 1px solid;
 }
@@ -326,7 +326,9 @@ textarea, input {
 #controls {
     max-width: 1280px;
     padding: 12px;
+    padding-right: 42px;
     margin: auto;
+    position: relative;
 }
 
 #buttonsCell {
@@ -343,7 +345,17 @@ textarea, input {
 
 #mainInput {
     width: 100%;
-    resize: none;
+    padding: 5px;
+}
+
+#attachButton {
+    position: absolute;
+    margin-top: 3px;
+    right: 0px;
+    background: url('img/attach.png');
+    width: 25px;
+    height: 25px;
+    border: 0px;
 }
 
 .blink {
@@ -415,7 +427,8 @@ textarea, input {
 .roomHeaderInfo {
     text-align: right;
     float: right;
-    margin-top: 15px;
+    margin-top: 0px;
+    margin-right: 30px;
 }
 
 /*** Room Info Dialog ***/
@@ -449,15 +462,32 @@ textarea, input {
     resize: vertical;
 }
 
+/*** Control Buttons ***/
+#controlButtons {
+    float: right;
+    margin-right: -4px;
+    padding-bottom: 6px;
+}
+
+.controlButton {
+    border: 0px;
+    width: 30px;
+    height: 30px;
+    padding-left: 3px;
+    padding-right: 3px;
+}
+
 /*** Participant list ***/
 
 #usersTableWrapper {
     float: right;
-    width: 120px;
+    clear: right;
+    width: 100px;
     height: 100%;
     overflow-y: auto;
 }
 
+/*
 #usersTable {
     width: 100%;
     border-collapse: collapse;
@@ -473,36 +503,66 @@ textarea, input {
     position: relative;
     background-color: #000;
 }
+*/
 
-.userAvatar .userAvatarImage {
-    position: absolute;
-    top: 0px;
+.userAvatar {
+}
+
+.userAvatarFrame {
+    border-radius: 46px;
+    width: 80px;
+    margin: auto;
+    position: relative;
+    border: 3px solid #aaa;
+    background-color: #aaa;
+}
+
+.userAvatarImage {
+    border-radius: 40px;
+    text-align: center;
     object-fit: cover;
-    width: 100%;
+    display: block;
 }
 
+/*
 .userAvatar .userAvatarGradient {
     position: absolute;
     bottom: 20px;
     width: 100%;
 }
+*/
 
-.userAvatar .userName {
-    position: absolute;
-    color: #fff;
-    margin: 2px;
-    bottom: 0px;
+.userName {
+    margin-top: 3px;
+    margin-bottom: 6px;
+    text-align: center;
     font-size: 12px;
     word-break: break-all;
 }
 
-.userAvatar .userPowerLevel {
+.userPowerLevel {
     position: absolute;
+    bottom: -1px;
+    height: 1px;
+    background-color: #f00;
+}
+
+.userPowerLevelBar {
+    display: inline;
+    position: absolute;
+    width: 2px;
+    height: 10px;
+/*    border: 1px solid #000;
+*/    background-color: #aaa;
+}
+
+.userPowerLevelMeter {
+    position: relative;
     bottom: 0px;
-    height: 2px;
     background-color: #f00;
 }
 
+/*
 .userPresence {
     text-align: center;
     font-size: 12px;
@@ -510,12 +570,15 @@ textarea, input {
     background-color: #aaa;
     border-bottom: 1px #ddd solid;
 }
+*/
 
 .online {
+    border-color: #38AF00;
     background-color: #38AF00;
 }
 
 .unavailable {
+    border-color: #FFCC00;
     background-color: #FFCC00;
 }
 
@@ -538,18 +601,21 @@ textarea, input {
         
 #messageTable td {
     padding: 0px;
+/*  border: 1px solid #888; */
 }
 
 .leftBlock {
-    width: 14em;
+    width: 6em;
     word-wrap: break-word;
     vertical-align: top;
     background-color: #fff;
-    color: #888;
+    color: #aaa;
     font-weight: medium;
     font-size: 12px;
     text-align: right;
+/*
     border-top: 1px #ddd solid;
+*/
 }
 
 .rightBlock {
@@ -560,13 +626,24 @@ textarea, input {
 }        
 
 .sender, .timestamp {
-    padding-right: 1em;
-    padding-left: 1em;
-    padding-top: 3px;
+/*    padding-top: 3px;
+*/}
+
+.timestamp {
+    font-size: 10px;
+    color: #ccc;
+    height: 13px;
+    margin-top: 4px;
+*/    transition-property: opacity;
+    transition-duration: 0.3s;
 }
 
 .sender {
-    margin-bottom: -3px;
+    font-size: 12px;
+/*    
+    margin-top: 5px;
+    margin-bottom: -9px;
+*/
 }
 
 .avatar {
@@ -577,7 +654,11 @@ textarea, input {
 }
 
 .avatarImage {
+    position: relative;
+    top: 5px;
     object-fit: cover;
+    border-radius: 32px;
+    margin-top: 4px;
 }
         
 .emote {
@@ -591,6 +672,7 @@ textarea, input {
 }
 
 .image {
+    border: 1px solid #888;
     display: block;
     max-width:320px;
     max-height:320px;
@@ -603,19 +685,23 @@ textarea, input {
 }
 
 .bubble {
+/*
     background-color: #eee;
     border: 1px solid #d8d8d8;
-    display: inline-block;
     margin-bottom: -1px;
-    max-width: 90%;
-    font-size: 14px;
-    word-wrap: break-word;
     padding-top: 7px;
     padding-bottom: 5px;
+    -webkit-text-size-adjust:100%
+    vertical-align: middle;
+*/
+    display: inline-block;
+    max-width: 90%;
     padding-left: 1em;
     padding-right: 1em;
-    vertical-align: middle;
-    -webkit-text-size-adjust:100%
+    padding-top: 2px;
+    padding-bottom: 2px;
+    font-size: 14px;
+    word-wrap: break-word;
 }
 
 .bubble img {
@@ -623,9 +709,11 @@ textarea, input {
     max-height: auto;
 }
 
+/*
 .differentUser td {
     padding-bottom: 5px ! important;
 }
+*/
 
 .mine {
     text-align: right;
@@ -635,13 +723,15 @@ textarea, input {
 .text.membership .bubble,
 .mine .text.emote .bubble,
 .mine .text.membership .bubble
-  {
+{
     background-color: transparent ! important;    
     border: 0px ! important;
 }
 
 .mine .text .bubble {
+/*
     background-color: #f8f8ff ! important;    
+*/
     text-align: left ! important;
 }
 
@@ -701,6 +791,8 @@ textarea, input {
     overflow: hidden;
     white-space: nowrap;
     text-overflow: ellipsis;
+    padding-left: 0.5em;
+    padding-right: 0.5em;
 }
 
 .recentsRoom {
@@ -751,7 +843,7 @@ textarea, input {
     padding-right: 10px;
     margin-right: 10px;
     height: 100%;
-    border-right: 1px solid #ddd;
+/*    border-right: 1px solid #ddd; */
     overflow-y: auto;
 }
 
diff --git a/syweb/webclient/img/attach.png b/syweb/webclient/img/attach.png
new file mode 100644
index 0000000000..d95eabaf00
--- /dev/null
+++ b/syweb/webclient/img/attach.png
Binary files differdiff --git a/syweb/webclient/img/settings.png b/syweb/webclient/img/settings.png
new file mode 100644
index 0000000000..ac99fe402b
--- /dev/null
+++ b/syweb/webclient/img/settings.png
Binary files differdiff --git a/syweb/webclient/img/video.png b/syweb/webclient/img/video.png
new file mode 100644
index 0000000000..e90afea0c1
--- /dev/null
+++ b/syweb/webclient/img/video.png
Binary files differdiff --git a/syweb/webclient/img/voice.png b/syweb/webclient/img/voice.png
new file mode 100644
index 0000000000..fe464999c0
--- /dev/null
+++ b/syweb/webclient/img/voice.png
Binary files differdiff --git a/syweb/webclient/index.html b/syweb/webclient/index.html
index 45be6c274b..f6487f381d 100644
--- a/syweb/webclient/index.html
+++ b/syweb/webclient/index.html
@@ -17,6 +17,8 @@
     <script src="js/angular-route.min.js"></script>
     <script src="js/angular-sanitize.min.js"></script>
     <script src="js/angular-animate.min.js"></script>
+    <script src="js/jquery.peity.min.js"></script>
+    <script src="js/angular-peity.js"></script>
     <script type='text/javascript' src="js/ui-bootstrap-tpls-0.11.2.js"></script>
     <script type='text/javascript' src='js/ng-infinite-scroll-matrix.js'></script>
     <script type='text/javascript' src='js/autofill-event.js'></script>
diff --git a/syweb/webclient/js/angular-peity.js b/syweb/webclient/js/angular-peity.js
new file mode 100644
index 0000000000..2acb647d91
--- /dev/null
+++ b/syweb/webclient/js/angular-peity.js
@@ -0,0 +1,69 @@
+var angularPeity = angular.module( 'angular-peity', [] );
+
+$.fn.peity.defaults.pie = {
+  fill: ["#ff0000", "#aaaaaa"],
+  radius: 4,
+}
+
+var buildChartDirective = function ( chartType ) {
+	return {
+		restrict: 'E',
+		scope: {
+			data: "=",
+			options: "="
+		},
+		link: function ( scope, element, attrs ) {
+
+            var options = {};
+            if ( scope.options ) {
+				options = scope.options;
+            }
+            
+            // N.B. live-binding to data by Matthew
+            scope.$watch('data', function () {
+    			var span = document.createElement( 'span' );
+    			span.textContent = scope.data.join();
+
+                if ( !attrs.class ) {
+                    span.className = "";
+                } else {
+                    span.className = attrs.class;
+                }
+
+                if (element[0].nodeType === 8) {
+                    element.replaceWith( span );
+                }
+                else if (element[0].firstChild) {
+                    element.empty();
+                    element[0].appendChild( span );
+                }
+                else {
+                    element[0].appendChild( span );
+                }
+
+                jQuery( span ).peity( chartType, options );
+            });
+		}
+	};
+};
+
+
+angularPeity.directive( 'pieChart', function () {
+
+	return buildChartDirective( "pie" );
+
+} );
+
+
+angularPeity.directive( 'barChart', function () {
+
+	return buildChartDirective( "bar" );
+
+} );
+
+
+angularPeity.directive( 'lineChart', function () {
+
+	return buildChartDirective( "line" );
+
+} );
diff --git a/syweb/webclient/js/jquery.peity.min.js b/syweb/webclient/js/jquery.peity.min.js
new file mode 100644
index 0000000000..054b83c5d8
--- /dev/null
+++ b/syweb/webclient/js/jquery.peity.min.js
@@ -0,0 +1,13 @@
+// Peity jQuery plugin version 3.0.2
+// (c) 2014 Ben Pickles
+//
+// http://benpickles.github.io/peity
+//
+// Released under MIT license.
+(function(h,w,i,v){var p=function(a,b){var d=w.createElementNS("http://www.w3.org/2000/svg",a);h(d).attr(b);return d},y="createElementNS"in w&&p("svg",{}).createSVGRect,e=h.fn.peity=function(a,b){y&&this.each(function(){var d=h(this),c=d.data("peity");c?(a&&(c.type=a),h.extend(c.opts,b)):(c=new x(d,a,h.extend({},e.defaults[a],b)),d.change(function(){c.draw()}).data("peity",c));c.draw()});return this},x=function(a,b,d){this.$el=a;this.type=b;this.opts=d},r=x.prototype;r.draw=function(){e.graphers[this.type].call(this,
+this.opts)};r.fill=function(){var a=this.opts.fill;return h.isFunction(a)?a:function(b,d){return a[d%a.length]}};r.prepare=function(a,b){this.svg||this.$el.hide().after(this.svg=p("svg",{"class":"peity"}));return h(this.svg).empty().data("peity",this).attr({height:b,width:a})};r.values=function(){return h.map(this.$el.text().split(this.opts.delimiter),function(a){return parseFloat(a)})};e.defaults={};e.graphers={};e.register=function(a,b,d){this.defaults[a]=b;this.graphers[a]=d};e.register("pie",
+{fill:["#ff9900","#fff4dd","#ffc66e"],radius:8},function(a){if(!a.delimiter){var b=this.$el.text().match(/[^0-9\.]/);a.delimiter=b?b[0]:","}b=this.values();if("/"==a.delimiter)var d=b[0],b=[d,i.max(0,b[1]-d)];for(var c=0,d=b.length,n=0;c<d;c++)n+=b[c];for(var c=2*a.radius,f=this.prepare(a.width||c,a.height||c),c=f.width(),f=f.height(),s=c/2,k=f/2,f=i.min(s,k),a=a.innerRadius,e=i.PI,q=this.fill(),g=this.scale=function(a,b){var c=2*a/n*e-e/2;return[b*i.cos(c)+s,b*i.sin(c)+k]},l=0,c=0;c<d;c++){var t=
+b[c],j=t/n;if(0!=j){if(1==j)if(a)var j=s-0.01,o=k-f,m=k-a,j=p("path",{d:["M",s,o,"A",f,f,0,1,1,j,o,"L",j,m,"A",a,a,0,1,0,s,m].join(" ")});else j=p("circle",{cx:s,cy:k,r:f});else o=l+t,m=["M"].concat(g(l,f),"A",f,f,0,0.5<j?1:0,1,g(o,f),"L"),a?m=m.concat(g(o,a),"A",a,a,0,0.5<j?1:0,0,g(l,a)):m.push(s,k),l+=t,j=p("path",{d:m.join(" ")});h(j).attr("fill",q.call(this,t,c,b));this.svg.appendChild(j)}}});e.register("donut",h.extend(!0,{},e.defaults.pie),function(a){a.innerRadius||(a.innerRadius=0.5*a.radius);
+e.graphers.pie.call(this,a)});e.register("line",{delimiter:",",fill:"#c6d9fd",height:16,min:0,stroke:"#4d89f9",strokeWidth:1,width:32},function(a){var b=this.values();1==b.length&&b.push(b[0]);for(var d=i.max.apply(i,a.max==v?b:b.concat(a.max)),c=i.min.apply(i,a.min==v?b:b.concat(a.min)),n=this.prepare(a.width,a.height),f=a.strokeWidth,e=n.width(),k=n.height()-f,h=d-c,d=this.x=function(a){return a*(e/(b.length-1))},n=this.y=function(a){var b=k;h&&(b-=(a-c)/h*k);return b+f/2},q=n(i.max(c,0)),g=[0,
+q],l=0;l<b.length;l++)g.push(d(l),n(b[l]));g.push(e,q);this.svg.appendChild(p("polygon",{fill:a.fill,points:g.join(" ")}));f&&this.svg.appendChild(p("polyline",{fill:"transparent",points:g.slice(2,g.length-2).join(" "),stroke:a.stroke,"stroke-width":f,"stroke-linecap":"square"}))});e.register("bar",{delimiter:",",fill:["#4D89F9"],height:16,min:0,padding:0.1,width:32},function(a){for(var b=this.values(),d=i.max.apply(i,a.max==v?b:b.concat(a.max)),c=i.min.apply(i,a.min==v?b:b.concat(a.min)),e=this.prepare(a.width,
+a.height),f=e.width(),h=e.height(),k=d-c,a=a.padding,e=this.fill(),r=this.x=function(a){return a*f/b.length},q=this.y=function(a){return h-(k?(a-c)/k*h:1)},g=0;g<b.length;g++){var l=r(g+a),t=r(g+1-a)-l,j=b[g],o=q(j),m=o,u;k?0>j?m=q(i.min(d,0)):o=q(i.max(c,0)):u=1;u=o-m;0==u&&(u=1,0<d&&k&&m--);this.svg.appendChild(p("rect",{fill:e.call(this,j,g,b),x:l,y:m,width:t,height:u}))}})})(jQuery,document,Math);
diff --git a/syweb/webclient/mobile.css b/syweb/webclient/mobile.css
index 6fa9221ccf..32b01c503d 100644
--- a/syweb/webclient/mobile.css
+++ b/syweb/webclient/mobile.css
@@ -1,4 +1,13 @@
 /*** Mobile voodoo ***/
+
+/** iPads **/
+@media all and (max-device-width: 768px) {
+    #roomRecentsTableWrapper {
+        display: none;
+    }
+}
+
+/** iPhones **/
 @media all and (max-device-width: 640px) {
             
     #messageTableWrapper {
@@ -37,11 +46,16 @@
         max-width: 640px ! important;
     }    
     
+    #controls {
+        padding: 0px;
+    }
+    
     #headerUserId,
     #roomHeader img,
     #userIdCell,
     #roomRecentsTableWrapper,
     #usersTableWrapper,
+    #controlButtons,
     .extraControls {
         display: none;
     }
@@ -64,6 +78,10 @@
         padding-top: 10px;
     }
     
+    .roomHeaderInfo {
+        margin-right: 0px;
+    }
+    
     #roomName {
         font-size: 12px ! important;
         margin-top: 0px ! important;
diff --git a/syweb/webclient/room/room-controller.js b/syweb/webclient/room/room-controller.js
index b878a1f718..be433d6e80 100644
--- a/syweb/webclient/room/room-controller.js
+++ b/syweb/webclient/room/room-controller.js
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
+angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput', 'angular-peity'])
 .controller('RoomController', ['$modal', '$filter', '$scope', '$timeout', '$routeParams', '$location', '$rootScope', 'matrixService', 'mPresence', 'eventHandlerService', 'mFileUpload', 'matrixPhoneService', 'MatrixCall', 'notificationService', 'modelService',
                                function($modal, $filter, $scope, $timeout, $routeParams, $location, $rootScope, matrixService, mPresence, eventHandlerService, mFileUpload, matrixPhoneService, MatrixCall, notificationService, modelService) {
    'use strict';
@@ -905,7 +905,20 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
         paginate(MESSAGES_PER_PAGINATION);
     };
 
-    $scope.startVoiceCall = function() {
+    $scope.checkWebRTC = function() {
+        if (!$rootScope.isWebRTCSupported()) {
+            alert("Your browser does not support WebRTC");
+            return false;
+        }
+        if ($scope.memberCount() != 2) {
+            alert("WebRTC calls are currently only supported on rooms with two members");
+            return false;
+        }
+        return true;
+    };
+    
+    $scope.startVoiceCall = function() {        
+        if (!$scope.checkWebRTC()) return;
         var call = new MatrixCall($scope.room_id);
         call.onError = $rootScope.onCallError;
         call.onHangup = $rootScope.onCallHangup;
@@ -916,6 +929,8 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
     };
 
     $scope.startVideoCall = function() {
+        if (!$scope.checkWebRTC()) return;
+
         var call = new MatrixCall($scope.room_id);
         call.onError = $rootScope.onCallError;
         call.onHangup = $rootScope.onCallHangup;
diff --git a/syweb/webclient/room/room.html b/syweb/webclient/room/room.html
index e59cc30edc..018d66f4fb 100644
--- a/syweb/webclient/room/room.html
+++ b/syweb/webclient/room/room.html
@@ -15,6 +15,15 @@
 
     <script type="text/ng-template" id="roomInfoTemplate.html">
         <div class="modal-body">
+            <span>
+               Invite a user: 
+                    <input ng-model="userIDToInvite" size="32" type="text" ng-enter="inviteUser()" ng-disabled="state.permission_denied" placeholder="User ID (ex:@user:homeserver)"/>     
+                    <button ng-click="inviteUser()" ng-disabled="state.permission_denied">Invite</button>
+            </span>
+            <br/>
+            <br/>
+            <button ng-click="leaveRoom()" ng-disabled="state.permission_denied">Leave Room</button>
+            </br/>
             <table class="room-info">
             <tr ng-repeat="(key, event) in roomInfo.stateEvents" class="room-info-event">
                 <td class="room-info-event-meta" width="30%">
@@ -57,6 +66,26 @@
 
     <div id="roomHeader">
         <a href ng-click="goToPage('/')"><img src="img/logo-small.png" width="100" height="43" alt="[matrix]"/></a>
+
+        <div id="controlButtons">
+            <button ng-click="startVoiceCall()" class="controlButton"
+                    style="background: url('img/voice.png')"
+                    ng-show="(currentCall == undefined || currentCall.state == 'ended')"
+                    ng-disabled="state.permission_denied"
+                    >
+            </button>
+            <button ng-click="startVideoCall()" class="controlButton"
+                    style="background: url('img/video.png')"
+                    ng-show="(currentCall == undefined || currentCall.state == 'ended')"
+                    ng-disabled="state.permission_denied"
+                    >
+            </button>
+            <button ng-click="openRoomInfo()" class="controlButton"
+                    style="background: url('img/settings.png')"
+                    >
+            </button>
+        </div>
+
         <div class="roomHeaderInfo">
 
             <div class="roomNameSection">
@@ -91,32 +120,30 @@
     <div id="roomRecentsTableWrapper">
         <div ng-include="'recents/recents.html'"></div>
     </div>
-
+    
     <div id="usersTableWrapper" ng-hide="state.permission_denied">
-        <table id="usersTable">
-            <tr ng-repeat="member in members | orderMembersList">
-                <td class="userAvatar mouse-pointer" ng-click="$parent.goToUserPage(member.id)" ng-class="member.membership == 'invite' ? 'invited' : ''">
-                    <img class="userAvatarImage" 
-                         ng-src="{{member.avatar_url || 'img/default-profile.png'}}" 
-                         alt="{{ member.displayname || member.id.substr(0, member.id.indexOf(':')) }}"
-                         title="{{ member.id }} - power: {{ member.powerLevel }}"
-                         width="80" height="80"/>
-                    <img class="userAvatarGradient" src="img/gradient.png" title="{{ member.id }}" width="80" height="24"/>
-                    <div class="userPowerLevel" ng-style="{'width': member.powerLevelNorm +'%'}"></div>
-                    <div class="userName">
-                        <div ng-show="member.displayname">
-                            {{ member.id | mUserDisplayName: room_id }}
-                        </div>
-                        <div ng-hide="member.displayname">
-                            {{ member.id.substr(0, member.id.indexOf(':')) }}<br/>
-                            {{ member.id.substr(member.id.indexOf(':')) }}
-                        </div>
-                    </div>
-                </td>
-                <td class="userPresence" ng-class="(member.presence === 'online' ? 'online' : (member.presence === 'unavailable' ? 'unavailable' : '')) + ' ' + (member.membership == 'invite' ? 'invited' : '')">
-                    <span ng-show="member.last_active_ago">{{ member.last_active_ago + (now - member.last_updated) | duration }}<br/>ago</span>
-                </td>
-        </table>
+        <div ng-repeat="member in members | orderMembersList" class="userAvatar">
+            <div class="userAvatarFrame" ng-class="(member.presence === 'online' ? 'online' : (member.presence === 'unavailable' ? 'unavailable' : '')) + ' ' + (member.membership == 'invite' ? 'invited' : '')">
+                <img class="userAvatarImage mouse-pointer" 
+                     ng-click="$parent.goToUserPage(member.id)"
+                     ng-src="{{member.avatar_url || 'img/default-profile.png'}}" 
+                     alt="{{ member.displayname || member.id.substr(0, member.id.indexOf(':')) }}"
+                     title="{{ member.id }} - power: {{ member.powerLevel }}"
+                     width="80" height="80"/>
+                <!-- <div class="userPowerLevel" ng-style="{'width': member.powerLevelNorm +'%'}"></div> -->
+            </div>
+            <div class="userName">
+                <pie-chart ng-show="member.powerLevelNorm" data="[ (member.powerLevelNorm + 0), (100 - member.powerLevelNorm) ]"></pie-chart>
+                <span ng-show="member.displayname">
+                    {{ member.id | mUserDisplayName: room_id }}
+                </span>
+                <span ng-hide="member.displayname">
+                    {{ member.id.substr(0, member.id.indexOf(':')) }}<br/>
+                    {{ member.id.substr(member.id.indexOf(':')) }}
+                </span>
+                <span ng-show="member.last_active_ago" style="color: #aaa">({{ member.last_active_ago + (now - member.last_updated) | duration }})</span>
+            </div>
+        </div>
     </div>
     
     <div id="messageTableWrapper" 
@@ -126,19 +153,20 @@
         <table id="messageTable" infinite-scroll="paginateMore()">
             <tr ng-repeat="msg in room.events"
                 ng-class="(room.events[$index + 1].user_id !== msg.user_id ? 'differentUser' : '') + (msg.user_id === state.user_id ? ' mine' : '')" scroll-item>
-                <td class="leftBlock">
-                    <div class="sender" ng-hide="room.events[$index - 1].user_id === msg.user_id"> {{ msg.__room_member.cnt.displayname || msg.user_id | mUserDisplayName: room_id }}</div>
+                <td class="leftBlock" ng-mouseover="state.showTs = 1" ng-mouseout="state.showTs = 0">
                     <div class="timestamp"
+                         ng-style="{ 'opacity': state.showTs ? 1.0 : 0.0 }"
                          ng-class="msg.echo_msg_state">
                         {{ (msg.origin_server_ts) | date:'MMM d HH:mm' }}
                     </div>
+                    <div class="sender" ng-hide="room.events[$index - 1].user_id === msg.user_id || msg.user_id === state.user_id"> {{ msg.__room_member.cnt.displayname || msg.user_id | mUserDisplayName: room_id }}</div>
                 </td>
                 <td class="avatar">
                     <!-- msg.__room_member.avatar_url is just backwards compat, and can be removed in the future. -->
                     <img class="avatarImage" ng-src="{{ msg.__room_member.cnt.avatar_url || msg.__room_member.avatar_url || 'img/default-profile.png' }}" width="32" height="32" title="{{msg.user_id}}"
                          ng-hide="room.events[$index - 1].user_id === msg.user_id || msg.user_id === state.user_id"/>
                 </td>
-                <td ng-class="(!msg.content.membership && ('m.room.topic' !== msg.type && 'm.room.name' !== msg.type))? (msg.content.msgtype === 'm.emote' ? 'emote text' : 'text') : 'membership text'">
+                <td style="vertical-align: bottom" ng-class="(!msg.content.membership && ('m.room.topic' !== msg.type && 'm.room.name' !== msg.type))? (msg.content.msgtype === 'm.emote' ? 'emote text' : 'text') : 'membership text'">
                     <div class="bubble" ng-dblclick="openJson(msg)">
                         <span ng-if="'join' === msg.content.membership && msg.changedKey === 'membership'">
                             {{ msg.content.displayname || members[msg.state_key].displayname || msg.state_key }} joined
@@ -222,49 +250,10 @@
 
     <div id="controlPanel">
         <div id="controls">
-            <table id="inputBarTable">
-                <tr>
-                    <td id="userIdCell" width="1px">
-                        {{ state.user_id }} 
-                    </td>
-                    <td width="*">
-                        <textarea id="mainInput" rows="1" ng-enter="send()"
-                                  ng-disabled="state.permission_denied"
-                                  ng-focus="true" autocomplete="off" tab-complete command-history/>
-                    </td>
-                    <td id="buttonsCell">
-                        <button ng-click="send()" ng-disabled="state.permission_denied">Send</button>
-                        <button m-file-input="imageFileToSend" class="extraControls" ng-disabled="state.permission_denied">Image</button>
-                    </td>
-                </tr>
-            </table>
-
-            <div class="extraControls">
-                <span>
-                   Invite a user: 
-                        <input ng-model="userIDToInvite" size="32" type="text" ng-enter="inviteUser()" ng-disabled="state.permission_denied" placeholder="User ID (ex:@user:homeserver)"/>     
-                        <button ng-click="inviteUser()" ng-disabled="state.permission_denied">Invite</button>
-                </span>
-                <button ng-click="leaveRoom()" ng-disabled="state.permission_denied">Leave</button>
-                <button ng-click="startVoiceCall()"
-                        ng-show="(currentCall == undefined || currentCall.state == 'ended')"
-                        ng-disabled="state.permission_denied || !isWebRTCSupported() || memberCount() != 2"
-                        title ="{{ !isWebRTCSupported() ? 'VoIP requires webRTC but your browser does not support it' : (memberCount() == 2 ? '' : 'VoIP calls can only be made in rooms with two participants') }}"
-                        >
-                    Voice Call
-                </button>
-                <button ng-click="startVideoCall()"
-                        ng-show="(currentCall == undefined || currentCall.state == 'ended')"
-                        ng-disabled="state.permission_denied || !isWebRTCSupported() || memberCount() != 2"
-                        title ="{{ !isWebRTCSupported() ? 'VoIP requires webRTC but your browser does not support it' : (memberCount() == 2 ? '' : 'VoIP calls can only be made in rooms with two participants') }}"
-                        >
-                    Video Call
-                </button>
-                <button ng-click="openRoomInfo()">
-                    Room Info
-                </button>
-            </div>
-        
+            <button id="attachButton" m-file-input="imageFileToSend" class="extraControls" ng-disabled="state.permission_denied"></button>
+            <textarea id="mainInput" rows="1" ng-enter="send()"
+                      ng-disabled="state.permission_denied"
+                      ng-focus="true" autocomplete="off" tab-complete command-history/>
             {{ feedback }}
             <div ng-show="state.stream_failure">
                 {{ state.stream_failure.data.error || "Connection failure" }}