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" }}
|