diff options
author | Paul "LeoNerd" Evans <paul@matrix.org> | 2014-11-17 16:59:24 +0000 |
---|---|---|
committer | Paul "LeoNerd" Evans <paul@matrix.org> | 2014-11-17 16:59:24 +0000 |
commit | 31a049eb692d37387a2db972da754f7ec56218c7 (patch) | |
tree | 9e5f47abad904d30c08d2f340b543a631e436894 /syweb/webclient/components | |
parent | Include room membership in room initialSync (diff) | |
parent | SYN-148: Add the alias after creating the room (diff) | |
download | synapse-31a049eb692d37387a2db972da754f7ec56218c7.tar.xz |
Merge branch 'develop' into room-initial-sync
Conflicts: synapse/handlers/message.py
Diffstat (limited to 'syweb/webclient/components')
-rw-r--r-- | syweb/webclient/components/fileInput/file-input-directive.js | 56 | ||||
-rw-r--r-- | syweb/webclient/components/fileUpload/file-upload-service.js | 180 | ||||
-rw-r--r-- | syweb/webclient/components/matrix/event-handler-service.js | 598 | ||||
-rw-r--r-- | syweb/webclient/components/matrix/event-stream-service.js | 160 | ||||
-rw-r--r-- | syweb/webclient/components/matrix/matrix-call.js | 645 | ||||
-rw-r--r-- | syweb/webclient/components/matrix/matrix-filter.js | 120 | ||||
-rw-r--r-- | syweb/webclient/components/matrix/matrix-phone-service.js | 155 | ||||
-rw-r--r-- | syweb/webclient/components/matrix/matrix-service.js | 762 | ||||
-rw-r--r-- | syweb/webclient/components/matrix/model-service.js | 172 | ||||
-rw-r--r-- | syweb/webclient/components/matrix/notification-service.js | 104 | ||||
-rw-r--r-- | syweb/webclient/components/matrix/presence-service.js | 113 | ||||
-rw-r--r-- | syweb/webclient/components/utilities/utilities-service.js | 151 |
12 files changed, 0 insertions, 3216 deletions
diff --git a/syweb/webclient/components/fileInput/file-input-directive.js b/syweb/webclient/components/fileInput/file-input-directive.js deleted file mode 100644 index 9c849a140f..0000000000 --- a/syweb/webclient/components/fileInput/file-input-directive.js +++ /dev/null @@ -1,56 +0,0 @@ -/* - 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: '<div ng-transclude></div><input ng-hide="true" type="file" accept="image/*"/>', - 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 deleted file mode 100644 index b544e29509..0000000000 --- a/syweb/webclient/components/fileUpload/file-upload-service.js +++ /dev/null @@ -1,180 +0,0 @@ -/* - 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 deleted file mode 100644 index a9c6eb34c7..0000000000 --- a/syweb/webclient/components/matrix/event-handler-service.js +++ /dev/null @@ -1,598 +0,0 @@ -/* -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', '$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"; - 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 = $filter("mRoomName")(event.room_id); - - notificationService.showNotification( - displayname + " (" + roomTitle + ")", - message, - member ? member.event.content.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<eventList.length; i++) { - if (eventList[i].event_id === event.redacts) { - console.log("Removing event " + event.redacts); - eventList.splice(i, 1); - break; - } - } - - console.log("Redacted an event."); - } - } - - /** - * 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 - */ - var getUserDisplayName = function(room_id, user_id) { - var displayName; - - // Get the user display name from the member list of the room - var member = modelService.getMember(room_id, user_id); - if (member) { - member = member.event; - } - if (member && member.content.displayname) { // Do not consider null displayname - displayName = member.content.displayname; - - // Disambiguate users who have the same displayname in the room - if (user_id !== matrixService.config().user_id) { - var room = modelService.getRoom(room_id); - - for (var member_id in room.current_room_state.members) { - if (room.current_room_state.members.hasOwnProperty(member_id) && member_id !== user_id) { - var member2 = room.current_room_state.members[member_id].event; - if (member2.content.displayname && member2.content.displayname === displayName) { - displayName = displayName + " (" + user_id + ")"; - break; - } - } - } - } - } - - // The user may not have joined the room yet. So try to resolve display name from presence data - // Note: This data may not be available - if (undefined === displayName && user_id in $rootScope.presence) { - displayName = $rootScope.presence[user_id].content.displayname; - } - - if (undefined === displayName) { - // By default, use the user ID - displayName = user_id; - } - return displayName; - }; - - return { - ROOM_CREATE_EVENT: ROOM_CREATE_EVENT, - MSG_EVENT: MSG_EVENT, - MEMBER_EVENT: MEMBER_EVENT, - PRESENCE_EVENT: PRESENCE_EVENT, - POWERLEVEL_EVENT: POWERLEVEL_EVENT, - CALL_EVENT: CALL_EVENT, - NAME_EVENT: NAME_EVENT, - TOPIC_EVENT: TOPIC_EVENT, - RESET_EVENT: RESET_EVENT, - - reset: function() { - reset(); - $rootScope.$broadcast(RESET_EVENT); - }, - - handleEvent: function(event, isLiveEvent, isStateEvent) { - - // Avoid duplicated events - // Needed for rooms where initialSync has not been done. - // In this case, we do not know where to start pagination. So, it starts from the END - // and we can have the same event (ex: joined, invitation) coming from the pagination - // AND from the event stream. - // FIXME: This workaround should be no more required when /initialSync on a particular room - // will be available (as opposite to the global /initialSync done at startup) - if (!isStateEvent) { // Do not consider state events - if (event.event_id && eventMap[event.event_id]) { - console.log("discarding duplicate event: " + JSON.stringify(event, undefined, 4)); - return; - } - else { - eventMap[event.event_id] = 1; - } - } - - if (event.type.indexOf('m.call.') === 0) { - handleCallEvent(event, isLiveEvent); - } - else { - switch(event.type) { - case "m.room.create": - handleRoomCreate(event, isLiveEvent); - break; - case "m.room.aliases": - handleRoomAliases(event, isLiveEvent); - break; - case "m.room.message": - handleMessage(event, isLiveEvent); - break; - case "m.room.member": - handleRoomMember(event, isLiveEvent, isStateEvent); - break; - case "m.presence": - handlePresence(event, isLiveEvent); - break; - case 'm.room.ops_levels': - case 'm.room.send_event_level': - case 'm.room.add_state_level': - case 'm.room.join_rules': - case 'm.room.power_levels': - handlePowerLevels(event, isLiveEvent); - break; - case 'm.room.name': - handleRoomName(event, isLiveEvent, isStateEvent); - break; - case 'm.room.topic': - handleRoomTopic(event, isLiveEvent, isStateEvent); - break; - case 'm.room.redaction': - handleRedaction(event, isLiveEvent); - break; - default: - // if it is a state event, then just add it in so it - // displays on the Room Info screen. - if (typeof(event.state_key) === "string") { // incls. 0-len strings - if (event.room_id) { - handleRoomStateEvent(event, isLiveEvent, false); - } - } - console.log("Unable to handle event type " + event.type); - // console.log(JSON.stringify(event, undefined, 4)); - break; - } - } - }, - - // isLiveEvents determines whether notifications should be shown, whether - // messages get appended to the start/end of lists, etc. - handleEvents: function(events, isLiveEvents, isStateEvents) { - for (var i=0; i<events.length; i++) { - this.handleEvent(events[i], isLiveEvents, isStateEvents); - } - }, - - // Handle messages from /initialSync or /messages - handleRoomMessages: function(room_id, messages, isLiveEvents, dir) { - var events = messages.chunk; - - // Handles messages according to their time order - if (dir && 'b' === dir) { - // paginateBackMessages requests messages to be in reverse chronological order - for (var i=0; i<events.length; i++) { - this.handleEvent(events[i], isLiveEvents, isLiveEvents); - } - - // Store how far back we've paginated - var room = modelService.getRoom(room_id); - room.old_room_state.pagination_token = messages.end; - - } - else { - // InitialSync returns messages in chronological order, so invert - // it to get most recent > 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].event; - - 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 deleted file mode 100644 index c03f0b953b..0000000000 --- a/syweb/webclient/components/matrix/event-stream-service.js +++ /dev/null @@ -1,160 +0,0 @@ -/* -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 deleted file mode 100644 index b560cf7daa..0000000000 --- a/syweb/webclient/components/matrix/matrix-call.js +++ /dev/null @@ -1,645 +0,0 @@ -/* -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); -} - -angular.module('MatrixCall', []) -.factory('MatrixCall', ['matrixService', 'matrixPhoneService', 'modelService', '$rootScope', '$timeout', function MatrixCallFactory(matrixService, matrixPhoneService, modelService, $rootScope, $timeout) { - $rootScope.isWebRTCSupported = function () { - navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia; - window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection; // but not mozRTCPeerConnection because its interface is not compatible - window.RTCSessionDescription = window.RTCSessionDescription || window.webkitRTCSessionDescription || window.mozRTCSessionDescription; - window.RTCIceCandidate = window.RTCIceCandidate || window.webkitRTCIceCandidate || window.mozRTCIceCandidate; - - return !!(navigator.getUserMedia || window.RTCPeerConnection || window.RTCSessionDescription || window.RTCIceCandidate); - }; - - 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.getRemoteVideoElement(), 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'; - - // This also applied to the Safari OpenWebRTC extension so let's just do this all the time at least for now - //if (window.mozRTCPeerConnection) { - // firefox's RTCPeerConnection doesn't add streams until it starts getting media on them - // so we need to figure out whether a video channel has been offered by ourselves. - if (this.msg.offer.sdp.indexOf('m=video') > -1) { - 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].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(); - }, 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.getRemoteVideoElement() && this.getRemoteVideoElement().pause) this.getRemoteVideoElement().pause(); - if (this.getLocalVideoElement() && this.getLocalVideoElement().pause) this.getLocalVideoElement().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; - - var videoEl = this.getLocalVideoElement(); - - if (videoEl && this.type == 'video') { - var vidTrack = stream.getVideoTracks()[0]; - videoEl.autoplay = true; - videoEl.src = URL.createObjectURL(stream); - videoEl.muted = true; - var self = this; - $timeout(function() { - var vel = self.getLocalVideoElement(); - if (vel.play) vel.play(); - }); - } - - this.localAVStream = stream; - 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; - - var localVidEl = this.getLocalVideoElement(); - - if (localVidEl && this.type == 'video') { - localVidEl.autoplay = true; - var vidTrack = stream.getVideoTracks()[0]; - localVidEl.src = URL.createObjectURL(stream); - localVidEl.muted = true; - var self = this; - $timeout(function() { - var vel = self.getLocalVideoElement(); - if (vel.play) vel.play(); - }); - } - - this.localAVStream = stream; - 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) { - if (this.state == 'ended') { - //console.log("Ignoring remote ICE candidate because call has ended"); - return; - } - console.log("Got remote ICE "+cand.sdpMid+" candidate: "+cand.candidate); - this.peerConn.addIceCandidate(new RTCIceCandidate(cand), function() {}, function(e) {}); - }; - - 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; - } - - var self = this; - this.peerConn.setLocalDescription(description, function() { - var content = { - version: 0, - call_id: self.call_id, - // OpenWebRTC appears to add extra stuff (like the DTLS fingerprint) to the description - // when setting it on the peerconnection. According to the spec it should only add ICE - // candidates. Any ICE candidates that have already been generated at this point will - // probably be sent both in the offer and separately. Ho hum. - offer: self.peerConn.localDescription, - lifetime: MatrixCall.CALL_TIMEOUT - }; - self.sendEventWithRetry('m.call.invite', content); - - $timeout(function() { - if (self.state == 'invite_sent') { - self.hangup('invite_timeout'); - } - }, MatrixCall.CALL_TIMEOUT); - - $rootScope.$apply(function() { - self.state = 'invite_sent'; - }); - }, function() { console.log("Error setting local description!"); }); - }; - - MatrixCall.prototype.createdAnswer = function(description) { - console.log("Created answer: "+description); - var self = this; - this.peerConn.setLocalDescription(description, function() { - var content = { - version: 0, - call_id: self.call_id, - answer: self.peerConn.localDescription - }; - self.sendEventWithRetry('m.call.answer', content); - $rootScope.$apply(function() { - self.state = 'connecting'; - }); - }, function() { console.log("Error setting local description!"); } ); - }; - - MatrixCall.prototype.getLocalOfferFailed = function(error) { - 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.getRemoteVideoElement() && this.remoteAVStream) { - var player = this.getRemoteVideoElement(); - player.autoplay = true; - player.src = URL.createObjectURL(this.remoteAVStream); - var self = this; - $timeout(function() { - var vel = self.getRemoteVideoElement(); - if (vel.play) vel.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.getRemoteVideoElement() && this.getRemoteVideoElement().pause) this.getRemoteVideoElement().pause(); - if (this.getLocalVideoElement() && this.getLocalVideoElement().pause) this.getLocalVideoElement().pause(); - this.state = 'ended'; - this.hangupParty = 'remote'; - this.hangupReason = msg.reason; - 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.localVideoSelector = this.localVideoSelector; - newCall.remoteVideoSelector = this.remoteVideoSelector; - 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); - }; - - MatrixCall.prototype.getLocalVideoElement = function() { - if (this.localVideoSelector) { - var t = angular.element(this.localVideoSelector); - if (t.length) return t[0]; - } - return null; - }; - - MatrixCall.prototype.getRemoteVideoElement = function() { - if (this.remoteVideoSelector) { - var t = angular.element(this.remoteVideoSelector); - if (t.length) return t[0]; - } - return null; - }; - - return MatrixCall; -}]); diff --git a/syweb/webclient/components/matrix/matrix-filter.js b/syweb/webclient/components/matrix/matrix-filter.js deleted file mode 100644 index aeebedc784..0000000000 --- a/syweb/webclient/components/matrix/matrix-filter.js +++ /dev/null @@ -1,120 +0,0 @@ -/* - 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].event; - if (member.state_key !== user_id) { - roomName = eventHandlerService.getUserDisplayName(room_id, member.state_key); - if (!roomName) { - roomName = member.state_key; - } - break; - } - } - } - else if (Object.keys(room.members).length === 1) { - // this could be just us (self-chat) or could be the other person - // in a room if they have invited us to the room. Find out which. - var otherUserId = Object.keys(room.members)[0]; - if (otherUserId === user_id) { - // it's us, we may have been invited to this room or it could - // be a self chat. - if (room.members[otherUserId].event.content.membership === "invite") { - // someone invited us, use the right ID. - roomName = eventHandlerService.getUserDisplayName(room_id, room.members[otherUserId].event.user_id); - if (!roomName) { - roomName = room.members[otherUserId].event.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) { - // 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 deleted file mode 100644 index 55dbbf522e..0000000000 --- a/syweb/webclient/components/matrix/matrix-phone-service.js +++ /dev/null @@ -1,155 +0,0 @@ -/* -Copyright 2014 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -'use strict'; - -angular.module('matrixPhoneService', []) -.factory('matrixPhoneService', ['$rootScope', '$injector', 'matrixService', 'eventHandlerService', function MatrixPhoneService($rootScope, $injector, matrixService, eventHandlerService) { - var matrixPhoneService = function() { - }; - - matrixPhoneService.INCOMING_CALL_EVENT = "INCOMING_CALL_EVENT"; - matrixPhoneService.REPLACED_CALL_EVENT = "REPLACED_CALL_EVENT"; - matrixPhoneService.allCalls = {}; - // a place to save candidates that come in for calls we haven't got invites for yet (when paginating backwards) - matrixPhoneService.candidatesByCall = {}; - - matrixPhoneService.callPlaced = function(call) { - matrixPhoneService.allCalls[call.call_id] = call; - }; - - $rootScope.$on(eventHandlerService.CALL_EVENT, function(ngEvent, event, isLive) { - if (event.user_id == matrixService.config().user_id) return; - - var msg = event.content; - - if (event.type == 'm.call.invite') { - if (event.age == undefined || msg.lifetime == undefined) { - // if the event doesn't have either an age (the HS is too old) or a lifetime - // (the sending client was too old when it sent it) then fall back to old behaviour - if (!isLive) return; // until matrix supports expiring messages - } - - if (event.age > msg.lifetime) { - console.log("Ignoring expired call event of type "+event.type); - return; - } - - var call = undefined; - if (!isLive) { - // if this event wasn't live then this call may already be over - call = matrixPhoneService.allCalls[msg.call_id]; - if (call && call.state == 'ended') { - return; - } - } - - var MatrixCall = $injector.get('MatrixCall'); - var call = new MatrixCall(event.room_id); - - if (!$rootScope.isWebRTCSupported()) { - console.log("Incoming call ID "+msg.call_id+" but this browser doesn't support WebRTC"); - // don't hang up the call: there could be other clients connected that do support WebRTC and declining the - // the call on their behalf would be really annoying. - // instead, we broadcast a fake call event with a non-functional call object - $rootScope.$broadcast(matrixPhoneService.INCOMING_CALL_EVENT, call); - return; - } - - call.call_id = msg.call_id; - call.initWithInvite(event); - matrixPhoneService.allCalls[call.call_id] = call; - - // if we stashed candidate events for that call ID, play them back now - if (!isLive && matrixPhoneService.candidatesByCall[call.call_id] != undefined) { - for (var i = 0; i < matrixPhoneService.candidatesByCall[call.call_id].length; ++i) { - call.gotRemoteIceCandidate(matrixPhoneService.candidatesByCall[call.call_id][i]); - } - } - - // Were we trying to call that user (room)? - var existingCall; - var callIds = Object.keys(matrixPhoneService.allCalls); - for (var i = 0; i < callIds.length; ++i) { - var thisCallId = callIds[i]; - var thisCall = matrixPhoneService.allCalls[thisCallId]; - - if (call.room_id == thisCall.room_id && thisCall.direction == 'outbound' - && (thisCall.state == 'wait_local_media' || thisCall.state == 'create_offer' || thisCall.state == 'invite_sent')) { - existingCall = thisCall; - break; - } - } - - if (existingCall) { - // If we've only got to wait_local_media or create_offer and we've got an invite, - // pick the incoming call because we know we haven't sent our invite yet - // otherwise, pick whichever call has the lowest call ID (by string comparison) - if (existingCall.state == 'wait_local_media' || existingCall.state == 'create_offer' || existingCall.call_id > call.call_id) { - console.log("Glare detected: answering incoming call "+call.call_id+" and canceling outgoing call "+existingCall.call_id); - existingCall.replacedBy(call); - call.answer(); - $rootScope.$broadcast(matrixPhoneService.REPLACED_CALL_EVENT, existingCall, call); - } else { - console.log("Glare detected: rejecting incoming call "+call.call_id+" and keeping outgoing call "+existingCall.call_id); - call.hangup(); - } - } else { - $rootScope.$broadcast(matrixPhoneService.INCOMING_CALL_EVENT, call); - } - } else if (event.type == 'm.call.answer') { - var call = matrixPhoneService.allCalls[msg.call_id]; - if (!call) { - console.log("Got answer for unknown call ID "+msg.call_id); - return; - } - call.receivedAnswer(msg); - } else if (event.type == 'm.call.candidates') { - var call = matrixPhoneService.allCalls[msg.call_id]; - if (!call && isLive) { - console.log("Got candidates for unknown call ID "+msg.call_id); - return; - } else if (!call) { - if (matrixPhoneService.candidatesByCall[msg.call_id] == undefined) { - matrixPhoneService.candidatesByCall[msg.call_id] = []; - } - matrixPhoneService.candidatesByCall[msg.call_id] = matrixPhoneService.candidatesByCall[msg.call_id].concat(msg.candidates); - } else { - for (var i = 0; i < msg.candidates.length; ++i) { - call.gotRemoteIceCandidate(msg.candidates[i]); - } - } - } else if (event.type == 'm.call.hangup') { - var call = matrixPhoneService.allCalls[msg.call_id]; - if (!call && isLive) { - console.log("Got hangup for unknown call ID "+msg.call_id); - } else if (!call) { - // if not live, store the fact that the call has ended because we're probably getting events backwards so - // the hangup will come before the invite - var MatrixCall = $injector.get('MatrixCall'); - var call = new MatrixCall(event.room_id); - call.call_id = msg.call_id; - call.initWithHangup(event); - matrixPhoneService.allCalls[msg.call_id] = call; - } else { - call.onHangupReceived(msg); - delete(matrixPhoneService.allCalls[msg.call_id]); - } - } - }); - - return matrixPhoneService; -}]); diff --git a/syweb/webclient/components/matrix/matrix-service.js b/syweb/webclient/components/matrix/matrix-service.js deleted file mode 100644 index 63051c4f47..0000000000 --- a/syweb/webclient/components/matrix/matrix-service.js +++ /dev/null @@ -1,762 +0,0 @@ -/* -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<flows.length; i++) { - var isThreePidFlow = false; - if (flows[i].stages) { - for (var j=0; j<flows[i].stages.length; j++) { - var regType = flows[i].stages[j]; - if (knownTypes.indexOf(regType) === -1) { - deferred.reject("Unknown type: "+regType); - return; - } - if (regType == "m.login.email.identity") { - isThreePidFlow = true; - } - if (!useCaptcha && regType == "m.login.recaptcha") { - console.error("Web client setup to not use captcha, but HS demands a captcha."); - deferred.reject({ - data: { - errcode: "M_CAPTCHA_NEEDED", - error: "Home server requires a captcha." - } - }); - return; - } - } - } - - if ( (isThreePidFlow && useThreePidFlow) || (!isThreePidFlow && !useThreePidFlow) ) { - flowIndex = i; - } - - if (knownTypes.indexOf(flows[i].type) == -1) { - deferred.reject("Unknown type: "+flows[i].type); - return; - } - } - - // looks like we can register fine, go ahead and do it. - console.log("Using flow " + JSON.stringify(flows[flowIndex])); - firstRegType = flows[flowIndex].type; - var sessionId = undefined; - - // generic response processor so it can loop as many times as required - var loginResponseFunc = function(response) { - if (response.data.session) { - sessionId = response.data.session; - } - console.log("login response: " + JSON.stringify(response.data)); - if (response.data.access_token) { - deferred.resolve(response); - } - else if (response.data.next) { - var nextType = response.data.next; - if (response.data.next instanceof Array) { - for (var i=0; i<response.data.next.length; i++) { - if (useThreePidFlow && response.data.next[i] == "m.login.email.identity") { - nextType = response.data.next[i]; - break; - } - else if (!useThreePidFlow && response.data.next[i] != "m.login.email.identity") { - nextType = response.data.next[i]; - break; - } - } - } - return doRegisterLogin(path, nextType, sessionId, user_name, password, threepidCreds).then( - loginResponseFunc, - function(err) { - deferred.reject(err); - } - ); - } - else { - deferred.reject("Unknown continuation: "+JSON.stringify(response)); - } - }; - - // set the ball rolling - doRegisterLogin(path, firstRegType, undefined, user_name, password, threepidCreds).then( - loginResponseFunc, - function(err) { - deferred.reject(err); - } - ); - - }, - function(err) { - deferred.reject(err); - } - ); - - return deferred.promise; - }, - - // Create a room - create: function(room_alias, visibility) { - // The REST path spec - var path = "/createRoom"; - - var req = { - "visibility": visibility - }; - if (room_alias) { - req.room_alias_name = room_alias; - } - - return doRequest("POST", path, undefined, req); - }, - - // Get the user's current state: his presence, the list of his rooms with - // the last {limit} events - initialSync: function(limit, feedback) { - // The REST path spec - - var path = "/initialSync"; - - var params = {}; - if (limit) { - params.limit = limit; - } - if (feedback) { - params.feedback = feedback; - } - - return doRequest("GET", path, params); - }, - - // get room state for a specific room - roomState: function(room_id) { - var path = "/rooms/" + encodeURIComponent(room_id) + "/state"; - return doRequest("GET", path); - }, - - // Joins a room - join: function(room_id) { - return this.membershipChange(room_id, undefined, "join"); - }, - - joinAlias: function(room_alias) { - var path = "/join/$room_alias"; - room_alias = encodeURIComponent(room_alias); - - path = path.replace("$room_alias", room_alias); - - // TODO: PUT with txn ID - return doRequest("POST", path, undefined, {}); - }, - - // Invite a user to a room - invite: function(room_id, user_id) { - return this.membershipChange(room_id, user_id, "invite"); - }, - - // Leaves a room - leave: function(room_id) { - return this.membershipChange(room_id, undefined, "leave"); - }, - - membershipChange: function(room_id, user_id, membershipValue) { - // The REST path spec - var path = "/rooms/$room_id/$membership"; - path = path.replace("$room_id", encodeURIComponent(room_id)); - path = path.replace("$membership", encodeURIComponent(membershipValue)); - - var data = {}; - if (user_id !== undefined) { - data = { user_id: user_id }; - } - - // TODO: Use PUT with transaction IDs - return doRequest("POST", path, undefined, data); - }, - - // Change the membership of an another user - setMembership: function(room_id, user_id, membershipValue, reason) { - - // The REST path spec - var path = "/rooms/$room_id/state/m.room.member/$user_id"; - path = path.replace("$room_id", encodeURIComponent(room_id)); - path = path.replace("$user_id", user_id); - - return doRequest("PUT", path, undefined, { - membership : membershipValue, - reason: reason - }); - }, - - // Bans a user from a room - ban: function(room_id, user_id, reason) { - var path = "/rooms/$room_id/ban"; - path = path.replace("$room_id", encodeURIComponent(room_id)); - - return doRequest("POST", path, undefined, { - user_id: user_id, - reason: reason - }); - }, - - // Unbans a user in a room - unban: function(room_id, user_id) { - // FIXME: To update when there will be homeserver API for unban - // For now, do an unban by resetting the user membership to "leave" - return this.setMembership(room_id, user_id, "leave"); - }, - - // Kicks a user from a room - kick: function(room_id, user_id, reason) { - // Set the user membership to "leave" to kick him - return this.setMembership(room_id, user_id, "leave", reason); - }, - - // Retrieves the room ID corresponding to a room alias - resolveRoomAlias:function(room_alias) { - var path = "/_matrix/client/api/v1/directory/room/$room_alias"; - room_alias = encodeURIComponent(room_alias); - - path = path.replace("$room_alias", room_alias); - - return doRequest("GET", path, undefined, {}); - }, - - setName: function(room_id, name) { - var data = { - name: name - }; - return this.sendStateEvent(room_id, "m.room.name", data); - }, - - setTopic: function(room_id, topic) { - var data = { - topic: topic - }; - return this.sendStateEvent(room_id, "m.room.topic", data); - }, - - - sendStateEvent: function(room_id, eventType, content, state_key) { - 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); - } - room_id = encodeURIComponent(room_id); - path = path.replace("$room_id", room_id); - - return doRequest("PUT", path, undefined, content); - }, - - sendEvent: function(room_id, eventType, txn_id, content) { - // The REST path spec - var path = "/rooms/$room_id/send/"+eventType+"/$txn_id"; - - if (!txn_id) { - txn_id = "m" + new Date().getTime(); - } - - // Like the cmd client, escape room ids - room_id = encodeURIComponent(room_id); - - // Customize it - path = path.replace("$room_id", room_id); - path = path.replace("$txn_id", txn_id); - - return doRequest("PUT", path, undefined, content); - }, - - sendMessage: function(room_id, txn_id, content) { - return this.sendEvent(room_id, 'm.room.message', txn_id, content); - }, - - // Send a text message - sendTextMessage: function(room_id, body, msg_id) { - var content = { - msgtype: "m.text", - body: body - }; - - return this.sendMessage(room_id, msg_id, content); - }, - - // Send an image message - sendImageMessage: function(room_id, image_url, image_body, msg_id) { - var content = { - msgtype: "m.image", - url: image_url, - info: image_body, - body: "Image" - }; - - return this.sendMessage(room_id, msg_id, content); - }, - - // Send an emote message - sendEmoteMessage: function(room_id, body, msg_id) { - var content = { - msgtype: "m.emote", - body: body - }; - - return this.sendMessage(room_id, msg_id, content); - }, - - redactEvent: function(room_id, event_id) { - var path = "/rooms/$room_id/redact/$event_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); - }, - - // get a snapshot of the members in a room. - getMemberList: function(room_id) { - // Like the cmd client, escape room ids - room_id = encodeURIComponent(room_id); - - var path = "/rooms/$room_id/members"; - path = path.replace("$room_id", room_id); - return doRequest("GET", path); - }, - - paginateBackMessages: function(room_id, from_token, limit) { - var path = "/rooms/$room_id/messages"; - path = path.replace("$room_id", encodeURIComponent(room_id)); - var params = { - from: from_token, - limit: limit, - dir: 'b' - }; - return doRequest("GET", path, params); - }, - - // get a list of public rooms on your home server - publicRooms: function() { - var path = "/publicRooms"; - return doRequest("GET", path); - }, - - // get a user's profile - getProfile: function(userId) { - return this.getProfileInfo(userId); - }, - - // get a display name for this user ID - getDisplayName: function(userId) { - return this.getProfileInfo(userId, "displayname"); - }, - - // get the profile picture url for this user ID - getProfilePictureUrl: function(userId) { - return this.getProfileInfo(userId, "avatar_url"); - }, - - // update your display name - setDisplayName: function(newName) { - var content = { - displayname: newName - }; - return this.setProfileInfo(content, "displayname"); - }, - - // update your profile picture url - setProfilePictureUrl: function(newUrl) { - var content = { - avatar_url: newUrl - }; - return this.setProfileInfo(content, "avatar_url"); - }, - - setProfileInfo: function(data, info_segment) { - var path = "/profile/$user/" + info_segment; - path = path.replace("$user", encodeURIComponent(config.user_id)); - return doRequest("PUT", path, undefined, data); - }, - - getProfileInfo: function(userId, info_segment) { - var path = "/profile/"+encodeURIComponent(userId); - if (info_segment) path += '/' + info_segment; - return doRequest("GET", path); - }, - - login: function(userId, password) { - // TODO We should be checking to make sure the client can support - // logging in to this HS, else use the fallback. - var path = "/login"; - var data = { - "type": "m.login.password", - "user": userId, - "password": password - }; - return doRequest("POST", path, undefined, data); - }, - - // hit the Identity Server for a 3PID request. - linkEmail: function(email, clientSecret, sendAttempt) { - var path = "/_matrix/identity/api/v1/validate/email/requestToken"; - var data = "clientSecret="+clientSecret+"&email=" + encodeURIComponent(email)+"&sendAttempt="+sendAttempt; - var headers = {}; - headers["Content-Type"] = "application/x-www-form-urlencoded"; - return doBaseRequest(config.identityServer, "POST", path, {}, data, headers); - }, - - authEmail: function(clientSecret, sid, code) { - var path = "/_matrix/identity/api/v1/validate/email/submitToken"; - var data = "token="+code+"&sid="+sid+"&clientSecret="+clientSecret; - var headers = {}; - headers["Content-Type"] = "application/x-www-form-urlencoded"; - return doBaseRequest(config.identityServer, "POST", path, {}, data, headers); - }, - - bindEmail: function(userId, tokenId, clientSecret) { - var path = "/_matrix/identity/api/v1/3pid/bind"; - var data = "mxid="+encodeURIComponent(userId)+"&sid="+tokenId+"&clientSecret="+clientSecret; - var headers = {}; - headers["Content-Type"] = "application/x-www-form-urlencoded"; - return doBaseRequest(config.identityServer, "POST", path, {}, data, headers); - }, - - lookup3pid: function(medium, address) { - var path = "/_matrix/identity/api/v1/lookup?medium="+encodeURIComponent(medium)+"&address="+encodeURIComponent(address); - return doBaseRequest(config.identityServer, "GET", path, {}, undefined, {}); - }, - - uploadContent: function(file) { - var path = "/_matrix/content"; - var headers = { - "Content-Type": undefined // undefined means angular will figure it out - }; - var params = { - access_token: config.access_token - }; - - // If the file is actually a Blob object, prevent $http from JSON-stringified it before sending - // (Equivalent to jQuery ajax processData = false) - var $httpParams; - if (file instanceof Blob) { - $httpParams = { - transformRequest: angular.identity - }; - } - - return doBaseRequest(config.homeserver, "POST", path, params, file, headers, $httpParams); - }, - - /** - * Start listening on /events - * @param {String} from the token from which to listen events to - * @param {Integer} serverTimeout the time in ms the server will hold open the connection - * @param {Integer} clientTimeout the timeout in ms used at the client HTTP request level - * @returns a promise - */ - getEventStream: function(from, serverTimeout, clientTimeout) { - var path = "/events"; - var params = { - from: from, - timeout: serverTimeout - }; - - var $httpParams; - if (clientTimeout) { - // If the Internet connection is lost, this timeout is used to be able to - // cancel the current request and notify the client so that it can retry with a new request. - $httpParams = { - timeout: clientTimeout - }; - } - - return doRequest("GET", path, params, undefined, $httpParams); - }, - - // Indicates if user authentications details are stored in cache - isUserLoggedIn: function() { - var config = this.config(); - - // User is considered logged in if his cache is not empty and contains - // an access token - if (config && config.access_token) { - return true; - } - else { - return false; - } - }, - - // Enum of presence state - presence: { - offline: "offline", - unavailable: "unavailable", - online: "online", - free_for_chat: "free_for_chat" - }, - - // Set the logged in user presence state - setUserPresence: function(presence) { - var path = "/presence/$user_id/status"; - path = path.replace("$user_id", encodeURIComponent(config.user_id)); - return doRequest("PUT", path, undefined, { - presence: presence - }); - }, - - - /****** Permanent storage of user information ******/ - - // Returns the current config - config: function() { - if (!config) { - config = localStorage.getItem("config"); - if (config) { - config = JSON.parse(config); - - // Reset the cache if the version loaded is not the expected one - if (configVersion !== config.version) { - config = undefined; - this.saveConfig(); - } - } - } - return config; - }, - - // Set a new config (Use saveConfig to actually store it permanently) - setConfig: function(newConfig) { - config = newConfig; - console.log("new IS: "+config.identityServer); - }, - - // Commits config into permanent storage - saveConfig: function() { - config.version = configVersion; - localStorage.setItem("config", JSON.stringify(config)); - }, - - - /****** Room aliases management ******/ - - /** - * Get the room_alias & room_display_name which are computed from data - * already retrieved from the server. - * @param {Room object} room one element of the array returned by the response - * of rooms() and publicRooms() - * @returns {Object} {room_alias: "...", room_display_name: "..."} - */ - getRoomAliasAndDisplayName: function(room) { - var result = { - room_alias: undefined, - room_display_name: undefined - }; - var alias = this.getRoomIdToAliasMapping(room.room_id); - if (alias) { - // use the existing alias from storage - result.room_alias = alias; - result.room_display_name = alias; - } - // XXX: this only lets us learn aliases from our local HS - we should - // make the client stop returning this if we can trust m.room.aliases state events - else if (room.aliases && room.aliases[0]) { - // save the mapping - // TODO: select the smarter alias from the array - this.createRoomIdToAliasMapping(room.room_id, room.aliases[0]); - result.room_display_name = room.aliases[0]; - result.room_alias = room.aliases[0]; - } - else if (room.membership === "invite" && "inviter" in room) { - result.room_display_name = room.inviter + "'s room"; - } - else { - // last resort use the room id - result.room_display_name = room.room_id; - } - return result; - }, - - createRoomIdToAliasMapping: function(roomId, alias) { - roomIdToAlias[roomId] = alias; - aliasToRoomId[alias] = roomId; - }, - - getRoomIdToAliasMapping: function(roomId) { - var alias = roomIdToAlias[roomId]; - //console.log("looking for alias for " + roomId + "; found: " + alias); - return alias; - }, - - getAliasToRoomIdMapping: function(alias) { - var roomId = aliasToRoomId[alias]; - //console.log("looking for roomId for " + alias + "; found: " + roomId); - return roomId; - }, - - /** - * Change or reset the power level of a user - * @param {String} room_id the room id - * @param {String} user_id the user id - * @param {Number} powerLevel The desired power level. - * If undefined, the user power level will be reset, ie he will use the default room user power level - * @param event The existing m.room.power_levels event if one exists. - * @returns {promise} an $http promise - */ - setUserPowerLevel: function(room_id, user_id, powerLevel, event) { - var content = {}; - if (event) { - // if there is an existing event, copy the content as it contains - // the power level values for other members which we do not want - // to modify. - content = angular.copy(event.content); - } - content[user_id] = powerLevel; - - var path = "/rooms/$room_id/state/m.room.power_levels"; - path = path.replace("$room_id", encodeURIComponent(room_id)); - - return doRequest("PUT", path, undefined, content); - }, - - getTurnServer: function() { - return doRequest("GET", "/voip/turnServer"); - } - - }; -}]); diff --git a/syweb/webclient/components/matrix/model-service.js b/syweb/webclient/components/matrix/model-service.js deleted file mode 100644 index 8e0ce8d1a9..0000000000 --- a/syweb/webclient/components/matrix/model-service.js +++ /dev/null @@ -1,172 +0,0 @@ -/* -Copyright 2014 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -'use strict'; - -/* -This service serves as the entry point for all models in the app. If access to -underlying data in a room is required, then this service should be used as the -dependency. -*/ -// NB: This is more explicit than linking top-level models to $rootScope -// in that by adding this service as a dep you are clearly saying "this X -// needs access to the underlying data store", rather than polluting the -// $rootScope. -angular.module('modelService', []) -.factory('modelService', ['matrixService', function(matrixService) { - - /***** Room Object *****/ - var Room = function Room(room_id) { - this.room_id = room_id; - this.old_room_state = new RoomState(); - this.current_room_state = new RoomState(); - this.events = []; // events which can be displayed on the UI. TODO move? - }; - Room.prototype = { - addMessageEvents: function addMessageEvents(events, toFront) { - for (var i=0; i<events.length; i++) { - this.addMessageEvent(events[i], toFront); - } - }, - - addMessageEvent: function addMessageEvent(event, toFront) { - // every message must reference the RoomMember which made it *at - // that time* so things like display names display correctly. - var stateAtTheTime = toFront ? this.old_room_state : this.current_room_state; - event.__room_member = stateAtTheTime.getStateEvent("m.room.member", event.user_id); - if (event.type === "m.room.member" && event.content.membership === "invite") { - // give information on both the inviter and invitee - event.__target_room_member = stateAtTheTime.getStateEvent("m.room.member", event.state_key); - } - - if (toFront) { - this.events.unshift(event); - } - else { - this.events.push(event); - } - }, - - addOrReplaceMessageEvent: function addOrReplaceMessageEvent(event, toFront) { - // Start looking from the tail since the first goal of this function - // is to find a message among the latest ones - for (var i = this.events.length - 1; i >= 0; i--) { - var storedEvent = this.events[i]; - if (storedEvent.event_id === event.event_id) { - // It's clobbering time! - this.events[i] = event; - return; - } - } - this.addMessageEvent(event, toFront); - }, - - leave: function leave() { - return matrixService.leave(this.room_id); - } - }; - - /***** Room State Object *****/ - var RoomState = function RoomState() { - // list of RoomMember - this.members = {}; - // state events, the key is a compound of event type + state_key - this.state_events = {}; - this.pagination_token = ""; - }; - RoomState.prototype = { - // get a state event for this room from this.state_events. State events - // are unique per type+state_key tuple, with a lot of events using 0-len - // state keys. To make it not Really Annoying to access, this method is - // provided which can just be given the type and it will return the - // 0-len event by default. - state: function state(type, state_key) { - if (!type) { - return undefined; // event type MUST be specified - } - if (!state_key) { - return this.state_events[type]; // treat as 0-len state key - } - return this.state_events[type + state_key]; - }, - - storeStateEvent: function storeState(event) { - this.state_events[event.type + event.state_key] = event; - if (event.type === "m.room.member") { - var rm = new RoomMember(); - rm.event = event; - this.members[event.state_key] = rm; - } - }, - - storeStateEvents: function storeState(events) { - if (!events) { - return; - } - for (var i=0; i<events.length; i++) { - this.storeStateEvent(events[i]); - } - }, - - getStateEvent: function getStateEvent(event_type, state_key) { - return this.state_events[event_type + state_key]; - } - }; - - /***** Room Member Object *****/ - var RoomMember = function RoomMember() { - this.event = {}; // the m.room.member event representing the RoomMember. - this.user = undefined; // the User - }; - - /***** User Object *****/ - var User = function User() { - this.event = {}; // the m.presence event representing the User. - }; - - // rooms are stored here when they come in. - var rooms = { - // roomid: <Room> - }; - - 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 deleted file mode 100644 index 9a911413c3..0000000000 --- a/syweb/webclient/components/matrix/notification-service.js +++ /dev/null @@ -1,104 +0,0 @@ -/* -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<bingWords.length; i++) { - var re = RegExp(bingWords[i], 'i'); - regexList.push(re); - } - } - return this.hasMatch(regexList, content); - }, - - hasMatch: function(regExps, content) { - if (!content || $.type(content) != "string") { - return false; - } - - if (regExps && regExps.length > 0) { - for (var i=0; i<regExps.length; i++) { - if (content.search(regExps[i]) != -1) { - return true; - } - } - } - return false; - }, - - showNotification: function(title, body, icon, onclick) { - var notification = new window.Notification( - title, - { - "body": body, - "icon": icon - } - ); - - if (onclick) { - notification.onclick = onclick; - } - - $timeout(function() { - notification.close(); - }, 5 * 1000); - } - }; - -}]); diff --git a/syweb/webclient/components/matrix/presence-service.js b/syweb/webclient/components/matrix/presence-service.js deleted file mode 100644 index b487e3d3bd..0000000000 --- a/syweb/webclient/components/matrix/presence-service.js +++ /dev/null @@ -1,113 +0,0 @@ -/* - 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 tracks user activity on the page to determine his presence state. - * Any state change will be sent to the Home Server. - */ -angular.module('mPresence', []) -.service('mPresence', ['$timeout', 'matrixService', function ($timeout, matrixService) { - - // Time in ms after that a user is considered as unavailable/away - var UNAVAILABLE_TIME = 3 * 60000; // 3 mins - - // The current presence state - var state = undefined; - - var self =this; - var timer; - - /** - * Start listening the user activity to evaluate his presence state. - * Any state change will be sent to the Home Server. - */ - this.start = function() { - if (undefined === state) { - // The user is online if he moves the mouser or press a key - document.onmousemove = resetTimer; - document.onkeypress = resetTimer; - - resetTimer(); - } - }; - - /** - * Stop tracking user activity - */ - this.stop = function() { - if (timer) { - $timeout.cancel(timer); - timer = undefined; - } - state = undefined; - }; - - /** - * Get the current presence state. - * @returns {matrixService.presence} the presence state - */ - this.getState = function() { - return state; - }; - - /** - * Set the presence state. - * If the state has changed, the Home Server will be notified. - * @param {matrixService.presence} newState the new presence state - */ - this.setState = function(newState) { - if (newState !== state) { - console.log("mPresence - New state: " + newState); - - state = newState; - - // Inform the HS on the new user state - matrixService.setUserPresence(state).then( - function() { - - }, - function(error) { - console.log("mPresence - Failed to send new presence state: " + JSON.stringify(error)); - }); - } - }; - - /** - * Callback called when the user made no action on the page for UNAVAILABLE_TIME ms. - * @private - */ - function onUnvailableTimerFire() { - self.setState(matrixService.presence.unavailable); - } - - /** - * Callback called when the user made an action on the page - * @private - */ - function resetTimer() { - // User is still here - self.setState(matrixService.presence.online); - - // Re-arm the timer - $timeout.cancel(timer); - timer = $timeout(onUnvailableTimerFire, UNAVAILABLE_TIME); - } - -}]); - - diff --git a/syweb/webclient/components/utilities/utilities-service.js b/syweb/webclient/components/utilities/utilities-service.js deleted file mode 100644 index b417cc5b39..0000000000 --- a/syweb/webclient/components/utilities/utilities-service.js +++ /dev/null @@ -1,151 +0,0 @@ -/* - Copyright 2014 OpenMarket Ltd - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ - -'use strict'; - -/* - * This service contains multipurpose helper functions. - */ -angular.module('mUtilities', []) -.service('mUtilities', ['$q', function ($q) { - /* - * Get the size of an image - * @param {File|Blob} imageFile the file containing the image - * @returns {promise} A promise that will be resolved by an object with 2 members: - * width & height - */ - this.getImageSize = function(imageFile) { - var deferred = $q.defer(); - - // Load the file into an html element - var img = document.createElement("img"); - - var reader = new FileReader(); - reader.onload = function(e) { - img.src = e.target.result; - - // Once ready, returns its size - img.onload = function() { - deferred.resolve({ - width: img.width, - height: img.height - }); - }; - img.onerror = function(e) { - deferred.reject(e); - }; - }; - reader.onerror = function(e) { - deferred.reject(e); - }; - reader.readAsDataURL(imageFile); - - return deferred.promise; - }; - - /* - * Resize the image to fit in a square of the side maxSize. - * The aspect ratio is kept. The returned image data uses JPEG compression. - * Source: http://hacks.mozilla.org/2011/01/how-to-develop-a-html5-image-uploader/ - * @param {File} imageFile the file containing the image - * @param {Integer} maxSize the max side size - * @returns {promise} A promise that will be resolved by a Blob object containing - * the resized image data - */ - this.resizeImage = function(imageFile, maxSize) { - var self = this; - var deferred = $q.defer(); - - var canvas = document.createElement("canvas"); - - var img = document.createElement("img"); - var reader = new FileReader(); - reader.onload = function(e) { - - img.src = e.target.result; - - // Once ready, returns its size - img.onload = function() { - var ctx = canvas.getContext("2d"); - ctx.drawImage(img, 0, 0); - - var MAX_WIDTH = maxSize; - var MAX_HEIGHT = maxSize; - var width = img.width; - var height = img.height; - - if (width > 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 |