major CSS overhaul to try to make things look a bit cleaner

This commit is contained in:
Matthew Hodgson 2014-11-11 04:39:16 +00:00
parent 303b455965
commit b765dc005b
11 changed files with 302 additions and 104 deletions

View File

@ -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;
object-fit: cover;
width: 100%;
.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;
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: 0px;
height: 2px;
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;
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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 864 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 B

View File

@ -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>

69
syweb/webclient/js/angular-peity.js vendored Normal file
View File

@ -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" );
} );

13
syweb/webclient/js/jquery.peity.min.js vendored Normal file
View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

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