summary refs log tree commit diff
path: root/syweb/webclient/room
diff options
context:
space:
mode:
Diffstat (limited to 'syweb/webclient/room')
-rw-r--r--syweb/webclient/room/room-controller.js581
-rw-r--r--syweb/webclient/room/room-directive.js276
-rw-r--r--syweb/webclient/room/room.html282
3 files changed, 0 insertions, 1139 deletions
diff --git a/syweb/webclient/room/room-controller.js b/syweb/webclient/room/room-controller.js
deleted file mode 100644
index df05a8e573..0000000000
--- a/syweb/webclient/room/room-controller.js
+++ /dev/null
@@ -1,581 +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.
-*/
-
-angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput', 'angular-peity'])
-.controller('RoomController', ['$modal', '$filter', '$scope', '$timeout', '$routeParams', '$location', '$rootScope', 'matrixService', 'mPresence', 'eventHandlerService', 'mFileUpload', 'matrixPhoneService', 'MatrixCall', 'modelService', 'recentsService', 'commandsService', 'mUserDisplayNameFilter',
-                               function($modal, $filter, $scope, $timeout, $routeParams, $location, $rootScope, matrixService, mPresence, eventHandlerService, mFileUpload, matrixPhoneService, MatrixCall, modelService, recentsService, commandsService, mUserDisplayNameFilter) {
-   'use strict';
-    var MESSAGES_PER_PAGINATION = 30;
-    var THUMBNAIL_SIZE = 320;
-    
-    // .html needs this
-    $scope.containsBingWord = eventHandlerService.eventContainsBingWord;
-
-    // Room ids. Computed and resolved in onInit
-    $scope.room_id = 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.imageURLToSend = "";
-    
-
-    // 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 there is a live event affecting us
-        if (isLive && event.room_id === $scope.room_id && event.state_key === $scope.state.user_id) {
-            // if someone else changed our state..
-            if (event.user_id !== $scope.state.user_id && "invite" !== event.content.membership && "join" !== event.content.membership) {    
-                if ("ban" === event.content.membership) {
-                    $scope.state.permission_denied = "You have been banned by " + mUserDisplayNameFilter(event.user_id);
-                }
-                else {
-                    $scope.state.permission_denied = "You have been kicked by " + mUserDisplayNameFilter(event.user_id);
-                }  
-            }
-            else {
-                scrollToBottom();
-            }
-        }
-    });
-
-    $scope.memberCount = function() {
-        return Object.keys($scope.room.now.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 updatePresenceTimes = 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(updatePresenceTimes, 5 * 1000);
-    };
-
-    $scope.send = function() {
-        var input = $('#mainInput').val();
-        
-        if (undefined === input || input === "") {
-            return;
-        }
-        
-        scrollToBottom(true);
-
-        // Store the command in the history
-        $rootScope.$broadcast("commandHistory:BROADCAST_NEW_HISTORY_ITEM(item)",
-                              input);
-
-        var isEmote = input.indexOf("/me ") === 0;
-        var promise;
-        if (!isEmote) {
-            promise = commandsService.processInput($scope.room_id, input);
-        }
-        var echo = false;
-        
-        
-        if (!promise) { // not a non-echoable command
-            echo = true;
-            if (isEmote) {
-                promise = matrixService.sendEmoteMessage($scope.room_id, input.substring(4));
-            }
-            else {
-                promise = matrixService.sendTextMessage($scope.room_id, input);
-            }
-        }
-        
-        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: (isEmote ? input.substring(4) : input),
-                    msgtype: (isEmote ? "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 = 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";
-                    }
-                });
-        }
-    };
-
-    // Tries to find a suitable room ID for this room.
-    $scope.onInit = function() {
-        console.log("onInit");
-
-        // Extract the room identifier being loaded
-        var room_id_or_alias;
-        if ($routeParams.room_id_or_alias) { // provided in the url
-            room_id_or_alias = decodeURIComponent($routeParams.room_id_or_alias);
-        }
-        
-        eventHandlerService.joinRoom(room_id_or_alias).then(function(roomId) {
-            $scope.room_id = roomId;
-            $scope.room = modelService.getRoom($scope.room_id);
-            
-            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;
-            }
-            
-            // Make recents highlight the current room
-            recentsService.setSelectedRoomId($scope.room_id);
-            
-            updatePresenceTimes();
-
-            // Allow pagination
-            $scope.state.can_paginate = true;
-
-            // Do a first pagination only if it is required (e.g. we've JUST joined a room and have no messages to display.)
-            // FIXME: Should be no more require when initialSync/{room_id} will be available
-            if ($scope.state.first_pagination) {
-                paginate(MESSAGES_PER_PAGINATION);
-            }
-            
-            // Scroll down as soon as possible so that we point to the last message
-            // if it already exists in memory
-            scrollToBottom(true);
-        },
-        function(err) {
-            if (err.data.errcode === "M_FORBIDDEN") {
-                $scope.state.permission_denied = "You do not have permission to join this room";
-            }
-            else {
-                console.log("Error: cannot join room: "+JSON.stringify(err));
-                $location.url("/");
-            }
-        });
-    };
-
-    $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;
-            });
-    };
-
-    // used to send an image based on just a URL, rather than uploading one
-    $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.fileToSend;
-    $scope.$watch("fileToSend", function(newValue, oldValue) {
-        if ($scope.fileToSend) {
-            // Upload this file
-            mFileUpload.uploadFileAndThumbnail($scope.fileToSend, THUMBNAIL_SIZE).then(
-                function(fileMessage) {
-                    // fileMessage is complete message structure, send it as is
-                    matrixService.sendMessage($scope.room_id, undefined, fileMessage).then(
-                        function() {
-                            console.log("File message sent");
-                        },
-                        function(error) {
-                            $scope.feedback = "Failed to send file message: " + error.data.error;
-                        });
-                },
-                function(error) {
-                    $scope.feedback = "Can't upload file";
-                }
-            );
-        }
-    });
-    
-    $scope.loadMoreHistory = function() {
-        paginate(MESSAGES_PER_PAGINATION);
-    };
-
-    $scope.checkWebRTC = function() {
-        if (!$rootScope.isWebRTCSupported()) {
-            alert("Your browser does not support WebRTC");
-            return false;
-        }
-        if ($scope.memberCount() != 2) {
-            alert("WebRTC calls are currently only supported on rooms with two members");
-            return false;
-        }
-        return true;
-    };
-    
-    $scope.startVoiceCall = function() {        
-        if (!$scope.checkWebRTC()) return;
-        var call = new MatrixCall($scope.room_id);
-        call.onError = $rootScope.onCallError;
-        call.onHangup = $rootScope.onCallHangup;
-        // remote video element is used for playing audio in voice calls
-        call.remoteVideoSelector = angular.element('#remoteVideo')[0];
-        call.placeVoiceCall();
-        $rootScope.currentCall = call;
-    };
-
-    $scope.startVideoCall = function() {
-        if (!$scope.checkWebRTC()) return;
-
-        var call = new MatrixCall($scope.room_id);
-        call.onError = $rootScope.onCallError;
-        call.onHangup = $rootScope.onCallHangup;
-        call.localVideoSelector = '#localVideo';
-        call.remoteVideoSelector = '#remoteVideo';
-        call.placeVideoCall();
-        $rootScope.currentCall = call;
-    };
-
-    $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 = modelService.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");
-    };
-    $scope.dismiss = $modalInstance.dismiss;
-})
-.controller('RoomInfoController', function($scope, $modalInstance, $filter, matrixService) {
-    console.log("Displaying room info.");
-    
-    $scope.userIDToInvite = "";
-    
-    $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.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;
-
-});
diff --git a/syweb/webclient/room/room-directive.js b/syweb/webclient/room/room-directive.js
deleted file mode 100644
index 4707d12c8c..0000000000
--- a/syweb/webclient/room/room-directive.js
+++ /dev/null
@@ -1,276 +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('RoomController')
-// XXX FIXME : This has tight coupling with $scope.room.now.members
-.directive('tabComplete', ['$timeout', function ($timeout) {
-    return function (scope, element, attrs) {
-        element.bind("keydown keypress", function (event) {
-            // console.log("event: " + event.which);
-            var TAB = 9;
-            var SHIFT = 16;
-            var keypressCode = event.which;
-            if (keypressCode === TAB) {
-                if (!scope.tabCompleting) { // cache our starting text
-                    scope.tabCompleteOriginal = element[0].value;
-                    scope.tabCompleting = true;
-                    scope.tabCompleteIndex = 0;
-                }
-                
-                // loop in the right direction
-                if (event.shiftKey) {
-                    scope.tabCompleteIndex--;
-                    if (scope.tabCompleteIndex < 0) {
-                        // wrap to the last search match, and fix up to a real
-                        // index value after we've matched
-                        scope.tabCompleteIndex = Number.MAX_VALUE;
-                    }
-                }
-                else {
-                    scope.tabCompleteIndex++;
-                }
-                
-
-                var searchIndex = 0;
-                var targetIndex = scope.tabCompleteIndex;
-                var text = scope.tabCompleteOriginal;
-                
-                // console.log("targetIndex: " + targetIndex + ", 
-                // text=" + text);
-                                    
-                // FIXME: use the correct regexp to recognise userIDs --M
-                //
-                // XXX: I don't really know what the point of this is. You
-                // WANT to match freeform text given you want to match display
-                // names AND user IDs. Surely you just want to get the last
-                // word out of the input text and that's that?
-                // Am I missing something here? -- Kegan
-                //
-                // You're not missing anything - my point was that we should
-                // explicitly define the syntax for user IDs /somewhere/.
-                // Meanwhile as long as the delimeters are well defined, we
-                // could just pick "the last word".  But to know what the
-                // correct delimeters are, we probably do need a formal
-                // syntax for user IDs to refer to... --Matthew
-                
-                var search = /@?([a-zA-Z0-9_\-:\.]+)$/.exec(text);
-
-                if (targetIndex === 0) { // 0 is always the original text
-                    element[0].value = text;                    
-                    // Force angular to wake up and update the input ng-model 
-                    // by firing up input event
-                    angular.element(element[0]).triggerHandler('input');
-                }
-                else if (search && search[1]) {
-                    // console.log("search found: " + search+" from "+text);
-                    var expansion;
-                    
-                    // FIXME: could do better than linear search here
-                    angular.forEach(scope.room.now.members, function(item, name) {
-                        if (item.event.content.displayname && searchIndex < targetIndex) {
-                            if (item.event.content.displayname.toLowerCase().indexOf(search[1].toLowerCase()) === 0) {
-                                expansion = item.event.content.displayname;
-                                searchIndex++;
-                            }
-                        }
-                    });
-                    if (searchIndex < targetIndex) { // then search raw mxids
-                        angular.forEach(scope.room.now.members, function(item, name) {
-                            if (searchIndex < targetIndex) {
-                                // === 1 because mxids are @username
-                                if (name.toLowerCase().indexOf(search[1].toLowerCase()) === 1) {
-                                    expansion = name;
-                                    searchIndex++;
-                                }
-                            }
-                        });
-                    }
-                    
-                    if (searchIndex === targetIndex || 
-                            targetIndex === Number.MAX_VALUE) {
-                        // xchat-style tab complete, add a colon if tab 
-                        // completing at the start of the text
-                        if (search[0].length === text.length)
-                            expansion += ": ";
-                        else
-                            expansion += " ";
-                        element[0].value = text.replace(/@?([a-zA-Z0-9_\-:\.]+)$/, expansion);
-                        // cancel blink
-                        element[0].className = "";     
-                        if (targetIndex === Number.MAX_VALUE) {
-                            // wrap the index around to the last index found
-                            scope.tabCompleteIndex = searchIndex;
-                            targetIndex = searchIndex;
-                        }
-                    }
-                    else {
-                        // console.log("wrapped!");
-                        element[0].className = "blink"; // XXX: slightly naughty to bypass angular
-                        $timeout(function() {
-                             element[0].className = "";
-                        }, 150);
-                        element[0].value = text;
-                        scope.tabCompleteIndex = 0;
-                    }
-
-                    // Force angular to wak up and update the input ng-model by
-                    // firing up input event
-                    angular.element(element[0]).triggerHandler('input');
-                }
-                else {
-                    scope.tabCompleteIndex = 0;
-                }
-                // prevent the default TAB operation (typically focus shifting)
-                event.preventDefault();
-            }
-            else if (keypressCode !== SHIFT && scope.tabCompleting) {
-                scope.tabCompleting = false;
-                scope.tabCompleteIndex = 0;
-            }
-        });
-    };
-}])
-// A directive which stores text sent into it and restores it via up/down arrows
-.directive('commandHistory', [ function() {
-    var BROADCAST_NEW_HISTORY_ITEM = "commandHistory:BROADCAST_NEW_HISTORY_ITEM(item)";
-
-    // Manage history of typed messages
-    // History is saved in sessionStorage 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,
-        
-        element: undefined,
-        roomId: undefined,
-
-        // The message the user has started to type before going into the history
-        typingMessage: undefined,
-
-        // Init/load data for the current room
-        init: function(element, roomId) {
-            this.roomId = roomId;
-            this.element = element;
-            var data = sessionStorage.getItem("history_" + this.roomId);
-            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_" + this.roomId, 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 = this.element.val();
-            }
-            else {
-                // If the user modified this line in history, keep the change
-                this.data[this.position] = this.element.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
-                this.element.val(this.data[this.position]);
-            }
-            else if (undefined !== this.typingMessage) {
-                // Go back to the message the user started to type
-                this.element.val(this.typingMessage);
-            }
-        }
-    };
-
-    return {
-        restrict: "AE",
-        scope: {
-            roomId: "=commandHistory"
-        },
-        link: function (scope, element, attrs) {
-            element.bind("keydown", function (event) {
-                var keycodePressed = event.which;
-                var UP_ARROW = 38;
-                var DOWN_ARROW = 40;
-                if (scope.roomId) {
-                    if (keycodePressed === UP_ARROW) {
-                        history.go(1);
-                        event.preventDefault();
-                    }
-                    else if (keycodePressed === DOWN_ARROW) {
-                        history.go(-1);
-                        event.preventDefault();
-                    }
-                }
-            });
-            
-            scope.$on(BROADCAST_NEW_HISTORY_ITEM, function(ngEvent, item) {
-                history.push(item);
-            });
-            
-            history.init(element, scope.roomId);
-        },
-        
-    }
-}])
-
-// A directive to anchor the scroller position at the bottom when the browser is resizing.
-// When the screen resizes, the bottom of the element remains the same, not the top.
-.directive('keepScroll', ['$window', function($window) {
-    return {
-        link: function(scope, elem, attrs) {
-
-            scope.windowHeight = $window.innerHeight;
-
-            // Listen to window size change
-            angular.element($window).bind('resize', function() {
-
-                // If the scroller is scrolled to the bottom, there is nothing to do.
-                // The browser will move it as expected
-                if (elem.scrollTop() + elem.height() !== elem[0].scrollHeight) {
-                    // Else, move the scroller position according to the window height change delta
-                    var windowHeightDelta = $window.innerHeight - scope.windowHeight;
-                    elem.scrollTop(elem.scrollTop() - windowHeightDelta);
-                }
-
-                // Store the new window height for the next screen size change
-                scope.windowHeight = $window.innerHeight;
-            });
-        }
-    };
-}]);
-
diff --git a/syweb/webclient/room/room.html b/syweb/webclient/room/room.html
deleted file mode 100644
index 8baf9eda71..0000000000
--- a/syweb/webclient/room/room.html
+++ /dev/null
@@ -1,282 +0,0 @@
-<div ng-controller="RoomController" data-ng-init="onInit()" class="room" style="height: 100%;">
-
-    <script type="text/ng-template" id="eventInfoTemplate.html">
-        <div class="modal-body">
-            <pre> {{event_selected | json}} </pre>
-        </div>
-        <div class="modal-footer">
-            <button ng-click="redact()" type="button" class="btn btn-danger redact-button" 
-             ng-disabled="!room.current_room_state.state('m.room.ops_levels').content.redact_level || !pow(room_id, state.user_id) || pow(room_id, state.user_id) < room.current_room_state.state('m.room.ops_levels').content.redact_level"
-             title="Delete this event on all home servers. This cannot be undone.">
-                Redact
-            </button>
-
-            <button ng-click="dismiss()" type="button" class="btn">
-                Close
-            </button>
-        </div>
-    </script>
-
-    <script type="text/ng-template" id="roomInfoTemplate.html">
-        <div class="modal-body">
-            <span>
-               Invite a user: 
-                    <input ng-model="userIDToInvite" size="32" type="text" ng-enter="inviteUser()" ng-disabled="state.permission_denied" placeholder="User ID (ex:@user:homeserver)"/>     
-                    <button ng-click="inviteUser()" ng-disabled="state.permission_denied">Invite</button>
-            </span>
-            <br/>
-            <br/>
-            <button ng-click="leaveRoom()" ng-disabled="state.permission_denied">Leave Room</button>
-            </br/>
-            <table class="room-info">
-            <tr ng-repeat="(key, event) in roomInfo.stateEvents" class="room-info-event">
-                <td class="room-info-event-meta" width="30%">
-                    <span class="monospace">{{ event.type }}</span>
-                    <span ng-show="event.state_key" class="monospace"> ({{event.state_key}})</span>
-                    <br/>
-                    {{ (event.origin_server_ts) | date:'MMM d HH:mm' }}
-                    <br/>
-                    Set by: <span class="monospace">{{ event.user_id }}</span>
-                    <br/>
-                    <span ng-show="event.required_power_level >= 0">Required power level: {{event.required_power_level}}<br/></span>
-                    <button ng-click="submit(event)" type="button" class="btn btn-success" ng-disabled="!event.content">
-                        Submit
-                    </button>
-                </td>
-                <td class="room-info-event-content" width="70%">
-                    <textarea class="room-info-textarea-content" msd-elastic ng-model="event.content" asjson></textarea> 
-                </td>
-            </tr>
-            <tr>
-                <td class="room-info-event-meta" width="30%">
-                    <input ng-model="roomInfo.newEvent.type" placeholder="your.event.type" />
-                    <br/>
-                    <button ng-click="submit(roomInfo.newEvent)" type="button" class="btn btn-success" ng-disabled="!roomInfo.newEvent.content || !roomInfo.newEvent.type">
-                        Submit
-                    </button>
-                </td>
-                <td class="room-info-event-content" width="70%">
-                    <textarea class="room-info-textarea-content" msd-elastic ng-model="roomInfo.newEvent.content" asjson></textarea>
-                </td>
-            </tr>
-            </table>
-        </div>
-        <div class="modal-footer">
-            <button ng-click="dismiss()" type="button" class="btn">
-                Close
-            </button>
-        </div>
-    </script>
-
-    <div id="roomHeader">
-        <a href ng-click="goToPage('/')"><img src="img/logo-small.png" width="100" height="43" alt="[matrix]"/></a>
-
-        <div id="controlButtons">
-            <button ng-click="startVoiceCall()" class="controlButton"
-                    style="background: url('img/voice.png')"
-                    ng-show="(currentCall == undefined || currentCall.state == 'ended')"
-                    ng-disabled="state.permission_denied"
-                    >
-            </button>
-            <button ng-click="startVideoCall()" class="controlButton"
-                    style="background: url('img/video.png')"
-                    ng-show="(currentCall == undefined || currentCall.state == 'ended')"
-                    ng-disabled="state.permission_denied"
-                    >
-            </button>
-            <button ng-click="openRoomInfo()" class="controlButton"
-                    style="background: url('img/settings.png')"
-                    >
-            </button>
-        </div>
-
-        <div class="roomHeaderInfo">
-
-            <div class="roomNameSection">
-                <div ng-hide="name.isEditing" ng-dblclick="name.editName()" id="roomName">
-                    {{ room_id | mRoomName }}
-                </div>
-                <form ng-submit="name.updateName()" ng-show="name.isEditing" class="roomNameForm">
-                    <input ng-model="name.newNameText" ng-blur="name.cancelEdit()" class="roomNameInput" placeholder="Room name"/>
-                </form>
-            </div>
-
-            <div class="roomTopicSection">
-                <button ng-hide="room.current_room_state.state_events['m.room.topic'].content.topic || topic.isEditing"
-                    ng-click="topic.editTopic()" class="roomTopicSetNew">
-                    Set Topic
-                </button>
-                <div ng-show="room.current_room_state.state_events['m.room.topic'].content.topic || topic.isEditing">
-                    <div ng-hide="topic.isEditing" ng-dblclick="topic.editTopic()" id="roomTopic"
-                         ng-bind-html="room.current_room_state.state_events['m.room.topic'].content.topic | limitTo: 200 | linky:'_blank'">
-                    </div>
-                    <form ng-submit="topic.updateTopic()" ng-show="topic.isEditing" class="roomTopicForm">
-                        <input ng-model="topic.newTopicText" ng-blur="topic.cancelEdit()" class="roomTopicInput"  placeholder="Topic"/>
-                    </form>
-                </div>
-            </div>
-        </div>
-    </div>
-
-    <div id="roomPage">
-    <div id="roomWrapper">
-        
-    <div id="roomRecentsTableWrapper">
-        <div ng-include="'recents/recents.html'"></div>
-    </div>
-    
-    <div id="usersTableWrapper" ng-hide="state.permission_denied">
-        <div ng-repeat="member in room.now.members | orderMembersList" class="userAvatar">
-            <div class="userAvatarFrame" ng-class="(member.user.event.content.presence === 'online' ? 'online' : (member.user.event.content.presence === 'unavailable' ? 'unavailable' : '')) + ' ' + (member.event.content.membership == 'invite' ? 'invited' : '')">
-                <img class="userAvatarImage mouse-pointer" 
-                     ng-click="$parent.goToUserPage(member.id)"
-                     ng-src="{{member.user.event.content.avatar_url || 'img/default-profile.png'}}" 
-                     alt="{{ member.user.event.content.displayname || member.id.substr(0, member.id.indexOf(':')) }}"
-                     title="{{ member.id }} - power: {{ member.power_level }}"
-                     width="80" height="80"/>
-                <!-- <div class="userPowerLevel" ng-style="{'width': member.powerLevelNorm +'%'}"></div> -->
-            </div>
-            <div class="userName">
-                <pie-chart ng-show="member.power_level_norm" data="[ (member.power_level_norm + 0), (100 - member.power_level_norm) ]"></pie-chart>
-                {{ member.id | mUserDisplayName:room_id:true }}
-                <span ng-show="member.user.event.content.last_active_ago" style="color: #aaa">({{ member.user.event.content.last_active_ago + (now - member.user.last_updated) | duration }})</span>
-            </div>
-        </div>
-    </div>
-    
-    <div id="messageTableWrapper" 
-         ng-hide="state.permission_denied" 
-         ng-style="{ 'visibility': state.messages_visibility }"
-         keep-scroll>
-        <table id="messageTable" infinite-scroll="paginateMore()">
-            <tr ng-repeat="msg in room.events"
-                ng-class="(room.events[$index - 1].user_id !== msg.user_id ? 'differentUser' : '') + (msg.user_id === state.user_id ? ' mine' : '')" scroll-item>
-                <td class="leftBlock" ng-mouseover="state.showTs = 1" ng-mouseout="state.showTs = 0">
-                    <div class="timestamp"
-                         ng-style="{ 'opacity': state.showTs ? 1.0 : 0.0 }"
-                         ng-class="msg.echo_msg_state">
-                        {{ (msg.origin_server_ts) | date:'MMM d HH:mm' }}
-                    </div>
-                    <div class="sender" ng-hide="room.events[$index - 1].user_id === msg.user_id || msg.user_id === state.user_id"> {{ msg.__room_member.cnt.displayname || msg.user_id | mUserDisplayName:room_id:true }}</div>
-                </td>
-                <td class="avatar">
-                    <!--  room.now.members[msg.user_id].user.event.content.avatar_url is just backwards compat, and can be removed in the future. Synapse didn't used to
-                          send m.room.member updates when avatar urls changed, so the image which should be visible here just going off room state isn't visible. We fix
-                          this by reading off the m.presence url -->
-                    <img class="avatarImage" ng-src="{{ msg.__room_member.cnt.avatar_url || msg.__room_member.content.avatar_url || room.now.members[msg.user_id].user.event.content.avatar_url || 'img/default-profile.png' }}" width="32" height="32" title="{{msg.user_id}}"
-                         ng-hide="room.events[$index - 1].user_id === msg.user_id || msg.user_id === state.user_id"/>
-                </td>
-                <td class="msg" ng-class="(!msg.content.membership && ('m.room.topic' !== msg.type && 'm.room.name' !== msg.type))? (msg.content.msgtype === 'm.emote' ? 'emote text' : 'text') : 'membership text'">
-                    <div class="bubble" ng-dblclick="openJson(msg)">
-                        <span ng-if="'join' === msg.content.membership && msg.changedKey === 'membership'">
-                            {{ msg.content.displayname || room.now.members[msg.state_key].user.event.content.displayname || msg.state_key }} joined
-                        </span>
-                        <span ng-if="'leave' === msg.content.membership && msg.changedKey === 'membership'">
-                            <span ng-if="msg.user_id === msg.state_key">
-                                <!-- FIXME: This seems like a synapse bug that the 'leave' content doesn't give the displayname... -->
-                                {{ msg.__room_member.cnt.displayname || room.now.members[msg.state_key].user.event.content.displayname || msg.state_key }} left
-                            </span>
-                            <span ng-if="msg.user_id !== msg.state_key && msg.prev_content">
-                                {{ msg.content.displayname || room.now.members[msg.user_id].user.event.content.displayname || msg.user_id }}
-                                {{ {"invite": "kicked", "join": "kicked", "ban": "unbanned"}[msg.prev_content.membership] }}
-                                {{ msg.__target_room_member.content.displayname || msg.state_key }}
-                                <span ng-if="'join' === msg.prev_content.membership && msg.content.reason">
-                                    : {{ msg.content.reason }}
-                                </span>
-                            </span>
-                        </span>
-                        <span ng-if="'invite' === msg.content.membership && msg.changedKey === 'membership' || 
-                                     'ban' === msg.content.membership && msg.changedKey === 'membership'">
-                            {{ msg.__room_member.cnt.displayname || msg.user_id }}
-                            {{ {"invite": "invited", "ban": "banned"}[msg.content.membership] }}
-                            {{ msg.__target_room_member.cnt.displayname || msg.state_key }}
-                            <span ng-if="msg.prev_content && 'ban' === msg.prev_content.membership && msg.content.reason">
-                                : {{ msg.content.reason }}
-                            </span>
-                        </span>                        
-                        <span ng-if="msg.changedKey === 'displayname'">
-                            {{ msg.user_id }} changed their display name from {{ msg.prev_content.displayname }} to {{ msg.content.displayname }}
-                        </span>
-                        
-                        <span ng-show='msg.type === "m.room.message"' ng-switch='msg.content.msgtype'>
-                            <span ng-switch-when="m.emote"
-                                  ng-class="containsBingWord(msg) && msg.user_id != state.user_id ? msg.echo_msg_state + ' messageBing' : msg.echo_msg_state"
-                                  ng-bind-html="'* ' + (msg.__room_member.cnt.displayname || msg.user_id) + ' ' + msg.content.body | linky:'_blank'"
-                                  />
-
-                            <span ng-switch-when="m.text"
-                                  class="message"
-                                  ng-class="containsBingWord(msg) && msg.user_id != state.user_id ? msg.echo_msg_state + ' messageBing' : msg.echo_msg_state"
-                                  ng-bind-html="(msg.content.format === 'org.matrix.custom.html') ? (msg.content.formatted_body | unsanitizedLinky) : (msg.content.body | linky:'_blank') "/>
-
-                             <div ng-switch-when="m.image">
-                                 <div ng-hide='msg.content.thumbnail_url' ng-style="msg.content.body.h && { 'height' : (msg.content.body.h < 320) ? msg.content.body.h : 320}">
-                                     <img class="image" ng-src="{{ msg.content.url }}"/>
-                                 </div>
-                                 <div ng-show='msg.content.thumbnail_url' ng-style="{ 'height' : msg.content.thumbnail_info.h }">
-                                     <img class="image mouse-pointer" ng-src="{{ msg.content.thumbnail_url }}"
-                                          ng-click="$parent.$parent.fullScreenImageURL = msg.content.url; $event.stopPropagation();"/>
-                                 </div>
-                             </div>
-
-                            <span ng-switch-when="m.file" ng-class="msg.echo_msg_state">
-                                <a href="{{ msg.content.url}}" target="_blank">{{ msg.content.body }}</a>
-                                <div ng-show='msg.content.thumbnail_url' ng-style="{ 'height' : msg.content.thumbnail_info.h }">
-                                    <a href="{{ msg.content.url}}" target="_blank">
-                                        <img class="image mouse-pointer" ng-src="{{ msg.content.thumbnail_url }}"/>
-                                    </a>
-                                </div>
-                            </span>
-                                
-                            <span ng-switch-default
-                                ng-class="containsBingWord(msg) && msg.user_id != state.user_id ? msg.echo_msg_state + ' messageBing' : msg.echo_msg_state"
-                                ng-bind-html="msg.content.body | linky:'_blank'"
-                                />
-                        </span>
-
-                        <span ng-show='msg.type === "m.call.invite" && msg.user_id == state.user_id'>Outgoing Call{{ isWebRTCSupported() ? '' : ' (But your browser does not support VoIP)' }}</span>
-                        <span ng-show='msg.type === "m.call.invite" && msg.user_id != state.user_id'>Incoming Call{{ isWebRTCSupported() ? '' : ' (But your browser does not support VoIP)' }}</span>
-
-                        <span ng-if="'m.room.topic' === msg.type">
-                            {{ msg.__room_member.cnt.displayname || msg.user_id }} changed the topic to: {{ msg.content.topic }}
-                        </span>
-
-                        <span ng-if="'m.room.name' === msg.type">
-                            {{ msg.__room_member.cnt.displayname || msg.user_id }} changed the room name to: {{ msg.content.name }}
-                        </span>
-
-                    </div>
-                </td>
-                <td class="rightBlock">
-                    <img class="avatarImage" ng-src="{{ room.now.members[msg.user_id].user.event.content.avatar_url || 'img/default-profile.png' }}" width="32" height="32"
-                         ng-hide="room.events[$index - 1].user_id === msg.user_id || msg.user_id !== state.user_id"/>
-                </td>
-            </tr>
-        </table>
-    </div>
-        
-    <div ng-show="state.permission_denied">
-        {{ state.permission_denied }}
-    </div>
-    
-    </div>
-    </div>
-
-    <div id="controlPanel">
-        <div id="controls">
-            <button id="attachButton" m-file-input="fileToSend" class="extraControls" ng-disabled="state.permission_denied"></button>
-            <textarea id="mainInput" rows="1" ng-enter="send()"
-                      ng-disabled="state.permission_denied"
-                      ng-focus="true" autocomplete="off" tab-complete command-history="room_id"/>
-            {{ feedback }}
-            <div ng-show="state.stream_failure">
-                {{ state.stream_failure.data.error || "Connection failure" }}
-            </div>
-        </div>
-    </div>
-
-    <div id="room-fullscreen-image" ng-show="fullScreenImageURL" ng-click="fullScreenImageURL = undefined;">
-        <img ng-src="{{ fullScreenImageURL }}"/>
-    </div>
-
- </div>