Skip to content
Snippets Groups Projects
Commit 5d273a0c authored by Mark Haines's avatar Mark Haines
Browse files

Remove syweb directory. pull in syweb as a dependency from github

parent da6df07a
No related branches found
No related tags found
No related merge requests found
Showing
with 4 additions and 9774 deletions
......@@ -26,12 +26,13 @@ def read(fname):
return open(os.path.join(os.path.dirname(__file__), fname)).read()
setup(
name="SynapseHomeServer",
version="0.0.1",
name="synapse",
version=read("VERSION"),
packages=find_packages(exclude=["tests", "tests.*"]),
description="Reference Synapse Home Server",
install_requires=[
"syutil==0.0.2",
"syweb==0.0.1",
"Twisted>=14.0.0",
"service_identity>=1.0.0",
"pyopenssl>=0.14",
......@@ -44,6 +45,7 @@ setup(
dependency_links=[
"https://github.com/matrix-org/syutil/tarball/v0.0.2#egg=syutil-0.0.2",
"https://github.com/pyca/pynacl/tarball/52dbe2dc33f1#egg=pynacl-0.3.0",
"https://github.com/matrix-org/matrix-angular-sdk/tarball/master/#egg=syweb-0.0.1",
],
setup_requires=[
"setuptools_trial",
......
Captcha can be enabled for this web client / home server. This file explains how to do that.
The captcha mechanism used is Google's ReCaptcha. This requires API keys from Google.
Getting keys
------------
Requires a public/private key pair from:
https://developers.google.com/recaptcha/
Setting Private ReCaptcha Key
-----------------------------
The private key is a config option on the home server config. If it is not
visible, you can generate it via --generate-config. Set the following value:
recaptcha_private_key: YOUR_PRIVATE_KEY
In addition, you MUST enable captchas via:
enable_registration_captcha: true
Setting Public ReCaptcha Key
----------------------------
The web client will look for the global variable webClientConfig for config
options. You should put your ReCaptcha public key there like so:
webClientConfig = {
useCaptcha: true,
recaptcha_public_key: "YOUR_PUBLIC_KEY"
}
This should be put in webclient/config.js which is already .gitignored, rather
than in the web client source files. You MUST set useCaptcha to true else a
ReCaptcha widget will not be generated.
Configuring IP used for auth
----------------------------
The ReCaptcha API requires that the IP address of the user who solved the
captcha is sent. If the client is connecting through a proxy or load balancer,
it may be required to use the X-Forwarded-For (XFF) header instead of the origin
IP address. This can be configured as an option on the home server like so:
captcha_ip_origin_is_x_forwarded: true
Basic Usage
-----------
The web client should automatically run when running the home server.
Alternatively, you can run it stand-alone:
$ python -m SimpleHTTPServer
Then, open this URL in a WEB browser::
http://127.0.0.1:8000/
/*
Copyright 2014 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
* Main controller
*/
'use strict';
angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'eventStreamService'])
.controller('MatrixWebClientController', ['$scope', '$location', '$rootScope', '$timeout', 'matrixService', 'mPresence', 'eventStreamService', 'eventHandlerService', 'matrixPhoneService', 'modelService',
function($scope, $location, $rootScope, $timeout, matrixService, mPresence, eventStreamService, eventHandlerService, matrixPhoneService, modelService) {
// Check current URL to avoid to display the logout button on the login page
$scope.location = $location.path();
// Update the location state when the ng location changed
$rootScope.$on('$routeChangeSuccess', function (event, current, previous) {
$scope.location = $location.path();
});
if (matrixService.isUserLoggedIn()) {
eventStreamService.resume();
mPresence.start();
}
$scope.user_id;
var config = matrixService.config();
if (config) {
$scope.user_id = matrixService.config().user_id;
}
/**
* Open a given page.
* @param {String} url url of the page
*/
$rootScope.goToPage = function(url) {
$location.url(url);
};
// Open the given user profile page
$scope.goToUserPage = function(user_id) {
if (user_id === $scope.user_id) {
$location.url("/settings");
}
else {
$location.url("/user/" + user_id);
}
};
$scope.leave = function(room_id) {
matrixService.leave(room_id).then(
function(response) {
console.log("Left room " + room_id);
},
function(error) {
console.log("Failed to leave room " + room_id + ": " + error.data.error);
});
};
// Logs the user out
$scope.logout = function() {
// kill the event stream
eventStreamService.stop();
// Do not update presence anymore
mPresence.stop();
// Clean permanent data
matrixService.setConfig({});
matrixService.saveConfig();
// Reset cached data
modelService.clearRooms();
eventHandlerService.reset();
// And go to the login page
$location.url("login");
};
// Listen to the event indicating that the access token is no longer valid.
// In this case, the user needs to log in again.
$scope.$on("M_UNKNOWN_TOKEN", function() {
console.log("Invalid access token -> log user out");
$scope.logout();
});
$rootScope.updateHeader = function() {
$scope.user_id = matrixService.config().user_id;
};
$rootScope.$watch('currentCall', function(newVal, oldVal) {
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
if (angular.element('#localVideo')[0].load) angular.element('#localVideo')[0].load();
if (angular.element('#remoteVideo')[0].load) angular.element('#remoteVideo')[0].load();
return;
}
var roomMembers = angular.copy(modelService.getRoom($rootScope.currentCall.room_id).current_room_state.members);
delete roomMembers[matrixService.config().user_id];
$rootScope.currentCall.user_id = Object.keys(roomMembers)[0];
// set it to the user ID until we fetch the display name
$rootScope.currentCall.userProfile = { displayname: $rootScope.currentCall.user_id };
matrixService.getProfile($rootScope.currentCall.user_id).then(
function(response) {
if (response.data.displayname) $rootScope.currentCall.userProfile.displayname = response.data.displayname;
if (response.data.avatar_url) $rootScope.currentCall.userProfile.avatar_url = response.data.avatar_url;
},
function(error) {
$scope.feedback = "Can't load user profile";
}
);
});
$rootScope.$watch('currentCall.state', function(newVal, oldVal) {
if (newVal == 'ringing') {
angular.element('#ringbackAudio')[0].pause();
angular.element('#ringAudio')[0].load();
angular.element('#ringAudio')[0].play();
} else if (newVal == 'invite_sent') {
angular.element('#ringAudio')[0].pause();
angular.element('#ringbackAudio')[0].load();
angular.element('#ringbackAudio')[0].play();
} else if (newVal == 'ended' && oldVal == 'connected') {
angular.element('#ringAudio')[0].pause();
angular.element('#ringbackAudio')[0].pause();
angular.element('#callendAudio')[0].play();
$scope.videoMode = undefined;
} else if (newVal == 'ended' && oldVal == 'invite_sent' && $rootScope.currentCall.hangupParty == 'remote') {
angular.element('#ringAudio')[0].pause();
angular.element('#ringbackAudio')[0].pause();
angular.element('#busyAudio')[0].play();
} else if (newVal == 'ended' && oldVal == 'invite_sent' && $rootScope.currentCall.hangupParty == 'local' && $rootScope.currentCall.hangupReason == 'invite_timeout') {
angular.element('#ringAudio')[0].pause();
angular.element('#ringbackAudio')[0].pause();
angular.element('#busyAudio')[0].play();
} else if (oldVal == 'invite_sent') {
angular.element('#ringbackAudio')[0].pause();
} else if (oldVal == 'ringing') {
angular.element('#ringAudio')[0].pause();
} else if (newVal == 'connected') {
$timeout(function() {
if ($scope.currentCall.type == 'video') $scope.videoMode = 'large';
}, 500);
}
if ($rootScope.currentCall && $rootScope.currentCall.type == 'video' && $rootScope.currentCall.state != 'connected') {
$scope.videoMode = 'mini';
}
});
$rootScope.$watch('currentCall.type', function(newVal, oldVal) {
// need to listen for this too as the type of the call won't be know when it's created
if ($rootScope.currentCall && $rootScope.currentCall.type == 'video' && $rootScope.currentCall.state != 'connected') {
$scope.videoMode = 'mini';
}
});
$rootScope.$on(matrixPhoneService.INCOMING_CALL_EVENT, function(ngEvent, call) {
console.log("incoming call");
if ($rootScope.currentCall && $rootScope.currentCall.state != 'ended') {
console.log("rejecting call because we're already in a call");
call.hangup();
return;
}
call.onError = $scope.onCallError;
call.onHangup = $scope.onCallHangup;
call.localVideoSelector = '#localVideo';
call.remoteVideoSelector = '#remoteVideo';
$rootScope.currentCall = call;
});
$rootScope.$on(matrixPhoneService.REPLACED_CALL_EVENT, function(ngEvent, oldCall, newCall) {
console.log("call ID "+oldCall.call_id+" has been replaced by call ID "+newCall.call_id+"!");
newCall.onError = $scope.onCallError;
newCall.onHangup = $scope.onCallHangup;
$rootScope.currentCall = newCall;
});
$scope.answerCall = function() {
$rootScope.currentCall.answer();
};
$scope.hangupCall = function() {
$rootScope.currentCall.hangup();
};
$rootScope.onCallError = function(errStr) {
$scope.feedback = errStr;
};
$rootScope.onCallHangup = function(call) {
if (call == $rootScope.currentCall) {
$timeout(function(){
if (call == $rootScope.currentCall) $rootScope.currentCall = undefined;
}, 4070);
}
};
}]);
/*
Copyright 2014 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
angular.module('matrixWebClient')
.directive('ngEnter', function () {
return function (scope, element, attrs) {
element.bind("keydown keypress", function (event) {
if(event.which === 13) {
scope.$apply(function () {
scope.$eval(attrs.ngEnter);
});
event.preventDefault();
}
});
};
})
.directive('ngFocus', ['$timeout', function($timeout) {
return {
link: function(scope, element, attr) {
// XXX: slightly evil hack to disable autofocus on iOS, as in general
// it causes more problems than it fixes, by bouncing the page
// around
if (!/(iPad|iPhone|iPod)/g.test(navigator.userAgent)) {
$timeout(function() { element[0].focus(); }, 0);
}
}
};
}])
.directive('asjson', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, ngModelCtrl) {
function isValidJson(model) {
var flag = true;
try {
angular.fromJson(model);
} catch (err) {
flag = false;
}
return flag;
};
function string2JSON(text) {
try {
var j = angular.fromJson(text);
ngModelCtrl.$setValidity('json', true);
return j;
} catch (err) {
//returning undefined results in a parser error as of angular-1.3-rc.0, and will not go through $validators
//return undefined
ngModelCtrl.$setValidity('json', false);
return text;
}
};
function JSON2String(object) {
return angular.toJson(object, true);
};
//$validators is an object, where key is the error
//ngModelCtrl.$validators.json = isValidJson;
//array pipelines
ngModelCtrl.$parsers.push(string2JSON);
ngModelCtrl.$formatters.push(JSON2String);
}
}
});
/*
Copyright 2014 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
angular.module('matrixWebClient')
.filter('duration', function() {
return function(time) {
if (!time) return;
var t = parseInt(time / 1000);
var s = t % 60;
var m = parseInt(t / 60) % 60;
var h = parseInt(t / (60 * 60)) % 24;
var d = parseInt(t / (60 * 60 * 24));
if (t < 60) {
if (t < 0) {
return "0s";
}
return s + "s";
}
if (t < 60 * 60) {
return m + "m"; // + s + "s";
}
if (t < 24 * 60 * 60) {
return h + "h"; // + m + "m";
}
return d + "d "; // + h + "h";
};
})
.filter('orderMembersList', function($sce) {
return function(members) {
var filtered = [];
var displayNames = {};
angular.forEach(members, function(value, key) {
value["id"] = key;
filtered.push( value );
});
filtered.sort(function (a, b) {
// Sort members on their last_active absolute time
a = a.user;
b = b.user;
var aLastActiveTS = 0, bLastActiveTS = 0;
if (a && a.event && a.event.content && a.event.content.last_active_ago !== undefined) {
aLastActiveTS = a.last_updated - a.event.content.last_active_ago;
}
if (b && b.event && b.event.content && b.event.content.last_active_ago !== undefined) {
bLastActiveTS = b.last_updated - b.event.content.last_active_ago;
}
if (aLastActiveTS || bLastActiveTS) {
return bLastActiveTS - aLastActiveTS;
}
else {
// If they do not have last_active_ago, sort them according to their presence state
// Online users go first amongs members who do not have last_active_ago
var presenceLevels = {
offline: 1,
unavailable: 2,
online: 4,
free_for_chat: 3
};
var aPresence = (a && a.event && a.event.content.presence in presenceLevels) ? presenceLevels[a.event.content.presence] : 0;
var bPresence = (b && b.event && b.event.content.presence in presenceLevels) ? presenceLevels[b.event.content.presence] : 0;
return bPresence - aPresence;
}
});
return filtered;
};
})
.filter('unsafe', ['$sce', function($sce) {
return function(text) {
return $sce.trustAsHtml(text);
};
}])
// Exactly the same as ngSanitize's linky but instead of pushing sanitized
// text in the addText function, we just push the raw text.
.filter('unsanitizedLinky', ['$sanitize', function($sanitize) {
var LINKY_URL_REGEXP =
/((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"]/,
MAILTO_REGEXP = /^mailto:/;
return function(text, target) {
if (!text) return text;
var match;
var raw = text;
var html = [];
var url;
var i;
while ((match = raw.match(LINKY_URL_REGEXP))) {
// We can not end in these as they are sometimes found at the end of the sentence
url = match[0];
// if we did not match ftp/http/mailto then assume mailto
if (match[2] == match[3]) url = 'mailto:' + url;
i = match.index;
addText(raw.substr(0, i));
addLink(url, match[0].replace(MAILTO_REGEXP, ''));
raw = raw.substring(i + match[0].length);
}
addText(raw);
return $sanitize(html.join(''));
function addText(text) {
if (!text) {
return;
}
html.push(text);
}
function addLink(url, text) {
html.push('<a ');
if (angular.isDefined(target)) {
html.push('target="');
html.push(target);
html.push('" ');
}
html.push('href="');
html.push(url);
html.push('">');
addText(text);
html.push('</a>');
}
};
}]);
/** Common layout **/
html {
height: 100%;
}
body {
height: 100%;
font-family: "Myriad Pro", "Myriad", Helvetica, Arial, sans-serif;
font-size: 12pt;
margin: 0px;
}
h1 {
font-size: 20pt;
}
a:link { color: #666; }
a:visited { color: #666; }
a:hover { color: #000; }
a:active { color: #000; }
textarea, input {
font-family: inherit;
font-size: inherit;
}
.page {
min-height: 100%;
margin-bottom: -32px; /* to make room for the footer */
}
#wrapper {
margin: auto;
max-width: 1280px;
padding-top: 40px;
padding-bottom: 40px;
padding-left: 20px;
padding-right: 20px;
}
#unsupportedBrowser {
padding-top: 240px;
text-align: center;
}
#header
{
position: absolute;
z-index: 2;
top: 0px;
width: 100%;
background-color: #333;
height: 32px;
}
#callBar {
float: left;
height: 32px;
margin: auto;
text-align: right;
line-height: 16px;
}
.callIcon {
margin-left: 4px;
margin-right: 4px;
margin-top: 8px;
transition: transform linear 0.5s;
transition: -webkit-transform linear 0.5s;
}
.callIcon.ended {
transform: rotateZ(45deg);
-webkit-transform: rotateZ(45deg);
filter: hue-rotate(-90deg);
-webkit-filter: hue-rotate(-90deg);
}
#callPeerImage {
width: 32px;
height: 32px;
border: none;
float: left;
}
#callPeerNameAndState {
float: left;
margin-left: 4px;
}
#callState {
font-size: 60%;
}
#callPeerName {
font-size: 80%;
}
#videoBackground {
position: absolute;
height: 100%;
width: 100%;
top: 0px;
left: 0px;
z-index: 1;
background-color: rgba(0,0,0,0.0);
pointer-events: none;
transition: background-color linear 500ms;
}
#videoBackground.large {
background-color: rgba(0,0,0,0.85);
pointer-events: auto;
}
#videoContainer {
position: relative;
top: 32px;
max-width: 1280px;
margin: auto;
}
#videoContainerPadding {
width: 1280px;
}
#localVideo {
position: absolute;
width: 128px;
height: 72px;
z-index: 1;
transition: left linear 500ms, top linear 500ms, width linear 500ms, height linear 500ms;
}
.mini #localVideo {
top: 0px;
left: 130px;
}
.large #localVideo {
top: 70px;
left: 20px;
}
.ended #localVideo {
-webkit-filter: grayscale(1);
filter: grayscale(1);
}
#remoteVideo {
position: relative;
height: auto;
transition: left linear 500ms, top linear 500ms, width linear 500ms, height linear 500ms;
}
.mini #remoteVideo {
left: 260px;
top: 0px;
width: 128px;
}
.large #remoteVideo {
left: 0px;
top: 50px;
width: 100%;
}
.ended #remoteVideo {
-webkit-filter: grayscale(1);
filter: grayscale(1);
}
#headerContent {
color: #ccc;
max-width: 1280px;
margin: auto;
text-align: right;
height: 32px;
line-height: 32px;
position: relative;
}
#headerContent a:link,
#headerContent a:visited,
#headerContent a:hover,
#headerContent a:active {
color: #fff;
}
#footer
{
width: 100%;
border-top: #666 1px solid;
background-color: #aaa;
height: 32px;
}
#footerContent
{
font-size: 8pt;
color: #fff;
max-width: 1280px;
margin: auto;
text-align: center;
height: 32px;
line-height: 32px;
}
#genericHeading
{
margin-top: 13px;
}
#feedback {
color: #800;
}
.mouse-pointer {
cursor: pointer;
}
.invited {
opacity: 0.2;
}
/*** Login Pages ***/
.loginWrapper {
text-align: center;
}
#recaptcha_area {
margin: auto
}
#loginForm {
text-align: left;
padding: 1em;
margin-bottom: 40px;
display: inline-block;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
-webkit-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
-moz-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
background-color: #f8f8f8;
border: 1px #ccc solid;
}
#loginForm input[type='radio'] {
margin-right: 1em;
}
#serverConfig {
text-align: center;
}
#serverConfig,
#serverConfig input,
#serverConfig button
{
font-size: 10pt ! important;
}
.smallPrint {
color: #888;
font-size: 9pt ! important;
font-style: italic ! important;
}
#serverConfig label {
display: inline-block;
text-align: right;
margin-right: 0.5em;
width: 7em;
}
#loginForm,
#loginForm input,
#loginForm button,
#loginForm select {
font-size: 18px;
}
/*** Room page ***/
#roomPage {
position: absolute;
top: 120px;
bottom: 120px;
left: 20px;
right: 20px;
}
#roomWrapper {
margin: auto;
max-width: 1280px;
height: 100%;
}
#roomHeader {
margin: auto;
padding-left: 20px;
padding-right: 20px;
padding-top: 53px;
max-width: 1280px;
}
#controlPanel {
position: absolute;
bottom: 0px;
width: 100%;
height: 70px;
background-color: #f8f8f8;
border-top: #aaa 1px solid;
}
#controls {
max-width: 1280px;
padding: 12px;
padding-right: 42px;
margin: auto;
position: relative;
}
#buttonsCell {
width: 150px;
}
#inputBarTable {
width: 100%;
}
#inputBarTable tr td {
padding: 1px 4px;
}
#mainInput {
width: 100%;
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 {
background-color: #faa;
}
.roomHighlight {
font-weight: bold;
}
.publicTable {
max-width: 480px;
width: 100%;
border-collapse: collapse;
}
.publicTable tr {
width: 100%;
}
.publicTable td {
vertical-align: text-top;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.publicRoomEntry {
max-width: 430px;
}
.publicRoomJoinedUsers {
width: 5em;
text-align: right;
font-size: 12px;
color: #888;
}
.publicRoomTopic {
color: #888;
font-size: 12px;
overflow: hidden;
padding-bottom: 5px;
border-bottom: 1px #ddd solid;
}
#roomName {
font-size: 16px;
text-align: right;
}
#roomTopic {
font-size: 13px;
text-align: right;
}
.roomNameInput, .roomTopicInput {
width: 100%;
}
.roomNameSection, .roomTopicSection {
text-align: right;
float: right;
width: 100%;
}
.roomNameSetNew, .roomTopicSetNew {
float: right;
}
.roomHeaderInfo {
text-align: right;
float: right;
margin-top: 0px;
margin-right: 30px;
}
/*** Room Info Dialog ***/
.room-info {
border-collapse: collapse;
width: 100%;
}
.room-info-event {
border-bottom: 1pt solid black;
}
.room-info-event-meta {
padding-top: 1em;
padding-bottom: 1em;
}
.room-info-event-content {
padding-top: 1em;
padding-bottom: 1em;
}
.monospace {
font-family: monospace;
}
.redact-button {
float: left
}
.room-info-textarea-content {
height: auto;
width: 100%;
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;
clear: right;
width: 101px;
height: 100%;
overflow-y: auto;
}
/*
#usersTable {
width: 100%;
border-collapse: collapse;
}
#usersTable td {
padding: 0px;
}
.userAvatar {
width: 80px;
height: 100px;
position: relative;
background-color: #000;
}
*/
.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%;
}
*/
.userName {
margin-top: 3px;
margin-bottom: 6px;
text-align: center;
font-size: 12px;
word-wrap: break-word;
}
.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;
background-color: #f00;
}
/*
.userPresence {
text-align: center;
font-size: 12px;
color: #fff;
background-color: #aaa;
border-bottom: 1px #ddd solid;
}
*/
.online {
border-color: #38AF00;
background-color: #38AF00;
}
.unavailable {
border-color: #FFCC00;
background-color: #FFCC00;
}
/*** Message table ***/
#messageTableWrapper {
height: 100%;
margin-right: 140px;
overflow-y: auto;
width: auto;
}
#messageTable {
margin: auto;
max-width: 1280px;
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
#messageTable td {
padding: 0px;
/* border: 1px solid #888; */
}
.leftBlock {
width: 7em;
word-wrap: break-word;
vertical-align: top;
background-color: #fff;
color: #aaa;
font-weight: medium;
font-size: 12px;
text-align: right;
/*
border-top: 1px #ddd solid;
*/
}
.rightBlock {
width: 32px;
color: #888;
line-height: 0;
vertical-align: top;
}
.sender, .timestamp {
/* padding-top: 3px;
*/}
.timestamp {
font-size: 10px;
color: #ccc;
height: 13px;
margin-top: 4px;
transition-property: opacity;
transition-duration: 0.3s;
}
.sender {
font-size: 12px;
/*
margin-top: 5px;
margin-bottom: -9px;
*/
}
.avatar {
width: 48px;
text-align: right;
vertical-align: top;
line-height: 0;
}
.avatarImage {
position: relative;
top: 5px;
object-fit: cover;
border-radius: 32px;
margin-top: 4px;
}
.emote {
background-color: transparent ! important;
border: 0px ! important;
}
.membership {
background-color: transparent ! important;
border: 0px ! important;
}
.image {
border: 1px solid #888;
display: block;
max-width:320px;
max-height:320px;
width: auto;
height: auto;
}
.text {
vertical-align: top;
}
.bubble {
/*
background-color: #eee;
border: 1px solid #d8d8d8;
margin-bottom: -1px;
padding-top: 7px;
padding-bottom: 5px;
-webkit-text-size-adjust:100%
vertical-align: middle;
*/
display: inline-block;
max-width: 80%;
padding-left: 1em;
padding-right: 1em;
padding-top: 2px;
padding-bottom: 2px;
font-size: 14px;
word-wrap: break-word;
}
.bubble img {
max-width: 100%;
max-height: auto;
}
.differentUser .msg {
padding-top: 14px ! important;
}
.mine {
text-align: right;
}
/*
.text.emote .bubble,
.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;
}
.bubble .message {
/* Wrap words and break lines on CR+LF */
white-space: pre-wrap;
}
.bubble .messagePending {
opacity: 0.3
}
.messageUnSent {
color: #F00;
}
.messageBing {
color: #00F;
}
#room-fullscreen-image {
position: absolute;
top: 0px;
height: 0px;
width: 100%;
height: 100%;
}
#room-fullscreen-image img {
max-width: 90%;
max-height: 90%;
bottom: 0;
left: 0;
margin: auto;
overflow: auto;
position: fixed;
right: 0;
top: 0;
-webkit-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.75);
-moz-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.75);
box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.75);
}
/*** Recents ***/
.recentsTable {
max-width: 480px;
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
.recentsTable tr {
width: 100%;
}
.recentsTable td {
vertical-align: text-top;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
padding-left: 0.5em;
padding-right: 0.5em;
}
.recentsRoom {
cursor: pointer;
}
.recentsRoom:hover {
background-color: #f8f8ff;
}
.recentsRoomSelected {
background-color: #eee;
}
.recentsRoomUnread {
background-color: #fee;
}
.recentsRoomBing {
background-color: #eef;
}
.recentsRoomName {
font-size: 16px;
padding-top: 7px;
width: auto;
}
.recentsPublicRoom {
font-weight: bold;
}
.recentsRoomSummaryUsersCount, .recentsRoomSummaryTS {
color: #888;
font-size: 12px;
width: 7em;
text-align: right;
}
.recentsRoomSummary {
color: #888;
font-size: 12px;
padding-bottom: 5px;
}
/* Do not show users count in the recents fragment displayed on the room page */
#roomPage .recentsRoomSummaryUsersCount {
width: 0em;
}
/*** Recents in the room page ***/
#roomRecentsTableWrapper {
float: left;
max-width: 320px;
padding-right: 10px;
margin-right: 10px;
height: 100%;
/* border-right: 1px solid #ddd; */
overflow-y: auto;
}
/*** Profile ***/
.profile-avatar {
width: 160px;
height: 160px;
display: table-cell;
vertical-align: middle;
text-align: center;
}
.profile-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
/*** User profile page ***/
#user-displayname {
font-size: 24px;
}
#user-displayname-input {
width: 160px;
max-width: 155px;
}
#user-save-button {
width: 160px;
font-size: 14px;
}
/*
Copyright 2014 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var matrixWebClient = angular.module('matrixWebClient', [
'ngRoute',
'MatrixWebClientController',
'LoginController',
'RegisterController',
'RoomController',
'HomeController',
'RecentsController',
'SettingsController',
'UserController',
'matrixService',
'matrixPhoneService',
'MatrixCall',
'eventStreamService',
'eventHandlerService',
'notificationService',
'recentsService',
'modelService',
'commandsService',
'infinite-scroll',
'ui.bootstrap',
'monospaced.elastic'
]);
matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider',
function($routeProvider, $provide, $httpProvider) {
$routeProvider.
when('/login', {
templateUrl: 'login/login.html'
}).
when('/register', {
templateUrl: 'login/register.html'
}).
when('/room/:room_id_or_alias', {
templateUrl: 'room/room.html'
}).
when('/room/', { // room URL with room alias in it (ex: http://127.0.0.1:8000/#/room/#public:localhost:8080) will come here.
// The reason is that 2nd hash key breaks routeProvider parameters cutting so that the URL will not match with
// the previous '/room/:room_id_or_alias' URL rule
templateUrl: 'room/room.html'
}).
when('/', {
templateUrl: 'home/home.html'
}).
when('/settings', {
templateUrl: 'settings/settings.html'
}).
when('/user/:user_matrix_id', {
templateUrl: 'user/user.html'
}).
otherwise({
redirectTo: '/'
});
$provide.factory('AccessTokenInterceptor', ['$q', '$rootScope',
function ($q, $rootScope) {
return {
responseError: function(rejection) {
if (rejection.status === 403 && "data" in rejection &&
"errcode" in rejection.data &&
rejection.data.errcode === "M_UNKNOWN_TOKEN") {
console.log("Got a 403 with an unknown token. Logging out.")
$rootScope.$broadcast("M_UNKNOWN_TOKEN");
}
return $q.reject(rejection);
}
};
}]);
$httpProvider.interceptors.push('AccessTokenInterceptor');
}]);
matrixWebClient.run(['$location', '$rootScope', 'matrixService', function($location, $rootScope, matrixService) {
// Check browser support
// Support IE from 9.0. AngularJS needs some tricks to run on IE8 and below
var version = parseFloat($.browser.version);
if ($.browser.msie && version < 9.0) {
$rootScope.unsupportedBrowser = {
browser: navigator.userAgent,
reason: "Internet Explorer is supported from version 9"
};
}
// The app requires localStorage
if(typeof(Storage) === "undefined") {
$rootScope.unsupportedBrowser = {
browser: navigator.userAgent,
reason: "It does not support HTML local storage"
};
}
// If user auth details are not in cache, go to the login page
if (!matrixService.isUserLoggedIn() &&
$location.path() !== "/login" &&
$location.path() !== "/register")
{
$location.path("login");
}
}]);
This diff is collapsed.
/*
Copyright 2014 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
/*
* Transform an element into an image file input button.
* Watch to the passed variable change. It will contain the selected HTML5 file object.
*/
angular.module('mFileInput', [])
.directive('mFileInput', function() {
return {
restrict: 'A',
transclude: 'true',
// FIXME: add back in accept="image/*" when needed - e.g. for avatars
template: '<div ng-transclude></div><input ng-hide="true" type="file"/>',
scope: {
selectedFile: '=mFileInput'
},
link: function(scope, element, attrs, ctrl) {
// Check if HTML5 file selection is supported
if (window.FileList) {
element.bind("click", function() {
element.find("input")[0].click();
element.find("input").bind("change", function(e) {
scope.selectedFile = this.files[0];
scope.$apply();
});
});
}
else {
setTimeout(function() {
element.attr("disabled", true);
element.attr("title", "The app uses the HTML5 File API to send files. Your browser does not support it.");
}, 1);
}
// Change the mouse icon on mouseover on this element
element.css("cursor", "pointer");
}
};
});
\ No newline at end of file
/*
Copyright 2014 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
// TODO determine if this is really required as a separate service to matrixService.
/*
* Upload an HTML5 file to a server
*/
angular.module('mFileUpload', ['matrixService', 'mUtilities'])
.service('mFileUpload', ['$q', 'matrixService', 'mUtilities', function ($q, matrixService, mUtilities) {
/*
* Upload an HTML5 file or blob to a server and returned a promise
* that will provide the URL of the uploaded file.
* @param {File|Blob} file the file data to send
*/
this.uploadFile = function(file) {
var deferred = $q.defer();
console.log("Uploading " + file.name + "... to /_matrix/content");
matrixService.uploadContent(file).then(
function(response) {
var content_url = response.data.content_token;
console.log(" -> Successfully uploaded! Available at " + content_url);
deferred.resolve(content_url);
},
function(error) {
console.log(" -> Failed to upload " + file.name);
deferred.reject(error);
}
);
return deferred.promise;
};
/*
* Upload an file plus generate a thumbnail of it (if possible) and upload it so that
* we will have all information to fulfill an file/image message request
* @param {File} file the file to send
* @param {Integer} thumbnailSize the max side size of the thumbnail to create
* @returns {promise} A promise that will be resolved by a message object
* ready to be send with the Matrix API
*/
this.uploadFileAndThumbnail = function(file, thumbnailSize) {
var self = this;
var deferred = $q.defer();
console.log("uploadFileAndThumbnail " + file.name + " - thumbnailSize: " + thumbnailSize);
// The message structure that will be returned in the promise will look something like:
var message = {
/*
msgtype: "m.image",
url: undefined,
body: "Image",
info: {
size: undefined,
w: undefined,
h: undefined,
mimetype: undefined
},
thumbnail_url: undefined,
thumbnail_info: {
size: undefined,
w: undefined,
h: undefined,
mimetype: undefined
}
*/
};
if (file.type.indexOf("image/") === 0) {
// it's an image - try to do clientside thumbnailing.
mUtilities.getImageSize(file).then(
function(size) {
console.log("image size: " + JSON.stringify(size));
// The final operation: send file
var uploadImage = function() {
self.uploadFile(file).then(
function(url) {
// Update message metadata
message.url = url;
message.msgtype = "m.image";
message.body = file.name;
message.info = {
size: file.size,
w: size.width,
h: size.height,
mimetype: file.type
};
// If there is no thumbnail (because the original image is smaller than thumbnailSize),
// reuse the original image info for thumbnail data
if (!message.thumbnail_url) {
message.thumbnail_url = message.url;
message.thumbnail_info = message.info;
}
// We are done
deferred.resolve(message);
},
function(error) {
console.log(" -> Can't upload image");
deferred.reject(error);
}
);
};
// Create a thumbnail if the image size exceeds thumbnailSize
if (Math.max(size.width, size.height) > thumbnailSize) {
console.log(" Creating thumbnail...");
mUtilities.resizeImage(file, thumbnailSize).then(
function(thumbnailBlob) {
// Get its size
mUtilities.getImageSize(thumbnailBlob).then(
function(thumbnailSize) {
console.log(" -> Thumbnail size: " + JSON.stringify(thumbnailSize));
// Upload it to the server
self.uploadFile(thumbnailBlob).then(
function(thumbnailUrl) {
// Update image message data
message.thumbnail_url = thumbnailUrl;
message.thumbnail_info = {
size: thumbnailBlob.size,
w: thumbnailSize.width,
h: thumbnailSize.height,
mimetype: thumbnailBlob.type
};
// Then, upload the original image
uploadImage();
},
function(error) {
console.log(" -> Can't upload thumbnail");
deferred.reject(error);
}
);
},
function(error) {
console.log(" -> Failed to get thumbnail size");
deferred.reject(error);
}
);
},
function(error) {
console.log(" -> Failed to create thumbnail: " + error);
deferred.reject(error);
}
);
}
else {
// No need of thumbnail
console.log(" Thumbnail is not required");
uploadImage();
}
},
function(error) {
console.log(" -> Failed to get image size");
deferred.reject(error);
}
);
}
else {
// it's a random file - just upload it.
self.uploadFile(file).then(
function(url) {
// Update message metadata
message.url = url;
message.msgtype = "m.file";
message.body = file.name;
message.info = {
size: file.size,
mimetype: file.type
};
// We are done
deferred.resolve(message);
},
function(error) {
console.log(" -> Can't upload file");
deferred.reject(error);
}
);
}
return deferred.promise;
};
}]);
/*
Copyright 2014 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
/*
This service contains logic for parsing and performing IRC style commands.
*/
angular.module('commandsService', [])
.factory('commandsService', ['$q', '$location', 'matrixService', 'modelService', function($q, $location, matrixService, modelService) {
// create a rejected promise with the given message
var reject = function(msg) {
var deferred = $q.defer();
deferred.reject({
data: {
error: msg
}
});
return deferred.promise;
};
// Change your nickname
var doNick = function(room_id, args) {
if (args) {
return matrixService.setDisplayName(args);
}
return reject("Usage: /nick <display_name>");
};
// Join a room
var doJoin = function(room_id, args) {
if (args) {
var matches = args.match(/^(\S+)$/);
if (matches) {
var room_alias = matches[1];
$location.url("room/" + room_alias);
// NB: We don't need to actually do the join, since that happens
// automatically if we are not joined onto a room already when
// the page loads.
return reject("Joining "+room_alias);
}
}
return reject("Usage: /join <room_alias>");
};
// Kick a user from the room with an optional reason
var doKick = function(room_id, args) {
if (args) {
var matches = args.match(/^(\S+?)( +(.*))?$/);
if (matches) {
return matrixService.kick(room_id, matches[1], matches[3]);
}
}
return reject("Usage: /kick <userId> [<reason>]");
};
// Ban a user from the room with an optional reason
var doBan = function(room_id, args) {
if (args) {
var matches = args.match(/^(\S+?)( +(.*))?$/);
if (matches) {
return matrixService.ban(room_id, matches[1], matches[3]);
}
}
return reject("Usage: /ban <userId> [<reason>]");
};
// Unban a user from the room
var doUnban = function(room_id, args) {
if (args) {
var matches = args.match(/^(\S+)$/);
if (matches) {
// Reset the user membership to "leave" to unban him
return matrixService.unban(room_id, matches[1]);
}
}
return reject("Usage: /unban <userId>");
};
// Define the power level of a user
var doOp = function(room_id, args) {
if (args) {
var matches = args.match(/^(\S+?)( +(\d+))?$/);
var powerLevel = 50; // default power level for op
if (matches) {
var user_id = matches[1];
if (matches.length === 4 && undefined !== matches[3]) {
powerLevel = parseInt(matches[3]);
}
if (powerLevel !== NaN) {
var powerLevelEvent = modelService.getRoom(room_id).current_room_state.state("m.room.power_levels");
return matrixService.setUserPowerLevel(room_id, user_id, powerLevel, powerLevelEvent);
}
}
}
return reject("Usage: /op <userId> [<power level>]");
};
// Reset the power level of a user
var doDeop = function(room_id, args) {
if (args) {
var matches = args.match(/^(\S+)$/);
if (matches) {
var powerLevelEvent = modelService.getRoom(room_id).current_room_state.state("m.room.power_levels");
return matrixService.setUserPowerLevel(room_id, args, undefined, powerLevelEvent);
}
}
return reject("Usage: /deop <userId>");
};
var commands = {
"nick": doNick,
"join": doJoin,
"kick": doKick,
"ban": doBan,
"unban": doUnban,
"op": doOp,
"deop": doDeop
};
return {
/**
* Process the given text for commands and perform them.
* @param {String} roomId The room in which the input was performed.
* @param {String} input The raw text input by the user.
* @return {Promise} A promise of the pending command, or null if the
* input is not a command.
*/
processInput: function(roomId, input) {
// trim any trailing whitespace, as it can confuse the parser for
// IRC-style commands
input = input.replace(/\s+$/, "");
if (input[0] === "/" && input[1] !== "/") {
var bits = input.match(/^(\S+?)( +(.*))?$/);
var cmd = bits[1].substring(1);
var args = bits[3];
if (commands[cmd]) {
return commands[cmd](roomId, args);
}
return reject("Unrecognised IRC-style command: " + cmd);
}
return null; // not a command
}
};
}]);
This diff is collapsed.
/*
Copyright 2014 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
/*
This service manages where in the event stream the web client currently is,
repolling the event stream, and provides methods to resume/pause/stop the event
stream. This service is not responsible for parsing event data. For that, see
the eventHandlerService.
*/
angular.module('eventStreamService', [])
.factory('eventStreamService', ['$q', '$timeout', 'matrixService', 'eventHandlerService', function($q, $timeout, matrixService, eventHandlerService) {
var END = "END";
var SERVER_TIMEOUT_MS = 30000;
var CLIENT_TIMEOUT_MS = 40000;
var ERR_TIMEOUT_MS = 5000;
var settings = {
from: "END",
to: undefined,
limit: undefined,
shouldPoll: true,
isActive: false
};
// interrupts the stream. Only valid if there is a stream conneciton
// open.
var interrupt = function(shouldPoll) {
console.log("[EventStream] interrupt("+shouldPoll+") "+
JSON.stringify(settings));
settings.shouldPoll = shouldPoll;
settings.isActive = false;
};
var saveStreamSettings = function() {
localStorage.setItem("streamSettings", JSON.stringify(settings));
};
var doEventStream = function(deferred) {
settings.shouldPoll = true;
settings.isActive = true;
deferred = deferred || $q.defer();
// run the stream from the latest token
matrixService.getEventStream(settings.from, SERVER_TIMEOUT_MS, CLIENT_TIMEOUT_MS).then(
function(response) {
if (!settings.isActive) {
console.log("[EventStream] Got response but now inactive. Dropping data.");
return;
}
settings.from = response.data.end;
console.log(
"[EventStream] Got response from "+settings.from+
" to "+response.data.end
);
eventHandlerService.handleEvents(response.data.chunk, true);
deferred.resolve(response);
if (settings.shouldPoll) {
$timeout(doEventStream, 0);
}
else {
console.log("[EventStream] Stopping poll.");
}
},
function(error) {
if (error.status === 403) {
settings.shouldPoll = false;
}
deferred.reject(error);
if (settings.shouldPoll) {
$timeout(doEventStream, ERR_TIMEOUT_MS);
}
else {
console.log("[EventStream] Stopping polling.");
}
}
);
return deferred.promise;
};
var startEventStream = function() {
settings.shouldPoll = true;
settings.isActive = true;
var deferred = $q.defer();
// Initial sync: get all information and the last 30 messages of all rooms of the user
// 30 messages should be enough to display a full page of messages in a room
// without requiring to make an additional request
matrixService.initialSync(30, false).then(
function(response) {
eventHandlerService.handleInitialSyncDone(response);
// Start event streaming from that point
settings.from = response.data.end;
doEventStream(deferred);
},
function(error) {
$scope.feedback = "Failure: " + error.data;
}
);
return deferred.promise;
};
return {
// expose these values for testing
SERVER_TIMEOUT: SERVER_TIMEOUT_MS,
CLIENT_TIMEOUT: CLIENT_TIMEOUT_MS,
// resume the stream from whereever it last got up to. Typically used
// when the page is opened.
resume: function() {
if (settings.isActive) {
console.log("[EventStream] Already active, ignoring resume()");
return;
}
console.log("[EventStream] resume "+JSON.stringify(settings));
return startEventStream();
},
// pause the stream. Resuming it will continue from the current position
pause: function() {
console.log("[EventStream] pause "+JSON.stringify(settings));
// kill any running stream
interrupt(false);
// save the latest token
saveStreamSettings();
},
// stop the stream and wipe the position in the stream. Typically used
// when logging out / logged out.
stop: function() {
console.log("[EventStream] stop "+JSON.stringify(settings));
// kill any running stream
interrupt(false);
// clear the latest token
settings.from = END;
saveStreamSettings();
}
};
}]);
This diff is collapsed.
/*
Copyright 2014 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
angular.module('matrixFilter', [])
// Compute the room name according to information we have
// TODO: It would be nice if this was stateless and had no dependencies. That would
// make the business logic here a lot easier to see.
.filter('mRoomName', ['$rootScope', 'matrixService', 'modelService', 'mUserDisplayNameFilter',
function($rootScope, matrixService, modelService, mUserDisplayNameFilter) {
return function(room_id) {
var roomName;
// If there is an alias, use it
// TODO: only one alias is managed for now
var alias = modelService.getRoomIdToAliasMapping(room_id);
var room = modelService.getRoom(room_id).current_room_state;
var room_name_event = room.state("m.room.name");
// Determine if it is a public room
var isPublicRoom = false;
if (room.state("m.room.join_rules") && room.state("m.room.join_rules").content) {
isPublicRoom = ("public" === room.state("m.room.join_rules").content.join_rule);
}
if (room_name_event) {
roomName = room_name_event.content.name;
}
else if (alias) {
roomName = alias;
}
else if (Object.keys(room.members).length > 0 && !isPublicRoom) { // Do not rename public room
var user_id = matrixService.config().user_id;
// this is a "one to one" room and should have the name of the other user.
if (Object.keys(room.members).length === 2) {
for (var i in room.members) {
if (!room.members.hasOwnProperty(i)) continue;
var member = room.members[i].event;
if (member.state_key !== user_id) {
roomName = mUserDisplayNameFilter(member.state_key, room_id);
if (!roomName) {
roomName = member.state_key;
}
break;
}
}
}
else if (Object.keys(room.members).length === 1) {
// this could be just us (self-chat) or could be the other person
// in a room if they have invited us to the room. Find out which.
var otherUserId = Object.keys(room.members)[0];
if (otherUserId === user_id) {
// it's us, we may have been invited to this room or it could
// be a self chat.
if (room.members[otherUserId].event.content.membership === "invite") {
// someone invited us, use the right ID.
roomName = mUserDisplayNameFilter(room.members[otherUserId].event.user_id, room_id);
if (!roomName) {
roomName = room.members[otherUserId].event.user_id;
}
}
else {
roomName = mUserDisplayNameFilter(otherUserId, room_id);
if (!roomName) {
roomName = user_id;
}
}
}
else { // it isn't us, so use their name if we know it.
roomName = mUserDisplayNameFilter(otherUserId, room_id);
if (!roomName) {
roomName = otherUserId;
}
}
}
else if (Object.keys(room.members).length === 0) {
// this shouldn't be possible
console.error("0 members in room >> " + room_id);
}
}
// Always show the alias in the room displayed name
if (roomName && alias && alias !== roomName) {
roomName += " (" + alias + ")";
}
if (undefined === roomName) {
// By default, use the room ID
roomName = room_id;
}
return roomName;
};
}])
// Return the user display name
.filter('mUserDisplayName', ['modelService', 'matrixService', function(modelService, matrixService) {
/**
* Return the display name of an user acccording to data already downloaded
* @param {String} user_id the id of the user
* @param {String} room_id the room id
* @param {boolean} wrap whether to insert whitespace into the userid (if displayname not available) to help it wrap
* @returns {String} A suitable display name for the user.
*/
return function(user_id, room_id, wrap) {
var displayName;
// Get the user display name from the member list of the room
var member = modelService.getMember(room_id, user_id);
if (member) {
member = member.event;
}
if (member && member.content.displayname) { // Do not consider null displayname
displayName = member.content.displayname;
// Disambiguate users who have the same displayname in the room
if (user_id !== matrixService.config().user_id) {
var room = modelService.getRoom(room_id);
for (var member_id in room.current_room_state.members) {
if (room.current_room_state.members.hasOwnProperty(member_id) && member_id !== user_id) {
var member2 = room.current_room_state.members[member_id].event;
if (member2.content.displayname && member2.content.displayname === displayName) {
displayName = displayName + " (" + user_id + ")";
break;
}
}
}
}
}
// The user may not have joined the room yet. So try to resolve display name from presence data
// Note: This data may not be available
if (undefined === displayName) {
var usr = modelService.getUser(user_id);
if (usr) {
displayName = usr.event.content.displayname;
}
}
if (undefined === displayName) {
// By default, use the 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;
}
}
return displayName;
};
}]);
/*
Copyright 2014 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
angular.module('matrixPhoneService', [])
.factory('matrixPhoneService', ['$rootScope', '$injector', 'matrixService', 'eventHandlerService', function MatrixPhoneService($rootScope, $injector, matrixService, eventHandlerService) {
var matrixPhoneService = function() {
};
matrixPhoneService.INCOMING_CALL_EVENT = "INCOMING_CALL_EVENT";
matrixPhoneService.REPLACED_CALL_EVENT = "REPLACED_CALL_EVENT";
matrixPhoneService.allCalls = {};
// a place to save candidates that come in for calls we haven't got invites for yet (when paginating backwards)
matrixPhoneService.candidatesByCall = {};
matrixPhoneService.callPlaced = function(call) {
matrixPhoneService.allCalls[call.call_id] = call;
};
$rootScope.$on(eventHandlerService.CALL_EVENT, function(ngEvent, event, isLive) {
if (event.user_id == matrixService.config().user_id) return;
var msg = event.content;
if (event.type == 'm.call.invite') {
if (event.age == undefined || msg.lifetime == undefined) {
// if the event doesn't have either an age (the HS is too old) or a lifetime
// (the sending client was too old when it sent it) then fall back to old behaviour
if (!isLive) return; // until matrix supports expiring messages
}
if (event.age > msg.lifetime) {
console.log("Ignoring expired call event of type "+event.type);
return;
}
var call = undefined;
if (!isLive) {
// if this event wasn't live then this call may already be over
call = matrixPhoneService.allCalls[msg.call_id];
if (call && call.state == 'ended') {
return;
}
}
var MatrixCall = $injector.get('MatrixCall');
var call = new MatrixCall(event.room_id);
if (!$rootScope.isWebRTCSupported()) {
console.log("Incoming call ID "+msg.call_id+" but this browser doesn't support WebRTC");
// don't hang up the call: there could be other clients connected that do support WebRTC and declining the
// the call on their behalf would be really annoying.
// instead, we broadcast a fake call event with a non-functional call object
$rootScope.$broadcast(matrixPhoneService.INCOMING_CALL_EVENT, call);
return;
}
call.call_id = msg.call_id;
call.initWithInvite(event);
matrixPhoneService.allCalls[call.call_id] = call;
// if we stashed candidate events for that call ID, play them back now
if (!isLive && matrixPhoneService.candidatesByCall[call.call_id] != undefined) {
for (var i = 0; i < matrixPhoneService.candidatesByCall[call.call_id].length; ++i) {
call.gotRemoteIceCandidate(matrixPhoneService.candidatesByCall[call.call_id][i]);
}
}
// Were we trying to call that user (room)?
var existingCall;
var callIds = Object.keys(matrixPhoneService.allCalls);
for (var i = 0; i < callIds.length; ++i) {
var thisCallId = callIds[i];
var thisCall = matrixPhoneService.allCalls[thisCallId];
if (call.room_id == thisCall.room_id && thisCall.direction == 'outbound'
&& (thisCall.state == 'wait_local_media' || thisCall.state == 'create_offer' || thisCall.state == 'invite_sent')) {
existingCall = thisCall;
break;
}
}
if (existingCall) {
// If we've only got to wait_local_media or create_offer and we've got an invite,
// pick the incoming call because we know we haven't sent our invite yet
// otherwise, pick whichever call has the lowest call ID (by string comparison)
if (existingCall.state == 'wait_local_media' || existingCall.state == 'create_offer' || existingCall.call_id > call.call_id) {
console.log("Glare detected: answering incoming call "+call.call_id+" and canceling outgoing call "+existingCall.call_id);
existingCall.replacedBy(call);
call.answer();
$rootScope.$broadcast(matrixPhoneService.REPLACED_CALL_EVENT, existingCall, call);
} else {
console.log("Glare detected: rejecting incoming call "+call.call_id+" and keeping outgoing call "+existingCall.call_id);
call.hangup();
}
} else {
$rootScope.$broadcast(matrixPhoneService.INCOMING_CALL_EVENT, call);
}
} else if (event.type == 'm.call.answer') {
var call = matrixPhoneService.allCalls[msg.call_id];
if (!call) {
console.log("Got answer for unknown call ID "+msg.call_id);
return;
}
call.receivedAnswer(msg);
} else if (event.type == 'm.call.candidates') {
var call = matrixPhoneService.allCalls[msg.call_id];
if (!call && isLive) {
console.log("Got candidates for unknown call ID "+msg.call_id);
return;
} else if (!call) {
if (matrixPhoneService.candidatesByCall[msg.call_id] == undefined) {
matrixPhoneService.candidatesByCall[msg.call_id] = [];
}
matrixPhoneService.candidatesByCall[msg.call_id] = matrixPhoneService.candidatesByCall[msg.call_id].concat(msg.candidates);
} else {
for (var i = 0; i < msg.candidates.length; ++i) {
call.gotRemoteIceCandidate(msg.candidates[i]);
}
}
} else if (event.type == 'm.call.hangup') {
var call = matrixPhoneService.allCalls[msg.call_id];
if (!call && isLive) {
console.log("Got hangup for unknown call ID "+msg.call_id);
} else if (!call) {
// if not live, store the fact that the call has ended because we're probably getting events backwards so
// the hangup will come before the invite
var MatrixCall = $injector.get('MatrixCall');
var call = new MatrixCall(event.room_id);
call.call_id = msg.call_id;
call.initWithHangup(event);
matrixPhoneService.allCalls[msg.call_id] = call;
} else {
call.onHangupReceived(msg);
delete(matrixPhoneService.allCalls[msg.call_id]);
}
}
});
return matrixPhoneService;
}]);
This diff is collapsed.
/*
Copyright 2014 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
/*
This service serves as the entry point for all models in the app. If access to
underlying data in a room is required, then this service should be used as the
dependency.
*/
// NB: This is more explicit than linking top-level models to $rootScope
// in that by adding this service as a dep you are clearly saying "this X
// needs access to the underlying data store", rather than polluting the
// $rootScope.
angular.module('modelService', [])
.factory('modelService', ['matrixService', function(matrixService) {
// alias / id lookups
var roomIdToAlias, aliasToRoomId;
var setRoomIdToAliasMapping = function(roomId, alias) {
roomIdToAlias[roomId] = alias;
aliasToRoomId[alias] = roomId;
};
// user > room member lookups
var userIdToRoomMember;
// main store
var rooms, users;
var init = function() {
roomIdToAlias = {};
aliasToRoomId = {};
userIdToRoomMember = {
// user_id: [RoomMember, RoomMember, ...]
};
// rooms are stored here when they come in.
rooms = {
// roomid: <Room>
};
users = {
// user_id: <User>
};
console.log("Models inited.");
};
init();
/***** Room Object *****/
var Room = function Room(room_id) {
this.room_id = room_id;
this.old_room_state = new RoomState();
this.current_room_state = new RoomState();
this.now = this.current_room_state; // makes html access shorter
this.events = []; // events which can be displayed on the UI. TODO move?
};
Room.prototype = {
addMessageEvents: function addMessageEvents(events, toFront) {
for (var i=0; i<events.length; i++) {
this.addMessageEvent(events[i], toFront);
}
},
addMessageEvent: function addMessageEvent(event, toFront) {
// every message must reference the RoomMember which made it *at
// that time* so things like display names display correctly.
var stateAtTheTime = toFront ? this.old_room_state : this.current_room_state;
event.__room_member = stateAtTheTime.getStateEvent("m.room.member", event.user_id);
if (event.type === "m.room.member" && event.content.membership === "invite") {
// give information on both the inviter and invitee
event.__target_room_member = stateAtTheTime.getStateEvent("m.room.member", event.state_key);
}
if (toFront) {
this.events.unshift(event);
}
else {
this.events.push(event);
}
},
addOrReplaceMessageEvent: function addOrReplaceMessageEvent(event, toFront) {
// Start looking from the tail since the first goal of this function
// is to find a message among the latest ones
for (var i = this.events.length - 1; i >= 0; i--) {
var storedEvent = this.events[i];
if (storedEvent.event_id === event.event_id) {
// It's clobbering time!
this.events[i] = event;
return;
}
}
this.addMessageEvent(event, toFront);
},
leave: function leave() {
return matrixService.leave(this.room_id);
}
};
/***** Room State Object *****/
var RoomState = function RoomState() {
// list of RoomMember
this.members = {};
// state events, the key is a compound of event type + state_key
this.state_events = {};
this.pagination_token = "";
};
RoomState.prototype = {
// get a state event for this room from this.state_events. State events
// are unique per type+state_key tuple, with a lot of events using 0-len
// state keys. To make it not Really Annoying to access, this method is
// provided which can just be given the type and it will return the
// 0-len event by default.
state: function state(type, state_key) {
if (!type) {
return undefined; // event type MUST be specified
}
if (!state_key) {
return this.state_events[type]; // treat as 0-len state key
}
return this.state_events[type + state_key];
},
storeStateEvent: function storeState(event) {
var keyIndex = event.state_key === undefined ? event.type : event.type + event.state_key;
this.state_events[keyIndex] = event;
if (event.type === "m.room.member") {
var userId = event.state_key;
var rm = new RoomMember();
rm.event = event;
rm.user = users[userId];
this.members[userId] = rm;
// add to lookup so new m.presence events update the user
if (!userIdToRoomMember[userId]) {
userIdToRoomMember[userId] = [];
}
userIdToRoomMember[userId].push(rm);
}
else if (event.type === "m.room.aliases") {
setRoomIdToAliasMapping(event.room_id, event.content.aliases[0]);
}
else if (event.type === "m.room.power_levels") {
// normalise power levels: find the max first.
var maxPowerLevel = 0;
for (var user_id in event.content) {
if (!event.content.hasOwnProperty(user_id) || user_id === "hsob_ts") continue; // XXX hsob_ts on some old rooms :(
maxPowerLevel = Math.max(maxPowerLevel, event.content[user_id]);
}
// set power level f.e room member
var defaultPowerLevel = event.content.default === undefined ? 0 : event.content.default;
for (var user_id in this.members) {
if (!this.members.hasOwnProperty(user_id)) continue;
var rm = this.members[user_id];
if (!rm) {
continue;
}
rm.power_level = event.content[user_id] === undefined ? defaultPowerLevel : event.content[user_id];
rm.power_level_norm = (rm.power_level * 100) / maxPowerLevel;
}
}
},
storeStateEvents: function storeState(events) {
if (!events) {
return;
}
for (var i=0; i<events.length; i++) {
this.storeStateEvent(events[i]);
}
},
getStateEvent: function getStateEvent(event_type, state_key) {
return this.state_events[event_type + state_key];
}
};
/***** Room Member Object *****/
var RoomMember = function RoomMember() {
this.event = {}; // the m.room.member event representing the RoomMember.
this.power_level_norm = 0;
this.power_level = 0;
this.user = undefined; // the User
};
/***** User Object *****/
var User = function User() {
this.event = {}; // the m.presence event representing the User.
this.last_updated = 0; // used with last_active_ago to work out last seen times
};
return {
getRoom: function(roomId) {
if(!rooms[roomId]) {
rooms[roomId] = new Room(roomId);
}
return rooms[roomId];
},
getRooms: function() {
return rooms;
},
/**
* Get the member object of a room member
* @param {String} room_id the room id
* @param {String} user_id the id of the user
* @returns {undefined | Object} the member object of this user in this room if he is part of the room
*/
getMember: function(room_id, user_id) {
var room = this.getRoom(room_id);
return room.current_room_state.members[user_id];
},
createRoomIdToAliasMapping: function(roomId, alias) {
setRoomIdToAliasMapping(roomId, alias);
},
getRoomIdToAliasMapping: function(roomId) {
var alias = roomIdToAlias[roomId];
//console.log("looking for alias for " + roomId + "; found: " + alias);
return alias;
},
getAliasToRoomIdMapping: function(alias) {
var roomId = aliasToRoomId[alias];
//console.log("looking for roomId for " + alias + "; found: " + roomId);
return roomId;
},
getUser: function(user_id) {
return users[user_id];
},
setUser: function(event) {
var usr = new User();
usr.event = event;
// migrate old data but clobber matching keys
if (users[event.content.user_id] && users[event.content.user_id].event) {
angular.extend(users[event.content.user_id].event, event);
usr = users[event.content.user_id];
}
else {
users[event.content.user_id] = usr;
}
usr.last_updated = new Date().getTime();
// update room members
var roomMembers = userIdToRoomMember[event.content.user_id];
if (roomMembers) {
for (var i=0; i<roomMembers.length; i++) {
var rm = roomMembers[i];
rm.user = usr;
}
}
},
/**
* Return the power level of an user in a particular room
* @param {String} room_id the room id
* @param {String} user_id the user id
* @returns {Number}
*/
getUserPowerLevel: function(room_id, user_id) {
var powerLevel = 0;
var room = this.getRoom(room_id).current_room_state;
if (room.state("m.room.power_levels")) {
if (user_id in room.state("m.room.power_levels").content) {
powerLevel = room.state("m.room.power_levels").content[user_id];
}
else {
// Use the room default user power
powerLevel = room.state("m.room.power_levels").content["default"];
}
}
return powerLevel;
},
/**
* Compute the room users number, ie the number of members who has joined the room.
* @param {String} room_id the room id
* @returns {undefined | Number} the room users number if available
*/
getUserCountInRoom: function(room_id) {
var memberCount;
var room = this.getRoom(room_id);
memberCount = 0;
for (var i in room.current_room_state.members) {
if (!room.current_room_state.members.hasOwnProperty(i)) continue;
var member = room.current_room_state.members[i].event;
if ("join" === member.content.membership) {
memberCount = memberCount + 1;
}
}
return memberCount;
},
/**
* Return the last message event of a room
* @param {String} room_id the room id
* @param {Boolean} filterFake true to not take into account fake messages
* @returns {undefined | Event} the last message event if available
*/
getLastMessage: function(room_id, filterEcho) {
var lastMessage;
var events = this.getRoom(room_id).events;
for (var i = events.length - 1; i >= 0; i--) {
var message = events[i];
// TODO: define a better marker than echo_msg_state
if (!filterEcho || undefined === message.echo_msg_state) {
lastMessage = message;
break;
}
}
return lastMessage;
},
clearRooms: function() {
init();
}
};
}]);
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment