Merge branch 'develop' of git+ssh://github.com/matrix-org/synapse into develop

This commit is contained in:
Matthew Hodgson 2014-11-13 11:58:54 +02:00
commit 28408a9f64
22 changed files with 515 additions and 111 deletions

3
.gitignore vendored
View File

@ -34,6 +34,7 @@ graph/*.png
graph/*.dot
**/webclient/config.js
webclient/test/environment-protractor.js
**/webclient/test/coverage/
**/webclient/test/environment-protractor.js
uploads

View File

@ -40,6 +40,8 @@ class FederationHandler(BaseHandler):
of the home server (including auth and state conflict resoultion)
b) converting events that were produced by local clients that may need
to be sent to remote home servers.
c) doing the necessary dances to invite remote users and join remote
rooms.
"""
def __init__(self, hs):
@ -102,6 +104,8 @@ class FederationHandler(BaseHandler):
logger.debug("Got event: %s", event.event_id)
# If we are currently in the process of joining this room, then we
# queue up events for later processing.
if event.room_id in self.room_queues:
self.room_queues[event.room_id].append(pdu)
return
@ -187,6 +191,8 @@ class FederationHandler(BaseHandler):
@log_function
@defer.inlineCallbacks
def backfill(self, dest, room_id, limit):
""" Trigger a backfill request to `dest` for the given `room_id`
"""
extremities = yield self.store.get_oldest_events_in_room(room_id)
pdus = yield self.replication_layer.backfill(
@ -212,6 +218,10 @@ class FederationHandler(BaseHandler):
@defer.inlineCallbacks
def send_invite(self, target_host, event):
""" Sends the invite to the remote server for signing.
Invites must be signed by the invitee's server before distribution.
"""
pdu = yield self.replication_layer.send_invite(
destination=target_host,
context=event.room_id,
@ -229,6 +239,17 @@ class FederationHandler(BaseHandler):
@log_function
@defer.inlineCallbacks
def do_invite_join(self, target_host, room_id, joinee, content, snapshot):
""" Attempts to join the `joinee` to the room `room_id` via the
server `target_host`.
This first triggers a /make_join/ request that returns a partial
event that we can fill out and sign. This is then sent to the
remote server via /send_join/ which responds with the state at that
event and the auth_chains.
We suspend processing of any received events from this room until we
have finished processing the join.
"""
pdu = yield self.replication_layer.make_join(
target_host,
room_id,
@ -313,6 +334,10 @@ class FederationHandler(BaseHandler):
@defer.inlineCallbacks
@log_function
def on_make_join_request(self, context, user_id):
""" We've received a /make_join/ request, so we create a partial
join event for the room and return that. We don *not* persist or
process it until the other server has signed it and sent it back.
"""
event = self.event_factory.create_event(
etype=RoomMemberEvent.TYPE,
content={"membership": Membership.JOIN},
@ -335,6 +360,9 @@ class FederationHandler(BaseHandler):
@defer.inlineCallbacks
@log_function
def on_send_join_request(self, origin, pdu):
""" We have received a join event for a room. Fully process it and
respond with the current state and auth chains.
"""
event = self.pdu_codec.event_from_pdu(pdu)
event.outlier = False
@ -403,6 +431,10 @@ class FederationHandler(BaseHandler):
@defer.inlineCallbacks
def on_invite_request(self, origin, pdu):
""" We've got an invite event. Process and persist it. Sign it.
Respond with the now signed event.
"""
event = self.pdu_codec.event_from_pdu(pdu)
event.outlier = True

View File

@ -279,13 +279,14 @@ class DataStore(RoomMemberStore, RoomStore,
)
if hasattr(event, "signatures"):
signatures = event.signatures.get(event.origin, {})
for key_id, signature_base64 in signatures.items():
signature_bytes = decode_base64(signature_base64)
self._store_event_origin_signature_txn(
txn, event.event_id, event.origin, key_id, signature_bytes,
)
logger.debug("sigs: %s", event.signatures)
for name, sigs in event.signatures.items():
for key_id, signature_base64 in sigs.items():
signature_bytes = decode_base64(signature_base64)
self._store_event_signature_txn(
txn, event.event_id, name, key_id,
signature_bytes,
)
for prev_event_id, prev_hashes in event.prev_events:
for alg, hash_base64 in prev_hashes.items():

View File

@ -470,12 +470,15 @@ class SQLBaseStore(object):
select_event_sql = "SELECT * FROM events WHERE event_id = ?"
for i, ev in enumerate(events):
signatures = self._get_event_origin_signatures_txn(
signatures = self._get_event_signatures_txn(
txn, ev.event_id,
)
ev.signatures = {
k: encode_base64(v) for k, v in signatures.items()
n: {
k: encode_base64(v) for k, v in s.items()
}
for n, s in signatures.items()
}
prevs = self._get_prev_events_and_state(txn, ev.event_id)

View File

@ -23,6 +23,14 @@ logger = logging.getLogger(__name__)
class EventFederationStore(SQLBaseStore):
""" Responsible for storing and serving up the various graphs associated
with an event. Including the main event graph and the auth chains for an
event.
Also has methods for getting the front (latest) and back (oldest) edges
of the event graphs. These are used to generate the parents for new events
and backfilling from another server respectively.
"""
def get_auth_chain(self, event_id):
return self.runInteraction(
@ -205,6 +213,8 @@ class EventFederationStore(SQLBaseStore):
return results
def get_min_depth(self, room_id):
""" For hte given room, get the minimum depth we have seen for it.
"""
return self.runInteraction(
"get_min_depth",
self._get_min_depth_interaction,
@ -240,6 +250,10 @@ class EventFederationStore(SQLBaseStore):
def _handle_prev_events(self, txn, outlier, event_id, prev_events,
room_id):
"""
For the given event, update the event edges table and forward and
backward extremities tables.
"""
for e_id, _ in prev_events:
# TODO (erikj): This could be done as a bulk insert
self._simple_insert_txn(
@ -267,8 +281,8 @@ class EventFederationStore(SQLBaseStore):
}
)
# We only insert as a forward extremity the new pdu if there are
# no other pdus that reference it as a prev pdu
# We only insert as a forward extremity the new event if there are
# no other events that reference it as a prev event
query = (
"INSERT OR IGNORE INTO %(table)s (event_id, room_id) "
"SELECT ?, ? WHERE NOT EXISTS ("
@ -284,7 +298,7 @@ class EventFederationStore(SQLBaseStore):
txn.execute(query, (event_id, room_id, event_id))
# Insert all the prev_pdus as a backwards thing, they'll get
# Insert all the prev_events as a backwards thing, they'll get
# deleted in a second if they're incorrect anyway.
for e_id, _ in prev_events:
# TODO (erikj): This could be done as a bulk insert
@ -299,7 +313,7 @@ class EventFederationStore(SQLBaseStore):
)
# Also delete from the backwards extremities table all ones that
# reference pdus that we have already seen
# reference events that we have already seen
query = (
"DELETE FROM event_backward_extremities WHERE EXISTS ("
"SELECT 1 FROM events "
@ -311,17 +325,14 @@ class EventFederationStore(SQLBaseStore):
txn.execute(query)
def get_backfill_events(self, room_id, event_list, limit):
"""Get a list of Events for a given topic that occured before (and
including) the pdus in pdu_list. Return a list of max size `limit`.
"""Get a list of Events for a given topic that occurred before (and
including) the events in event_list. Return a list of max size `limit`
Args:
txn
room_id (str)
event_list (list)
limit (int)
Return:
list: A list of PduTuples
"""
return self.runInteraction(
"get_backfill_events",
@ -334,7 +345,6 @@ class EventFederationStore(SQLBaseStore):
room_id, repr(event_list), limit
)
# We seed the pdu_results with the things from the pdu_list.
event_results = event_list
front = event_list
@ -373,5 +383,4 @@ class EventFederationStore(SQLBaseStore):
front = new_front
event_results += new_front
# We also want to update the `prev_pdus` attributes before returning.
return self._get_events_txn(txn, event_results)

View File

@ -37,15 +37,15 @@ CREATE INDEX IF NOT EXISTS event_reference_hashes_id ON event_reference_hashes (
);
CREATE TABLE IF NOT EXISTS event_origin_signatures (
CREATE TABLE IF NOT EXISTS event_signatures (
event_id TEXT,
origin TEXT,
signature_name TEXT,
key_id TEXT,
signature BLOB,
CONSTRAINT uniqueness UNIQUE (event_id, key_id)
);
CREATE INDEX IF NOT EXISTS event_origin_signatures_id ON event_origin_signatures (
CREATE INDEX IF NOT EXISTS event_signatures_id ON event_signatures (
event_id
);

View File

@ -30,4 +30,17 @@ CREATE TABLE IF NOT EXISTS state_groups_state(
CREATE TABLE IF NOT EXISTS event_to_state_groups(
event_id TEXT NOT NULL,
state_group INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS state_groups_id ON state_groups(id);
CREATE INDEX IF NOT EXISTS state_groups_state_id ON state_groups_state(
state_group
);
CREATE INDEX IF NOT EXISTS state_groups_state_tuple ON state_groups_state(
room_id, type, state_key
);
CREATE INDEX IF NOT EXISTS event_to_state_groups_id ON event_to_state_groups(
event_id
);

View File

@ -103,24 +103,30 @@ class SignatureStore(SQLBaseStore):
or_ignore=True,
)
def _get_event_origin_signatures_txn(self, txn, event_id):
def _get_event_signatures_txn(self, txn, event_id):
"""Get all the signatures for a given PDU.
Args:
txn (cursor):
event_id (str): Id for the Event.
Returns:
A dict of key_id -> signature_bytes.
A dict of sig name -> dict(key_id -> signature_bytes)
"""
query = (
"SELECT key_id, signature"
" FROM event_origin_signatures"
"SELECT signature_name, key_id, signature"
" FROM event_signatures"
" WHERE event_id = ? "
)
txn.execute(query, (event_id, ))
return dict(txn.fetchall())
rows = txn.fetchall()
def _store_event_origin_signature_txn(self, txn, event_id, origin, key_id,
res = {}
for name, key, sig in rows:
res.setdefault(name, {})[key] = sig
return res
def _store_event_signature_txn(self, txn, event_id, signature_name, key_id,
signature_bytes):
"""Store a signature from the origin server for a PDU.
Args:
@ -132,10 +138,10 @@ class SignatureStore(SQLBaseStore):
"""
self._simple_insert_txn(
txn,
"event_origin_signatures",
"event_signatures",
{
"event_id": event_id,
"origin": origin,
"signature_name": signature_name,
"key_id": key_id,
"signature": buffer(signature_bytes),
},

View File

@ -14,43 +14,71 @@
# limitations under the License.
from ._base import SQLBaseStore
from twisted.internet import defer
class StateStore(SQLBaseStore):
""" Keeps track of the state at a given event.
This is done by the concept of `state groups`. Every event is a assigned
a state group (identified by an arbitrary string), which references a
collection of state events. The current state of an event is then the
collection of state events referenced by the event's state group.
Hence, every change in the current state causes a new state group to be
generated. However, if no change happens (e.g., if we get a message event
with only one parent it inherits the state group from its parent.)
There are three tables:
* `state_groups`: Stores group name, first event with in the group and
room id.
* `event_to_state_groups`: Maps events to state groups.
* `state_groups_state`: Maps state group to state events.
"""
@defer.inlineCallbacks
def get_state_groups(self, event_ids):
groups = set()
for event_id in event_ids:
group = yield self._simple_select_one_onecol(
table="event_to_state_groups",
keyvalues={"event_id": event_id},
retcol="state_group",
allow_none=True,
)
if group:
groups.add(group)
""" Get the state groups for the given list of event_ids
res = {}
for group in groups:
state_ids = yield self._simple_select_onecol(
table="state_groups_state",
keyvalues={"state_group": group},
retcol="event_id",
)
state = []
for state_id in state_ids:
s = yield self.get_event(
state_id,
The return value is a dict mapping group names to lists of events.
"""
def f(txn):
groups = set()
for event_id in event_ids:
group = self._simple_select_one_onecol_txn(
txn,
table="event_to_state_groups",
keyvalues={"event_id": event_id},
retcol="state_group",
allow_none=True,
)
if s:
state.append(s)
if group:
groups.add(group)
res[group] = state
res = {}
for group in groups:
state_ids = self._simple_select_onecol_txn(
txn,
table="state_groups_state",
keyvalues={"state_group": group},
retcol="event_id",
)
state = []
for state_id in state_ids:
s = self._get_events_txn(
txn,
[state_id],
)
if s:
state.extend(s)
defer.returnValue(res)
res[group] = state
return res
return self.runInteraction(
"get_state_groups",
f,
)
def store_state_groups(self, event):
return self.runInteraction(

View File

@ -812,6 +812,14 @@ textarea, input {
background-color: #eee;
}
.recentsRoomUnread {
background-color: #fee;
}
.recentsRoomBing {
background-color: #eef;
}
.recentsRoomName {
font-size: 16px;
padding-top: 7px;

View File

@ -31,6 +31,7 @@ var matrixWebClient = angular.module('matrixWebClient', [
'eventStreamService',
'eventHandlerService',
'notificationService',
'recentsService',
'modelService',
'infinite-scroll',
'ui.bootstrap',

View File

@ -95,14 +95,22 @@ function(matrixService, $rootScope, $q, $timeout, $filter, mPresence, notificati
modelService.createRoomIdToAliasMapping(event.room_id, event.content.aliases[0]);
};
var containsBingWord = function(event) {
if (!event.content || !event.content.body) {
return false;
}
return notificationService.containsBingWord(
matrixService.config().user_id,
matrixService.config().display_name,
matrixService.config().bingWords,
event.content.body
);
};
var displayNotification = function(event) {
if (window.Notification && event.user_id != matrixService.config().user_id) {
var shouldBing = notificationService.containsBingWord(
matrixService.config().user_id,
matrixService.config().display_name,
matrixService.config().bingWords,
event.content.body
);
var shouldBing = containsBingWord(event);
// Ideally we would notify only when the window is hidden (i.e. document.hidden = true).
//
@ -529,6 +537,10 @@ function(matrixService, $rootScope, $q, $timeout, $filter, mPresence, notificati
resetRoomMessages(room_id);
},
eventContainsBingWord: function(event) {
return containsBingWord(event);
},
/**
* Return the last message event of a room
* @param {String} room_id the room id

View File

@ -82,7 +82,7 @@ angular.module('MatrixCall', [])
});
}
// FIXME: we should prevent any class from being placed or accepted before this has finished
// FIXME: we should prevent any calls from being placed or accepted before this has finished
MatrixCall.getTurnServer();
MatrixCall.CALL_TIMEOUT = 60000;
@ -92,7 +92,8 @@ angular.module('MatrixCall', [])
var pc;
if (window.mozRTCPeerConnection) {
var iceServers = [];
if (MatrixCall.turnServer) {
// https://github.com/EricssonResearch/openwebrtc/issues/85
if (MatrixCall.turnServer /*&& !this.isOpenWebRTC()*/) {
if (MatrixCall.turnServer.uris) {
for (var i = 0; i < MatrixCall.turnServer.uris.length; i++) {
iceServers.push({
@ -110,7 +111,8 @@ angular.module('MatrixCall', [])
pc = new window.mozRTCPeerConnection({"iceServers":iceServers});
} else {
var iceServers = [];
if (MatrixCall.turnServer) {
// https://github.com/EricssonResearch/openwebrtc/issues/85
if (MatrixCall.turnServer /*&& !this.isOpenWebRTC()*/) {
if (MatrixCall.turnServer.uris) {
iceServers.push({
'urls': MatrixCall.turnServer.uris,
@ -492,6 +494,8 @@ angular.module('MatrixCall', [])
$timeout(function() {
var vel = self.getRemoteVideoElement();
if (vel.play) vel.play();
// OpenWebRTC does not support oniceconnectionstatechange yet
if (self.isOpenWebRTC()) self.state = 'connected';
});
}
};
@ -641,5 +645,15 @@ angular.module('MatrixCall', [])
return null;
};
MatrixCall.prototype.isOpenWebRTC = function() {
var scripts = angular.element('script');
for (var i = 0; i < scripts.length; i++) {
if (scripts[i].src.indexOf("owr.js") > -1) {
return true;
}
}
return false;
};
return MatrixCall;
}]);

View File

@ -23,7 +23,7 @@ This serves to isolate the caller from changes to the underlying url paths, as
well as attach common params (e.g. access_token) to requests.
*/
angular.module('matrixService', [])
.factory('matrixService', ['$http', '$q', '$rootScope', function($http, $q, $rootScope) {
.factory('matrixService', ['$http', '$q', function($http, $q) {
/*
* Permanent storage of user information

View File

@ -0,0 +1,99 @@
/*
Copyright 2014 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
/*
This service manages shared state between *instances* of recent lists. The
recents controller will hook into this central service to get things like:
- which rooms should be highlighted
- which rooms have been binged
- which room is currently selected
- etc.
This is preferable to polluting the $rootScope with recents specific info, and
makes the dependency on this shared state *explicit*.
*/
angular.module('recentsService', [])
.factory('recentsService', ['$rootScope', 'eventHandlerService', function($rootScope, eventHandlerService) {
// notify listeners when variables in the service are updated. We need to do
// this since we do not tie them to any scope.
var BROADCAST_SELECTED_ROOM_ID = "recentsService:BROADCAST_SELECTED_ROOM_ID(room_id)";
var selectedRoomId = undefined;
var BROADCAST_UNREAD_MESSAGES = "recentsService:BROADCAST_UNREAD_MESSAGES(room_id, unreadCount)";
var unreadMessages = {
// room_id: <number>
};
var BROADCAST_UNREAD_BING_MESSAGES = "recentsService:BROADCAST_UNREAD_BING_MESSAGES(room_id, event)";
var unreadBingMessages = {
// room_id: bingEvent
};
// listen for new unread messages
$rootScope.$on(eventHandlerService.MSG_EVENT, function(ngEvent, event, isLive) {
if (isLive && event.room_id !== selectedRoomId) {
if (eventHandlerService.eventContainsBingWord(event)) {
if (!unreadBingMessages[event.room_id]) {
unreadBingMessages[event.room_id] = {};
}
unreadBingMessages[event.room_id] = event;
$rootScope.$broadcast(BROADCAST_UNREAD_BING_MESSAGES, event.room_id, event);
}
if (!unreadMessages[event.room_id]) {
unreadMessages[event.room_id] = 0;
}
unreadMessages[event.room_id] += 1;
$rootScope.$broadcast(BROADCAST_UNREAD_MESSAGES, event.room_id, unreadMessages[event.room_id]);
}
});
return {
BROADCAST_SELECTED_ROOM_ID: BROADCAST_SELECTED_ROOM_ID,
BROADCAST_UNREAD_MESSAGES: BROADCAST_UNREAD_MESSAGES,
getSelectedRoomId: function() {
return selectedRoomId;
},
setSelectedRoomId: function(room_id) {
selectedRoomId = room_id;
$rootScope.$broadcast(BROADCAST_SELECTED_ROOM_ID, room_id);
},
getUnreadMessages: function() {
return unreadMessages;
},
getUnreadBingMessages: function() {
return unreadBingMessages;
},
markAsRead: function(room_id) {
if (unreadMessages[room_id]) {
unreadMessages[room_id] = 0;
}
if (unreadBingMessages[room_id]) {
unreadBingMessages[room_id] = undefined;
}
$rootScope.$broadcast(BROADCAST_UNREAD_MESSAGES, room_id, 0);
$rootScope.$broadcast(BROADCAST_UNREAD_BING_MESSAGES, room_id, undefined);
}
};
}]);

View File

@ -44,6 +44,7 @@
<script src="components/matrix/event-stream-service.js"></script>
<script src="components/matrix/event-handler-service.js"></script>
<script src="components/matrix/notification-service.js"></script>
<script src="components/matrix/recents-service.js"></script>
<script src="components/matrix/model-service.js"></script>
<script src="components/matrix/presence-service.js"></script>
<script src="components/fileInput/file-input-directive.js"></script>

View File

@ -17,18 +17,37 @@
'use strict';
angular.module('RecentsController', ['matrixService', 'matrixFilter'])
.controller('RecentsController', ['$rootScope', '$scope', 'eventHandlerService', 'modelService',
function($rootScope, $scope, eventHandlerService, modelService) {
.controller('RecentsController', ['$rootScope', '$scope', 'eventHandlerService', 'modelService', 'recentsService',
function($rootScope, $scope, eventHandlerService, modelService, recentsService) {
// Expose the service to the view
$scope.eventHandlerService = eventHandlerService;
// retrieve all rooms and expose them
$scope.rooms = modelService.getRooms();
// $rootScope of the parent where the recents component is included can override this value
// in order to highlight a specific room in the list
$rootScope.recentsSelectedRoomID;
// track the selected room ID: the html will use this
$scope.recentsSelectedRoomID = recentsService.getSelectedRoomId();
$scope.$on(recentsService.BROADCAST_SELECTED_ROOM_ID, function(ngEvent, room_id) {
$scope.recentsSelectedRoomID = room_id;
});
// track the list of unread messages: the html will use this
$scope.unreadMessages = recentsService.getUnreadMessages();
$scope.$on(recentsService.BROADCAST_UNREAD_MESSAGES, function(ngEvent, room_id, unreadCount) {
$scope.unreadMessages = recentsService.getUnreadMessages();
});
// track the list of unread BING messages: the html will use this
$scope.unreadBings = recentsService.getUnreadBingMessages();
$scope.$on(recentsService.BROADCAST_UNREAD_BING_MESSAGES, function(ngEvent, room_id, event) {
$scope.unreadBings = recentsService.getUnreadBingMessages();
});
$scope.selectRoom = function(room) {
recentsService.markAsRead(room.room_id);
$rootScope.goToPage('room/' + (room.room_alias ? room.room_alias : room.room_id) );
};
}]);

View File

@ -1,9 +1,9 @@
<div ng-controller="RecentsController">
<table class="recentsTable">
<tbody ng-repeat="(index, room) in rooms | orderRecents"
ng-click="goToPage('room/' + (room.room_alias ? room.room_alias : room.room_id) )"
class="recentsRoom"
ng-class="{'recentsRoomSelected': (room.room_id === recentsSelectedRoomID)}">
ng-click="selectRoom(room)"
class="recentsRoom"
ng-class="{'recentsRoomSelected': (room.room_id === recentsSelectedRoomID), 'recentsRoomBing': (unreadBings[room.room_id]), 'recentsRoomUnread': (unreadMessages[room.room_id])}">
<tr>
<td ng-class="room.current_room_state.state('m.room.join_rules').content.join_rule == 'public' ? 'recentsRoomName recentsPublicRoom' : 'recentsRoomName'">
{{ room.room_id | mRoomName }}

View File

@ -15,21 +15,14 @@ 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', 'notificationService', 'modelService',
function($modal, $filter, $scope, $timeout, $routeParams, $location, $rootScope, matrixService, mPresence, eventHandlerService, mFileUpload, matrixPhoneService, MatrixCall, notificationService, modelService) {
.controller('RoomController', ['$modal', '$filter', '$scope', '$timeout', '$routeParams', '$location', '$rootScope', 'matrixService', 'mPresence', 'eventHandlerService', 'mFileUpload', 'matrixPhoneService', 'MatrixCall', 'notificationService', 'modelService', 'recentsService',
function($modal, $filter, $scope, $timeout, $routeParams, $location, $rootScope, matrixService, mPresence, eventHandlerService, mFileUpload, matrixPhoneService, MatrixCall, notificationService, modelService, recentsService) {
'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
);
};
$scope.containsBingWord = eventHandlerService.eventContainsBingWord;
// Room ids. Computed and resolved in onInit
$scope.room_id = undefined;
@ -46,12 +39,8 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput', 'a
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
@ -162,7 +151,6 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput', 'a
$scope.$on(eventHandlerService.MSG_EVENT, function(ngEvent, event, isLive) {
if (isLive && event.room_id === $scope.room_id) {
scrollToBottom();
}
});
@ -804,7 +792,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput', 'a
console.log("onInit3");
// Make recents highlight the current room
$scope.recentsSelectedRoomID = $scope.room_id;
recentsService.setSelectedRoomId($scope.room_id);
// Init the history for this room
history.init();
@ -841,19 +829,6 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput', 'a
}
);
};
$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() {
@ -923,7 +898,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput', 'a
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.remoteVideoSelector = angular.element('#remoteVideo')[0];
call.placeVoiceCall();
$rootScope.currentCall = call;
};
@ -1091,6 +1066,21 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput', 'a
})
.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) {

View File

@ -203,7 +203,7 @@
<span ng-show='msg.content.msgtype === "m.text"'
class="message"
ng-class="containsBingWord(msg.content.body) && msg.user_id != state.user_id ? msg.echo_msg_state + ' messageBing' : msg.echo_msg_state"
ng-class="containsBingWord(msg) && msg.user_id != state.user_id ? msg.echo_msg_state + ' messageBing' : msg.echo_msg_state"
ng-bind-html="(msg.content.msgtype === 'm.text' && msg.type === 'm.room.message' && msg.content.format === 'org.matrix.custom.html') ?
(msg.content.formatted_body | unsanitizedLinky) :
(msg.content.msgtype === 'm.text' && msg.type === 'm.room.message') ? (msg.content.body | linky:'_blank') : '' "/>

View File

@ -52,18 +52,32 @@ module.exports = function(config) {
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
'../login/**/*.js': 'coverage',
'../room/**/*.js': 'coverage',
'../components/**/*.js': 'coverage',
'../user/**/*.js': 'coverage',
'../home/**/*.js': 'coverage',
'../recents/**/*.js': 'coverage',
'../settings/**/*.js': 'coverage',
'../app.js': 'coverage'
},
// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['progress', 'junit'],
reporters: ['progress', 'junit', 'coverage'],
junitReporter: {
outputFile: 'test-results.xml',
suite: ''
},
coverageReporter: {
type: 'cobertura',
dir: 'coverage/',
file: 'coverage.xml'
},
// web server port
port: 9876,

View File

@ -0,0 +1,153 @@
describe('RecentsService', function() {
var scope;
var MSG_EVENT = "__test__";
var testEventContainsBingWord, testIsLive, testEvent;
var eventHandlerService = {
MSG_EVENT: MSG_EVENT,
eventContainsBingWord: function(event) {
return testEventContainsBingWord;
}
};
// setup the service and mocked dependencies
beforeEach(function() {
// set default mock values
testEventContainsBingWord = false;
testIsLive = true;
testEvent = {
content: {
body: "Hello world",
msgtype: "m.text"
},
user_id: "@alfred:localhost",
room_id: "!fl1bb13:localhost",
event_id: "fwuegfw@localhost"
}
// mocked dependencies
module(function ($provide) {
$provide.value('eventHandlerService', eventHandlerService);
});
// tested service
module('recentsService');
});
beforeEach(inject(function($rootScope) {
scope = $rootScope;
}));
it('should start with no unread messages.', inject(
function(recentsService) {
expect(recentsService.getUnreadMessages()).toEqual({});
expect(recentsService.getUnreadBingMessages()).toEqual({});
}));
it('should NOT add an unread message to the room currently selected.', inject(
function(recentsService) {
recentsService.setSelectedRoomId(testEvent.room_id);
scope.$broadcast(MSG_EVENT, testEvent, testIsLive);
expect(recentsService.getUnreadMessages()).toEqual({});
expect(recentsService.getUnreadBingMessages()).toEqual({});
}));
it('should add an unread message to the room NOT currently selected.', inject(
function(recentsService) {
recentsService.setSelectedRoomId("!someotherroomid:localhost");
scope.$broadcast(MSG_EVENT, testEvent, testIsLive);
var unread = {};
unread[testEvent.room_id] = 1;
expect(recentsService.getUnreadMessages()).toEqual(unread);
}));
it('should add an unread message and an unread bing message if a message contains a bing word.', inject(
function(recentsService) {
recentsService.setSelectedRoomId("!someotherroomid:localhost");
testEventContainsBingWord = true;
scope.$broadcast(MSG_EVENT, testEvent, testIsLive);
var unread = {};
unread[testEvent.room_id] = 1;
expect(recentsService.getUnreadMessages()).toEqual(unread);
var bing = {};
bing[testEvent.room_id] = testEvent;
expect(recentsService.getUnreadBingMessages()).toEqual(bing);
}));
it('should clear both unread and unread bing messages when markAsRead is called.', inject(
function(recentsService) {
recentsService.setSelectedRoomId("!someotherroomid:localhost");
testEventContainsBingWord = true;
scope.$broadcast(MSG_EVENT, testEvent, testIsLive);
var unread = {};
unread[testEvent.room_id] = 1;
expect(recentsService.getUnreadMessages()).toEqual(unread);
var bing = {};
bing[testEvent.room_id] = testEvent;
expect(recentsService.getUnreadBingMessages()).toEqual(bing);
recentsService.markAsRead(testEvent.room_id);
unread[testEvent.room_id] = 0;
bing[testEvent.room_id] = undefined;
expect(recentsService.getUnreadMessages()).toEqual(unread);
expect(recentsService.getUnreadBingMessages()).toEqual(bing);
}));
it('should not add messages as unread if they are not live.', inject(
function(recentsService) {
testIsLive = false;
recentsService.setSelectedRoomId("!someotherroomid:localhost");
testEventContainsBingWord = true;
scope.$broadcast(MSG_EVENT, testEvent, testIsLive);
expect(recentsService.getUnreadMessages()).toEqual({});
expect(recentsService.getUnreadBingMessages()).toEqual({});
}));
it('should increment the unread message count.', inject(
function(recentsService) {
recentsService.setSelectedRoomId("!someotherroomid:localhost");
scope.$broadcast(MSG_EVENT, testEvent, testIsLive);
var unread = {};
unread[testEvent.room_id] = 1;
expect(recentsService.getUnreadMessages()).toEqual(unread);
scope.$broadcast(MSG_EVENT, testEvent, testIsLive);
unread[testEvent.room_id] = 2;
expect(recentsService.getUnreadMessages()).toEqual(unread);
}));
it('should set the bing event to the latest message to contain a bing word.', inject(
function(recentsService) {
recentsService.setSelectedRoomId("!someotherroomid:localhost");
testEventContainsBingWord = true;
scope.$broadcast(MSG_EVENT, testEvent, testIsLive);
var nextEvent = angular.copy(testEvent);
nextEvent.content.body = "Goodbye cruel world.";
nextEvent.event_id = "erfuerhfeaaaa@localhost";
scope.$broadcast(MSG_EVENT, nextEvent, testIsLive);
var bing = {};
bing[testEvent.room_id] = nextEvent;
expect(recentsService.getUnreadBingMessages()).toEqual(bing);
}));
it('should do nothing when marking an unknown room ID as read.', inject(
function(recentsService) {
recentsService.markAsRead("!someotherroomid:localhost");
expect(recentsService.getUnreadMessages()).toEqual({});
expect(recentsService.getUnreadBingMessages()).toEqual({});
}));
});