From 89ba802b23bf1fd22afbc5e9a4b3b732264e3c18 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Tue, 4 Nov 2014 15:57:23 +0000 Subject: Move webclient to a python module so that it can be installed --- syweb/webclient/room/room-controller.js | 1094 +++++++++++++++++++++++++++++++ 1 file changed, 1094 insertions(+) create mode 100644 syweb/webclient/room/room-controller.js (limited to 'syweb/webclient/room/room-controller.js') diff --git a/syweb/webclient/room/room-controller.js b/syweb/webclient/room/room-controller.js new file mode 100644 index 0000000000..2e0f5faff0 --- /dev/null +++ b/syweb/webclient/room/room-controller.js @@ -0,0 +1,1094 @@ +/* +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. +*/ + +angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput']) +.controller('RoomController', ['$modal', '$filter', '$scope', '$timeout', '$routeParams', '$location', '$rootScope', 'matrixService', 'mPresence', 'eventHandlerService', 'mFileUpload', 'matrixPhoneService', 'MatrixCall', 'notificationService', 'modelService', + function($modal, $filter, $scope, $timeout, $routeParams, $location, $rootScope, matrixService, mPresence, eventHandlerService, mFileUpload, matrixPhoneService, MatrixCall, notificationService, modelService) { + 'use strict'; + var MESSAGES_PER_PAGINATION = 30; + var THUMBNAIL_SIZE = 320; + + // .html needs this + $scope.containsBingWord = function(content) { + return notificationService.containsBingWord( + matrixService.config().user_id, + matrixService.config().display_name, + matrixService.config().bingWords, + content + ); + }; + + // Room ids. Computed and resolved in onInit + $scope.room_id = undefined; + $scope.room_alias = undefined; + + $scope.state = { + user_id: matrixService.config().user_id, + permission_denied: undefined, // If defined, this string contains the reason why the user cannot join the room + first_pagination: true, // this is toggled off when the first pagination is done + can_paginate: false, // this is toggled off when we are not ready yet to paginate or when we run out of items + paginating: false, // used to avoid concurrent pagination requests pulling in dup contents + stream_failure: undefined, // the response when the stream fails + waiting_for_joined_event: false, // true when the join request is pending. Back to false once the corresponding m.room.member event is received + messages_visibility: "hidden", // In order to avoid flickering when scrolling down the message table at the page opening, delay the message table display + }; + $scope.members = {}; + $scope.autoCompleting = false; + $scope.autoCompleteIndex = 0; + $scope.autoCompleteOriginal = ""; + + $scope.imageURLToSend = ""; + $scope.userIDToInvite = ""; + + + // vars and functions for updating the name + $scope.name = { + isEditing: false, + newNameText: "", + editName: function() { + if ($scope.name.isEditing) { + console.log("Warning: Already editing name."); + return; + }; + + var nameEvent = $scope.room.current_room_state.state_events['m.room.name']; + if (nameEvent) { + $scope.name.newNameText = nameEvent.content.name; + } + else { + $scope.name.newNameText = ""; + } + + // Force focus to the input + $timeout(function() { + angular.element('.roomNameInput').focus(); + }, 0); + + $scope.name.isEditing = true; + }, + updateName: function() { + console.log("Updating name to "+$scope.name.newNameText); + matrixService.setName($scope.room_id, $scope.name.newNameText).then( + function() { + }, + function(error) { + $scope.feedback = "Request failed: " + error.data.error; + } + ); + + $scope.name.isEditing = false; + }, + cancelEdit: function() { + $scope.name.isEditing = false; + } + }; + + // vars and functions for updating the topic + $scope.topic = { + isEditing: false, + newTopicText: "", + editTopic: function() { + if ($scope.topic.isEditing) { + console.log("Warning: Already editing topic."); + return; + } + var topicEvent = $scope.room.current_room_state.state_events['m.room.topic']; + if (topicEvent) { + $scope.topic.newTopicText = topicEvent.content.topic; + } + else { + $scope.topic.newTopicText = ""; + } + + // Force focus to the input + $timeout(function() { + angular.element('.roomTopicInput').focus(); + }, 0); + + $scope.topic.isEditing = true; + }, + updateTopic: function() { + console.log("Updating topic to "+$scope.topic.newTopicText); + matrixService.setTopic($scope.room_id, $scope.topic.newTopicText).then( + function() { + }, + function(error) { + $scope.feedback = "Request failed: " + error.data.error; + } + ); + + $scope.topic.isEditing = false; + }, + cancelEdit: function() { + $scope.topic.isEditing = false; + } + }; + + var scrollToBottom = function(force) { + console.log("Scrolling to bottom"); + + // Do not autoscroll to the bottom to display the new event if the user is not at the bottom. + // Exception: in case where the event is from the user, we want to force scroll to the bottom + var objDiv = document.getElementById("messageTableWrapper"); + // add a 10px buffer to this check so if the message list is not *quite* + // at the bottom it still scrolls since it basically is at the bottom. + if ((10 + objDiv.offsetHeight + objDiv.scrollTop >= objDiv.scrollHeight) || force) { + + $timeout(function() { + objDiv.scrollTop = objDiv.scrollHeight; + + // Show the message table once the first scrolldown is done + if ("visible" !== $scope.state.messages_visibility) { + $timeout(function() { + $scope.state.messages_visibility = "visible"; + }, 0); + } + }, 0); + } + }; + + $scope.$on(eventHandlerService.MSG_EVENT, function(ngEvent, event, isLive) { + if (isLive && event.room_id === $scope.room_id) { + + scrollToBottom(); + } + }); + + $scope.$on(eventHandlerService.MEMBER_EVENT, function(ngEvent, event, isLive) { + if (isLive && event.room_id === $scope.room_id) { + if ($scope.state.waiting_for_joined_event) { + // The user has successfully joined the room, we can getting data for this room + $scope.state.waiting_for_joined_event = false; + onInit3(); + } + else if (event.state_key === $scope.state.user_id && "invite" !== event.membership && "join" !== event.membership) { + var user; + + if ($scope.members[event.user_id]) { + user = $scope.members[event.user_id].displayname; + } + if (user) { + user = user + " (" + event.user_id + ")"; + } + else { + user = event.user_id; + } + + if ("ban" === event.membership) { + $scope.state.permission_denied = "You have been banned by " + user; + } + else { + $scope.state.permission_denied = "You have been kicked by " + user; + } + } + else { + scrollToBottom(); + updateMemberList(event); + + // Notify when a user joins + if ((document.hidden || matrixService.presence.unavailable === mPresence.getState()) + && event.state_key !== $scope.state.user_id && "join" === event.membership) { + var userName = event.content.displayname; + if (!userName) { + userName = event.state_key; + } + notificationService.showNotification( + userName + + " (" + (matrixService.getRoomIdToAliasMapping(event.room_id) || event.room_id) + ")", + userName + " joined", + event.content.avatar_url ? event.content.avatar_url : undefined, + function() { + console.log("notification.onclick() room=" + event.room_id); + $rootScope.goToPage('room/' + event.room_id); + } + ); + } + } + } + }); + + $scope.$on(eventHandlerService.PRESENCE_EVENT, function(ngEvent, event, isLive) { + if (isLive) { + updatePresence(event); + } + }); + + $scope.$on(eventHandlerService.POWERLEVEL_EVENT, function(ngEvent, event, isLive) { + if (isLive && event.room_id === $scope.room_id) { + for (var user_id in event.content) { + updateUserPowerLevel(user_id); + } + } + }); + + $scope.memberCount = function() { + return Object.keys($scope.members).length; + }; + + $scope.paginateMore = function() { + if ($scope.state.can_paginate) { + // console.log("Paginating more."); + paginate(MESSAGES_PER_PAGINATION); + } + }; + + var paginate = function(numItems) { + //console.log("paginate " + numItems + " and first_pagination is " + $scope.state.first_pagination); + if ($scope.state.paginating || !$scope.room_id) { + return; + } + else { + $scope.state.paginating = true; + } + + console.log("paginateBackMessages from " + $scope.room.old_room_state.pagination_token + " for " + numItems); + var originalTopRow = $("#messageTable>tbody>tr:first")[0]; + + // Paginate events from the point in cache + matrixService.paginateBackMessages($scope.room_id, $scope.room.old_room_state.pagination_token, numItems).then( + function(response) { + + eventHandlerService.handleRoomMessages($scope.room_id, response.data, false, 'b'); + if (response.data.chunk.length < MESSAGES_PER_PAGINATION) { + // no more messages to paginate. this currently never gets turned true again, as we never + // expire paginated contents in the current implementation. + $scope.state.can_paginate = false; + } + + $scope.state.paginating = false; + + var wrapper = $("#messageTableWrapper")[0]; + var table = $("#messageTable")[0]; + // console.log("wrapper height=" + wrapper.clientHeight + ", table scrollHeight=" + table.scrollHeight); + + if ($scope.state.can_paginate) { + // check we don't have to pull in more messages + // n.b. we dispatch through a timeout() to allow the digest to run otherwise the .height methods are stale + $timeout(function() { + if (table.scrollHeight < wrapper.clientHeight) { + paginate(MESSAGES_PER_PAGINATION); + scrollToBottom(); + } + }, 0); + } + + if ($scope.state.first_pagination) { + scrollToBottom(true); + $scope.state.first_pagination = false; + } + else { + // lock the scroll position + $timeout(function() { + // FIXME: this risks a flicker before the scrollTop is actually updated, but we have to + // dispatch it into a function in order to first update the layout. The right solution + // might be to implement it as a directive, more like + // http://stackoverflow.com/questions/23736647/how-to-retain-scroll-position-of-ng-repeat-in-angularjs + // however, this specific solution breaks because it measures the rows height before + // the contents are interpolated. + wrapper.scrollTop = originalTopRow ? (originalTopRow.offsetTop + wrapper.scrollTop) : 0; + }, 0); + } + }, + function(error) { + console.log("Failed to paginateBackMessages: " + JSON.stringify(error)); + $scope.state.paginating = false; + } + ); + }; + + var updateMemberList = function(chunk) { + if (chunk.room_id != $scope.room_id) return; + + + // set target_user_id to keep things clear + var target_user_id = chunk.state_key; + + var isNewMember = !(target_user_id in $scope.members); + if (isNewMember) { + + // Ignore banned and kicked (leave) people + if ("ban" === chunk.membership || "leave" === chunk.membership) { + return; + } + + // FIXME: why are we copying these fields around inside chunk? + if ("presence" in chunk.content) { + chunk.presence = chunk.content.presence; + } + if ("last_active_ago" in chunk.content) { + chunk.last_active_ago = chunk.content.last_active_ago; + $scope.now = new Date().getTime(); + chunk.last_updated = $scope.now; + } + if ("displayname" in chunk.content) { + chunk.displayname = chunk.content.displayname; + } + if ("avatar_url" in chunk.content) { + chunk.avatar_url = chunk.content.avatar_url; + } + $scope.members[target_user_id] = chunk; + + if (target_user_id in $rootScope.presence) { + updatePresence($rootScope.presence[target_user_id]); + } + } + else { + // selectively update membership and presence else it will nuke the picture and displayname too :/ + + // Remove banned and kicked (leave) people + if ("ban" === chunk.membership || "leave" === chunk.membership) { + delete $scope.members[target_user_id]; + return; + } + + var member = $scope.members[target_user_id]; + member.membership = chunk.content.membership; + if ("presence" in chunk.content) { + member.presence = chunk.content.presence; + } + if ("last_active_ago" in chunk.content) { + member.last_active_ago = chunk.content.last_active_ago; + $scope.now = new Date().getTime(); + member.last_updated = $scope.now; + } + } + }; + + var updateMemberListPresenceAge = function() { + $scope.now = new Date().getTime(); + // TODO: don't bother polling every 5s if we know none of our counters are younger than 1 minute + $timeout(updateMemberListPresenceAge, 5 * 1000); + }; + + var updatePresence = function(chunk) { + if (!(chunk.content.user_id in $scope.members)) { + console.log("updatePresence: Unknown member for chunk " + JSON.stringify(chunk)); + return; + } + var member = $scope.members[chunk.content.user_id]; + + // XXX: why not just pass the chunk straight through? + if ("presence" in chunk.content) { + member.presence = chunk.content.presence; + } + + if ("last_active_ago" in chunk.content) { + member.last_active_ago = chunk.content.last_active_ago; + $scope.now = new Date().getTime(); + member.last_updated = $scope.now; + } + + // this may also contain a new display name or avatar url, so check. + if ("displayname" in chunk.content) { + member.displayname = chunk.content.displayname; + } + + if ("avatar_url" in chunk.content) { + member.avatar_url = chunk.content.avatar_url; + } + }; + + var updateUserPowerLevel = function(user_id) { + var member = $scope.members[user_id]; + if (member) { + member.powerLevel = eventHandlerService.getUserPowerLevel($scope.room_id, user_id); + + normaliseMembersPowerLevels(); + } + }; + + // Normalise users power levels so that the user with the higher power level + // will have a bar covering 100% of the width of his avatar + var normaliseMembersPowerLevels = function() { + // Find the max power level + var maxPowerLevel = 0; + for (var i in $scope.members) { + if (!$scope.members.hasOwnProperty(i)) continue; + + var member = $scope.members[i]; + if (member.powerLevel) { + maxPowerLevel = Math.max(maxPowerLevel, member.powerLevel); + } + } + + // Normalized them on a 0..100% scale to be use in css width + if (maxPowerLevel) { + for (var i in $scope.members) { + if (!$scope.members.hasOwnProperty(i)) continue; + + var member = $scope.members[i]; + member.powerLevelNorm = (member.powerLevel * 100) / maxPowerLevel; + } + } + }; + + $scope.send = function() { + var input = $('#mainInput').val(); + + if (undefined === input || input === "") { + return; + } + + scrollToBottom(true); + + // Store the command in the history + history.push(input); + + var promise; + var cmd; + var args; + var echo = false; + + // Check for IRC style commands first + // trim any trailing whitespace, as it can confuse the parser for IRC-style commands + input = input.replace(/\s+$/, ""); + + if (input[0] === "/" && input[1] !== "/") { + var bits = input.match(/^(\S+?)( +(.*))?$/); + cmd = bits[1]; + args = bits[3]; + + console.log("cmd: " + cmd + ", args: " + args); + + switch (cmd) { + case "/me": + promise = matrixService.sendEmoteMessage($scope.room_id, args); + echo = true; + break; + + case "/nick": + // Change user display name + if (args) { + promise = matrixService.setDisplayName(args); + } + else { + $scope.feedback = "Usage: /nick "; + } + break; + + case "/join": + // Join a room + if (args) { + var matches = args.match(/^(\S+)$/); + if (matches) { + var room_alias = matches[1]; + if (room_alias.indexOf(':') == -1) { + // FIXME: actually track the :domain style name of our homeserver + // with or without port as is appropriate and append it at this point + } + + var room_id = matrixService.getAliasToRoomIdMapping(room_alias); + console.log("joining " + room_alias + " id=" + room_id); + if ($scope.room) { // TODO actually check that you = join + // don't send a join event for a room you're already in. + $location.url("room/" + room_alias); + } + else { + promise = matrixService.joinAlias(room_alias).then( + function(response) { + // TODO: factor out the common housekeeping whenever we try to join a room or alias + matrixService.roomState(response.room_id).then( + function(response) { + eventHandlerService.handleEvents(response.data, false, true); + }, + function(error) { + $scope.feedback = "Failed to get room state for: " + response.room_id; + } + ); + $location.url("room/" + room_alias); + }, + function(error) { + $scope.feedback = "Can't join room: " + JSON.stringify(error.data); + } + ); + } + } + } + else { + $scope.feedback = "Usage: /join "; + } + break; + + case "/kick": + // Kick a user from the room with an optional reason + if (args) { + var matches = args.match(/^(\S+?)( +(.*))?$/); + if (matches) { + promise = matrixService.kick($scope.room_id, matches[1], matches[3]); + } + } + + if (!promise) { + $scope.feedback = "Usage: /kick []"; + } + break; + + case "/ban": + // Ban a user from the room with an optional reason + if (args) { + var matches = args.match(/^(\S+?)( +(.*))?$/); + if (matches) { + promise = matrixService.ban($scope.room_id, matches[1], matches[3]); + } + } + + if (!promise) { + $scope.feedback = "Usage: /ban []"; + } + break; + + case "/unban": + // Unban a user from the room + if (args) { + var matches = args.match(/^(\S+)$/); + if (matches) { + // Reset the user membership to "leave" to unban him + promise = matrixService.unban($scope.room_id, matches[1]); + } + } + + if (!promise) { + $scope.feedback = "Usage: /unban "; + } + break; + + case "/op": + // Define the power level of a user + if (args) { + var matches = args.match(/^(\S+?)( +(\d+))?$/); + var powerLevel = 50; // default power level for op + if (matches) { + var user_id = matches[1]; + if (matches.length === 4 && undefined !== matches[3]) { + powerLevel = parseInt(matches[3]); + } + if (powerLevel !== NaN) { + var powerLevelEvent = $scope.room.current_room_state.state("m.room.power_levels"); + promise = matrixService.setUserPowerLevel($scope.room_id, user_id, powerLevel, powerLevelEvent); + } + } + } + + if (!promise) { + $scope.feedback = "Usage: /op []"; + } + break; + + case "/deop": + // Reset the power level of a user + if (args) { + var matches = args.match(/^(\S+)$/); + if (matches) { + var powerLevelEvent = $scope.room.current_room_state.state("m.room.power_levels"); + promise = matrixService.setUserPowerLevel($scope.room_id, args, undefined, powerLevelEvent); + } + } + + if (!promise) { + $scope.feedback = "Usage: /deop "; + } + break; + + default: + $scope.feedback = ("Unrecognised IRC-style command: " + cmd); + break; + } + } + + // By default send this as a message unless it's an IRC-style command + if (!promise && !cmd) { + // Make the request + promise = matrixService.sendTextMessage($scope.room_id, input); + echo = true; + } + + if (echo) { + // Echo the message to the room + // To do so, create a minimalist fake text message event and add it to the in-memory list of room messages + var echoMessage = { + content: { + body: (cmd === "/me" ? args : input), + msgtype: (cmd === "/me" ? "m.emote" : "m.text"), + }, + origin_server_ts: new Date().getTime(), // fake a timestamp + room_id: $scope.room_id, + type: "m.room.message", + user_id: $scope.state.user_id, + echo_msg_state: "messagePending" // Add custom field to indicate the state of this fake message to HTML + }; + + $('#mainInput').val(''); + $scope.room.addMessageEvent(echoMessage); + scrollToBottom(); + } + + if (promise) { + // Reset previous feedback + $scope.feedback = ""; + + promise.then( + function(response) { + console.log("Request successfully sent"); + + if (echo) { + // Mark this fake message event with its allocated event_id + // When the true message event will come from the events stream (in handleMessage), + // we will be able to replace the fake one by the true one + echoMessage.event_id = response.data.event_id; + } + else { + $('#mainInput').val(''); + } + }, + function(error) { + $scope.feedback = "Request failed: " + error.data.error; + + if (echoMessage) { + // Mark the message as unsent for the rest of the page life + echoMessage.origin_server_ts = "Unsent"; + echoMessage.echo_msg_state = "messageUnSent"; + } + }); + } + }; + + $scope.onInit = function() { + console.log("onInit"); + + // Does the room ID provided in the URL? + var room_id_or_alias; + if ($routeParams.room_id_or_alias) { + room_id_or_alias = decodeURIComponent($routeParams.room_id_or_alias); + } + + if (room_id_or_alias && '!' === room_id_or_alias[0]) { + // Yes. We can go on right now + $scope.room_id = room_id_or_alias; + $scope.room_alias = matrixService.getRoomIdToAliasMapping($scope.room_id); + onInit2(); + } + else { + // No. The URL contains the room alias. Get this alias. + if (room_id_or_alias) { + // The room alias was passed urlencoded, use it as is + $scope.room_alias = room_id_or_alias; + } + else { + // Else get the room alias by hand from the URL + // ie: extract #public:localhost:8080 from http://127.0.0.1:8000/#/room/#public:localhost:8080 + if (3 === location.hash.split("#").length) { + $scope.room_alias = "#" + location.hash.split("#")[2]; + } + else { + // In case of issue, go to the default page + console.log("Error: cannot extract room alias"); + $location.url("/"); + return; + } + } + + // Need a room ID required in Matrix API requests + console.log("Resolving alias: " + $scope.room_alias); + matrixService.resolveRoomAlias($scope.room_alias).then(function(response) { + $scope.room_id = response.data.room_id; + console.log(" -> Room ID: " + $scope.room_id); + + // Now, we can go on + onInit2(); + }, + function () { + // In case of issue, go to the default page + console.log("Error: cannot resolve room alias"); + $location.url("/"); + }); + } + }; + + var onInit2 = function() { + console.log("onInit2"); + // ============================= + $scope.room = modelService.getRoom($scope.room_id); + // ============================= + + // Scroll down as soon as possible so that we point to the last message + // if it already exists in memory + scrollToBottom(true); + + // Make sure the initialSync has been before going further + eventHandlerService.waitForInitialSyncCompletion().then( + function() { + + var needsToJoin = true; + + // The room members is available in the data fetched by initialSync + if ($scope.room) { + + var messages = $scope.room.events; + + if (0 === messages.length + || (1 === messages.length && "m.room.member" === messages[0].type && "invite" === messages[0].content.membership && $scope.state.user_id === messages[0].state_key)) { + // If we just joined a room, we won't have this history from initial sync, so we should try to paginate it anyway + $scope.state.first_pagination = true; + } + else { + // There is no need to do a 1st pagination (initialSync provided enough to fill a page) + $scope.state.first_pagination = false; + } + + var members = $scope.room.current_room_state.members; + + // Update the member list + for (var i in members) { + if (!members.hasOwnProperty(i)) continue; + + var member = members[i]; + updateMemberList(member); + } + + // Check if the user has already join the room + if ($scope.state.user_id in members) { + if ("join" === members[$scope.state.user_id].membership) { + needsToJoin = false; + } + } + } + + // Do we to join the room before starting? + if (needsToJoin) { + $scope.state.waiting_for_joined_event = true; + matrixService.join($scope.room_id).then( + function() { + // TODO: factor out the common housekeeping whenever we try to join a room or alias + matrixService.roomState($scope.room_id).then( + function(response) { + eventHandlerService.handleEvents(response.data, false, true); + }, + function(error) { + console.error("Failed to get room state for: " + $scope.room_id); + } + ); + + // onInit3 will be called once the joined m.room.member event is received from the events stream + // This avoids to get the joined information twice in parallel: + // - one from the events stream + // - one from the pagination because the pagination window covers this event ts + console.log("Joined room "+$scope.room_id); + }, + function(reason) { + console.log("Can't join room: " + JSON.stringify(reason)); + // FIXME: what if it wasn't a perms problem? + $scope.state.permission_denied = "You do not have permission to join this room"; + }); + } + else { + onInit3(); + } + } + ); + }; + + var onInit3 = function() { + console.log("onInit3"); + + // Make recents highlight the current room + $scope.recentsSelectedRoomID = $scope.room_id; + + // Init the history for this room + history.init(); + + // Get the up-to-date the current member list + matrixService.getMemberList($scope.room_id).then( + function(response) { + for (var i = 0; i < response.data.chunk.length; i++) { + var chunk = response.data.chunk[i]; + updateMemberList(chunk); + + // Add his power level + updateUserPowerLevel(chunk.user_id); + } + + // Arm list timing update timer + updateMemberListPresenceAge(); + + // Allow pagination + $scope.state.can_paginate = true; + + // Do a first pagination only if it is required + // FIXME: Should be no more require when initialSync/{room_id} will be available + if ($scope.state.first_pagination) { + paginate(MESSAGES_PER_PAGINATION); + } + else { + // There are already messages, go to the last message + scrollToBottom(true); + } + }, + function(error) { + $scope.feedback = "Failed get member list: " + error.data.error; + } + ); + }; + + $scope.inviteUser = function() { + + matrixService.invite($scope.room_id, $scope.userIDToInvite).then( + function() { + console.log("Invited."); + $scope.feedback = "Invite successfully sent to " + $scope.userIDToInvite; + $scope.userIDToInvite = ""; + }, + function(reason) { + $scope.feedback = "Failure: " + reason.data.error; + }); + }; + + $scope.leaveRoom = function() { + + matrixService.leave($scope.room_id).then( + function(response) { + console.log("Left room " + $scope.room_id); + $location.url("home"); + }, + function(error) { + $scope.feedback = "Failed to leave room: " + error.data.error; + }); + }; + + $scope.sendImage = function(url, body) { + scrollToBottom(true); + + matrixService.sendImageMessage($scope.room_id, url, body).then( + function() { + console.log("Image sent"); + }, + function(error) { + $scope.feedback = "Failed to send image: " + error.data.error; + }); + }; + + $scope.imageFileToSend; + $scope.$watch("imageFileToSend", function(newValue, oldValue) { + if ($scope.imageFileToSend) { + // Upload this image with its thumbnail to Internet + mFileUpload.uploadImageAndThumbnail($scope.imageFileToSend, THUMBNAIL_SIZE).then( + function(imageMessage) { + // imageMessage is complete message structure, send it as is + matrixService.sendMessage($scope.room_id, undefined, imageMessage).then( + function() { + console.log("Image message sent"); + }, + function(error) { + $scope.feedback = "Failed to send image message: " + error.data.error; + }); + }, + function(error) { + $scope.feedback = "Can't upload image"; + } + ); + } + }); + + $scope.loadMoreHistory = function() { + paginate(MESSAGES_PER_PAGINATION); + }; + + $scope.startVoiceCall = function() { + var call = new MatrixCall($scope.room_id); + call.onError = $rootScope.onCallError; + call.onHangup = $rootScope.onCallHangup; + // remote video element is used for playing audio in voice calls + call.remoteVideoElement = angular.element('#remoteVideo')[0]; + call.placeVoiceCall(); + $rootScope.currentCall = call; + }; + + $scope.startVideoCall = function() { + var call = new MatrixCall($scope.room_id); + call.onError = $rootScope.onCallError; + call.onHangup = $rootScope.onCallHangup; + call.localVideoElement = angular.element('#localVideo')[0]; + call.remoteVideoElement = angular.element('#remoteVideo')[0]; + call.placeVideoCall(); + $rootScope.currentCall = call; + }; + + // Manage history of typed messages + // History is saved in sessionStoratge so that it survives when the user + // navigates through the rooms and when it refreshes the page + var history = { + // The list of typed messages. Index 0 is the more recents + data: [], + + // The position in the history currently displayed + position: -1, + + // The message the user has started to type before going into the history + typingMessage: undefined, + + // Init/load data for the current room + init: function() { + var data = sessionStorage.getItem("history_" + $scope.room_id); + if (data) { + this.data = JSON.parse(data); + } + }, + + // Store a message in the history + push: function(message) { + this.data.unshift(message); + + // Update the session storage + sessionStorage.setItem("history_" + $scope.room_id, JSON.stringify(this.data)); + + // Reset history position + this.position = -1; + this.typingMessage = undefined; + }, + + // Move in the history + go: function(offset) { + + if (-1 === this.position) { + // User starts to go to into the history, save the current line + this.typingMessage = $('#mainInput').val(); + } + else { + // If the user modified this line in history, keep the change + this.data[this.position] = $('#mainInput').val(); + } + + // Bounds the new position to valid data + var newPosition = this.position + offset; + newPosition = Math.max(-1, newPosition); + newPosition = Math.min(newPosition, this.data.length - 1); + this.position = newPosition; + + if (-1 !== this.position) { + // Show the message from the history + $('#mainInput').val(this.data[this.position]); + } + else if (undefined !== this.typingMessage) { + // Go back to the message the user started to type + $('#mainInput').val(this.typingMessage); + } + } + }; + + // Make history singleton methods available from HTML + $scope.history = { + goUp: function($event) { + if ($scope.room_id) { + history.go(1); + } + $event.preventDefault(); + }, + goDown: function($event) { + if ($scope.room_id) { + history.go(-1); + } + $event.preventDefault(); + } + }; + + $scope.openJson = function(content) { + $scope.event_selected = angular.copy(content); + + // FIXME: Pre-calculated event data should be stripped in a nicer way. + $scope.event_selected.__room_member = undefined; + $scope.event_selected.__target_room_member = undefined; + + // scope this so the template can check power levels and enable/disable + // buttons + $scope.pow = eventHandlerService.getUserPowerLevel; + + var modalInstance = $modal.open({ + templateUrl: 'eventInfoTemplate.html', + controller: 'EventInfoController', + scope: $scope + }); + + modalInstance.result.then(function(action) { + if (action === "redact") { + var eventId = $scope.event_selected.event_id; + console.log("Redacting event ID " + eventId); + matrixService.redactEvent( + $scope.event_selected.room_id, + eventId + ).then(function(response) { + console.log("Redaction = " + JSON.stringify(response)); + }, function(error) { + console.error("Failed to redact event: "+JSON.stringify(error)); + if (error.data.error) { + $scope.feedback = error.data.error; + } + }); + } + }, function() { + // any dismiss code + }); + }; + + $scope.openRoomInfo = function() { + $scope.roomInfo = {}; + $scope.roomInfo.newEvent = { + content: {}, + type: "", + state_key: "" + }; + + var stateEvents = $scope.room.current_room_state.state_events; + // The modal dialog will 2-way bind this field, so we MUST make a deep + // copy of the state events else we will be *actually adjusing our view + // of the world* when fiddling with the JSON!! Apparently parse/stringify + // is faster than jQuery's extend when doing deep copies. + $scope.roomInfo.stateEvents = JSON.parse(JSON.stringify(stateEvents)); + var modalInstance = $modal.open({ + templateUrl: 'roomInfoTemplate.html', + controller: 'RoomInfoController', + size: 'lg', + scope: $scope + }); + }; + +}]) +.controller('EventInfoController', function($scope, $modalInstance) { + console.log("Displaying modal dialog for >>>> " + JSON.stringify($scope.event_selected)); + $scope.redact = function() { + console.log("User level = "+$scope.pow($scope.room_id, $scope.state.user_id)+ + " Redact level = "+$scope.room.current_room_state.state_events["m.room.ops_levels"].content.redact_level); + console.log("Redact event >> " + JSON.stringify($scope.event_selected)); + $modalInstance.close("redact"); + }; +}) +.controller('RoomInfoController', function($scope, $modalInstance, $filter, matrixService) { + console.log("Displaying room info."); + + $scope.submit = function(event) { + if (event.content) { + console.log("submit >>> " + JSON.stringify(event.content)); + matrixService.sendStateEvent($scope.room_id, event.type, + event.content, event.state_key).then(function(response) { + $modalInstance.dismiss(); + }, function(err) { + $scope.feedback = err.data.error; + } + ); + } + }; + + $scope.dismiss = $modalInstance.dismiss; + +}); -- cgit 1.5.1 From d4c20c472b82e2a8851374743ec5075a58bb71ff Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 6 Nov 2014 14:23:14 +0000 Subject: Use mRoomName on join notifications as well. --- syweb/webclient/room/room-controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'syweb/webclient/room/room-controller.js') diff --git a/syweb/webclient/room/room-controller.js b/syweb/webclient/room/room-controller.js index 2e0f5faff0..a2bc23195d 100644 --- a/syweb/webclient/room/room-controller.js +++ b/syweb/webclient/room/room-controller.js @@ -207,7 +207,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput']) } notificationService.showNotification( userName + - " (" + (matrixService.getRoomIdToAliasMapping(event.room_id) || event.room_id) + ")", + " (" + $filter("mRoomName")(event.room_id) + ")", userName + " joined", event.content.avatar_url ? event.content.avatar_url : undefined, function() { -- cgit 1.5.1 From e3c3f5a6d04bfbc0256010e9fb4dad7616ebbcc5 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 6 Nov 2014 14:52:22 +0000 Subject: Swap from using raw m.room.member events for room members to using actual RoomMember objects, so User objects can be tacked on. Update tests. --- .../components/matrix/event-handler-service.js | 9 +++-- syweb/webclient/components/matrix/matrix-call.js | 2 +- syweb/webclient/components/matrix/matrix-filter.js | 8 ++--- syweb/webclient/components/matrix/model-service.js | 4 ++- syweb/webclient/recents/recents-filter.js | 3 ++ syweb/webclient/room/room-controller.js | 4 +-- .../test/unit/event-handler-service.spec.js | 38 ++++++++++++++-------- syweb/webclient/test/unit/filters.spec.js | 12 ++++--- syweb/webclient/test/unit/model-service.spec.js | 2 +- 9 files changed, 52 insertions(+), 30 deletions(-) (limited to 'syweb/webclient/room/room-controller.js') diff --git a/syweb/webclient/components/matrix/event-handler-service.js b/syweb/webclient/components/matrix/event-handler-service.js index 38a6efced7..a9c6eb34c7 100644 --- a/syweb/webclient/components/matrix/event-handler-service.js +++ b/syweb/webclient/components/matrix/event-handler-service.js @@ -141,7 +141,7 @@ function(matrixService, $rootScope, $q, $timeout, $filter, mPresence, notificati notificationService.showNotification( displayname + " (" + roomTitle + ")", message, - member ? member.avatar_url : undefined, + member ? member.event.content.avatar_url : undefined, function() { console.log("notification.onclick() room=" + event.room_id); $rootScope.goToPage('room/' + event.room_id); @@ -306,6 +306,9 @@ function(matrixService, $rootScope, $q, $timeout, $filter, mPresence, notificati // Get the user display name from the member list of the room var member = modelService.getMember(room_id, user_id); + if (member) { + member = member.event; + } if (member && member.content.displayname) { // Do not consider null displayname displayName = member.content.displayname; @@ -315,7 +318,7 @@ function(matrixService, $rootScope, $q, $timeout, $filter, mPresence, notificati for (var member_id in room.current_room_state.members) { if (room.current_room_state.members.hasOwnProperty(member_id) && member_id !== user_id) { - var member2 = room.current_room_state.members[member_id]; + var member2 = room.current_room_state.members[member_id].event; if (member2.content.displayname && member2.content.displayname === displayName) { displayName = displayName + " (" + user_id + ")"; break; @@ -551,7 +554,7 @@ function(matrixService, $rootScope, $q, $timeout, $filter, mPresence, notificati for (var i in room.current_room_state.members) { if (!room.current_room_state.members.hasOwnProperty(i)) continue; - var member = room.current_room_state.members[i]; + var member = room.current_room_state.members[i].event; if ("join" === member.content.membership) { memberCount = memberCount + 1; diff --git a/syweb/webclient/components/matrix/matrix-call.js b/syweb/webclient/components/matrix/matrix-call.js index 5a2807c755..465b2b7807 100644 --- a/syweb/webclient/components/matrix/matrix-call.js +++ b/syweb/webclient/components/matrix/matrix-call.js @@ -214,7 +214,7 @@ angular.module('MatrixCall', []) var self = this; var roomMembers = modelService.getRoom(this.room_id).current_room_state.members; - if (roomMembers[matrixService.config().user_id].membership != 'join') { + if (roomMembers[matrixService.config().user_id].event.content.membership != 'join') { console.log("We need to join the room before we can accept this call"); matrixService.join(this.room_id).then(function() { self.answer(); diff --git a/syweb/webclient/components/matrix/matrix-filter.js b/syweb/webclient/components/matrix/matrix-filter.js index e84c197c76..aeebedc784 100644 --- a/syweb/webclient/components/matrix/matrix-filter.js +++ b/syweb/webclient/components/matrix/matrix-filter.js @@ -53,7 +53,7 @@ function($rootScope, matrixService, eventHandlerService, modelService) { for (var i in room.members) { if (!room.members.hasOwnProperty(i)) continue; - var member = room.members[i]; + var member = room.members[i].event; if (member.state_key !== user_id) { roomName = eventHandlerService.getUserDisplayName(room_id, member.state_key); if (!roomName) { @@ -70,11 +70,11 @@ function($rootScope, matrixService, eventHandlerService, modelService) { if (otherUserId === user_id) { // it's us, we may have been invited to this room or it could // be a self chat. - if (room.members[otherUserId].content.membership === "invite") { + if (room.members[otherUserId].event.content.membership === "invite") { // someone invited us, use the right ID. - roomName = eventHandlerService.getUserDisplayName(room_id, room.members[otherUserId].user_id); + roomName = eventHandlerService.getUserDisplayName(room_id, room.members[otherUserId].event.user_id); if (!roomName) { - roomName = room.members[otherUserId].user_id; + roomName = room.members[otherUserId].event.user_id; } } else { diff --git a/syweb/webclient/components/matrix/model-service.js b/syweb/webclient/components/matrix/model-service.js index 8b2ee877b1..8e0ce8d1a9 100644 --- a/syweb/webclient/components/matrix/model-service.js +++ b/syweb/webclient/components/matrix/model-service.js @@ -106,7 +106,9 @@ angular.module('modelService', []) storeStateEvent: function storeState(event) { this.state_events[event.type + event.state_key] = event; if (event.type === "m.room.member") { - this.members[event.state_key] = event; + var rm = new RoomMember(); + rm.event = event; + this.members[event.state_key] = rm; } }, diff --git a/syweb/webclient/recents/recents-filter.js b/syweb/webclient/recents/recents-filter.js index 39c2359967..cfbc6f4bd8 100644 --- a/syweb/webclient/recents/recents-filter.js +++ b/syweb/webclient/recents/recents-filter.js @@ -30,6 +30,9 @@ angular.module('RecentsController') // Show the room only if the user has joined it or has been invited // (ie, do not show it if he has been banned) var member = modelService.getMember(room_id, user_id); + if (member) { + member = member.event; + } room.recent.me = member; if (member && ("invite" === member.content.membership || "join" === member.content.membership)) { if ("invite" === member.content.membership) { diff --git a/syweb/webclient/room/room-controller.js b/syweb/webclient/room/room-controller.js index a2bc23195d..d3fb85b9dc 100644 --- a/syweb/webclient/room/room-controller.js +++ b/syweb/webclient/room/room-controller.js @@ -754,13 +754,13 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput']) for (var i in members) { if (!members.hasOwnProperty(i)) continue; - var member = members[i]; + var member = members[i].event; updateMemberList(member); } // Check if the user has already join the room if ($scope.state.user_id in members) { - if ("join" === members[$scope.state.user_id].membership) { + if ("join" === members[$scope.state.user_id].event.content.membership) { needsToJoin = false; } } diff --git a/syweb/webclient/test/unit/event-handler-service.spec.js b/syweb/webclient/test/unit/event-handler-service.spec.js index 023abec98b..2a4dc3b5a5 100644 --- a/syweb/webclient/test/unit/event-handler-service.spec.js +++ b/syweb/webclient/test/unit/event-handler-service.spec.js @@ -36,20 +36,28 @@ describe('EventHandlerService', function() { current_room_state: { members: { "@adam:matrix.org": { - content: { membership: "join" }, - user_id: "@adam:matrix.org" + event: { + content: { membership: "join" }, + user_id: "@adam:matrix.org" + } }, "@beth:matrix.org": { - content: { membership: "invite" }, - user_id: "@beth:matrix.org" + event: { + content: { membership: "invite" }, + user_id: "@beth:matrix.org" + } }, "@charlie:matrix.org": { - content: { membership: "join" }, - user_id: "@charlie:matrix.org" + event: { + content: { membership: "join" }, + user_id: "@charlie:matrix.org" + } }, "@danice:matrix.org": { - content: { membership: "leave" }, - user_id: "@danice:matrix.org" + event: { + content: { membership: "leave" }, + user_id: "@danice:matrix.org" + } } } } @@ -70,12 +78,16 @@ describe('EventHandlerService', function() { current_room_state: { members: { "@adam:matrix.org": { - content: { membership: "join" }, - user_id: "@adam:matrix.org" + event: { + content: { membership: "join" }, + user_id: "@adam:matrix.org" + } }, "@beth:matrix.org": { - content: { membership: "join" }, - user_id: "@beth:matrix.org" + event: { + content: { membership: "join" }, + user_id: "@beth:matrix.org" + } } }, s: { @@ -102,4 +114,4 @@ describe('EventHandlerService', function() { num = eventHandlerService.getUserPowerLevel(roomId, "@unknown:matrix.org"); expect(num).toEqual(50); })); -}); \ No newline at end of file +}); diff --git a/syweb/webclient/test/unit/filters.spec.js b/syweb/webclient/test/unit/filters.spec.js index 2e8d0c4036..7324a8e028 100644 --- a/syweb/webclient/test/unit/filters.spec.js +++ b/syweb/webclient/test/unit/filters.spec.js @@ -86,11 +86,13 @@ describe('mRoomName filter', function() { inviter_user_id = user_id; } this.s["m.room.member" + user_id] = { - content: { - membership: membership - }, - state_key: user_id, - user_id: inviter_user_id + event: { + content: { + membership: membership + }, + state_key: user_id, + user_id: inviter_user_id + } }; this.members[user_id] = this.s["m.room.member" + user_id]; } diff --git a/syweb/webclient/test/unit/model-service.spec.js b/syweb/webclient/test/unit/model-service.spec.js index 2e012efe90..e2fa8ceba3 100644 --- a/syweb/webclient/test/unit/model-service.spec.js +++ b/syweb/webclient/test/unit/model-service.spec.js @@ -25,6 +25,6 @@ describe('ModelService', function() { }); var user = modelService.getMember(roomId, userId); - expect(user.state_key).toEqual(userId); + expect(user.event.state_key).toEqual(userId); })); }); -- cgit 1.5.1 From 7d15452c3037b887a85d0b65916de90b2d2c4573 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 7 Nov 2014 17:56:28 +0000 Subject: Various fixes to try & make openwebrtc safari extension work (still doesn't work). --- syweb/webclient/app-controller.js | 8 +- syweb/webclient/app.css | 12 +- syweb/webclient/components/matrix/matrix-call.js | 146 +++++++++++++++-------- syweb/webclient/index.html | 4 +- syweb/webclient/room/room-controller.js | 4 +- 5 files changed, 107 insertions(+), 67 deletions(-) (limited to 'syweb/webclient/room/room-controller.js') diff --git a/syweb/webclient/app-controller.js b/syweb/webclient/app-controller.js index 2d82a42cf8..bbcf4ab5f6 100644 --- a/syweb/webclient/app-controller.js +++ b/syweb/webclient/app-controller.js @@ -112,8 +112,8 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even if (!$rootScope.currentCall) { // This causes the still frame to be flushed out of the video elements, // avoiding a flash of the last frame of the previous call when starting the next - angular.element('#localVideo')[0].load(); - angular.element('#remoteVideo')[0].load(); + if (angular.element('#localVideo')[0].load) angular.element('#localVideo')[0].load(); + if (angular.element('#remoteVideo')[0].load) angular.element('#remoteVideo')[0].load(); return; } @@ -187,8 +187,8 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even } call.onError = $scope.onCallError; call.onHangup = $scope.onCallHangup; - call.localVideoElement = angular.element('#localVideo')[0]; - call.remoteVideoElement = angular.element('#remoteVideo')[0]; + call.localVideoSelector = '#localVideo'; + call.remoteVideoSelector = '#remoteVideo'; $rootScope.currentCall = call; }); diff --git a/syweb/webclient/app.css b/syweb/webclient/app.css index 5ab8e2b8fd..be2a73872d 100755 --- a/syweb/webclient/app.css +++ b/syweb/webclient/app.css @@ -136,17 +136,17 @@ textarea, input { transition: left linear 500ms, top linear 500ms, width linear 500ms, height linear 500ms; } -#localVideo.mini { +.mini #localVideo { top: 0px; left: 130px; } -#localVideo.large { +.large #localVideo { top: 70px; left: 20px; } -#localVideo.ended { +.ended #localVideo { -webkit-filter: grayscale(1); filter: grayscale(1); } @@ -157,19 +157,19 @@ textarea, input { transition: left linear 500ms, top linear 500ms, width linear 500ms, height linear 500ms; } -#remoteVideo.mini { +.mini #remoteVideo { left: 260px; top: 0px; width: 128px; } -#remoteVideo.large { +.large #remoteVideo { left: 0px; top: 50px; width: 100%; } -#remoteVideo.ended { +.ended #remoteVideo { -webkit-filter: grayscale(1); filter: grayscale(1); } diff --git a/syweb/webclient/components/matrix/matrix-call.js b/syweb/webclient/components/matrix/matrix-call.js index 0631649946..b560cf7daa 100644 --- a/syweb/webclient/components/matrix/matrix-call.js +++ b/syweb/webclient/components/matrix/matrix-call.js @@ -35,14 +35,14 @@ var forAllTracksOnStream = function(s, f) { forAllAudioTracksOnStream(s, f); } -navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia; -window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection; // but not mozRTCPeerConnection because its interface is not compatible -window.RTCSessionDescription = window.RTCSessionDescription || window.webkitRTCSessionDescription || window.mozRTCSessionDescription; -window.RTCIceCandidate = window.RTCIceCandidate || window.webkitRTCIceCandidate || window.mozRTCIceCandidate; - angular.module('MatrixCall', []) .factory('MatrixCall', ['matrixService', 'matrixPhoneService', 'modelService', '$rootScope', '$timeout', function MatrixCallFactory(matrixService, matrixPhoneService, modelService, $rootScope, $timeout) { $rootScope.isWebRTCSupported = function () { + navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia; + window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection; // but not mozRTCPeerConnection because its interface is not compatible + window.RTCSessionDescription = window.RTCSessionDescription || window.webkitRTCSessionDescription || window.mozRTCSessionDescription; + window.RTCIceCandidate = window.RTCIceCandidate || window.webkitRTCIceCandidate || window.mozRTCIceCandidate; + return !!(navigator.getUserMedia || window.RTCPeerConnection || window.RTCSessionDescription || window.RTCIceCandidate); }; @@ -57,7 +57,7 @@ angular.module('MatrixCall', []) this.candidateSendTries = 0; var self = this; - $rootScope.$watch(this.remoteVideoElement, function (oldValue, newValue) { + $rootScope.$watch(this.getRemoteVideoElement(), function (oldValue, newValue) { self.tryPlayRemoteStream(); }); @@ -252,8 +252,8 @@ angular.module('MatrixCall', []) // pausing now keeps the last frame (ish) of the video call in the video element // rather than it just turning black straight away - if (this.remoteVideoElement) this.remoteVideoElement.pause(); - if (this.localVideoElement) this.localVideoElement.pause(); + if (this.getRemoteVideoElement() && this.getRemoteVideoElement().pause) this.getRemoteVideoElement().pause(); + if (this.getLocalVideoElement() && this.getLocalVideoElement().pause) this.getLocalVideoElement().pause(); this.stopAllMedia(); if (this.peerConn) this.peerConn.close(); @@ -278,11 +278,18 @@ angular.module('MatrixCall', []) } if (this.state == 'ended') return; - if (this.localVideoElement && this.type == 'video') { + var videoEl = this.getLocalVideoElement(); + + if (videoEl && this.type == 'video') { var vidTrack = stream.getVideoTracks()[0]; - this.localVideoElement.src = URL.createObjectURL(stream); - this.localVideoElement.muted = true; - this.localVideoElement.play(); + videoEl.autoplay = true; + videoEl.src = URL.createObjectURL(stream); + videoEl.muted = true; + var self = this; + $timeout(function() { + var vel = self.getLocalVideoElement(); + if (vel.play) vel.play(); + }); } this.localAVStream = stream; @@ -306,11 +313,18 @@ angular.module('MatrixCall', []) MatrixCall.prototype.gotUserMediaForAnswer = function(stream) { if (this.state == 'ended') return; - if (this.localVideoElement && this.type == 'video') { + var localVidEl = this.getLocalVideoElement(); + + if (localVidEl && this.type == 'video') { + localVidEl.autoplay = true; var vidTrack = stream.getVideoTracks()[0]; - this.localVideoElement.src = URL.createObjectURL(stream); - this.localVideoElement.muted = true; - this.localVideoElement.play(); + localVidEl.src = URL.createObjectURL(stream); + localVidEl.muted = true; + var self = this; + $timeout(function() { + var vel = self.getLocalVideoElement(); + if (vel.play) vel.play(); + }); } this.localAVStream = stream; @@ -339,11 +353,11 @@ angular.module('MatrixCall', []) } MatrixCall.prototype.gotRemoteIceCandidate = function(cand) { - console.log("Got remote ICE "+cand.sdpMid+" candidate: "+cand.candidate); if (this.state == 'ended') { - console.log("Ignoring remote ICE candidate because call has ended"); + //console.log("Ignoring remote ICE candidate because call has ended"); return; } + console.log("Got remote ICE "+cand.sdpMid+" candidate: "+cand.candidate); this.peerConn.addIceCandidate(new RTCIceCandidate(cand), function() {}, function(e) {}); }; @@ -363,41 +377,46 @@ angular.module('MatrixCall', []) return; } - this.peerConn.setLocalDescription(description); - - var content = { - version: 0, - call_id: this.call_id, - offer: description, - lifetime: MatrixCall.CALL_TIMEOUT - }; - this.sendEventWithRetry('m.call.invite', content); - var self = this; - $timeout(function() { - if (self.state == 'invite_sent') { - self.hangup('invite_timeout'); - } - }, MatrixCall.CALL_TIMEOUT); + this.peerConn.setLocalDescription(description, function() { + var content = { + version: 0, + call_id: self.call_id, + // OpenWebRTC appears to add extra stuff (like the DTLS fingerprint) to the description + // when setting it on the peerconnection. According to the spec it should only add ICE + // candidates. Any ICE candidates that have already been generated at this point will + // probably be sent both in the offer and separately. Ho hum. + offer: self.peerConn.localDescription, + lifetime: MatrixCall.CALL_TIMEOUT + }; + self.sendEventWithRetry('m.call.invite', content); + + $timeout(function() { + if (self.state == 'invite_sent') { + self.hangup('invite_timeout'); + } + }, MatrixCall.CALL_TIMEOUT); - $rootScope.$apply(function() { - self.state = 'invite_sent'; - }); + $rootScope.$apply(function() { + self.state = 'invite_sent'; + }); + }, function() { console.log("Error setting local description!"); }); }; MatrixCall.prototype.createdAnswer = function(description) { console.log("Created answer: "+description); - this.peerConn.setLocalDescription(description); - var content = { - version: 0, - call_id: this.call_id, - answer: description - }; - this.sendEventWithRetry('m.call.answer', content); var self = this; - $rootScope.$apply(function() { - self.state = 'connecting'; - }); + this.peerConn.setLocalDescription(description, function() { + var content = { + version: 0, + call_id: self.call_id, + answer: self.peerConn.localDescription + }; + self.sendEventWithRetry('m.call.answer', content); + $rootScope.$apply(function() { + self.state = 'connecting'; + }); + }, function() { console.log("Error setting local description!"); } ); }; MatrixCall.prototype.getLocalOfferFailed = function(error) { @@ -465,10 +484,15 @@ angular.module('MatrixCall', []) }; MatrixCall.prototype.tryPlayRemoteStream = function(event) { - if (this.remoteVideoElement && this.remoteAVStream) { - var player = this.remoteVideoElement; + if (this.getRemoteVideoElement() && this.remoteAVStream) { + var player = this.getRemoteVideoElement(); + player.autoplay = true; player.src = URL.createObjectURL(this.remoteAVStream); - player.play(); + var self = this; + $timeout(function() { + var vel = self.getRemoteVideoElement(); + if (vel.play) vel.play(); + }); } }; @@ -500,8 +524,8 @@ angular.module('MatrixCall', []) MatrixCall.prototype.onHangupReceived = function(msg) { console.log("Hangup received"); - if (this.remoteVideoElement) this.remoteVideoElement.pause(); - if (this.localVideoElement) this.localVideoElement.pause(); + if (this.getRemoteVideoElement() && this.getRemoteVideoElement().pause) this.getRemoteVideoElement().pause(); + if (this.getLocalVideoElement() && this.getLocalVideoElement().pause) this.getLocalVideoElement().pause(); this.state = 'ended'; this.hangupParty = 'remote'; this.hangupReason = msg.reason; @@ -524,8 +548,8 @@ angular.module('MatrixCall', []) newCall.gotUserMediaForAnswer(this.localAVStream); delete(this.localAVStream); } - newCall.localVideoElement = this.localVideoElement; - newCall.remoteVideoElement = this.remoteVideoElement; + newCall.localVideoSelector = this.localVideoSelector; + newCall.remoteVideoSelector = this.remoteVideoSelector; this.successor = newCall; this.hangup(true); }; @@ -601,5 +625,21 @@ angular.module('MatrixCall', []) }, delayMs); }; + MatrixCall.prototype.getLocalVideoElement = function() { + if (this.localVideoSelector) { + var t = angular.element(this.localVideoSelector); + if (t.length) return t[0]; + } + return null; + }; + + MatrixCall.prototype.getRemoteVideoElement = function() { + if (this.remoteVideoSelector) { + var t = angular.element(this.remoteVideoSelector); + if (t.length) return t[0]; + } + return null; + }; + return MatrixCall; }]); diff --git a/syweb/webclient/index.html b/syweb/webclient/index.html index 992e8d3377..45be6c274b 100644 --- a/syweb/webclient/index.html +++ b/syweb/webclient/index.html @@ -53,8 +53,8 @@
- - +
+
diff --git a/syweb/webclient/room/room-controller.js b/syweb/webclient/room/room-controller.js index d3fb85b9dc..b878a1f718 100644 --- a/syweb/webclient/room/room-controller.js +++ b/syweb/webclient/room/room-controller.js @@ -919,8 +919,8 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput']) var call = new MatrixCall($scope.room_id); call.onError = $rootScope.onCallError; call.onHangup = $rootScope.onCallHangup; - call.localVideoElement = angular.element('#localVideo')[0]; - call.remoteVideoElement = angular.element('#remoteVideo')[0]; + call.localVideoSelector = '#localVideo'; + call.remoteVideoSelector = '#remoteVideo'; call.placeVideoCall(); $rootScope.currentCall = call; }; -- cgit 1.5.1 From b765dc005ba22cb41e01439f1b07d567d2de68da Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 11 Nov 2014 04:39:16 +0000 Subject: major CSS overhaul to try to make things look a bit cleaner --- syweb/webclient/app.css | 150 ++++++++++++++++++++++++++------ syweb/webclient/img/attach.png | Bin 0 -> 473 bytes syweb/webclient/img/settings.png | Bin 0 -> 864 bytes syweb/webclient/img/video.png | Bin 0 -> 604 bytes syweb/webclient/img/voice.png | Bin 0 -> 659 bytes syweb/webclient/index.html | 2 + syweb/webclient/js/angular-peity.js | 69 +++++++++++++++ syweb/webclient/js/jquery.peity.min.js | 13 +++ syweb/webclient/mobile.css | 18 ++++ syweb/webclient/room/room-controller.js | 19 +++- syweb/webclient/room/room.html | 131 +++++++++++++--------------- 11 files changed, 300 insertions(+), 102 deletions(-) create mode 100644 syweb/webclient/img/attach.png create mode 100644 syweb/webclient/img/settings.png create mode 100644 syweb/webclient/img/video.png create mode 100644 syweb/webclient/img/voice.png create mode 100644 syweb/webclient/js/angular-peity.js create mode 100644 syweb/webclient/js/jquery.peity.min.js (limited to 'syweb/webclient/room/room-controller.js') diff --git a/syweb/webclient/app.css b/syweb/webclient/app.css index be2a73872d..e2f99f3975 100755 --- a/syweb/webclient/app.css +++ b/syweb/webclient/app.css @@ -318,7 +318,7 @@ textarea, input { position: absolute; bottom: 0px; width: 100%; - height: 100px; + height: 70px; background-color: #f8f8f8; border-top: #aaa 1px solid; } @@ -326,7 +326,9 @@ textarea, input { #controls { max-width: 1280px; padding: 12px; + padding-right: 42px; margin: auto; + position: relative; } #buttonsCell { @@ -343,7 +345,17 @@ textarea, input { #mainInput { width: 100%; - resize: none; + padding: 5px; +} + +#attachButton { + position: absolute; + margin-top: 3px; + right: 0px; + background: url('img/attach.png'); + width: 25px; + height: 25px; + border: 0px; } .blink { @@ -415,7 +427,8 @@ textarea, input { .roomHeaderInfo { text-align: right; float: right; - margin-top: 15px; + margin-top: 0px; + margin-right: 30px; } /*** Room Info Dialog ***/ @@ -449,15 +462,32 @@ textarea, input { resize: vertical; } +/*** Control Buttons ***/ +#controlButtons { + float: right; + margin-right: -4px; + padding-bottom: 6px; +} + +.controlButton { + border: 0px; + width: 30px; + height: 30px; + padding-left: 3px; + padding-right: 3px; +} + /*** Participant list ***/ #usersTableWrapper { float: right; - width: 120px; + clear: right; + width: 100px; height: 100%; overflow-y: auto; } +/* #usersTable { width: 100%; border-collapse: collapse; @@ -473,36 +503,66 @@ textarea, input { position: relative; background-color: #000; } +*/ -.userAvatar .userAvatarImage { - position: absolute; - top: 0px; +.userAvatar { +} + +.userAvatarFrame { + border-radius: 46px; + width: 80px; + margin: auto; + position: relative; + border: 3px solid #aaa; + background-color: #aaa; +} + +.userAvatarImage { + border-radius: 40px; + text-align: center; object-fit: cover; - width: 100%; + display: block; } +/* .userAvatar .userAvatarGradient { position: absolute; bottom: 20px; width: 100%; } +*/ -.userAvatar .userName { - position: absolute; - color: #fff; - margin: 2px; - bottom: 0px; +.userName { + margin-top: 3px; + margin-bottom: 6px; + text-align: center; font-size: 12px; word-break: break-all; } -.userAvatar .userPowerLevel { +.userPowerLevel { position: absolute; + bottom: -1px; + height: 1px; + background-color: #f00; +} + +.userPowerLevelBar { + display: inline; + position: absolute; + width: 2px; + height: 10px; +/* border: 1px solid #000; +*/ background-color: #aaa; +} + +.userPowerLevelMeter { + position: relative; bottom: 0px; - height: 2px; background-color: #f00; } +/* .userPresence { text-align: center; font-size: 12px; @@ -510,12 +570,15 @@ textarea, input { background-color: #aaa; border-bottom: 1px #ddd solid; } +*/ .online { + border-color: #38AF00; background-color: #38AF00; } .unavailable { + border-color: #FFCC00; background-color: #FFCC00; } @@ -538,18 +601,21 @@ textarea, input { #messageTable td { padding: 0px; +/* border: 1px solid #888; */ } .leftBlock { - width: 14em; + width: 6em; word-wrap: break-word; vertical-align: top; background-color: #fff; - color: #888; + color: #aaa; font-weight: medium; font-size: 12px; text-align: right; +/* border-top: 1px #ddd solid; +*/ } .rightBlock { @@ -560,13 +626,24 @@ textarea, input { } .sender, .timestamp { - padding-right: 1em; - padding-left: 1em; - padding-top: 3px; +/* padding-top: 3px; +*/} + +.timestamp { + font-size: 10px; + color: #ccc; + height: 13px; + margin-top: 4px; +*/ transition-property: opacity; + transition-duration: 0.3s; } .sender { - margin-bottom: -3px; + font-size: 12px; +/* + margin-top: 5px; + margin-bottom: -9px; +*/ } .avatar { @@ -577,7 +654,11 @@ textarea, input { } .avatarImage { + position: relative; + top: 5px; object-fit: cover; + border-radius: 32px; + margin-top: 4px; } .emote { @@ -591,6 +672,7 @@ textarea, input { } .image { + border: 1px solid #888; display: block; max-width:320px; max-height:320px; @@ -603,19 +685,23 @@ textarea, input { } .bubble { +/* background-color: #eee; border: 1px solid #d8d8d8; - display: inline-block; margin-bottom: -1px; - max-width: 90%; - font-size: 14px; - word-wrap: break-word; padding-top: 7px; padding-bottom: 5px; + -webkit-text-size-adjust:100% + vertical-align: middle; +*/ + display: inline-block; + max-width: 90%; padding-left: 1em; padding-right: 1em; - vertical-align: middle; - -webkit-text-size-adjust:100% + padding-top: 2px; + padding-bottom: 2px; + font-size: 14px; + word-wrap: break-word; } .bubble img { @@ -623,9 +709,11 @@ textarea, input { max-height: auto; } +/* .differentUser td { padding-bottom: 5px ! important; } +*/ .mine { text-align: right; @@ -635,13 +723,15 @@ textarea, input { .text.membership .bubble, .mine .text.emote .bubble, .mine .text.membership .bubble - { +{ background-color: transparent ! important; border: 0px ! important; } .mine .text .bubble { +/* background-color: #f8f8ff ! important; +*/ text-align: left ! important; } @@ -701,6 +791,8 @@ textarea, input { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; + padding-left: 0.5em; + padding-right: 0.5em; } .recentsRoom { @@ -751,7 +843,7 @@ textarea, input { padding-right: 10px; margin-right: 10px; height: 100%; - border-right: 1px solid #ddd; +/* border-right: 1px solid #ddd; */ overflow-y: auto; } diff --git a/syweb/webclient/img/attach.png b/syweb/webclient/img/attach.png new file mode 100644 index 0000000000..d95eabaf00 Binary files /dev/null and b/syweb/webclient/img/attach.png differ diff --git a/syweb/webclient/img/settings.png b/syweb/webclient/img/settings.png new file mode 100644 index 0000000000..ac99fe402b Binary files /dev/null and b/syweb/webclient/img/settings.png differ diff --git a/syweb/webclient/img/video.png b/syweb/webclient/img/video.png new file mode 100644 index 0000000000..e90afea0c1 Binary files /dev/null and b/syweb/webclient/img/video.png differ diff --git a/syweb/webclient/img/voice.png b/syweb/webclient/img/voice.png new file mode 100644 index 0000000000..fe464999c0 Binary files /dev/null and b/syweb/webclient/img/voice.png differ diff --git a/syweb/webclient/index.html b/syweb/webclient/index.html index 45be6c274b..f6487f381d 100644 --- a/syweb/webclient/index.html +++ b/syweb/webclient/index.html @@ -17,6 +17,8 @@ + + diff --git a/syweb/webclient/js/angular-peity.js b/syweb/webclient/js/angular-peity.js new file mode 100644 index 0000000000..2acb647d91 --- /dev/null +++ b/syweb/webclient/js/angular-peity.js @@ -0,0 +1,69 @@ +var angularPeity = angular.module( 'angular-peity', [] ); + +$.fn.peity.defaults.pie = { + fill: ["#ff0000", "#aaaaaa"], + radius: 4, +} + +var buildChartDirective = function ( chartType ) { + return { + restrict: 'E', + scope: { + data: "=", + options: "=" + }, + link: function ( scope, element, attrs ) { + + var options = {}; + if ( scope.options ) { + options = scope.options; + } + + // N.B. live-binding to data by Matthew + scope.$watch('data', function () { + var span = document.createElement( 'span' ); + span.textContent = scope.data.join(); + + if ( !attrs.class ) { + span.className = ""; + } else { + span.className = attrs.class; + } + + if (element[0].nodeType === 8) { + element.replaceWith( span ); + } + else if (element[0].firstChild) { + element.empty(); + element[0].appendChild( span ); + } + else { + element[0].appendChild( span ); + } + + jQuery( span ).peity( chartType, options ); + }); + } + }; +}; + + +angularPeity.directive( 'pieChart', function () { + + return buildChartDirective( "pie" ); + +} ); + + +angularPeity.directive( 'barChart', function () { + + return buildChartDirective( "bar" ); + +} ); + + +angularPeity.directive( 'lineChart', function () { + + return buildChartDirective( "line" ); + +} ); diff --git a/syweb/webclient/js/jquery.peity.min.js b/syweb/webclient/js/jquery.peity.min.js new file mode 100644 index 0000000000..054b83c5d8 --- /dev/null +++ b/syweb/webclient/js/jquery.peity.min.js @@ -0,0 +1,13 @@ +// Peity jQuery plugin version 3.0.2 +// (c) 2014 Ben Pickles +// +// http://benpickles.github.io/peity +// +// Released under MIT license. +(function(h,w,i,v){var p=function(a,b){var d=w.createElementNS("http://www.w3.org/2000/svg",a);h(d).attr(b);return d},y="createElementNS"in w&&p("svg",{}).createSVGRect,e=h.fn.peity=function(a,b){y&&this.each(function(){var d=h(this),c=d.data("peity");c?(a&&(c.type=a),h.extend(c.opts,b)):(c=new x(d,a,h.extend({},e.defaults[a],b)),d.change(function(){c.draw()}).data("peity",c));c.draw()});return this},x=function(a,b,d){this.$el=a;this.type=b;this.opts=d},r=x.prototype;r.draw=function(){e.graphers[this.type].call(this, +this.opts)};r.fill=function(){var a=this.opts.fill;return h.isFunction(a)?a:function(b,d){return a[d%a.length]}};r.prepare=function(a,b){this.svg||this.$el.hide().after(this.svg=p("svg",{"class":"peity"}));return h(this.svg).empty().data("peity",this).attr({height:b,width:a})};r.values=function(){return h.map(this.$el.text().split(this.opts.delimiter),function(a){return parseFloat(a)})};e.defaults={};e.graphers={};e.register=function(a,b,d){this.defaults[a]=b;this.graphers[a]=d};e.register("pie", +{fill:["#ff9900","#fff4dd","#ffc66e"],radius:8},function(a){if(!a.delimiter){var b=this.$el.text().match(/[^0-9\.]/);a.delimiter=b?b[0]:","}b=this.values();if("/"==a.delimiter)var d=b[0],b=[d,i.max(0,b[1]-d)];for(var c=0,d=b.length,n=0;cj?m=q(i.min(d,0)):o=q(i.max(c,0)):u=1;u=o-m;0==u&&(u=1,0