From 89ba802b23bf1fd22afbc5e9a4b3b732264e3c18 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Tue, 4 Nov 2014 15:57:23 +0000 Subject: Move webclient to a python module so that it can be installed --- .../components/fileInput/file-input-directive.js | 56 ++ .../components/fileUpload/file-upload-service.js | 180 +++++ .../components/matrix/event-handler-service.js | 603 ++++++++++++++++ .../components/matrix/event-stream-service.js | 160 +++++ syweb/webclient/components/matrix/matrix-call.js | 607 ++++++++++++++++ syweb/webclient/components/matrix/matrix-filter.js | 108 +++ .../components/matrix/matrix-phone-service.js | 155 +++++ .../webclient/components/matrix/matrix-service.js | 759 +++++++++++++++++++++ syweb/webclient/components/matrix/model-service.js | 170 +++++ .../components/matrix/notification-service.js | 104 +++ .../components/matrix/presence-service.js | 113 +++ .../components/utilities/utilities-service.js | 151 ++++ 12 files changed, 3166 insertions(+) create mode 100644 syweb/webclient/components/fileInput/file-input-directive.js create mode 100644 syweb/webclient/components/fileUpload/file-upload-service.js create mode 100644 syweb/webclient/components/matrix/event-handler-service.js create mode 100644 syweb/webclient/components/matrix/event-stream-service.js create mode 100644 syweb/webclient/components/matrix/matrix-call.js create mode 100644 syweb/webclient/components/matrix/matrix-filter.js create mode 100644 syweb/webclient/components/matrix/matrix-phone-service.js create mode 100644 syweb/webclient/components/matrix/matrix-service.js create mode 100644 syweb/webclient/components/matrix/model-service.js create mode 100644 syweb/webclient/components/matrix/notification-service.js create mode 100644 syweb/webclient/components/matrix/presence-service.js create mode 100644 syweb/webclient/components/utilities/utilities-service.js (limited to 'syweb/webclient/components') diff --git a/syweb/webclient/components/fileInput/file-input-directive.js b/syweb/webclient/components/fileInput/file-input-directive.js new file mode 100644 index 0000000000..9c849a140f --- /dev/null +++ b/syweb/webclient/components/fileInput/file-input-directive.js @@ -0,0 +1,56 @@ +/* + 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', + template: '
', + 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 diff --git a/syweb/webclient/components/fileUpload/file-upload-service.js b/syweb/webclient/components/fileUpload/file-upload-service.js new file mode 100644 index 0000000000..b544e29509 --- /dev/null +++ b/syweb/webclient/components/fileUpload/file-upload-service.js @@ -0,0 +1,180 @@ +/* + 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 image file plus generate a thumbnail of it and upload it so that + * we will have all information to fulfill an image message request data. + * @param {File} imageFile the imageFile to send + * @param {Integer} thumbnailSize the max side size of the thumbnail to create + * @returns {promise} A promise that will be resolved by a image message object + * ready to be send with the Matrix API + */ + this.uploadImageAndThumbnail = function(imageFile, thumbnailSize) { + var self = this; + var deferred = $q.defer(); + + console.log("uploadImageAndThumbnail " + imageFile.name + " - thumbnailSize: " + thumbnailSize); + + // The message structure that will be returned in the promise + var imageMessage = { + 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 + } + }; + + // First, get the image size + mUtilities.getImageSize(imageFile).then( + function(size) { + console.log("image size: " + JSON.stringify(size)); + + // The final operation: send imageFile + var uploadImage = function() { + self.uploadFile(imageFile).then( + function(url) { + // Update message metadata + imageMessage.url = url; + imageMessage.info = { + size: imageFile.size, + w: size.width, + h: size.height, + mimetype: imageFile.type + }; + + // If there is no thumbnail (because the original image is smaller than thumbnailSize), + // reuse the original image info for thumbnail data + if (!imageMessage.thumbnail_url) { + imageMessage.thumbnail_url = imageMessage.url; + imageMessage.thumbnail_info = imageMessage.info; + } + + // We are done + deferred.resolve(imageMessage); + }, + 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(imageFile, 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 + imageMessage.thumbnail_url = thumbnailUrl; + imageMessage.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); + } + ); + + return deferred.promise; + }; + +}]); diff --git a/syweb/webclient/components/matrix/event-handler-service.js b/syweb/webclient/components/matrix/event-handler-service.js new file mode 100644 index 0000000000..027c80a1b6 --- /dev/null +++ b/syweb/webclient/components/matrix/event-handler-service.js @@ -0,0 +1,603 @@ +/* +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 handles what should happen when you get an event. This service does +not care where the event came from, it only needs enough context to be able to +process them. Events may be coming from the event stream, the REST API (via +direct GETs or via a pagination stream API), etc. + +Typically, this service will store events and broadcast them to any listeners +(e.g. controllers) via $broadcast. +*/ +angular.module('eventHandlerService', []) +.factory('eventHandlerService', ['matrixService', '$rootScope', '$q', '$timeout', 'mPresence', 'notificationService', 'modelService', +function(matrixService, $rootScope, $q, $timeout, mPresence, notificationService, modelService) { + var ROOM_CREATE_EVENT = "ROOM_CREATE_EVENT"; + var MSG_EVENT = "MSG_EVENT"; + var MEMBER_EVENT = "MEMBER_EVENT"; + var PRESENCE_EVENT = "PRESENCE_EVENT"; + var POWERLEVEL_EVENT = "POWERLEVEL_EVENT"; + var CALL_EVENT = "CALL_EVENT"; + var NAME_EVENT = "NAME_EVENT"; + var TOPIC_EVENT = "TOPIC_EVENT"; + var RESET_EVENT = "RESET_EVENT"; // eventHandlerService has been resetted + + // used for dedupping events - could be expanded in future... + // FIXME: means that we leak memory over time (along with lots of the rest + // of the app, given we never try to reap memory yet) + var eventMap = {}; + + // TODO: Remove this and replace with modelService.User objects. + $rootScope.presence = {}; + + var initialSyncDeferred; + + var reset = function() { + initialSyncDeferred = $q.defer(); + + $rootScope.presence = {}; + + eventMap = {}; + }; + reset(); + + var resetRoomMessages = function(room_id) { + var room = modelService.getRoom(room_id); + room.events = []; + }; + + // Generic method to handle events data + var handleRoomStateEvent = function(event, isLiveEvent, addToRoomMessages) { + var room = modelService.getRoom(event.room_id); + if (addToRoomMessages) { + // some state events are displayed as messages, so add them. + room.addMessageEvent(event, !isLiveEvent); + } + + if (isLiveEvent) { + // update the current room state with the latest state + room.current_room_state.storeStateEvent(event); + } + else { + var eventTs = event.origin_server_ts; + var storedEvent = room.current_room_state.getStateEvent(event.type, event.state_key); + if (storedEvent) { + if (storedEvent.origin_server_ts < eventTs) { + // the incoming event is newer, use it. + room.current_room_state.storeStateEvent(event); + } + } + } + // TODO: handle old_room_state + }; + + var handleRoomCreate = function(event, isLiveEvent) { + $rootScope.$broadcast(ROOM_CREATE_EVENT, event, isLiveEvent); + }; + + var handleRoomAliases = function(event, isLiveEvent) { + matrixService.createRoomIdToAliasMapping(event.room_id, event.content.aliases[0]); + }; + + var displayNotification = function(event) { + if (window.Notification && event.user_id != matrixService.config().user_id) { + var shouldBing = notificationService.containsBingWord( + matrixService.config().user_id, + matrixService.config().display_name, + matrixService.config().bingWords, + event.content.body + ); + + // Ideally we would notify only when the window is hidden (i.e. document.hidden = true). + // + // However, Chrome on Linux and OSX currently returns document.hidden = false unless the window is + // explicitly showing a different tab. So we need another metric to determine hiddenness - we + // simply use idle time. If the user has been idle enough that their presence goes to idle, then + // we also display notifs when things happen. + // + // This is far far better than notifying whenever anything happens anyway, otherwise you get spammed + // to death with notifications when the window is in the foreground, which is horrible UX (especially + // if you have not defined any bingers and so get notified for everything). + var isIdle = (document.hidden || matrixService.presence.unavailable === mPresence.getState()); + + // We need a way to let people get notifications for everything, if they so desire. The way to do this + // is to specify zero bingwords. + var bingWords = matrixService.config().bingWords; + if (bingWords === undefined || bingWords.length === 0) { + shouldBing = true; + } + + if (shouldBing && isIdle) { + console.log("Displaying notification for "+JSON.stringify(event)); + var member = modelService.getMember(event.room_id, event.user_id); + var displayname = getUserDisplayName(event.room_id, event.user_id); + + var message = event.content.body; + if (event.content.msgtype === "m.emote") { + message = "* " + displayname + " " + message; + } + else if (event.content.msgtype === "m.image") { + message = displayname + " sent an image."; + } + + var roomTitle = matrixService.getRoomIdToAliasMapping(event.room_id); + var theRoom = modelService.getRoom(event.room_id); + if (!roomTitle && theRoom.current_room_state.state("m.room.name") && theRoom.current_room_state.state("m.room.name").content) { + roomTitle = theRoom.current_room_state.state("m.room.name").content.name; + } + + if (!roomTitle) { + roomTitle = event.room_id; + } + + notificationService.showNotification( + displayname + " (" + roomTitle + ")", + message, + member ? member.avatar_url : undefined, + function() { + console.log("notification.onclick() room=" + event.room_id); + $rootScope.goToPage('room/' + event.room_id); + } + ); + } + } + }; + + var handleMessage = function(event, isLiveEvent) { + // Check for empty event content + var hasContent = false; + for (var prop in event.content) { + hasContent = true; + break; + } + if (!hasContent) { + // empty json object is a redacted event, so ignore. + return; + } + + // ======================= + + var room = modelService.getRoom(event.room_id); + + if (event.user_id !== matrixService.config().user_id) { + room.addMessageEvent(event, !isLiveEvent); + displayNotification(event); + } + else { + // we may have locally echoed this, so we should replace the event + // instead of just adding. + room.addOrReplaceMessageEvent(event, !isLiveEvent); + } + + // TODO send delivery receipt if isLiveEvent + + $rootScope.$broadcast(MSG_EVENT, event, isLiveEvent); + }; + + var handleRoomMember = function(event, isLiveEvent, isStateEvent) { + var room = modelService.getRoom(event.room_id); + + // did something change? + var memberChanges = undefined; + if (!isStateEvent) { + // could be a membership change, display name change, etc. + // Find out which one. + if ((event.prev_content === undefined && event.content.membership) || (event.prev_content && (event.prev_content.membership !== event.content.membership))) { + memberChanges = "membership"; + } + else if (event.prev_content && (event.prev_content.displayname !== event.content.displayname)) { + memberChanges = "displayname"; + } + // mark the key which changed + event.changedKey = memberChanges; + } + + + // modify state before adding the message so it points to the right thing. + // The events are copied to avoid referencing the same event when adding + // the message (circular json structures) + if (isStateEvent || isLiveEvent) { + var newEvent = angular.copy(event); + newEvent.cnt = event.content; + room.current_room_state.storeStateEvent(newEvent); + } + else if (!isLiveEvent) { + // mutate the old room state + var oldEvent = angular.copy(event); + oldEvent.cnt = event.content; + if (event.prev_content) { + // the m.room.member event we are handling is the NEW event. When + // we keep going back in time, we want the PREVIOUS value for displaying + // names/etc, hence the clobber here. + oldEvent.cnt = event.prev_content; + } + + if (event.changedKey === "membership" && event.content.membership === "join") { + // join has a prev_content but it doesn't contain all the info unlike the join, so use that. + oldEvent.cnt = event.content; + } + + room.old_room_state.storeStateEvent(oldEvent); + } + + // If there was a change we want to display, dump it in the message + // list. This has to be done after room state is updated. + if (memberChanges) { + room.addMessageEvent(event, !isLiveEvent); + } + + + + $rootScope.$broadcast(MEMBER_EVENT, event, isLiveEvent, isStateEvent); + }; + + var handlePresence = function(event, isLiveEvent) { + $rootScope.presence[event.content.user_id] = event; + $rootScope.$broadcast(PRESENCE_EVENT, event, isLiveEvent); + }; + + var handlePowerLevels = function(event, isLiveEvent) { + handleRoomStateEvent(event, isLiveEvent); + $rootScope.$broadcast(POWERLEVEL_EVENT, event, isLiveEvent); + }; + + var handleRoomName = function(event, isLiveEvent, isStateEvent) { + console.log("handleRoomName room_id: " + event.room_id + " - isLiveEvent: " + isLiveEvent + " - name: " + event.content.name); + handleRoomStateEvent(event, isLiveEvent, !isStateEvent); + $rootScope.$broadcast(NAME_EVENT, event, isLiveEvent); + }; + + + var handleRoomTopic = function(event, isLiveEvent, isStateEvent) { + console.log("handleRoomTopic room_id: " + event.room_id + " - isLiveEvent: " + isLiveEvent + " - topic: " + event.content.topic); + handleRoomStateEvent(event, isLiveEvent, !isStateEvent); + $rootScope.$broadcast(TOPIC_EVENT, event, isLiveEvent); + }; + + var handleCallEvent = function(event, isLiveEvent) { + $rootScope.$broadcast(CALL_EVENT, event, isLiveEvent); + if (event.type === 'm.call.invite') { + var room = modelService.getRoom(event.room_id); + room.addMessageEvent(event, !isLiveEvent); + } + }; + + var handleRedaction = function(event, isLiveEvent) { + if (!isLiveEvent) { + // we have nothing to remove, so just ignore it. + console.log("Received redacted event: "+JSON.stringify(event)); + return; + } + + // we need to remove something possibly: do we know the redacted + // event ID? + if (eventMap[event.redacts]) { + var room = modelService.getRoom(event.room_id); + // remove event from list of messages in this room. + var eventList = room.events; + for (var i=0; i oldest + for (var i=events.length - 1; i>=0; i--) { + this.handleEvent(events[i], isLiveEvents, isLiveEvents); + } + // Store where to start pagination + var room = modelService.getRoom(room_id); + room.old_room_state.pagination_token = messages.start; + } + }, + + handleInitialSyncDone: function(response) { + console.log("# handleInitialSyncDone"); + + var rooms = response.data.rooms; + for (var i = 0; i < rooms.length; ++i) { + var room = rooms[i]; + + // FIXME: This is ming: the HS should be sending down the m.room.member + // event for the invite in .state but it isn't, so fudge it for now. + if (room.inviter && room.membership === "invite") { + var me = matrixService.config().user_id; + var fakeEvent = { + event_id: "__FAKE__" + room.room_id, + user_id: room.inviter, + origin_server_ts: 0, + room_id: room.room_id, + state_key: me, + type: "m.room.member", + content: { + membership: "invite" + } + }; + if (!room.state) { + room.state = []; + } + room.state.push(fakeEvent); + console.log("RECV /initialSync invite >> "+room.room_id); + } + + var newRoom = modelService.getRoom(room.room_id); + newRoom.current_room_state.storeStateEvents(room.state); + newRoom.old_room_state.storeStateEvents(room.state); + + // this should be done AFTER storing state events since these + // messages may make the old_room_state diverge. + if ("messages" in room) { + this.handleRoomMessages(room.room_id, room.messages, false); + newRoom.current_room_state.pagination_token = room.messages.end; + newRoom.old_room_state.pagination_token = room.messages.start; + } + } + var presence = response.data.presence; + this.handleEvents(presence, false); + + initialSyncDeferred.resolve(response); + }, + + // Returns a promise that resolves when the initialSync request has been processed + waitForInitialSyncCompletion: function() { + return initialSyncDeferred.promise; + }, + + resetRoomMessages: function(room_id) { + resetRoomMessages(room_id); + }, + + /** + * 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 = modelService.getRoom(room_id).events; + for (var i = events.length - 1; i >= 0; i--) { + var message = events[i]; + + if (!filterEcho || undefined === message.echo_msg_state) { + lastMessage = message; + break; + } + } + + return lastMessage; + }, + + /** + * 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 + */ + getUsersCountInRoom: function(room_id) { + var memberCount; + + var room = modelService.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]; + + if ("join" === member.content.membership) { + memberCount = memberCount + 1; + } + } + + return memberCount; + }, + + /** + * 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} a value between 0 and 10 + */ + getUserPowerLevel: function(room_id, user_id) { + var powerLevel = 0; + var room = modelService.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; + }, + + /** + * 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 + * @returns {String} the user displayname or user_id if not available + */ + getUserDisplayName: function(room_id, user_id) { + return getUserDisplayName(room_id, user_id); + } + }; +}]); diff --git a/syweb/webclient/components/matrix/event-stream-service.js b/syweb/webclient/components/matrix/event-stream-service.js new file mode 100644 index 0000000000..c03f0b953b --- /dev/null +++ b/syweb/webclient/components/matrix/event-stream-service.js @@ -0,0 +1,160 @@ +/* +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 { + // 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(); + } + }; + +}]); diff --git a/syweb/webclient/components/matrix/matrix-call.js b/syweb/webclient/components/matrix/matrix-call.js new file mode 100644 index 0000000000..5a2807c755 --- /dev/null +++ b/syweb/webclient/components/matrix/matrix-call.js @@ -0,0 +1,607 @@ +/* +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'; + +var forAllVideoTracksOnStream = function(s, f) { + var tracks = s.getVideoTracks(); + for (var i = 0; i < tracks.length; i++) { + f(tracks[i]); + } +} + +var forAllAudioTracksOnStream = function(s, f) { + var tracks = s.getAudioTracks(); + for (var i = 0; i < tracks.length; i++) { + f(tracks[i]); + } +} + +var forAllTracksOnStream = function(s, f) { + forAllVideoTracksOnStream(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; + +// Returns true if the browser supports all required features to make WebRTC call +var isWebRTCSupported = function () { + return !!(navigator.getUserMedia || window.RTCPeerConnection || window.RTCSessionDescription || window.RTCIceCandidate); +}; + +angular.module('MatrixCall', []) +.factory('MatrixCall', ['matrixService', 'matrixPhoneService', 'modelService', '$rootScope', '$timeout', function MatrixCallFactory(matrixService, matrixPhoneService, modelService, $rootScope, $timeout) { + $rootScope.isWebRTCSupported = isWebRTCSupported(); + + var MatrixCall = function(room_id) { + this.room_id = room_id; + this.call_id = "c" + new Date().getTime(); + this.state = 'fledgling'; + this.didConnect = false; + + // a queue for candidates waiting to go out. We try to amalgamate candidates into a single candidate message where possible + this.candidateSendQueue = []; + this.candidateSendTries = 0; + + var self = this; + $rootScope.$watch(this.remoteVideoElement, function (oldValue, newValue) { + self.tryPlayRemoteStream(); + }); + + } + + MatrixCall.getTurnServer = function() { + matrixService.getTurnServer().then(function(response) { + if (response.data.uris) { + console.log("Got TURN URIs: "+response.data.uris); + MatrixCall.turnServer = response.data; + $rootScope.haveTurn = true; + // re-fetch when we're about to reach the TTL + $timeout(MatrixCall.getTurnServer, MatrixCall.turnServer.ttl * 1000 * 0.9); + } else { + console.log("Got no TURN URIs from HS"); + $rootScope.haveTurn = false; + } + }, function(error) { + console.log("Failed to get TURN URIs"); + MatrixCall.turnServer = {}; + $timeout(MatrixCall.getTurnServer, 60000); + }); + } + + // FIXME: we should prevent any class from being placed or accepted before this has finished + MatrixCall.getTurnServer(); + + MatrixCall.CALL_TIMEOUT = 60000; + MatrixCall.FALLBACK_STUN_SERVER = 'stun:stun.l.google.com:19302'; + + MatrixCall.prototype.createPeerConnection = function() { + var pc; + if (window.mozRTCPeerConnection) { + var iceServers = []; + if (MatrixCall.turnServer) { + if (MatrixCall.turnServer.uris) { + for (var i = 0; i < MatrixCall.turnServer.uris.length; i++) { + iceServers.push({ + 'url': MatrixCall.turnServer.uris[i], + 'username': MatrixCall.turnServer.username, + 'credential': MatrixCall.turnServer.password, + }); + } + } else { + console.log("No TURN server: using fallback STUN server"); + iceServers.push({ 'url' : MatrixCall.FALLBACK_STUN_SERVER }); + } + } + + pc = new window.mozRTCPeerConnection({"iceServers":iceServers}); + } else { + var iceServers = []; + if (MatrixCall.turnServer) { + if (MatrixCall.turnServer.uris) { + iceServers.push({ + 'urls': MatrixCall.turnServer.uris, + 'username': MatrixCall.turnServer.username, + 'credential': MatrixCall.turnServer.password, + }); + } else { + console.log("No TURN server: using fallback STUN server"); + iceServers.push({ 'urls' : MatrixCall.FALLBACK_STUN_SERVER }); + } + } + + pc = new window.RTCPeerConnection({"iceServers":iceServers}); + } + var self = this; + pc.oniceconnectionstatechange = function() { self.onIceConnectionStateChanged(); }; + pc.onsignalingstatechange = function() { self.onSignallingStateChanged(); }; + pc.onicecandidate = function(c) { self.gotLocalIceCandidate(c); }; + pc.onaddstream = function(s) { self.onAddStream(s); }; + return pc; + } + + MatrixCall.prototype.getUserMediaVideoContraints = function(callType) { + switch (callType) { + case 'voice': + return ({audio: true, video: false}); + case 'video': + return ({audio: true, video: { + mandatory: { + minWidth: 640, + maxWidth: 640, + minHeight: 360, + maxHeight: 360, + } + }}); + } + }; + + MatrixCall.prototype.placeVoiceCall = function() { + this.placeCallWithConstraints(this.getUserMediaVideoContraints('voice')); + this.type = 'voice'; + }; + + MatrixCall.prototype.placeVideoCall = function(config) { + this.placeCallWithConstraints(this.getUserMediaVideoContraints('video')); + this.type = 'video'; + }; + + MatrixCall.prototype.placeCallWithConstraints = function(constraints) { + var self = this; + matrixPhoneService.callPlaced(this); + navigator.getUserMedia(constraints, function(s) { self.gotUserMediaForInvite(s); }, function(e) { self.getUserMediaFailed(e); }); + this.state = 'wait_local_media'; + this.direction = 'outbound'; + this.config = constraints; + }; + + MatrixCall.prototype.initWithInvite = function(event) { + this.msg = event.content; + this.peerConn = this.createPeerConnection(); + this.peerConn.setRemoteDescription(new RTCSessionDescription(this.msg.offer), this.onSetRemoteDescriptionSuccess, this.onSetRemoteDescriptionError); + this.state = 'ringing'; + this.direction = 'inbound'; + + 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) { + this.type = 'video'; + } else { + this.type = 'voice'; + } + } + + var self = this; + $timeout(function() { + if (self.state == 'ringing') { + self.state = 'ended'; + self.hangupParty = 'remote'; // effectively + self.stopAllMedia(); + if (self.peerConn.signalingState != 'closed') self.peerConn.close(); + if (self.onHangup) self.onHangup(self); + } + }, this.msg.lifetime - event.age); + }; + + // perverse as it may seem, sometimes we want to instantiate a call with a hangup message + // (because when getting the state of the room on load, events come in reverse order and + // we want to remember that a call has been hung up) + MatrixCall.prototype.initWithHangup = function(event) { + this.msg = event.content; + this.state = 'ended'; + }; + + MatrixCall.prototype.answer = function() { + console.log("Answering call "+this.call_id); + + var self = this; + + var roomMembers = modelService.getRoom(this.room_id).current_room_state.members; + if (roomMembers[matrixService.config().user_id].membership != 'join') { + console.log("We need to join the room before we can accept this call"); + matrixService.join(this.room_id).then(function() { + self.answer(); + }, function() { + console.log("Failed to join room: can't answer call!"); + self.onError("Unable to join room to answer call!"); + self.hangup(); + }); + return; + } + + if (!this.localAVStream && !this.waitForLocalAVStream) { + navigator.getUserMedia(this.getUserMediaVideoContraints(this.type), function(s) { self.gotUserMediaForAnswer(s); }, function(e) { self.getUserMediaFailed(e); }); + this.state = 'wait_local_media'; + } else if (this.localAVStream) { + this.gotUserMediaForAnswer(this.localAVStream); + } else if (this.waitForLocalAVStream) { + this.state = 'wait_local_media'; + } + }; + + MatrixCall.prototype.stopAllMedia = function() { + if (this.localAVStream) { + forAllTracksOnStream(this.localAVStream, function(t) { + if (t.stop) t.stop(); + }); + } + if (this.remoteAVStream) { + forAllTracksOnStream(this.remoteAVStream, function(t) { + if (t.stop) t.stop(); + }); + } + }; + + MatrixCall.prototype.hangup = function(reason, suppressEvent) { + console.log("Ending call "+this.call_id); + + // 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(); + + this.stopAllMedia(); + if (this.peerConn) this.peerConn.close(); + + this.hangupParty = 'local'; + this.hangupReason = reason; + + var content = { + version: 0, + call_id: this.call_id, + reason: reason + }; + this.sendEventWithRetry('m.call.hangup', content); + this.state = 'ended'; + if (this.onHangup && !suppressEvent) this.onHangup(this); + }; + + MatrixCall.prototype.gotUserMediaForInvite = function(stream) { + if (this.successor) { + this.successor.gotUserMediaForAnswer(stream); + return; + } + if (this.state == 'ended') return; + + if (this.localVideoElement && this.type == 'video') { + var vidTrack = stream.getVideoTracks()[0]; + this.localVideoElement.src = URL.createObjectURL(stream); + this.localVideoElement.muted = true; + this.localVideoElement.play(); + } + + this.localAVStream = stream; + var audioTracks = stream.getAudioTracks(); + for (var i = 0; i < audioTracks.length; i++) { + audioTracks[i].enabled = true; + } + this.peerConn = this.createPeerConnection(); + this.peerConn.addStream(stream); + var self = this; + this.peerConn.createOffer(function(d) { + self.gotLocalOffer(d); + }, function(e) { + self.getLocalOfferFailed(e); + }); + $rootScope.$apply(function() { + self.state = 'create_offer'; + }); + }; + + MatrixCall.prototype.gotUserMediaForAnswer = function(stream) { + if (this.state == 'ended') return; + + if (this.localVideoElement && this.type == 'video') { + var vidTrack = stream.getVideoTracks()[0]; + this.localVideoElement.src = URL.createObjectURL(stream); + this.localVideoElement.muted = true; + this.localVideoElement.play(); + } + + this.localAVStream = stream; + var audioTracks = stream.getAudioTracks(); + for (var i = 0; i < audioTracks.length; i++) { + audioTracks[i].enabled = true; + } + this.peerConn.addStream(stream); + var self = this; + var constraints = { + 'mandatory': { + 'OfferToReceiveAudio': true, + 'OfferToReceiveVideo': this.type == 'video' + }, + }; + this.peerConn.createAnswer(function(d) { self.createdAnswer(d); }, function(e) {}, constraints); + // This can't be in an apply() because it's called by a predecessor call under glare conditions :( + self.state = 'create_answer'; + }; + + MatrixCall.prototype.gotLocalIceCandidate = function(event) { + if (event.candidate) { + console.log("Got local ICE "+event.candidate.sdpMid+" candidate: "+event.candidate.candidate); + this.sendCandidate(event.candidate); + } + } + + 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"); + return; + } + this.peerConn.addIceCandidate(new RTCIceCandidate(cand), function() {}, function(e) {}); + }; + + MatrixCall.prototype.receivedAnswer = function(msg) { + if (this.state == 'ended') return; + + this.peerConn.setRemoteDescription(new RTCSessionDescription(msg.answer), this.onSetRemoteDescriptionSuccess, this.onSetRemoteDescriptionError); + this.state = 'connecting'; + }; + + + MatrixCall.prototype.gotLocalOffer = function(description) { + console.log("Created offer: "+description); + + if (this.state == 'ended') { + console.log("Ignoring newly created offer on call ID "+this.call_id+" because the call has ended"); + 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); + + $rootScope.$apply(function() { + self.state = 'invite_sent'; + }); + }; + + 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'; + }); + }; + + MatrixCall.prototype.getLocalOfferFailed = function(error) { + this.onError("Failed to start audio for call!"); + }; + + MatrixCall.prototype.getUserMediaFailed = function() { + this.onError("Couldn't start capturing! Is your microphone set up?"); + this.hangup(); + }; + + MatrixCall.prototype.onIceConnectionStateChanged = function() { + if (this.state == 'ended') return; // because ICE can still complete as we're ending the call + console.log("Ice connection state changed to: "+this.peerConn.iceConnectionState); + // ideally we'd consider the call to be connected when we get media but chrome doesn't implement nay of the 'onstarted' events yet + if (this.peerConn.iceConnectionState == 'completed' || this.peerConn.iceConnectionState == 'connected') { + var self = this; + $rootScope.$apply(function() { + self.state = 'connected'; + self.didConnect = true; + }); + } else if (this.peerConn.iceConnectionState == 'failed') { + this.hangup('ice_failed'); + } + }; + + MatrixCall.prototype.onSignallingStateChanged = function() { + console.log("call "+this.call_id+": Signalling state changed to: "+this.peerConn.signalingState); + }; + + MatrixCall.prototype.onSetRemoteDescriptionSuccess = function() { + console.log("Set remote description"); + }; + + MatrixCall.prototype.onSetRemoteDescriptionError = function(e) { + console.log("Failed to set remote description"+e); + }; + + MatrixCall.prototype.onAddStream = function(event) { + console.log("Stream added"+event); + + var s = event.stream; + + this.remoteAVStream = s; + + if (this.direction == 'inbound') { + if (s.getVideoTracks().length > 0) { + this.type = 'video'; + } else { + this.type = 'voice'; + } + } + + var self = this; + forAllTracksOnStream(s, function(t) { + // not currently implemented in chrome + t.onstarted = self.onRemoteStreamTrackStarted; + }); + + event.stream.onended = function(e) { self.onRemoteStreamEnded(e); }; + // not currently implemented in chrome + event.stream.onstarted = function(e) { self.onRemoteStreamStarted(e); }; + + this.tryPlayRemoteStream(); + }; + + MatrixCall.prototype.tryPlayRemoteStream = function(event) { + if (this.remoteVideoElement && this.remoteAVStream) { + var player = this.remoteVideoElement; + player.src = URL.createObjectURL(this.remoteAVStream); + player.play(); + } + }; + + MatrixCall.prototype.onRemoteStreamStarted = function(event) { + var self = this; + $rootScope.$apply(function() { + self.state = 'connected'; + }); + }; + + MatrixCall.prototype.onRemoteStreamEnded = function(event) { + console.log("Remote stream ended"); + var self = this; + $rootScope.$apply(function() { + self.state = 'ended'; + self.hangupParty = 'remote'; + self.stopAllMedia(); + if (self.peerConn.signalingState != 'closed') self.peerConn.close(); + if (self.onHangup) self.onHangup(self); + }); + }; + + MatrixCall.prototype.onRemoteStreamTrackStarted = function(event) { + var self = this; + $rootScope.$apply(function() { + self.state = 'connected'; + }); + }; + + MatrixCall.prototype.onHangupReceived = function(msg) { + console.log("Hangup received"); + if (this.remoteVideoElement) this.remoteVideoElement.pause(); + if (this.localVideoElement) this.localVideoElement.pause(); + this.state = 'ended'; + this.hangupParty = 'remote'; + this.hangupReason = msg.reason; + this.stopAllMedia(); + if (this.peerConn && this.peerConn.signalingState != 'closed') this.peerConn.close(); + if (this.onHangup) this.onHangup(this); + }; + + MatrixCall.prototype.replacedBy = function(newCall) { + console.log(this.call_id+" being replaced by "+newCall.call_id); + if (this.state == 'wait_local_media') { + console.log("Telling new call to wait for local media"); + newCall.waitForLocalAVStream = true; + } else if (this.state == 'create_offer') { + console.log("Handing local stream to new call"); + newCall.gotUserMediaForAnswer(this.localAVStream); + delete(this.localAVStream); + } else if (this.state == 'invite_sent') { + console.log("Handing local stream to new call"); + newCall.gotUserMediaForAnswer(this.localAVStream); + delete(this.localAVStream); + } + newCall.localVideoElement = this.localVideoElement; + newCall.remoteVideoElement = this.remoteVideoElement; + this.successor = newCall; + this.hangup(true); + }; + + MatrixCall.prototype.sendEventWithRetry = function(evType, content) { + var ev = { type:evType, content:content, tries:1 }; + var self = this; + matrixService.sendEvent(this.room_id, evType, undefined, content).then(this.eventSent, function(error) { self.eventSendFailed(ev, error); } ); + }; + + MatrixCall.prototype.eventSent = function() { + }; + + MatrixCall.prototype.eventSendFailed = function(ev, error) { + if (ev.tries > 5) { + console.log("Failed to send event of type "+ev.type+" on attempt "+ev.tries+". Giving up."); + return; + } + var delayMs = 500 * Math.pow(2, ev.tries); + console.log("Failed to send event of type "+ev.type+". Retrying in "+delayMs+"ms"); + ++ev.tries; + var self = this; + $timeout(function() { + matrixService.sendEvent(self.room_id, ev.type, undefined, ev.content).then(self.eventSent, function(error) { self.eventSendFailed(ev, error); } ); + }, delayMs); + }; + + // Sends candidates with are sent in a special way because we try to amalgamate them into one message + MatrixCall.prototype.sendCandidate = function(content) { + this.candidateSendQueue.push(content); + var self = this; + if (this.candidateSendTries == 0) $timeout(function() { self.sendCandidateQueue(); }, 100); + }; + + MatrixCall.prototype.sendCandidateQueue = function(content) { + if (this.candidateSendQueue.length == 0) return; + + var cands = this.candidateSendQueue; + this.candidateSendQueue = []; + ++this.candidateSendTries; + var content = { + version: 0, + call_id: this.call_id, + candidates: cands + }; + var self = this; + console.log("Attempting to send "+cands.length+" candidates"); + matrixService.sendEvent(self.room_id, 'm.call.candidates', undefined, content).then(function() { self.candsSent(); }, function(error) { self.candsSendFailed(cands, error); } ); + }; + + MatrixCall.prototype.candsSent = function() { + this.candidateSendTries = 0; + this.sendCandidateQueue(); + }; + + MatrixCall.prototype.candsSendFailed = function(cands, error) { + for (var i = 0; i < cands.length; ++i) { + this.candidateSendQueue.push(cands[i]); + } + + if (this.candidateSendTries > 5) { + console.log("Failed to send candidates on attempt "+this.candidateSendTries+". Giving up for now."); + this.candidateSendTries = 0; + return; + } + + var delayMs = 500 * Math.pow(2, this.candidateSendTries); + ++this.candidateSendTries; + console.log("Failed to send candidates. Retrying in "+delayMs+"ms"); + var self = this; + $timeout(function() { + self.sendCandidateQueue(); + }, delayMs); + }; + + return MatrixCall; +}]); diff --git a/syweb/webclient/components/matrix/matrix-filter.js b/syweb/webclient/components/matrix/matrix-filter.js new file mode 100644 index 0000000000..4d264e93f3 --- /dev/null +++ b/syweb/webclient/components/matrix/matrix-filter.js @@ -0,0 +1,108 @@ +/* + 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', 'eventHandlerService', 'modelService', +function($rootScope, matrixService, eventHandlerService, modelService) { + return function(room_id) { + var roomName; + + // If there is an alias, use it + // TODO: only one alias is managed for now + var alias = matrixService.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]; + if (member.state_key !== user_id) { + roomName = eventHandlerService.getUserDisplayName(room_id, 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].content.membership === "invite") { + // someone invited us, use the right ID. + roomName = eventHandlerService.getUserDisplayName(room_id, room.members[otherUserId].user_id); + } + else { + roomName = eventHandlerService.getUserDisplayName(room_id, otherUserId); + } + } + else { // it isn't us, so use their name if we know it. + roomName = eventHandlerService.getUserDisplayName(room_id, 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', ['eventHandlerService', function(eventHandlerService) { + return function(user_id, room_id) { + return eventHandlerService.getUserDisplayName(room_id, user_id); + }; +}]); diff --git a/syweb/webclient/components/matrix/matrix-phone-service.js b/syweb/webclient/components/matrix/matrix-phone-service.js new file mode 100644 index 0000000000..06465ed821 --- /dev/null +++ b/syweb/webclient/components/matrix/matrix-phone-service.js @@ -0,0 +1,155 @@ +/* +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 (!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; +}]); diff --git a/syweb/webclient/components/matrix/matrix-service.js b/syweb/webclient/components/matrix/matrix-service.js new file mode 100644 index 0000000000..fedfb8910d --- /dev/null +++ b/syweb/webclient/components/matrix/matrix-service.js @@ -0,0 +1,759 @@ +/* +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 wraps up Matrix API calls. + +This serves to isolate the caller from changes to the underlying url paths, as +well as attach common params (e.g. access_token) to requests. +*/ +angular.module('matrixService', []) +.factory('matrixService', ['$http', '$q', '$rootScope', function($http, $q, $rootScope) { + + /* + * Permanent storage of user information + * The config contains: + * - homeserver url + * - Identity server url + * - user_id + * - access_token + * - version: the version of this cache + */ + var config; + + var roomIdToAlias = {}; + var aliasToRoomId = {}; + + // Current version of permanent storage + var configVersion = 0; + var prefixPath = "/_matrix/client/api/v1"; + var MAPPING_PREFIX = "alias_for_"; + + var doRequest = function(method, path, params, data, $httpParams) { + if (!config) { + console.warn("No config exists. Cannot perform request to "+path); + return; + } + + // Inject the access token + if (!params) { + params = {}; + } + + params.access_token = config.access_token; + + if (path.indexOf(prefixPath) !== 0) { + path = prefixPath + path; + } + + return doBaseRequest(config.homeserver, method, path, params, data, undefined, $httpParams); + }; + + var doBaseRequest = function(baseUrl, method, path, params, data, headers, $httpParams) { + + var request = { + method: method, + url: baseUrl + path, + params: params, + data: data, + headers: headers + }; + + // Add additional $http parameters + if ($httpParams) { + angular.extend(request, $httpParams); + } + + return $http(request); + }; + + var doRegisterLogin = function(path, loginType, sessionId, userName, password, threepidCreds) { + var data = {}; + if (loginType === "m.login.recaptcha") { + var challengeToken = Recaptcha.get_challenge(); + var captchaEntry = Recaptcha.get_response(); + data = { + type: "m.login.recaptcha", + challenge: challengeToken, + response: captchaEntry + }; + } + else if (loginType === "m.login.email.identity") { + data = { + threepidCreds: threepidCreds + }; + } + else if (loginType === "m.login.password") { + data = { + user: userName, + password: password + }; + } + + if (sessionId) { + data.session = sessionId; + } + data.type = loginType; + console.log("doRegisterLogin >>> " + loginType); + return doRequest("POST", path, undefined, data); + }; + + return { + /****** Home server API ******/ + prefix: prefixPath, + + // Register an user + register: function(user_name, password, threepidCreds, useCaptcha) { + // registration is composed of multiple requests, to check you can + // register, then to actually register. This deferred will fire when + // all the requests are done, along with the final response. + var deferred = $q.defer(); + var path = "/register"; + + // check we can actually register with this HS. + doRequest("GET", path, undefined, undefined).then( + function(response) { + console.log("/register [1] : "+JSON.stringify(response)); + var flows = response.data.flows; + var knownTypes = [ + "m.login.password", + "m.login.recaptcha", + "m.login.email.identity" + ]; + // if they entered 3pid creds, we want to use a flow which uses it. + var useThreePidFlow = threepidCreds != undefined; + var flowIndex = 0; + var firstRegType = undefined; + + for (var i=0; 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) { + this.state_events[event.type + event.state_key] = event; + if (event.type === "m.room.member") { + this.members[event.state_key] = event; + } + }, + + storeStateEvents: function storeState(events) { + if (!events) { + return; + } + for (var i=0; i + }; + + console.log("Models inited."); + + 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]; + } + + }; +}]); diff --git a/syweb/webclient/components/matrix/notification-service.js b/syweb/webclient/components/matrix/notification-service.js new file mode 100644 index 0000000000..9a911413c3 --- /dev/null +++ b/syweb/webclient/components/matrix/notification-service.js @@ -0,0 +1,104 @@ +/* +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 notifications: enabling, creating and showing them. This +also contains 'bing word' logic. +*/ +angular.module('notificationService', []) +.factory('notificationService', ['$timeout', function($timeout) { + + var getLocalPartFromUserId = function(user_id) { + if (!user_id) { + return null; + } + var localpartRegex = /@(.*):\w+/i + var results = localpartRegex.exec(user_id); + if (results && results.length == 2) { + return results[1]; + } + return null; + }; + + return { + + containsBingWord: function(userId, displayName, bingWords, content) { + // case-insensitive name check for user_id OR display_name if they exist + var userRegex = ""; + if (userId) { + var localpart = getLocalPartFromUserId(userId); + if (localpart) { + localpart = localpart.toLocaleLowerCase(); + userRegex += "\\b" + localpart + "\\b"; + } + } + if (displayName) { + displayName = displayName.toLocaleLowerCase(); + if (userRegex.length > 0) { + userRegex += "|"; + } + userRegex += "\\b" + displayName + "\\b"; + } + + var regexList = [new RegExp(userRegex, 'i')]; + + // bing word list check + if (bingWords && bingWords.length > 0) { + for (var i=0; i 0) { + for (var i=0; i height) { + if (width > MAX_WIDTH) { + height *= MAX_WIDTH / width; + width = MAX_WIDTH; + } + } else { + if (height > MAX_HEIGHT) { + width *= MAX_HEIGHT / height; + height = MAX_HEIGHT; + } + } + canvas.width = width; + canvas.height = height; + var ctx = canvas.getContext("2d"); + ctx.drawImage(img, 0, 0, width, height); + + // Extract image data in the same format as the original one. + // The 0.7 compression value will work with formats that supports it like JPEG. + var dataUrl = canvas.toDataURL(imageFile.type, 0.7); + deferred.resolve(self.dataURItoBlob(dataUrl)); + }; + img.onerror = function(e) { + deferred.reject(e); + }; + }; + reader.onerror = function(e) { + deferred.reject(e); + }; + reader.readAsDataURL(imageFile); + + return deferred.promise; + }; + + /* + * Convert a dataURI string to a blob + * Source: http://stackoverflow.com/a/17682951 + * @param {String} dataURI the dataURI can be a base64 encoded string or an URL encoded string. + * @returns {Blob} the blob + */ + this.dataURItoBlob = function(dataURI) { + // convert base64 to raw binary data held in a string + // doesn't handle URLEncoded DataURIs + var byteString; + if (dataURI.split(',')[0].indexOf('base64') >= 0) + byteString = atob(dataURI.split(',')[1]); + else + byteString = unescape(dataURI.split(',')[1]); + // separate out the mime component + var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0]; + + // write the bytes of the string to an ArrayBuffer + var ab = new ArrayBuffer(byteString.length); + var ia = new Uint8Array(ab); + for (var i = 0; i < byteString.length; i++) { + ia[i] = byteString.charCodeAt(i); + } + + // write the ArrayBuffer to a blob, and you're done + return new Blob([ab],{type: mimeString}); + }; + +}]); \ No newline at end of file -- cgit 1.5.1 From 4facbe02fbe53aafebfd68d88ae09c9ae77f3cd3 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 4 Nov 2014 17:48:47 +0000 Subject: URL encoding bugfix and add more tests. --- .../webclient/components/matrix/matrix-service.js | 2 +- syweb/webclient/test/unit/matrix-service.spec.js | 46 +++++++++++++++++++--- 2 files changed, 41 insertions(+), 7 deletions(-) (limited to 'syweb/webclient/components') diff --git a/syweb/webclient/components/matrix/matrix-service.js b/syweb/webclient/components/matrix/matrix-service.js index fedfb8910d..5b63fb4a3b 100644 --- a/syweb/webclient/components/matrix/matrix-service.js +++ b/syweb/webclient/components/matrix/matrix-service.js @@ -267,7 +267,7 @@ angular.module('matrixService', []) // get room state for a specific room roomState: function(room_id) { - var path = "/rooms/" + room_id + "/state"; + var path = "/rooms/" + encodeURIComponent(room_id) + "/state"; return doRequest("GET", path); }, diff --git a/syweb/webclient/test/unit/matrix-service.spec.js b/syweb/webclient/test/unit/matrix-service.spec.js index 3c5163e478..29d2ca7be7 100644 --- a/syweb/webclient/test/unit/matrix-service.spec.js +++ b/syweb/webclient/test/unit/matrix-service.spec.js @@ -1,5 +1,5 @@ describe('MatrixService', function() { - var scope, httpBackend, createController; + var scope, httpBackend; var BASE = "http://example.com"; var PREFIX = "/_matrix/client/api/v1"; var URL = BASE + PREFIX; @@ -7,7 +7,7 @@ describe('MatrixService', function() { beforeEach(module('matrixService')); - beforeEach(inject(function($rootScope, $httpBackend, $controller) { + beforeEach(inject(function($rootScope, $httpBackend) { httpBackend = $httpBackend; scope = $rootScope; })); @@ -17,6 +17,40 @@ describe('MatrixService', function() { httpBackend.verifyNoOutstandingRequest(); }); + it('should be able to POST /createRoom with an alias', inject(function(matrixService) { + matrixService.setConfig({ + access_token: "foobar", + homeserver: "http://example.com" + }); + var alias = "flibble"; + matrixService.create(alias).then(function(response) { + expect(response.data).toEqual({}); + }); + + httpBackend.expectPOST(URL + "/createRoom?access_token=foobar", + { + room_alias_name: alias + }) + .respond({}); + httpBackend.flush(); + })); + + it('should be able to GET /initialSync', inject(function(matrixService) { + matrixService.setConfig({ + access_token: "foobar", + homeserver: "http://example.com" + }); + var limit = 15; + matrixService.initialSync(limit).then(function(response) { + expect(response.data).toEqual([]); + }); + + httpBackend.expectGET( + URL + "/initialSync?access_token=foobar&limit=15") + .respond([]); + httpBackend.flush(); + })); + it('should be able to GET /rooms/$roomid/state', inject(function(matrixService) { matrixService.setConfig({ access_token: "foobar", @@ -26,8 +60,8 @@ describe('MatrixService', function() { expect(response.data).toEqual([]); }); - httpBackend.expect('GET', - URL + "/rooms/" + roomId + "/state?access_token=foobar") + httpBackend.expectGET( + URL + "/rooms/" + encodeURIComponent(roomId) + "/state?access_token=foobar") .respond([]); httpBackend.flush(); })); @@ -41,7 +75,7 @@ describe('MatrixService', function() { expect(response.data).toEqual({}); }); - httpBackend.expect('POST', + httpBackend.expectPOST( URL + "/join/" + encodeURIComponent(roomId) + "?access_token=foobar") .respond({}); httpBackend.flush(); @@ -56,7 +90,7 @@ describe('MatrixService', function() { expect(response.data).toEqual({}); }); - httpBackend.expect('POST', + httpBackend.expectPOST( URL + "/rooms/" + encodeURIComponent(roomId) + "/join?access_token=foobar") .respond({}); httpBackend.flush(); -- cgit 1.5.1 From 9f6d1b10ad5a9098a8f72157875ce97fc44bc423 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 5 Nov 2014 11:21:55 +0000 Subject: Be sure to urlencode/decode event types correctly in both the web client and HS. --- synapse/rest/room.py | 2 +- syweb/webclient/components/matrix/matrix-service.js | 4 ++-- syweb/webclient/test/unit/matrix-service.spec.js | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) (limited to 'syweb/webclient/components') diff --git a/synapse/rest/room.py b/synapse/rest/room.py index ec0ce78fda..d97babea08 100644 --- a/synapse/rest/room.py +++ b/synapse/rest/room.py @@ -148,7 +148,7 @@ class RoomStateEventRestServlet(RestServlet): content = _parse_json(request) event = self.event_factory.create_event( - etype=event_type, + etype=urllib.unquote(event_type), content=content, room_id=urllib.unquote(room_id), user_id=user.to_string(), diff --git a/syweb/webclient/components/matrix/matrix-service.js b/syweb/webclient/components/matrix/matrix-service.js index 5b63fb4a3b..e1e5b88b3e 100644 --- a/syweb/webclient/components/matrix/matrix-service.js +++ b/syweb/webclient/components/matrix/matrix-service.js @@ -375,9 +375,9 @@ angular.module('matrixService', []) sendStateEvent: function(room_id, eventType, content, state_key) { - var path = "/rooms/$room_id/state/"+eventType; + var path = "/rooms/$room_id/state/"+ encodeURIComponent(eventType); if (state_key !== undefined) { - path += "/" + state_key; + path += "/" + encodeURIComponent(state_key); } room_id = encodeURIComponent(room_id); path = path.replace("$room_id", room_id); diff --git a/syweb/webclient/test/unit/matrix-service.spec.js b/syweb/webclient/test/unit/matrix-service.spec.js index 95a43057c4..b54163a641 100644 --- a/syweb/webclient/test/unit/matrix-service.spec.js +++ b/syweb/webclient/test/unit/matrix-service.spec.js @@ -238,7 +238,7 @@ describe('MatrixService', function() { homeserver: "http://example.com" }); var roomId = "!fh38hfwfwef:example.com"; - var eventType = "com.example.events.test"; + var eventType = "com.example.events.test:special@characters"; var content = { testing: "1 2 3" }; @@ -262,11 +262,11 @@ describe('MatrixService', function() { homeserver: "http://example.com" }); var roomId = "!fh38hfwfwef:example.com"; - var eventType = "com.example.events.test"; + var eventType = "com.example.events.test:special@characters"; var content = { testing: "1 2 3" }; - var stateKey = "version1"; + var stateKey = "version:1"; matrixService.sendStateEvent(roomId, eventType, content, stateKey).then( function(response) { expect(response.data).toEqual({}); -- cgit 1.5.1 From 42081b1937127979f3fb0a673eefb866cb4de64e Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 5 Nov 2014 11:28:22 +0000 Subject: Don't urlencode event types just yet so older HSes don't 500. Skip the tests which test for urlencoding, and add a TODO in matrixService. --- syweb/webclient/components/matrix/matrix-service.js | 4 +++- syweb/webclient/test/unit/matrix-service.spec.js | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) (limited to 'syweb/webclient/components') diff --git a/syweb/webclient/components/matrix/matrix-service.js b/syweb/webclient/components/matrix/matrix-service.js index e1e5b88b3e..8ff2999e2d 100644 --- a/syweb/webclient/components/matrix/matrix-service.js +++ b/syweb/webclient/components/matrix/matrix-service.js @@ -375,7 +375,9 @@ angular.module('matrixService', []) sendStateEvent: function(room_id, eventType, content, state_key) { - var path = "/rooms/$room_id/state/"+ encodeURIComponent(eventType); + var path = "/rooms/$room_id/state/"+ eventType; + // TODO: uncomment this when matrix.org is updated, else all state events 500. + // var path = "/rooms/$room_id/state/"+ encodeURIComponent(eventType); if (state_key !== undefined) { path += "/" + encodeURIComponent(state_key); } diff --git a/syweb/webclient/test/unit/matrix-service.spec.js b/syweb/webclient/test/unit/matrix-service.spec.js index b54163a641..ed290f2ff3 100644 --- a/syweb/webclient/test/unit/matrix-service.spec.js +++ b/syweb/webclient/test/unit/matrix-service.spec.js @@ -231,7 +231,7 @@ describe('MatrixService', function() { httpBackend.flush(); })); - it('should be able to send generic state events without a state key', inject( + xit('should be able to send generic state events without a state key', inject( function(matrixService) { matrixService.setConfig({ access_token: "foobar", @@ -255,7 +255,7 @@ describe('MatrixService', function() { httpBackend.flush(); })); - it('should be able to send generic state events with a state key', inject( + xit('should be able to send generic state events with a state key', inject( function(matrixService) { matrixService.setConfig({ access_token: "foobar", -- cgit 1.5.1 From 988a8526b5a75a988fffd9ab5c3b4abbd2a41840 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 5 Nov 2014 14:35:41 +0000 Subject: Finish matrixService unit tests. Add missing encodeURIComponent to path args. --- .../webclient/components/matrix/matrix-service.js | 11 +- syweb/webclient/test/unit/matrix-service.spec.js | 288 ++++++++++++++++----- 2 files changed, 234 insertions(+), 65 deletions(-) (limited to 'syweb/webclient/components') diff --git a/syweb/webclient/components/matrix/matrix-service.js b/syweb/webclient/components/matrix/matrix-service.js index 8ff2999e2d..63051c4f47 100644 --- a/syweb/webclient/components/matrix/matrix-service.js +++ b/syweb/webclient/components/matrix/matrix-service.js @@ -443,7 +443,8 @@ angular.module('matrixService', []) redactEvent: function(room_id, event_id) { var path = "/rooms/$room_id/redact/$event_id"; - path = path.replace("$room_id", room_id); + path = path.replace("$room_id", encodeURIComponent(room_id)); + // TODO: encodeURIComponent when HS updated. path = path.replace("$event_id", event_id); var content = {}; return doRequest("POST", path, undefined, content); @@ -461,7 +462,7 @@ angular.module('matrixService', []) paginateBackMessages: function(room_id, from_token, limit) { var path = "/rooms/$room_id/messages"; - path = path.replace("$room_id", room_id); + path = path.replace("$room_id", encodeURIComponent(room_id)); var params = { from: from_token, limit: limit, @@ -509,12 +510,12 @@ angular.module('matrixService', []) setProfileInfo: function(data, info_segment) { var path = "/profile/$user/" + info_segment; - path = path.replace("$user", config.user_id); + path = path.replace("$user", encodeURIComponent(config.user_id)); return doRequest("PUT", path, undefined, data); }, getProfileInfo: function(userId, info_segment) { - var path = "/profile/"+userId + var path = "/profile/"+encodeURIComponent(userId); if (info_segment) path += '/' + info_segment; return doRequest("GET", path); }, @@ -633,7 +634,7 @@ angular.module('matrixService', []) // Set the logged in user presence state setUserPresence: function(presence) { var path = "/presence/$user_id/status"; - path = path.replace("$user_id", config.user_id); + path = path.replace("$user_id", encodeURIComponent(config.user_id)); return doRequest("PUT", path, undefined, { presence: presence }); diff --git a/syweb/webclient/test/unit/matrix-service.spec.js b/syweb/webclient/test/unit/matrix-service.spec.js index 2ca9a24323..4959f2395d 100644 --- a/syweb/webclient/test/unit/matrix-service.spec.js +++ b/syweb/webclient/test/unit/matrix-service.spec.js @@ -5,6 +5,11 @@ describe('MatrixService', function() { var URL = BASE + PREFIX; var roomId = "!wejigf387t34:matrix.org"; + var CONFIG = { + access_token: "foobar", + homeserver: BASE + }; + beforeEach(module('matrixService')); beforeEach(inject(function($rootScope, $httpBackend) { @@ -19,10 +24,7 @@ describe('MatrixService', function() { it('should be able to POST /createRoom with an alias', inject( function(matrixService) { - matrixService.setConfig({ - access_token: "foobar", - homeserver: "http://example.com" - }); + matrixService.setConfig(CONFIG); var alias = "flibble"; matrixService.create(alias).then(function(response) { expect(response.data).toEqual({}); @@ -37,10 +39,7 @@ describe('MatrixService', function() { })); it('should be able to GET /initialSync', inject(function(matrixService) { - matrixService.setConfig({ - access_token: "foobar", - homeserver: "http://example.com" - }); + matrixService.setConfig(CONFIG); var limit = 15; matrixService.initialSync(limit).then(function(response) { expect(response.data).toEqual([]); @@ -54,10 +53,7 @@ describe('MatrixService', function() { it('should be able to GET /rooms/$roomid/state', inject( function(matrixService) { - matrixService.setConfig({ - access_token: "foobar", - homeserver: "http://example.com" - }); + matrixService.setConfig(CONFIG); matrixService.roomState(roomId).then(function(response) { expect(response.data).toEqual([]); }); @@ -70,10 +66,7 @@ describe('MatrixService', function() { })); it('should be able to POST /join', inject(function(matrixService) { - matrixService.setConfig({ - access_token: "foobar", - homeserver: "http://example.com" - }); + matrixService.setConfig(CONFIG); matrixService.joinAlias(roomId).then(function(response) { expect(response.data).toEqual({}); }); @@ -88,10 +81,7 @@ describe('MatrixService', function() { it('should be able to POST /rooms/$roomid/join', inject( function(matrixService) { - matrixService.setConfig({ - access_token: "foobar", - homeserver: "http://example.com" - }); + matrixService.setConfig(CONFIG); matrixService.join(roomId).then(function(response) { expect(response.data).toEqual({}); }); @@ -106,10 +96,7 @@ describe('MatrixService', function() { it('should be able to POST /rooms/$roomid/invite', inject( function(matrixService) { - matrixService.setConfig({ - access_token: "foobar", - homeserver: "http://example.com" - }); + matrixService.setConfig(CONFIG); var inviteUserId = "@user:example.com"; matrixService.invite(roomId, inviteUserId).then(function(response) { expect(response.data).toEqual({}); @@ -127,10 +114,7 @@ describe('MatrixService', function() { it('should be able to POST /rooms/$roomid/leave', inject( function(matrixService) { - matrixService.setConfig({ - access_token: "foobar", - homeserver: "http://example.com" - }); + matrixService.setConfig(CONFIG); matrixService.leave(roomId).then(function(response) { expect(response.data).toEqual({}); }); @@ -145,10 +129,7 @@ describe('MatrixService', function() { it('should be able to POST /rooms/$roomid/ban', inject( function(matrixService) { - matrixService.setConfig({ - access_token: "foobar", - homeserver: "http://example.com" - }); + matrixService.setConfig(CONFIG); var userId = "@example:example.com"; var reason = "Because."; matrixService.ban(roomId, userId, reason).then(function(response) { @@ -168,10 +149,7 @@ describe('MatrixService', function() { it('should be able to GET /directory/room/$alias', inject( function(matrixService) { - matrixService.setConfig({ - access_token: "foobar", - homeserver: "http://example.com" - }); + matrixService.setConfig(CONFIG); var alias = "#test:example.com"; var roomId = "!wefuhewfuiw:example.com"; matrixService.resolveRoomAlias(alias).then(function(response) { @@ -190,10 +168,7 @@ describe('MatrixService', function() { })); it('should be able to send m.room.name', inject(function(matrixService) { - matrixService.setConfig({ - access_token: "foobar", - homeserver: "http://example.com" - }); + matrixService.setConfig(CONFIG); var roomId = "!fh38hfwfwef:example.com"; var name = "Room Name"; matrixService.setName(roomId, name).then(function(response) { @@ -211,10 +186,7 @@ describe('MatrixService', function() { })); it('should be able to send m.room.topic', inject(function(matrixService) { - matrixService.setConfig({ - access_token: "foobar", - homeserver: "http://example.com" - }); + matrixService.setConfig(CONFIG); var roomId = "!fh38hfwfwef:example.com"; var topic = "A room topic can go here."; matrixService.setTopic(roomId, topic).then(function(response) { @@ -233,10 +205,7 @@ describe('MatrixService', function() { it('should be able to send generic state events without a state key', inject( function(matrixService) { - matrixService.setConfig({ - access_token: "foobar", - homeserver: "http://example.com" - }); + matrixService.setConfig(CONFIG); var roomId = "!fh38hfwfwef:example.com"; var eventType = "com.example.events.test"; var content = { @@ -259,10 +228,7 @@ describe('MatrixService', function() { // 500 matrix.org xit('should be able to send generic state events with a state key', inject( function(matrixService) { - matrixService.setConfig({ - access_token: "foobar", - homeserver: "http://example.com" - }); + matrixService.setConfig(CONFIG); var roomId = "!fh38hfwfwef:example.com"; var eventType = "com.example.events.test:special@characters"; var content = { @@ -285,10 +251,7 @@ describe('MatrixService', function() { it('should be able to PUT generic events ', inject( function(matrixService) { - matrixService.setConfig({ - access_token: "foobar", - homeserver: "http://example.com" - }); + matrixService.setConfig(CONFIG); var roomId = "!fh38hfwfwef:example.com"; var eventType = "com.example.events.test"; var txnId = "42"; @@ -311,10 +274,7 @@ describe('MatrixService', function() { it('should be able to PUT text messages ', inject( function(matrixService) { - matrixService.setConfig({ - access_token: "foobar", - homeserver: "http://example.com" - }); + matrixService.setConfig(CONFIG); var roomId = "!fh38hfwfwef:example.com"; var body = "ABC 123"; matrixService.sendTextMessage(roomId, body).then( @@ -333,4 +293,212 @@ describe('MatrixService', function() { .respond({}); httpBackend.flush(); })); + + it('should be able to PUT emote messages ', inject( + function(matrixService) { + matrixService.setConfig(CONFIG); + var roomId = "!fh38hfwfwef:example.com"; + var body = "ABC 123"; + matrixService.sendEmoteMessage(roomId, body).then( + function(response) { + expect(response.data).toEqual({}); + }); + + httpBackend.expectPUT( + new RegExp(URL + "/rooms/" + encodeURIComponent(roomId) + + "/send/m.room.message/(.*)" + + "?access_token=foobar"), + { + body: body, + msgtype: "m.emote" + }) + .respond({}); + httpBackend.flush(); + })); + + it('should be able to POST redactions', inject( + function(matrixService) { + matrixService.setConfig(CONFIG); + var roomId = "!fh38hfwfwef:example.com"; + var eventId = "fwefwexample.com"; + matrixService.redactEvent(roomId, eventId).then( + function(response) { + expect(response.data).toEqual({}); + }); + + httpBackend.expectPOST(URL + "/rooms/" + encodeURIComponent(roomId) + + "/redact/" + encodeURIComponent(eventId) + + "?access_token=foobar") + .respond({}); + httpBackend.flush(); + })); + + it('should be able to GET /directory/room/$alias', inject( + function(matrixService) { + matrixService.setConfig(CONFIG); + var alias = "#test:example.com"; + var roomId = "!wefuhewfuiw:example.com"; + matrixService.resolveRoomAlias(alias).then(function(response) { + expect(response.data).toEqual({ + room_id: roomId + }); + }); + + httpBackend.expectGET( + URL + "/directory/room/" + encodeURIComponent(alias) + + "?access_token=foobar") + .respond({ + room_id: roomId + }); + httpBackend.flush(); + })); + + it('should be able to GET /rooms/$roomid/members', inject( + function(matrixService) { + matrixService.setConfig(CONFIG); + var roomId = "!wefuhewfuiw:example.com"; + matrixService.getMemberList(roomId).then(function(response) { + expect(response.data).toEqual({}); + }); + + httpBackend.expectGET( + URL + "/rooms/" + encodeURIComponent(roomId) + + "/members?access_token=foobar") + .respond({}); + httpBackend.flush(); + })); + + it('should be able to paginate a room', inject( + function(matrixService) { + matrixService.setConfig(CONFIG); + var roomId = "!wefuhewfuiw:example.com"; + var from = "3t_44e_54z"; + var limit = 20; + matrixService.paginateBackMessages(roomId, from, limit).then(function(response) { + expect(response.data).toEqual({}); + }); + + httpBackend.expectGET( + URL + "/rooms/" + encodeURIComponent(roomId) + + "/messages?access_token=foobar&dir=b&from="+ + encodeURIComponent(from)+"&limit="+limit) + .respond({}); + httpBackend.flush(); + })); + + it('should be able to GET /publicRooms', inject( + function(matrixService) { + matrixService.setConfig(CONFIG); + matrixService.publicRooms().then(function(response) { + expect(response.data).toEqual({}); + }); + + httpBackend.expectGET( + new RegExp(URL + "/publicRooms(.*)")) + .respond({}); + httpBackend.flush(); + })); + + it('should be able to GET /profile/$userid/displayname', inject( + function(matrixService) { + matrixService.setConfig(CONFIG); + var userId = "@foo:example.com"; + matrixService.getDisplayName(userId).then(function(response) { + expect(response.data).toEqual({}); + }); + + httpBackend.expectGET(URL + "/profile/" + encodeURIComponent(userId) + + "/displayname?access_token=foobar") + .respond({}); + httpBackend.flush(); + })); + + it('should be able to GET /profile/$userid/avatar_url', inject( + function(matrixService) { + matrixService.setConfig(CONFIG); + var userId = "@foo:example.com"; + matrixService.getProfilePictureUrl(userId).then(function(response) { + expect(response.data).toEqual({}); + }); + + httpBackend.expectGET(URL + "/profile/" + encodeURIComponent(userId) + + "/avatar_url?access_token=foobar") + .respond({}); + httpBackend.flush(); + })); + + it('should be able to PUT /profile/$me/avatar_url', inject( + function(matrixService) { + var testConfig = angular.copy(CONFIG); + testConfig.user_id = "@bob:example.com"; + matrixService.setConfig(testConfig); + var url = "http://example.com/mypic.jpg"; + matrixService.setProfilePictureUrl(url).then(function(response) { + expect(response.data).toEqual({}); + }); + httpBackend.expectPUT(URL + "/profile/" + + encodeURIComponent(testConfig.user_id) + + "/avatar_url?access_token=foobar", + { + avatar_url: url + }) + .respond({}); + httpBackend.flush(); + })); + + it('should be able to PUT /profile/$me/displayname', inject( + function(matrixService) { + var testConfig = angular.copy(CONFIG); + testConfig.user_id = "@bob:example.com"; + matrixService.setConfig(testConfig); + var displayname = "Bob Smith"; + matrixService.setDisplayName(displayname).then(function(response) { + expect(response.data).toEqual({}); + }); + httpBackend.expectPUT(URL + "/profile/" + + encodeURIComponent(testConfig.user_id) + + "/displayname?access_token=foobar", + { + displayname: displayname + }) + .respond({}); + httpBackend.flush(); + })); + + it('should be able to login with password', inject( + function(matrixService) { + matrixService.setConfig(CONFIG); + var userId = "@bob:example.com"; + var password = "monkey"; + matrixService.login(userId, password).then(function(response) { + expect(response.data).toEqual({}); + }); + httpBackend.expectPOST(new RegExp(URL+"/login(.*)"), + { + user: userId, + password: password, + type: "m.login.password" + }) + .respond({}); + httpBackend.flush(); + })); + + it('should be able to PUT presence status', inject( + function(matrixService) { + var testConfig = angular.copy(CONFIG); + testConfig.user_id = "@bob:example.com"; + matrixService.setConfig(testConfig); + var status = "unavailable"; + matrixService.setUserPresence(status).then(function(response) { + expect(response.data).toEqual({}); + }); + httpBackend.expectPUT(URL+"/presence/"+ + encodeURIComponent(testConfig.user_id)+ + "/status?access_token=foobar", + { + presence: status + }) + .respond({}); + httpBackend.flush(); + })); }); -- cgit 1.5.1 From a92092340b5de022d1e48ecd4176cfb9b200b4d6 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 6 Nov 2014 11:14:31 +0000 Subject: Fix broken tests which were previously skipped. --- syweb/webclient/components/matrix/matrix-filter.js | 14 +++++++++++++- syweb/webclient/test/unit/filters.spec.js | 19 +++++++------------ 2 files changed, 20 insertions(+), 13 deletions(-) (limited to 'syweb/webclient/components') diff --git a/syweb/webclient/components/matrix/matrix-filter.js b/syweb/webclient/components/matrix/matrix-filter.js index 4d264e93f3..e84c197c76 100644 --- a/syweb/webclient/components/matrix/matrix-filter.js +++ b/syweb/webclient/components/matrix/matrix-filter.js @@ -38,7 +38,7 @@ function($rootScope, matrixService, eventHandlerService, modelService) { 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; } @@ -56,6 +56,9 @@ function($rootScope, matrixService, eventHandlerService, modelService) { var member = room.members[i]; if (member.state_key !== user_id) { roomName = eventHandlerService.getUserDisplayName(room_id, member.state_key); + if (!roomName) { + roomName = member.state_key; + } break; } } @@ -70,13 +73,22 @@ function($rootScope, matrixService, eventHandlerService, modelService) { if (room.members[otherUserId].content.membership === "invite") { // someone invited us, use the right ID. roomName = eventHandlerService.getUserDisplayName(room_id, room.members[otherUserId].user_id); + if (!roomName) { + roomName = room.members[otherUserId].user_id; + } } else { roomName = eventHandlerService.getUserDisplayName(room_id, otherUserId); + if (!roomName) { + roomName = user_id; + } } } else { // it isn't us, so use their name if we know it. roomName = eventHandlerService.getUserDisplayName(room_id, otherUserId); + if (!roomName) { + roomName = otherUserId; + } } } else if (Object.keys(room.members).length === 0) { diff --git a/syweb/webclient/test/unit/filters.spec.js b/syweb/webclient/test/unit/filters.spec.js index f037425208..2e8d0c4036 100644 --- a/syweb/webclient/test/unit/filters.spec.js +++ b/syweb/webclient/test/unit/filters.spec.js @@ -121,9 +121,8 @@ describe('mRoomName filter', function() { /**** ROOM ALIAS ****/ - // FIXME - xit("should show the room alias if one exists for private (invite join_rules) rooms if a room name doesn't exist.", function() { - var testAlias = "#thealias:matrix.org"; + it("should show the room alias if one exists for private (invite join_rules) rooms if a room name doesn't exist.", function() { + testAlias = "#thealias:matrix.org"; testUserId = "@me:matrix.org"; testRoomState.setJoinRule("invite"); testRoomState.setMember(testUserId, "join"); @@ -131,9 +130,8 @@ describe('mRoomName filter', function() { expect(output).toEqual(testAlias); }); - // FIXME - xit("should show the room alias if one exists for public (public join_rules) rooms if a room name doesn't exist.", function() { - var testAlias = "#thealias:matrix.org"; + it("should show the room alias if one exists for public (public join_rules) rooms if a room name doesn't exist.", function() { + testAlias = "#thealias:matrix.org"; testUserId = "@me:matrix.org"; testRoomState.setJoinRule("public"); testRoomState.setMember(testUserId, "join"); @@ -172,8 +170,7 @@ describe('mRoomName filter', function() { expect(output).toEqual(testDisplayName); }); - // FIXME - xit("should show your user ID for private (invite join_rules) rooms if a room name and alias don't exist and it is a self-chat and they don't have a display name set.", function() { + it("should show your user ID for private (invite join_rules) rooms if a room name and alias don't exist and it is a self-chat and they don't have a display name set.", function() { testUserId = "@me:matrix.org"; testRoomState.setJoinRule("private"); testRoomState.setMember(testUserId, "join"); @@ -194,8 +191,7 @@ describe('mRoomName filter', function() { expect(output).toEqual(testOtherDisplayName); }); - // FIXME - xit("should show the other user's ID for private (invite join_rules) rooms if a room name and alias don't exist and it is a 1:1-chat and they don't have a display name set.", function() { + it("should show the other user's ID for private (invite join_rules) rooms if a room name and alias don't exist and it is a 1:1-chat and they don't have a display name set.", function() { testUserId = "@me:matrix.org"; otherUserId = "@alice:matrix.org"; testRoomState.setJoinRule("private"); @@ -220,8 +216,7 @@ describe('mRoomName filter', function() { expect(output).toEqual(testOtherDisplayName); }); - // FIXME - xit("should show the other user's ID for private (invite join_rules) rooms if you are invited to it and the inviter doesn't have a display name.", function() { + it("should show the other user's ID for private (invite join_rules) rooms if you are invited to it and the inviter doesn't have a display name.", function() { testUserId = "@me:matrix.org"; testDisplayName = "Me"; otherUserId = "@alice:matrix.org"; -- cgit 1.5.1 From 8bcd36377a04bede2e2d74dcd7f18742d0982ad5 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 6 Nov 2014 13:37:05 +0000 Subject: Factor out room name logic: mRoomName is the canonical source. --- .../webclient/components/matrix/event-handler-service.js | 16 ++++------------ syweb/webclient/test/unit/register-controller.spec.js | 4 ++-- 2 files changed, 6 insertions(+), 14 deletions(-) (limited to 'syweb/webclient/components') diff --git a/syweb/webclient/components/matrix/event-handler-service.js b/syweb/webclient/components/matrix/event-handler-service.js index 027c80a1b6..38a6efced7 100644 --- a/syweb/webclient/components/matrix/event-handler-service.js +++ b/syweb/webclient/components/matrix/event-handler-service.js @@ -26,8 +26,8 @@ Typically, this service will store events and broadcast them to any listeners (e.g. controllers) via $broadcast. */ angular.module('eventHandlerService', []) -.factory('eventHandlerService', ['matrixService', '$rootScope', '$q', '$timeout', 'mPresence', 'notificationService', 'modelService', -function(matrixService, $rootScope, $q, $timeout, mPresence, notificationService, modelService) { +.factory('eventHandlerService', ['matrixService', '$rootScope', '$q', '$timeout', '$filter', 'mPresence', 'notificationService', 'modelService', +function(matrixService, $rootScope, $q, $timeout, $filter, mPresence, notificationService, modelService) { var ROOM_CREATE_EVENT = "ROOM_CREATE_EVENT"; var MSG_EVENT = "MSG_EVENT"; var MEMBER_EVENT = "MEMBER_EVENT"; @@ -135,16 +135,8 @@ function(matrixService, $rootScope, $q, $timeout, mPresence, notificationService else if (event.content.msgtype === "m.image") { message = displayname + " sent an image."; } - - var roomTitle = matrixService.getRoomIdToAliasMapping(event.room_id); - var theRoom = modelService.getRoom(event.room_id); - if (!roomTitle && theRoom.current_room_state.state("m.room.name") && theRoom.current_room_state.state("m.room.name").content) { - roomTitle = theRoom.current_room_state.state("m.room.name").content.name; - } - - if (!roomTitle) { - roomTitle = event.room_id; - } + + var roomTitle = $filter("mRoomName")(event.room_id); notificationService.showNotification( displayname + " (" + roomTitle + ")", diff --git a/syweb/webclient/test/unit/register-controller.spec.js b/syweb/webclient/test/unit/register-controller.spec.js index ce6ef1f4e2..b5c7842358 100644 --- a/syweb/webclient/test/unit/register-controller.spec.js +++ b/syweb/webclient/test/unit/register-controller.spec.js @@ -76,8 +76,8 @@ describe("RegisterController ", function() { scope.account.pwd1 = "password"; scope.account.pwd2 = "password"; scope.account.desired_user_id = "bob"; - scope.register(); - rootScope.$digest(); + scope.register(); // this depends on the result of a deferred + rootScope.$digest(); // which is delivered after the digest expect(scope.feedback).not.toEqual(prevFeedback); }); -- cgit 1.5.1 From e3c3f5a6d04bfbc0256010e9fb4dad7616ebbcc5 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 6 Nov 2014 14:52:22 +0000 Subject: Swap from using raw m.room.member events for room members to using actual RoomMember objects, so User objects can be tacked on. Update tests. --- .../components/matrix/event-handler-service.js | 9 +++-- syweb/webclient/components/matrix/matrix-call.js | 2 +- syweb/webclient/components/matrix/matrix-filter.js | 8 ++--- syweb/webclient/components/matrix/model-service.js | 4 ++- syweb/webclient/recents/recents-filter.js | 3 ++ syweb/webclient/room/room-controller.js | 4 +-- .../test/unit/event-handler-service.spec.js | 38 ++++++++++++++-------- syweb/webclient/test/unit/filters.spec.js | 12 ++++--- syweb/webclient/test/unit/model-service.spec.js | 2 +- 9 files changed, 52 insertions(+), 30 deletions(-) (limited to 'syweb/webclient/components') diff --git a/syweb/webclient/components/matrix/event-handler-service.js b/syweb/webclient/components/matrix/event-handler-service.js index 38a6efced7..a9c6eb34c7 100644 --- a/syweb/webclient/components/matrix/event-handler-service.js +++ b/syweb/webclient/components/matrix/event-handler-service.js @@ -141,7 +141,7 @@ function(matrixService, $rootScope, $q, $timeout, $filter, mPresence, notificati notificationService.showNotification( displayname + " (" + roomTitle + ")", message, - member ? member.avatar_url : undefined, + member ? member.event.content.avatar_url : undefined, function() { console.log("notification.onclick() room=" + event.room_id); $rootScope.goToPage('room/' + event.room_id); @@ -306,6 +306,9 @@ function(matrixService, $rootScope, $q, $timeout, $filter, mPresence, notificati // 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; @@ -315,7 +318,7 @@ function(matrixService, $rootScope, $q, $timeout, $filter, mPresence, notificati 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]; + var member2 = room.current_room_state.members[member_id].event; if (member2.content.displayname && member2.content.displayname === displayName) { displayName = displayName + " (" + user_id + ")"; break; @@ -551,7 +554,7 @@ function(matrixService, $rootScope, $q, $timeout, $filter, mPresence, notificati 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]; + var member = room.current_room_state.members[i].event; if ("join" === member.content.membership) { memberCount = memberCount + 1; diff --git a/syweb/webclient/components/matrix/matrix-call.js b/syweb/webclient/components/matrix/matrix-call.js index 5a2807c755..465b2b7807 100644 --- a/syweb/webclient/components/matrix/matrix-call.js +++ b/syweb/webclient/components/matrix/matrix-call.js @@ -214,7 +214,7 @@ angular.module('MatrixCall', []) var self = this; var roomMembers = modelService.getRoom(this.room_id).current_room_state.members; - if (roomMembers[matrixService.config().user_id].membership != 'join') { + if (roomMembers[matrixService.config().user_id].event.content.membership != 'join') { console.log("We need to join the room before we can accept this call"); matrixService.join(this.room_id).then(function() { self.answer(); diff --git a/syweb/webclient/components/matrix/matrix-filter.js b/syweb/webclient/components/matrix/matrix-filter.js index e84c197c76..aeebedc784 100644 --- a/syweb/webclient/components/matrix/matrix-filter.js +++ b/syweb/webclient/components/matrix/matrix-filter.js @@ -53,7 +53,7 @@ function($rootScope, matrixService, eventHandlerService, modelService) { for (var i in room.members) { if (!room.members.hasOwnProperty(i)) continue; - var member = room.members[i]; + var member = room.members[i].event; if (member.state_key !== user_id) { roomName = eventHandlerService.getUserDisplayName(room_id, member.state_key); if (!roomName) { @@ -70,11 +70,11 @@ function($rootScope, matrixService, eventHandlerService, modelService) { 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].content.membership === "invite") { + if (room.members[otherUserId].event.content.membership === "invite") { // someone invited us, use the right ID. - roomName = eventHandlerService.getUserDisplayName(room_id, room.members[otherUserId].user_id); + roomName = eventHandlerService.getUserDisplayName(room_id, room.members[otherUserId].event.user_id); if (!roomName) { - roomName = room.members[otherUserId].user_id; + roomName = room.members[otherUserId].event.user_id; } } else { diff --git a/syweb/webclient/components/matrix/model-service.js b/syweb/webclient/components/matrix/model-service.js index 8b2ee877b1..8e0ce8d1a9 100644 --- a/syweb/webclient/components/matrix/model-service.js +++ b/syweb/webclient/components/matrix/model-service.js @@ -106,7 +106,9 @@ angular.module('modelService', []) storeStateEvent: function storeState(event) { this.state_events[event.type + event.state_key] = event; if (event.type === "m.room.member") { - this.members[event.state_key] = event; + var rm = new RoomMember(); + rm.event = event; + this.members[event.state_key] = rm; } }, diff --git a/syweb/webclient/recents/recents-filter.js b/syweb/webclient/recents/recents-filter.js index 39c2359967..cfbc6f4bd8 100644 --- a/syweb/webclient/recents/recents-filter.js +++ b/syweb/webclient/recents/recents-filter.js @@ -30,6 +30,9 @@ angular.module('RecentsController') // Show the room only if the user has joined it or has been invited // (ie, do not show it if he has been banned) var member = modelService.getMember(room_id, user_id); + if (member) { + member = member.event; + } room.recent.me = member; if (member && ("invite" === member.content.membership || "join" === member.content.membership)) { if ("invite" === member.content.membership) { diff --git a/syweb/webclient/room/room-controller.js b/syweb/webclient/room/room-controller.js index a2bc23195d..d3fb85b9dc 100644 --- a/syweb/webclient/room/room-controller.js +++ b/syweb/webclient/room/room-controller.js @@ -754,13 +754,13 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput']) for (var i in members) { if (!members.hasOwnProperty(i)) continue; - var member = members[i]; + var member = members[i].event; updateMemberList(member); } // Check if the user has already join the room if ($scope.state.user_id in members) { - if ("join" === members[$scope.state.user_id].membership) { + if ("join" === members[$scope.state.user_id].event.content.membership) { needsToJoin = false; } } diff --git a/syweb/webclient/test/unit/event-handler-service.spec.js b/syweb/webclient/test/unit/event-handler-service.spec.js index 023abec98b..2a4dc3b5a5 100644 --- a/syweb/webclient/test/unit/event-handler-service.spec.js +++ b/syweb/webclient/test/unit/event-handler-service.spec.js @@ -36,20 +36,28 @@ describe('EventHandlerService', function() { current_room_state: { members: { "@adam:matrix.org": { - content: { membership: "join" }, - user_id: "@adam:matrix.org" + event: { + content: { membership: "join" }, + user_id: "@adam:matrix.org" + } }, "@beth:matrix.org": { - content: { membership: "invite" }, - user_id: "@beth:matrix.org" + event: { + content: { membership: "invite" }, + user_id: "@beth:matrix.org" + } }, "@charlie:matrix.org": { - content: { membership: "join" }, - user_id: "@charlie:matrix.org" + event: { + content: { membership: "join" }, + user_id: "@charlie:matrix.org" + } }, "@danice:matrix.org": { - content: { membership: "leave" }, - user_id: "@danice:matrix.org" + event: { + content: { membership: "leave" }, + user_id: "@danice:matrix.org" + } } } } @@ -70,12 +78,16 @@ describe('EventHandlerService', function() { current_room_state: { members: { "@adam:matrix.org": { - content: { membership: "join" }, - user_id: "@adam:matrix.org" + event: { + content: { membership: "join" }, + user_id: "@adam:matrix.org" + } }, "@beth:matrix.org": { - content: { membership: "join" }, - user_id: "@beth:matrix.org" + event: { + content: { membership: "join" }, + user_id: "@beth:matrix.org" + } } }, s: { @@ -102,4 +114,4 @@ describe('EventHandlerService', function() { num = eventHandlerService.getUserPowerLevel(roomId, "@unknown:matrix.org"); expect(num).toEqual(50); })); -}); \ No newline at end of file +}); diff --git a/syweb/webclient/test/unit/filters.spec.js b/syweb/webclient/test/unit/filters.spec.js index 2e8d0c4036..7324a8e028 100644 --- a/syweb/webclient/test/unit/filters.spec.js +++ b/syweb/webclient/test/unit/filters.spec.js @@ -86,11 +86,13 @@ describe('mRoomName filter', function() { inviter_user_id = user_id; } this.s["m.room.member" + user_id] = { - content: { - membership: membership - }, - state_key: user_id, - user_id: inviter_user_id + event: { + content: { + membership: membership + }, + state_key: user_id, + user_id: inviter_user_id + } }; this.members[user_id] = this.s["m.room.member" + user_id]; } diff --git a/syweb/webclient/test/unit/model-service.spec.js b/syweb/webclient/test/unit/model-service.spec.js index 2e012efe90..e2fa8ceba3 100644 --- a/syweb/webclient/test/unit/model-service.spec.js +++ b/syweb/webclient/test/unit/model-service.spec.js @@ -25,6 +25,6 @@ describe('ModelService', function() { }); var user = modelService.getMember(roomId, userId); - expect(user.state_key).toEqual(userId); + expect(user.event.state_key).toEqual(userId); })); }); -- cgit 1.5.1 From 4b256cab317ab02da549ce64a33911743f1b9d6f Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 6 Nov 2014 16:48:01 +0000 Subject: Don't cache isWebRTCSupported because whether webRTC is supported might change part-way through the page's lifecycle if your webrtc support comes from some kind of injected content script (hello OpenWebRTC Sarafi extension) --- syweb/webclient/components/matrix/matrix-call.js | 9 +++------ syweb/webclient/components/matrix/matrix-phone-service.js | 2 +- syweb/webclient/index.html | 2 +- syweb/webclient/room/room.html | 12 ++++++------ 4 files changed, 11 insertions(+), 14 deletions(-) (limited to 'syweb/webclient/components') diff --git a/syweb/webclient/components/matrix/matrix-call.js b/syweb/webclient/components/matrix/matrix-call.js index 465b2b7807..c13083298e 100644 --- a/syweb/webclient/components/matrix/matrix-call.js +++ b/syweb/webclient/components/matrix/matrix-call.js @@ -40,14 +40,11 @@ window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConne window.RTCSessionDescription = window.RTCSessionDescription || window.webkitRTCSessionDescription || window.mozRTCSessionDescription; window.RTCIceCandidate = window.RTCIceCandidate || window.webkitRTCIceCandidate || window.mozRTCIceCandidate; -// Returns true if the browser supports all required features to make WebRTC call -var isWebRTCSupported = function () { - return !!(navigator.getUserMedia || window.RTCPeerConnection || window.RTCSessionDescription || window.RTCIceCandidate); -}; - angular.module('MatrixCall', []) .factory('MatrixCall', ['matrixService', 'matrixPhoneService', 'modelService', '$rootScope', '$timeout', function MatrixCallFactory(matrixService, matrixPhoneService, modelService, $rootScope, $timeout) { - $rootScope.isWebRTCSupported = isWebRTCSupported(); + $rootScope.isWebRTCSupported = function () { + return !!(navigator.getUserMedia || window.RTCPeerConnection || window.RTCSessionDescription || window.RTCIceCandidate); + }; var MatrixCall = function(room_id) { this.room_id = room_id; diff --git a/syweb/webclient/components/matrix/matrix-phone-service.js b/syweb/webclient/components/matrix/matrix-phone-service.js index 06465ed821..55dbbf522e 100644 --- a/syweb/webclient/components/matrix/matrix-phone-service.js +++ b/syweb/webclient/components/matrix/matrix-phone-service.js @@ -60,7 +60,7 @@ angular.module('matrixPhoneService', []) var MatrixCall = $injector.get('MatrixCall'); var call = new MatrixCall(event.room_id); - if (!isWebRTCSupported()) { + 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. diff --git a/syweb/webclient/index.html b/syweb/webclient/index.html index 3ed968a5ea..992e8d3377 100644 --- a/syweb/webclient/index.html +++ b/syweb/webclient/index.html @@ -85,7 +85,7 @@ - + diff --git a/syweb/webclient/room/room.html b/syweb/webclient/room/room.html index ca5669a732..e59cc30edc 100644 --- a/syweb/webclient/room/room.html +++ b/syweb/webclient/room/room.html @@ -182,8 +182,8 @@ (msg.content.formatted_body | unsanitizedLinky) : (msg.content.msgtype === 'm.text' && msg.type === 'm.room.message') ? (msg.content.body | linky:'_blank') : '' "/> - Outgoing Call{{ isWebRTCSupported ? '' : ' (But your browser does not support VoIP)' }} - Incoming Call{{ isWebRTCSupported ? '' : ' (But your browser does not support VoIP)' }} + Outgoing Call{{ isWebRTCSupported() ? '' : ' (But your browser does not support VoIP)' }} + Incoming Call{{ isWebRTCSupported() ? '' : ' (But your browser does not support VoIP)' }}
@@ -248,15 +248,15 @@ -- cgit 1.5.1