diff --git a/docs/client-server/OLD_specification.rst b/docs/client-server/OLD_specification.rst
index 47fba5eeac..425ae57d93 100644
--- a/docs/client-server/OLD_specification.rst
+++ b/docs/client-server/OLD_specification.rst
@@ -4,7 +4,7 @@ Matrix Client-Server API
.. WARNING::
- This specification is old. Please see /docs/specification.rst instead.
+ This specification is old. Please see matrix-doc/specification instead.
diff --git a/docs/implementation-notes/architecture.rst b/docs/implementation-notes/architecture.rst
new file mode 100644
index 0000000000..98050428b9
--- /dev/null
+++ b/docs/implementation-notes/architecture.rst
@@ -0,0 +1,68 @@
+Synapse Architecture
+====================
+
+As of the end of Oct 2014, Synapse's overall architecture looks like::
+
+ synapse
+ .-----------------------------------------------------.
+ | Notifier |
+ | ^ | |
+ | | | |
+ | .------------|------. |
+ | | handlers/ | | |
+ | | v | |
+ | | Event*Handler <--------> rest/* <=> Client
+ | | Rooms*Handler | |
+ HSes <=> federation/* <==> FederationHandler | |
+ | | | PresenceHandler | |
+ | | | TypingHandler | |
+ | | '-------------------' |
+ | | | | |
+ | | state/* | |
+ | | | | |
+ | | v v |
+ | `--------------> storage/* |
+ | | |
+ '--------------------------|--------------------------'
+ v
+ .----.
+ | DB |
+ '----'
+
+* Handlers: business logic of synapse itself. Follows a set contract of BaseHandler:
+
+ - BaseHandler gives us onNewRoomEvent which: (TODO: flesh this out and make it less cryptic):
+
+ + handle_state(event)
+ + auth(event)
+ + persist_event(event)
+ + notify notifier or federation(event)
+
+ - PresenceHandler: use distributor to get EDUs out of Federation. Very
+ lightweight logic built on the distributor
+ - TypingHandler: use distributor to get EDUs out of Federation. Very
+ lightweight logic built on the distributor
+ - EventsHandler: handles the events stream...
+ - FederationHandler: - gets PDU from Federation Layer; turns into an event;
+ follows basehandler functionality.
+ - RoomsHandler: does all the room logic, including members - lots of classes in
+ RoomsHandler.
+ - ProfileHandler: talks to the storage to store/retrieve profile info.
+
+* EventFactory: generates events of particular event types.
+* Notifier: Backs the events handler
+* REST: Interfaces handlers and events to the outside world via HTTP/JSON.
+ Converts events back and forth from JSON.
+* Federation: holds the HTTP client & server to talk to other servers. Does
+ replication to make sure there's nothing missing in the graph. Handles
+ reliability. Handles txns.
+* Distributor: generic event bus. used for presence & typing only currently.
+ Notifier could be implemented using Distributor - so far we are only using for
+ things which actually /require/ dynamic pluggability however as it can
+ obfuscate the actual flow of control.
+* Auth: helper singleton to say whether a given event is allowed to do a given
+ thing (TODO: put this on the diagram)
+* State: helper singleton: does state conflict resolution. You give it an event
+ and it tells you if it actually updates the state or not, and annotates the
+ event up properly and handles merge conflict resolution.
+* Storage: abstracts the storage engine.
diff --git a/docs/implementation-notes/python_architecture.rst b/docs/implementation-notes/python_architecture.rst
index 8beaa615d0..2a5a2613c4 100644
--- a/docs/implementation-notes/python_architecture.rst
+++ b/docs/implementation-notes/python_architecture.rst
@@ -1,3 +1,9 @@
+.. WARNING::
+ These architecture notes are spectacularly old, and date back to when Synapse
+ was just federation code in isolation. This should be merged into the main
+ spec.
+
+
= Server to Server =
== Server to Server Stack ==
diff --git a/synapse/http/content_repository.py b/synapse/http/content_repository.py
index 3159ffff0a..1306b35271 100644
--- a/synapse/http/content_repository.py
+++ b/synapse/http/content_repository.py
@@ -129,6 +129,14 @@ class ContentRepoResource(resource.Resource):
logger.info("Sending file %s", file_path)
f = open(file_path, 'rb')
request.setHeader('Content-Type', content_type)
+
+ # cache for at least a day.
+ # XXX: we might want to turn this off for data we don't want to recommend
+ # caching as it's sensitive or private - or at least select private.
+ # don't bother setting Expires as all our matrix clients are smart enough to
+ # be happy with Cache-Control (right?)
+ request.setHeader('Cache-Control', 'public,max-age=86400,s-maxage=86400')
+
d = FileSender().beginFileTransfer(f, request)
# after the file has been sent, clean up and finish the request
diff --git a/syweb/webclient/app-controller.js b/syweb/webclient/app-controller.js
index 2d82a42cf8..bbcf4ab5f6 100644
--- a/syweb/webclient/app-controller.js
+++ b/syweb/webclient/app-controller.js
@@ -112,8 +112,8 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
if (!$rootScope.currentCall) {
// This causes the still frame to be flushed out of the video elements,
// avoiding a flash of the last frame of the previous call when starting the next
- angular.element('#localVideo')[0].load();
- angular.element('#remoteVideo')[0].load();
+ if (angular.element('#localVideo')[0].load) angular.element('#localVideo')[0].load();
+ if (angular.element('#remoteVideo')[0].load) angular.element('#remoteVideo')[0].load();
return;
}
@@ -187,8 +187,8 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
}
call.onError = $scope.onCallError;
call.onHangup = $scope.onCallHangup;
- call.localVideoElement = angular.element('#localVideo')[0];
- call.remoteVideoElement = angular.element('#remoteVideo')[0];
+ call.localVideoSelector = '#localVideo';
+ call.remoteVideoSelector = '#remoteVideo';
$rootScope.currentCall = call;
});
diff --git a/syweb/webclient/app.css b/syweb/webclient/app.css
index 5ab8e2b8fd..23ec42f128 100755
--- a/syweb/webclient/app.css
+++ b/syweb/webclient/app.css
@@ -136,17 +136,17 @@ textarea, input {
transition: left linear 500ms, top linear 500ms, width linear 500ms, height linear 500ms;
}
-#localVideo.mini {
+.mini #localVideo {
top: 0px;
left: 130px;
}
-#localVideo.large {
+.large #localVideo {
top: 70px;
left: 20px;
}
-#localVideo.ended {
+.ended #localVideo {
-webkit-filter: grayscale(1);
filter: grayscale(1);
}
@@ -157,19 +157,19 @@ textarea, input {
transition: left linear 500ms, top linear 500ms, width linear 500ms, height linear 500ms;
}
-#remoteVideo.mini {
+.mini #remoteVideo {
left: 260px;
top: 0px;
width: 128px;
}
-#remoteVideo.large {
+.large #remoteVideo {
left: 0px;
top: 50px;
width: 100%;
}
-#remoteVideo.ended {
+.ended #remoteVideo {
-webkit-filter: grayscale(1);
filter: grayscale(1);
}
@@ -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,19 @@ textarea, input {
#mainInput {
width: 100%;
- resize: none;
+ padding: 5px;
+ resize: vertical;
+}
+
+#attachButton {
+ position: absolute;
+ cursor: pointer;
+ margin-top: 3px;
+ right: 0px;
+ background: url('img/attach.png');
+ width: 25px;
+ height: 25px;
+ border: 0px;
}
.blink {
@@ -415,7 +429,8 @@ textarea, input {
.roomHeaderInfo {
text-align: right;
float: right;
- margin-top: 15px;
+ margin-top: 0px;
+ margin-right: 30px;
}
/*** Room Info Dialog ***/
@@ -449,15 +464,33 @@ textarea, input {
resize: vertical;
}
+/*** Control Buttons ***/
+#controlButtons {
+ float: right;
+ margin-right: -4px;
+ padding-bottom: 6px;
+}
+
+.controlButton {
+ cursor: pointer;
+ border: 0px;
+ width: 30px;
+ height: 30px;
+ margin-left: 3px;
+ margin-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 +506,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;
+ word-wrap: break-word;
+}
+
+.userPowerLevel {
+ position: absolute;
+ bottom: -1px;
+ height: 1px;
+ background-color: #f00;
}
-.userAvatar .userPowerLevel {
+.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 +573,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 +604,21 @@ textarea, input {
#messageTable td {
padding: 0px;
+/* border: 1px solid #888; */
}
.leftBlock {
- width: 14em;
+ width: 7em;
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 +629,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 +657,11 @@ textarea, input {
}
.avatarImage {
+ position: relative;
+ top: 5px;
object-fit: cover;
+ border-radius: 32px;
+ margin-top: 4px;
}
.emote {
@@ -591,6 +675,7 @@ textarea, input {
}
.image {
+ border: 1px solid #888;
display: block;
max-width:320px;
max-height:320px;
@@ -603,19 +688,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,8 +712,8 @@ textarea, input {
max-height: auto;
}
-.differentUser td {
- padding-bottom: 5px ! important;
+.differentUser .msg {
+ padding-top: 14px ! important;
}
.mine {
@@ -635,13 +724,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 +792,8 @@ textarea, input {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
+ padding-left: 0.5em;
+ padding-right: 0.5em;
}
.recentsRoom {
@@ -751,7 +844,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/components/matrix/event-handler-service.js b/syweb/webclient/components/matrix/event-handler-service.js
index a9c6eb34c7..f51031f4cd 100644
--- a/syweb/webclient/components/matrix/event-handler-service.js
+++ b/syweb/webclient/components/matrix/event-handler-service.js
@@ -299,10 +299,12 @@ function(matrixService, $rootScope, $q, $timeout, $filter, mPresence, notificati
* Return the display name of an user acccording to data already downloaded
* @param {String} room_id the room id
* @param {String} user_id the id of the user
+ * @param {boolean} wrap whether to insert whitespace into the userid (if displayname not available) to help it wrap
* @returns {String} the user displayname or user_id if not available
*/
- var getUserDisplayName = function(room_id, user_id) {
+ var getUserDisplayName = function(room_id, user_id, wrap) {
var displayName;
+ // XXX: this is getting called *way* too often - at least once per every room member per every digest...
// Get the user display name from the member list of the room
var member = modelService.getMember(room_id, user_id);
@@ -336,8 +338,16 @@ function(matrixService, $rootScope, $q, $timeout, $filter, mPresence, notificati
if (undefined === displayName) {
// By default, use the user ID
- displayName = user_id;
+ if (wrap && user_id.indexOf(':') >= 0) {
+ displayName = user_id.substr(0, user_id.indexOf(':')) + " " + user_id.substr(user_id.indexOf(':'));
+ }
+ else {
+ displayName = user_id;
+ }
}
+
+ //console.log("getUserDisplayName(" + room_id + ", " + user_id + ", " + wrap +") = " + displayName);
+
return displayName;
};
@@ -589,10 +599,11 @@ function(matrixService, $rootScope, $q, $timeout, $filter, mPresence, notificati
* Return the display name of an user acccording to data already downloaded
* @param {String} room_id the room id
* @param {String} user_id the id of the user
+ * @param {boolean} wrap whether to insert whitespace into the userid (if displayname not available) to help it wrap
* @returns {String} the user displayname or user_id if not available
*/
- getUserDisplayName: function(room_id, user_id) {
- return getUserDisplayName(room_id, user_id);
+ getUserDisplayName: function(room_id, user_id, wrap) {
+ return getUserDisplayName(room_id, user_id, wrap);
}
};
}]);
diff --git a/syweb/webclient/components/matrix/matrix-call.js b/syweb/webclient/components/matrix/matrix-call.js
index c13083298e..b560cf7daa 100644
--- a/syweb/webclient/components/matrix/matrix-call.js
+++ b/syweb/webclient/components/matrix/matrix-call.js
@@ -35,14 +35,14 @@ var forAllTracksOnStream = function(s, f) {
forAllAudioTracksOnStream(s, f);
}
-navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
-window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection; // but not mozRTCPeerConnection because its interface is not compatible
-window.RTCSessionDescription = window.RTCSessionDescription || window.webkitRTCSessionDescription || window.mozRTCSessionDescription;
-window.RTCIceCandidate = window.RTCIceCandidate || window.webkitRTCIceCandidate || window.mozRTCIceCandidate;
-
angular.module('MatrixCall', [])
.factory('MatrixCall', ['matrixService', 'matrixPhoneService', 'modelService', '$rootScope', '$timeout', function MatrixCallFactory(matrixService, matrixPhoneService, modelService, $rootScope, $timeout) {
$rootScope.isWebRTCSupported = function () {
+ navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
+ window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection; // but not mozRTCPeerConnection because its interface is not compatible
+ window.RTCSessionDescription = window.RTCSessionDescription || window.webkitRTCSessionDescription || window.mozRTCSessionDescription;
+ window.RTCIceCandidate = window.RTCIceCandidate || window.webkitRTCIceCandidate || window.mozRTCIceCandidate;
+
return !!(navigator.getUserMedia || window.RTCPeerConnection || window.RTCSessionDescription || window.RTCIceCandidate);
};
@@ -57,7 +57,7 @@ angular.module('MatrixCall', [])
this.candidateSendTries = 0;
var self = this;
- $rootScope.$watch(this.remoteVideoElement, function (oldValue, newValue) {
+ $rootScope.$watch(this.getRemoteVideoElement(), function (oldValue, newValue) {
self.tryPlayRemoteStream();
});
@@ -175,7 +175,8 @@ angular.module('MatrixCall', [])
this.state = 'ringing';
this.direction = 'inbound';
- if (window.mozRTCPeerConnection) {
+ // This also applied to the Safari OpenWebRTC extension so let's just do this all the time at least for now
+ //if (window.mozRTCPeerConnection) {
// firefox's RTCPeerConnection doesn't add streams until it starts getting media on them
// so we need to figure out whether a video channel has been offered by ourselves.
if (this.msg.offer.sdp.indexOf('m=video') > -1) {
@@ -183,7 +184,7 @@ angular.module('MatrixCall', [])
} else {
this.type = 'voice';
}
- }
+ //}
var self = this;
$timeout(function() {
@@ -251,8 +252,8 @@ angular.module('MatrixCall', [])
// pausing now keeps the last frame (ish) of the video call in the video element
// rather than it just turning black straight away
- if (this.remoteVideoElement) this.remoteVideoElement.pause();
- if (this.localVideoElement) this.localVideoElement.pause();
+ if (this.getRemoteVideoElement() && this.getRemoteVideoElement().pause) this.getRemoteVideoElement().pause();
+ if (this.getLocalVideoElement() && this.getLocalVideoElement().pause) this.getLocalVideoElement().pause();
this.stopAllMedia();
if (this.peerConn) this.peerConn.close();
@@ -277,11 +278,18 @@ angular.module('MatrixCall', [])
}
if (this.state == 'ended') return;
- if (this.localVideoElement && this.type == 'video') {
+ var videoEl = this.getLocalVideoElement();
+
+ if (videoEl && this.type == 'video') {
var vidTrack = stream.getVideoTracks()[0];
- this.localVideoElement.src = URL.createObjectURL(stream);
- this.localVideoElement.muted = true;
- this.localVideoElement.play();
+ videoEl.autoplay = true;
+ videoEl.src = URL.createObjectURL(stream);
+ videoEl.muted = true;
+ var self = this;
+ $timeout(function() {
+ var vel = self.getLocalVideoElement();
+ if (vel.play) vel.play();
+ });
}
this.localAVStream = stream;
@@ -305,11 +313,18 @@ angular.module('MatrixCall', [])
MatrixCall.prototype.gotUserMediaForAnswer = function(stream) {
if (this.state == 'ended') return;
- if (this.localVideoElement && this.type == 'video') {
+ var localVidEl = this.getLocalVideoElement();
+
+ if (localVidEl && this.type == 'video') {
+ localVidEl.autoplay = true;
var vidTrack = stream.getVideoTracks()[0];
- this.localVideoElement.src = URL.createObjectURL(stream);
- this.localVideoElement.muted = true;
- this.localVideoElement.play();
+ localVidEl.src = URL.createObjectURL(stream);
+ localVidEl.muted = true;
+ var self = this;
+ $timeout(function() {
+ var vel = self.getLocalVideoElement();
+ if (vel.play) vel.play();
+ });
}
this.localAVStream = stream;
@@ -338,11 +353,11 @@ angular.module('MatrixCall', [])
}
MatrixCall.prototype.gotRemoteIceCandidate = function(cand) {
- console.log("Got remote ICE "+cand.sdpMid+" candidate: "+cand.candidate);
if (this.state == 'ended') {
- console.log("Ignoring remote ICE candidate because call has ended");
+ //console.log("Ignoring remote ICE candidate because call has ended");
return;
}
+ console.log("Got remote ICE "+cand.sdpMid+" candidate: "+cand.candidate);
this.peerConn.addIceCandidate(new RTCIceCandidate(cand), function() {}, function(e) {});
};
@@ -362,41 +377,46 @@ angular.module('MatrixCall', [])
return;
}
- this.peerConn.setLocalDescription(description);
-
- var content = {
- version: 0,
- call_id: this.call_id,
- offer: description,
- lifetime: MatrixCall.CALL_TIMEOUT
- };
- this.sendEventWithRetry('m.call.invite', content);
-
var self = this;
- $timeout(function() {
- if (self.state == 'invite_sent') {
- self.hangup('invite_timeout');
- }
- }, MatrixCall.CALL_TIMEOUT);
+ this.peerConn.setLocalDescription(description, function() {
+ var content = {
+ version: 0,
+ call_id: self.call_id,
+ // OpenWebRTC appears to add extra stuff (like the DTLS fingerprint) to the description
+ // when setting it on the peerconnection. According to the spec it should only add ICE
+ // candidates. Any ICE candidates that have already been generated at this point will
+ // probably be sent both in the offer and separately. Ho hum.
+ offer: self.peerConn.localDescription,
+ lifetime: MatrixCall.CALL_TIMEOUT
+ };
+ self.sendEventWithRetry('m.call.invite', content);
+
+ $timeout(function() {
+ if (self.state == 'invite_sent') {
+ self.hangup('invite_timeout');
+ }
+ }, MatrixCall.CALL_TIMEOUT);
- $rootScope.$apply(function() {
- self.state = 'invite_sent';
- });
+ $rootScope.$apply(function() {
+ self.state = 'invite_sent';
+ });
+ }, function() { console.log("Error setting local description!"); });
};
MatrixCall.prototype.createdAnswer = function(description) {
console.log("Created answer: "+description);
- this.peerConn.setLocalDescription(description);
- var content = {
- version: 0,
- call_id: this.call_id,
- answer: description
- };
- this.sendEventWithRetry('m.call.answer', content);
var self = this;
- $rootScope.$apply(function() {
- self.state = 'connecting';
- });
+ this.peerConn.setLocalDescription(description, function() {
+ var content = {
+ version: 0,
+ call_id: self.call_id,
+ answer: self.peerConn.localDescription
+ };
+ self.sendEventWithRetry('m.call.answer', content);
+ $rootScope.$apply(function() {
+ self.state = 'connecting';
+ });
+ }, function() { console.log("Error setting local description!"); } );
};
MatrixCall.prototype.getLocalOfferFailed = function(error) {
@@ -464,10 +484,15 @@ angular.module('MatrixCall', [])
};
MatrixCall.prototype.tryPlayRemoteStream = function(event) {
- if (this.remoteVideoElement && this.remoteAVStream) {
- var player = this.remoteVideoElement;
+ if (this.getRemoteVideoElement() && this.remoteAVStream) {
+ var player = this.getRemoteVideoElement();
+ player.autoplay = true;
player.src = URL.createObjectURL(this.remoteAVStream);
- player.play();
+ var self = this;
+ $timeout(function() {
+ var vel = self.getRemoteVideoElement();
+ if (vel.play) vel.play();
+ });
}
};
@@ -499,8 +524,8 @@ angular.module('MatrixCall', [])
MatrixCall.prototype.onHangupReceived = function(msg) {
console.log("Hangup received");
- if (this.remoteVideoElement) this.remoteVideoElement.pause();
- if (this.localVideoElement) this.localVideoElement.pause();
+ if (this.getRemoteVideoElement() && this.getRemoteVideoElement().pause) this.getRemoteVideoElement().pause();
+ if (this.getLocalVideoElement() && this.getLocalVideoElement().pause) this.getLocalVideoElement().pause();
this.state = 'ended';
this.hangupParty = 'remote';
this.hangupReason = msg.reason;
@@ -523,8 +548,8 @@ angular.module('MatrixCall', [])
newCall.gotUserMediaForAnswer(this.localAVStream);
delete(this.localAVStream);
}
- newCall.localVideoElement = this.localVideoElement;
- newCall.remoteVideoElement = this.remoteVideoElement;
+ newCall.localVideoSelector = this.localVideoSelector;
+ newCall.remoteVideoSelector = this.remoteVideoSelector;
this.successor = newCall;
this.hangup(true);
};
@@ -600,5 +625,21 @@ angular.module('MatrixCall', [])
}, delayMs);
};
+ MatrixCall.prototype.getLocalVideoElement = function() {
+ if (this.localVideoSelector) {
+ var t = angular.element(this.localVideoSelector);
+ if (t.length) return t[0];
+ }
+ return null;
+ };
+
+ MatrixCall.prototype.getRemoteVideoElement = function() {
+ if (this.remoteVideoSelector) {
+ var t = angular.element(this.remoteVideoSelector);
+ if (t.length) return t[0];
+ }
+ return null;
+ };
+
return MatrixCall;
}]);
diff --git a/syweb/webclient/components/matrix/matrix-filter.js b/syweb/webclient/components/matrix/matrix-filter.js
index aeebedc784..69de97b055 100644
--- a/syweb/webclient/components/matrix/matrix-filter.js
+++ b/syweb/webclient/components/matrix/matrix-filter.js
@@ -114,7 +114,7 @@ function($rootScope, matrixService, eventHandlerService, modelService) {
// Return the user display name
.filter('mUserDisplayName', ['eventHandlerService', function(eventHandlerService) {
- return function(user_id, room_id) {
- return eventHandlerService.getUserDisplayName(room_id, user_id);
+ return function(user_id, room_id, wrap) {
+ return eventHandlerService.getUserDisplayName(room_id, user_id, wrap);
};
}]);
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 992e8d3377..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>
@@ -53,8 +55,8 @@
<div id="videoBackground" ng-class="videoMode">
<div id="videoContainer" ng-class="videoMode">
<div id="videoContainerPadding"></div>
- <video id="localVideo" ng-class="[videoMode, currentCall.state]" ng-show="currentCall && currentCall.type == 'video' && (currentCall.state == 'connected' || currentCall.state == 'connecting' || currentCall.state == 'invite_sent' || currentCall.state == 'ended')"></video>
- <video id="remoteVideo" ng-class="[videoMode, currentCall.state]" ng-show="currentCall && currentCall.type == 'video' && (currentCall.state == 'connected' || (currentCall.state == 'ended' && currentCall.didConnect))"></video>
+ <div ng-class="[videoMode, currentCall.state]" ng-show="currentCall && currentCall.type == 'video' && (currentCall.state == 'connected' || currentCall.state == 'connecting' || currentCall.state == 'invite_sent' || currentCall.state == 'ended')"><video id="localVideo"></video></div>
+ <div ng-class="[videoMode, currentCall.state]" ng-show="currentCall && currentCall.type == 'video' && (currentCall.state == 'connected' || (currentCall.state == 'ended' && currentCall.didConnect))"><video id="remoteVideo"></video></div>
</div>
</div>
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 d3fb85b9dc..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,11 +929,13 @@ 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;
- call.localVideoElement = angular.element('#localVideo')[0];
- call.remoteVideoElement = angular.element('#remoteVideo')[0];
+ call.localVideoSelector = '#localVideo';
+ call.remoteVideoSelector = '#remoteVideo';
call.placeVideoCall();
$rootScope.currentCall = call;
};
diff --git a/syweb/webclient/room/room.html b/syweb/webclient/room/room.html
index e59cc30edc..430a37afd4 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">
@@ -74,8 +103,8 @@
Set Topic
</button>
<div ng-show="room.current_room_state.state_events['m.room.topic'].content.topic || topic.isEditing">
- <div ng-hide="topic.isEditing" ng-dblclick="topic.editTopic()" id="roomTopic">
- {{ room.current_room_state.state_events['m.room.topic'].content.topic | limitTo: 200}}
+ <div ng-hide="topic.isEditing" ng-dblclick="topic.editTopic()" id="roomTopic"
+ ng-bind-html="room.current_room_state.state_events['m.room.topic'].content.topic | limitTo: 200 | linky:'_blank'">
</div>
<form ng-submit="topic.updateTopic()" ng-show="topic.isEditing" class="roomTopicForm">
<input ng-model="topic.newTopicText" ng-blur="topic.cancelEdit()" class="roomTopicInput" placeholder="Topic"/>
@@ -91,32 +120,24 @@
<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>
+ {{ member.id | mUserDisplayName:room_id:true }}
+ <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"
@@ -125,20 +146,21 @@
keep-scroll>
<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>
+ ng-class="(room.events[$index - 1].user_id !== msg.user_id ? 'differentUser' : '') + (msg.user_id === state.user_id ? ' mine' : '')" scroll-item>
+ <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:true }}</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 class="msg" 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 +244,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" }}
diff --git a/syweb/webclient/test/karma.conf.js b/syweb/webclient/test/karma.conf.js
index 7ce958adc9..5f0642ca33 100644
--- a/syweb/webclient/test/karma.conf.js
+++ b/syweb/webclient/test/karma.conf.js
@@ -22,6 +22,8 @@ module.exports = function(config) {
'../js/angular-route.js',
'../js/angular-animate.js',
'../js/angular-sanitize.js',
+ '../js/jquery.peity.min.js',
+ '../js/angular-peity.js',
'../js/ng-infinite-scroll-matrix.js',
'../js/ui-bootstrap*',
'../js/elastic.js',
|