Browse Source

Merge branch 'release-0.23'

pull/175/head v0.23.3
Simon Eisenmann 11 years ago
parent
commit
3324a6fc08
  1. 19
      debian/changelog
  2. 5
      server.conf.in
  3. 7
      static/js/controllers/usersettingscontroller.js
  4. 1
      static/js/directives/page.js
  5. 2
      static/js/directives/presentation.js
  6. 11
      static/js/directives/roombar.js
  7. 2
      static/js/directives/settings.js
  8. 81
      static/js/libs/pdf/compatibility.js
  9. 1524
      static/js/libs/pdf/pdf.js
  10. 10730
      static/js/libs/pdf/pdf.worker.js
  11. 27
      static/js/mediastream/webrtc.js
  12. 34
      static/js/services/rooms.js
  13. 2
      static/partials/chatroom.html
  14. 2
      static/partials/page/welcome.html
  15. 7
      static/partials/roombar.html

19
debian/changelog vendored

@ -1,3 +1,22 @@
spreed-webrtc-server (0.23.3) precise; urgency=low
* Improved room bar room change and leave buttons.
* Never hide room bar completely.
* Stay in prior room when join fails.
* Stay in prior room when PIN prompt was aborted.
* Updated to PDF.js 1.0.907.
* Enhanced example CSP to support for PDF and WebODF presentations.
* Fixed Firefox screen sharing interop.
* Fixed Firefox file transfer interop.
* Fixed peer connection to create and offer when user media failed.
* Only show room bar when there is no peer.
* Hide welcome screen when there is a peer.
* Avoid dead ends in room join UI when connection is lost and reestablished.
* Avoid showing settings automatically when not connected or still in authorizing phase.
* Added some missing CSS classes to allow easier UI mods.
-- Simon Eisenmann <simon@struktur.de> Fri, 19 Dec 2014 17:15:10 +0100
spreed-webrtc-server (0.23.2) precise; urgency=low spreed-webrtc-server (0.23.2) precise; urgency=low
* Do not build combined Javascript in strict mode to avoid compatibility issues. * Do not build combined Javascript in strict mode to avoid compatibility issues.

5
server.conf.in

@ -107,8 +107,9 @@ serverRealm = local
; The currently recommended CSP is: ; The currently recommended CSP is:
; default-src 'self'; ; default-src 'self';
; style-src 'self' 'unsafe-inline'; ; style-src 'self' 'unsafe-inline';
; img-src 'self' data:; ; img-src 'self' data: blob:;
; connect-src 'self' wss://server:port/ws; ; connect-src 'self' wss://server:port/ws blob:;
; font-src 'self' data: blob:;
;contentSecurityPolicy = ;contentSecurityPolicy =
; Content-Security-Policy-Report-Only HTTP response header value. Use this ; Content-Security-Policy-Report-Only HTTP response header value. Use this
; to test your CSP before putting it into production. ; to test your CSP before putting it into production.

7
static/js/controllers/usersettingscontroller.js

@ -23,7 +23,7 @@
define([], function() { define([], function() {
// UsersettingsController // UsersettingsController
return ["$scope", "$element", "mediaStream", "safeApply", function($scope, $element, mediaStream, safeApply) { return ["$scope", "$element", "mediaStream", "safeApply", "$window", function($scope, $element, mediaStream, safeApply, $window) {
$scope.withUsersForget = true; $scope.withUsersForget = true;
@ -65,7 +65,10 @@ define([], function() {
this.forgetUserid = function() { this.forgetUserid = function() {
mediaStream.users.forget(); mediaStream.users.forget();
mediaStream.connector.forgetAndReconnect(); mediaStream.webrtc.doHangup("forgetUserid");
$window.setTimeout(function() {
mediaStream.connector.forgetAndReconnect();
}, 0);
}; };
}]; }];

1
static/js/directives/page.js

@ -33,6 +33,7 @@ define(['text!partials/page.html', 'text!partials/page/welcome.html'], function(
}); });
$scope.$on("room.random", function(ev, roomdata) { $scope.$on("room.random", function(ev, roomdata) {
// Show welcome page on room random events. // Show welcome page on room random events.
$scope.layout.roombar = false;
$timeout(function() { $timeout(function() {
$scope.page = "page/welcome.html"; $scope.page = "page/welcome.html";
}); });

2
static/js/directives/presentation.js

@ -429,7 +429,7 @@ define(['jquery', 'underscore', 'text!partials/presentation.html', 'bigscreen'],
}; };
var connector = function(token, peercall) { var connector = function(token, peercall) {
console.log("XXX connector", token, peercall, peers); //console.log("XXX connector", token, peercall, peers);
if (peers.hasOwnProperty(peercall.id)) { if (peers.hasOwnProperty(peercall.id)) {
// Already got a connection. // Already got a connection.
return; return;

11
static/js/directives/roombar.js

@ -23,7 +23,7 @@
define(['underscore', 'angular', 'text!partials/roombar.html'], function(_, angular, template) { define(['underscore', 'angular', 'text!partials/roombar.html'], function(_, angular, template) {
// roomBar // roomBar
return ["$window", "rooms", function($window, rooms) { return ["$window", "rooms", "$timeout", function($window, rooms, $timeout) {
var link = function($scope, $element) { var link = function($scope, $element) {
@ -33,7 +33,7 @@ define(['underscore', 'angular', 'text!partials/roombar.html'], function(_, angu
}; };
//console.log("roomBar directive link", arguments); //console.log("roomBar directive link", arguments);
$scope.layout.roombar = true; //$scope.layout.roombar = true;
$scope.save = function() { $scope.save = function() {
if ($scope.roombarform.$invalid) { if ($scope.roombarform.$invalid) {
@ -52,6 +52,9 @@ define(['underscore', 'angular', 'text!partials/roombar.html'], function(_, angu
$scope.$on("room.updated", function(ev, room) { $scope.$on("room.updated", function(ev, room) {
$scope.currentRoomName = $scope.newRoomName = room.Name; $scope.currentRoomName = $scope.newRoomName = room.Name;
if ($scope.currentRoomName && !$scope.peer) {
$scope.layout.roombar = true;
}
}); });
$scope.$on("room.left", clearRoomName); $scope.$on("room.left", clearRoomName);
@ -63,7 +66,9 @@ define(['underscore', 'angular', 'text!partials/roombar.html'], function(_, angu
}); });
$scope.$watch("layout.roombar", function(value) { $scope.$watch("layout.roombar", function(value) {
$element.find("input").focus(); $timeout(function() {
$element.find("input").focus();
});
}); });
$scope.$watch("peer", function(peer) { $scope.$watch("peer", function(peer) {

2
static/js/directives/settings.js

@ -146,7 +146,7 @@ define(['jquery', 'underscore', 'text!partials/settings.html'], function($, _, t
}); });
$scope.maybeShowSettings = function() { $scope.maybeShowSettings = function() {
if ($scope.autoshowSettings) { if ($scope.autoshowSettings && mediaStream.connector.connected && !appData.authorizing()) {
$scope.autoshowSettings = false; $scope.autoshowSettings = false;
if (!$scope.loadedUser) { if (!$scope.loadedUser) {
$scope.layout.settings = true; $scope.layout.settings = true;

81
static/js/libs/pdf/compatibility.js

@ -167,35 +167,49 @@ if (typeof PDFJS === 'undefined') {
// The worker will be using XHR, so we can save time and disable worker. // The worker will be using XHR, so we can save time and disable worker.
PDFJS.disableWorker = true; PDFJS.disableWorker = true;
Object.defineProperty(xhrPrototype, 'responseType', {
get: function xmlHttpRequestGetResponseType() {
return this._responseType || 'text';
},
set: function xmlHttpRequestSetResponseType(value) {
if (value === 'text' || value === 'arraybuffer') {
this._responseType = value;
if (value === 'arraybuffer' &&
typeof this.overrideMimeType === 'function') {
this.overrideMimeType('text/plain; charset=x-user-defined');
}
}
}
});
// Support: IE9 // Support: IE9
if (typeof VBArray !== 'undefined') { if (typeof VBArray !== 'undefined') {
Object.defineProperty(xhrPrototype, 'response', { Object.defineProperty(xhrPrototype, 'response', {
get: function xmlHttpRequestResponseGet() { get: function xmlHttpRequestResponseGet() {
return new Uint8Array(new VBArray(this.responseBody).toArray()); if (this.responseType === 'arraybuffer') {
return new Uint8Array(new VBArray(this.responseBody).toArray());
} else {
return this.responseText;
}
} }
}); });
return; return;
} }
// other browsers Object.defineProperty(xhrPrototype, 'response', {
function responseTypeSetter() { get: function xmlHttpRequestResponseGet() {
// will be only called to set "arraybuffer" if (this.responseType !== 'arraybuffer') {
this.overrideMimeType('text/plain; charset=x-user-defined'); return this.responseText;
} }
if (typeof xhr.overrideMimeType === 'function') { var text = this.responseText;
Object.defineProperty(xhrPrototype, 'responseType', var i, n = text.length;
{ set: responseTypeSetter }); var result = new Uint8Array(n);
} for (i = 0; i < n; ++i) {
function responseGetter() { result[i] = text.charCodeAt(i) & 0xFF;
var text = this.responseText; }
var i, n = text.length; return result.buffer;
var result = new Uint8Array(n);
for (i = 0; i < n; ++i) {
result[i] = text.charCodeAt(i) & 0xFF;
} }
return result.buffer; });
}
Object.defineProperty(xhrPrototype, 'response', { get: responseGetter });
})(); })();
// window.btoa (base64 encode function) ? // window.btoa (base64 encode function) ?
@ -237,7 +251,7 @@ if (typeof PDFJS === 'undefined') {
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
window.atob = function (input) { window.atob = function (input) {
input = input.replace(/=+$/, ''); input = input.replace(/=+$/, '');
if (input.length % 4 == 1) { if (input.length % 4 === 1) {
throw new Error('bad atob input'); throw new Error('bad atob input');
} }
for ( for (
@ -293,7 +307,7 @@ if (typeof PDFJS === 'undefined') {
var dataset = {}; var dataset = {};
for (var j = 0, jj = this.attributes.length; j < jj; j++) { for (var j = 0, jj = this.attributes.length; j < jj; j++) {
var attribute = this.attributes[j]; var attribute = this.attributes[j];
if (attribute.name.substring(0, 5) != 'data-') { if (attribute.name.substring(0, 5) !== 'data-') {
continue; continue;
} }
var key = attribute.name.substring(5).replace(/\-([a-z])/g, var key = attribute.name.substring(5).replace(/\-([a-z])/g,
@ -416,7 +430,7 @@ if (typeof PDFJS === 'undefined') {
function isDisabled(node) { function isDisabled(node) {
return node.disabled || (node.parentNode && isDisabled(node.parentNode)); return node.disabled || (node.parentNode && isDisabled(node.parentNode));
} }
if (navigator.userAgent.indexOf('Opera') != -1) { if (navigator.userAgent.indexOf('Opera') !== -1) {
// use browser detection since we cannot feature-check this bug // use browser detection since we cannot feature-check this bug
document.addEventListener('click', ignoreIfTargetDisabled, true); document.addEventListener('click', ignoreIfTargetDisabled, true);
} }
@ -467,6 +481,7 @@ if (typeof PDFJS === 'undefined') {
if (isSafari || isOldAndroid) { if (isSafari || isOldAndroid) {
PDFJS.disableRange = true; PDFJS.disableRange = true;
PDFJS.disableStream = true;
} }
})(); })();
@ -481,7 +496,7 @@ if (typeof PDFJS === 'undefined') {
} }
})(); })();
// Support: IE<11, Chrome<21, Android<4.4 // Support: IE<11, Chrome<21, Android<4.4, Safari<6
(function checkSetPresenceInImageData() { (function checkSetPresenceInImageData() {
// IE < 11 will use window.CanvasPixelArray which lacks set function. // IE < 11 will use window.CanvasPixelArray which lacks set function.
if (window.CanvasPixelArray) { if (window.CanvasPixelArray) {
@ -495,21 +510,21 @@ if (typeof PDFJS === 'undefined') {
} else { } else {
// Old Chrome and Android use an inaccessible CanvasPixelArray prototype. // Old Chrome and Android use an inaccessible CanvasPixelArray prototype.
// Because we cannot feature detect it, we rely on user agent parsing. // Because we cannot feature detect it, we rely on user agent parsing.
var polyfill = false; var polyfill = false, versionMatch;
if (navigator.userAgent.indexOf('Chrom') >= 0) { if (navigator.userAgent.indexOf('Chrom') >= 0) {
var versionMatch = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./); versionMatch = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
if (versionMatch && parseInt(versionMatch[2]) < 21) { // Chrome < 21 lacks the set function.
// Chrome < 21 lacks the set function. polyfill = versionMatch && parseInt(versionMatch[2]) < 21;
polyfill = true;
}
} else if (navigator.userAgent.indexOf('Android') >= 0) { } else if (navigator.userAgent.indexOf('Android') >= 0) {
// Android < 4.4 lacks the set function. // Android < 4.4 lacks the set function.
// Android >= 4.4 will contain Chrome in the user agent, // Android >= 4.4 will contain Chrome in the user agent,
// thus pass the Chrome check above and not reach this block. // thus pass the Chrome check above and not reach this block.
var isOldAndroid = /Android\s[0-4][^\d]/g.test(navigator.userAgent); polyfill = /Android\s[0-4][^\d]/g.test(navigator.userAgent);
if (isOldAndroid) { } else if (navigator.userAgent.indexOf('Safari') >= 0) {
polyfill = true; versionMatch = navigator.userAgent.
} match(/Version\/([0-9]+)\.([0-9]+)\.([0-9]+) Safari\//);
// Safari < 6 lacks the set function.
polyfill = versionMatch && parseInt(versionMatch[1]) < 6;
} }
if (polyfill) { if (polyfill) {

1524
static/js/libs/pdf/pdf.js

File diff suppressed because it is too large Load Diff

10730
static/js/libs/pdf/pdf.worker.js vendored

File diff suppressed because it is too large Load Diff

27
static/js/mediastream/webrtc.js

@ -487,17 +487,12 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u
// Connect. // Connect.
xfer.setInitiate(true); xfer.setInitiate(true);
xfer.createPeerConnection(_.bind(function() { xfer.createPeerConnection(_.bind(function(pc) {
xfer.e.on("negotiationNeeded", _.bind(function(event, currentxfer) { xfer.e.on("negotiationNeeded", _.bind(function(event, currentxfer) {
this.sendOfferWhenNegotiationNeeded(currentxfer, id); this.sendOfferWhenNegotiationNeeded(currentxfer, id);
}, this)); }, this));
_.defer(pc.negotiationNeeded);
}, this)); }, this));
/*
xfer.createOffer(_.bind(function(sessionDescription, currentxfer) {
console.log("Sending xfer offer with sessionDescription", sessionDescription, currentxfer.id);
// TODO(longsleep): Support sending this through data channel too if we have one.
this.api.sendOffer(id, sessionDescription);
}, this));*/
}; };
@ -563,17 +558,12 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u
// Connect. // Connect.
peerscreenshare.setInitiate(true); //XXX(longsleep): This creates a data channel which is not needed. peerscreenshare.setInitiate(true); //XXX(longsleep): This creates a data channel which is not needed.
peerscreenshare.createPeerConnection(_.bind(function() { peerscreenshare.createPeerConnection(_.bind(function(pc) {
peerscreenshare.e.on("negotiationNeeded", _.bind(function(event, currentscreenshare) { peerscreenshare.e.on("negotiationNeeded", _.bind(function(event, currentscreenshare) {
this.sendOfferWhenNegotiationNeeded(currentscreenshare, id); this.sendOfferWhenNegotiationNeeded(currentscreenshare, id);
}, this)); }, this));
_.defer(pc.negotiationNeeded);
}, this)); }, this));
/*
peerscreenshare.createOffer(_.bind(function(sessionDescription, currentscreenshare) {
console.log("Sending screen share offer with sessionDescription", sessionDescription, currentscreenshare.id);
// TODO(longsleep): Support sending this through data channel too if we have one.
this.api.sendOffer(id, sessionDescription);
}, this));*/
}; };
@ -653,14 +643,11 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u
this.usermedia.applyAudioMute(this.audioMute); this.usermedia.applyAudioMute(this.audioMute);
this.e.triggerHandler("usermedia", [this.usermedia]); this.e.triggerHandler("usermedia", [this.usermedia]);
this.usermedia.addToPeerConnection(peerconnection); this.usermedia.addToPeerConnection(peerconnection);
} else {
_.defer(peerconnection.negotiationNeeded);
} }
this.started = true; this.started = true;
if (this.initiator) { if (!this.initiator) {
/*currentcall.createOffer(_.bind(function(sessionDescription, currentcall) {
console.log("Sending offer with sessionDescription", sessionDescription, currentcall.id);
this.api.sendOffer(currentcall.id, sessionDescription);
}, this));*/
} else {
this.calleeStart(); this.calleeStart();
} }
currentcall.e.on("negotiationNeeded", _.bind(function(event, currentcall) { currentcall.e.on("negotiationNeeded", _.bind(function(event, currentcall) {

34
static/js/services/rooms.js

@ -30,6 +30,7 @@ define([
var url = restURL.api("rooms"); var url = restURL.api("rooms");
var requestedRoomName = ""; var requestedRoomName = "";
var priorRoomName = null;
var helloedRoomName = null; var helloedRoomName = null;
var currentRoom = null; var currentRoom = null;
var randomRoom = null; var randomRoom = null;
@ -49,7 +50,7 @@ define([
roompin.requestInteractively(requestedRoomName).then(joinRequestedRoom, roompin.requestInteractively(requestedRoomName).then(joinRequestedRoom,
function() { function() {
console.log("Authentication cancelled, try a different room."); console.log("Authentication cancelled, try a different room.");
rooms.joinDefault(); rooms.joinPriorOrDefault(true);
}); });
break; break;
case "authorization_not_required": case "authorization_not_required":
@ -58,11 +59,8 @@ define([
break; break;
case "room_join_requires_account": case "room_join_requires_account":
console.log("Room join requires a logged in user."); console.log("Room join requires a logged in user.");
alertify.dialog.notify("", translation._("Please sign in to create rooms."), function() { alertify.dialog.notify("", translation._("Please sign in to create rooms."));
rooms.joinDefault(); rooms.joinPriorOrDefault(true);
}, function() {
rooms.joinDefault();
});
break; break;
default: default:
console.log("Unknown error", error, "while joining room ", requestedRoomName); console.log("Unknown error", error, "while joining room ", requestedRoomName);
@ -71,20 +69,24 @@ define([
}; };
var joinRequestedRoom = function() { var joinRequestedRoom = function() {
if (appData.authorizing()) { if (!connector.connected || appData.authorizing()) {
// Do nothing while authorizing. // Do nothing while not connected or authorizing.
return; return;
} }
if (!connector.connected || !currentRoom || requestedRoomName !== currentRoom.Name) { if (!currentRoom || requestedRoomName !== currentRoom.Name) {
requestedRoomName = requestedRoomName ? requestedRoomName : ""; requestedRoomName = requestedRoomName ? requestedRoomName : "";
if (helloedRoomName !== requestedRoomName) { if (helloedRoomName !== requestedRoomName) {
console.log("Joining room", [requestedRoomName]);
helloedRoomName = requestedRoomName; helloedRoomName = requestedRoomName;
var myHelloedRoomName = helloedRoomName;
_.defer(function() {
if (helloedRoomName === myHelloedRoomName) {
helloedRoomName = null;
}
});
console.log("Joining room", [requestedRoomName]);
api.sendHello(requestedRoomName, roompin.get(requestedRoomName), function(room) { api.sendHello(requestedRoomName, roompin.get(requestedRoomName), function(room) {
helloedRoomName = null;
setCurrentRoom(room); setCurrentRoom(room);
}, function(error) { }, function(error) {
helloedRoomName = null;
joinFailed(error); joinFailed(error);
}); });
} }
@ -98,6 +100,7 @@ define([
var priorRoom = currentRoom; var priorRoom = currentRoom;
currentRoom = room; currentRoom = room;
if (priorRoom) { if (priorRoom) {
priorRoomName = priorRoom.Name;
console.log("Left room", priorRoom.Name); console.log("Left room", priorRoom.Name);
$rootScope.$broadcast("room.left", priorRoom.Name); $rootScope.$broadcast("room.left", priorRoom.Name);
} }
@ -219,6 +222,13 @@ define([
joinDefault: function(replace) { joinDefault: function(replace) {
return rooms.joinByName("", replace); return rooms.joinByName("", replace);
}, },
joinPriorOrDefault: function(replace) {
if (!priorRoomName || requestedRoomName === priorRoomName) {
rooms.joinDefault(replace);
} else {
rooms.joinByName(priorRoomName, replace);
}
},
link: function(room) { link: function(room) {
var name = room ? room.Name : null; var name = room ? room.Name : null;
if (!name) { if (!name) {

2
static/partials/chatroom.html

@ -4,7 +4,7 @@
<div class="btn-group"> <div class="btn-group">
<button ng-if="!isgroupchat" class="btn btn-sm btn-primary" title="{{_('Start video call')}}" ng-click="doCall()"><i class="fa fa-phone fa-fw"></i></button> <button ng-if="!isgroupchat" class="btn btn-sm btn-primary" title="{{_('Start video call')}}" ng-click="doCall()"><i class="fa fa-phone fa-fw"></i></button>
<button class="btn btn-sm btn-primary btn-fileupload" title="{{_('Upload files')}}"><i class="fa fa-upload fa-fw"></i></button> <button class="btn btn-sm btn-primary btn-fileupload" title="{{_('Upload files')}}"><i class="fa fa-upload fa-fw"></i></button>
<button class="btn btn-sm btn-primary" title="{{_('Share my location')}}" ng-click="shareGeolocation()"><i class="fa fa-location-arrow fa-fw"></i></button> <button class="btn btn-sm btn-primary btn-locationshare" title="{{_('Share my location')}}" ng-click="shareGeolocation()"><i class="fa fa-location-arrow fa-fw"></i></button>
</div> </div>
<div class="btn-group pull-right"> <div class="btn-group pull-right">
<button class="btn btn-sm btn-default" title="{{_('Clear chat')}}" ng-click="doClear()"><i class="fa fa-eraser fa-fw"></i></button> <button class="btn btn-sm btn-default" title="{{_('Clear chat')}}" ng-click="doClear()"><i class="fa fa-eraser fa-fw"></i></button>

2
static/partials/page/welcome.html

@ -1,4 +1,4 @@
<div welcome ng-form="welcome" class="welcome container-fluid"> <div welcome ng-form="welcome" class="welcome container-fluid" ng-show="!peer">
<div class="welcome-logo"></div> <div class="welcome-logo"></div>
<div class="welcome-container" ng-controller="UsersettingsController as usersettings"> <div class="welcome-container" ng-controller="UsersettingsController as usersettings">
<h1>{{_("Enter a room name")}}</h1> <h1>{{_("Enter a room name")}}</h1>

7
static/partials/roombar.html

@ -1,10 +1,13 @@
<div class="roombar overlaybar form-horizontal" ng-hide="currentRoomName===null" ng-class="{notvisible: !layout.roombar}"> <div class="roombar overlaybar form-horizontal" ng-class="{notvisible: !layout.roombar}">
<a class="overlaybar-button" ng-model="layout.roombar" btn-checkbox btn-checkbox-true="true" btn-checkbox-false="false" title="{{_('Change room')}}"><i class="fa fa-pencil-square-o"></i></a> <a class="overlaybar-button" ng-model="layout.roombar" btn-checkbox btn-checkbox-true="true" btn-checkbox-false="false" title="{{_('Change room')}}"><i class="fa fa-pencil-square-o"></i></a>
<form name="roombarform" class="overlaybar-content form-group"> <form name="roombarform" class="overlaybar-content form-group">
<label class="pull-left control-label hidden-xs">{{_('Room')}}</label> <label class="pull-left control-label hidden-xs">{{_('Room')}}</label>
<div class="pull-left">
<span ng-if="currentRoomName"><a class="btn btn-danger btn-sm" title="{{_('Leave room')}}" ng-click="exit()"><i class="fa fa-sign-out"></i></a></span>
</div>
<div class="pull-left"> <div class="pull-left">
<div class="input-group"> <div class="input-group">
<input class="form-control input-sm" ng-model="newRoomName" ng-maxlength="30" on-enter="save()" type="text" placeholder="{{_('Main')}}"></input><span ng-if="currentRoomName && roombarform.$pristine" class="input-group-btn"><a class="btn btn-default btn-sm" title="{{_('Leave room')}}" ng-click="exit()"><i class="fa fa-sign-out"></i></a></span><span ng-if="!currentRoomName || !roombarform.$pristine" class="input-group-btn"><a class="btn btn-default btn-sm" title="{{_('Change room')}}" ng-disabled="roombarform.$invalid" ng-click="save()"><i class="fa fa-sign-in"></i></a></span> <input class="form-control input-sm" ng-model="newRoomName" ng-maxlength="30" on-enter="save()" type="text" placeholder="{{_('Main')}}"></input><span class="input-group-btn"><a class="btn btn-default btn-sm" title="{{_('Change room')}}" ng-disabled="roombarform.$invalid" ng-click="save()"><i class="fa fa-sign-in"></i></a></span>
</div> </div>
</div> </div>
<div class="pull-left"> <div class="pull-left">

Loading…
Cancel
Save