diff --git a/.gitignore b/.gitignore
index 3d14ac8c03..339a99e0d6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py
index 99655c8bb0..5e096f4652 100644
--- a/synapse/handlers/federation.py
+++ b/synapse/handlers/federation.py
@@ -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
diff --git a/synapse/storage/__init__.py b/synapse/storage/__init__.py
index 72290eb5a0..d8f351a675 100644
--- a/synapse/storage/__init__.py
+++ b/synapse/storage/__init__.py
@@ -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():
diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py
index a1ee0318f6..670387b04a 100644
--- a/synapse/storage/_base.py
+++ b/synapse/storage/_base.py
@@ -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)
diff --git a/synapse/storage/event_federation.py b/synapse/storage/event_federation.py
index a027db3868..6c559f8f63 100644
--- a/synapse/storage/event_federation.py
+++ b/synapse/storage/event_federation.py
@@ -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)
diff --git a/synapse/storage/schema/event_signatures.sql b/synapse/storage/schema/event_signatures.sql
index 5491c7ecec..4efa8a3e63 100644
--- a/synapse/storage/schema/event_signatures.sql
+++ b/synapse/storage/schema/event_signatures.sql
@@ -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
);
diff --git a/synapse/storage/schema/state.sql b/synapse/storage/schema/state.sql
index b44c56b519..44f7aafb27 100644
--- a/synapse/storage/schema/state.sql
+++ b/synapse/storage/schema/state.sql
@@ -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
);
\ No newline at end of file
diff --git a/synapse/storage/signatures.py b/synapse/storage/signatures.py
index 84a49088a2..d90e08fff1 100644
--- a/synapse/storage/signatures.py
+++ b/synapse/storage/signatures.py
@@ -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()
+
+ res = {}
+
+ for name, key, sig in rows:
+ res.setdefault(name, {})[key] = sig
+
+ return res
- def _store_event_origin_signature_txn(self, txn, event_id, origin, key_id,
+ 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),
},
diff --git a/synapse/storage/state.py b/synapse/storage/state.py
index 2f3a70b4e5..55ea567793 100644
--- a/synapse/storage/state.py
+++ b/synapse/storage/state.py
@@ -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)
-
- 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,
+ """ Get the state groups for the given list of event_ids
+
+ 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)
+
+ res[group] = state
- defer.returnValue(res)
+ return res
+
+ return self.runInteraction(
+ "get_state_groups",
+ f,
+ )
def store_state_groups(self, event):
return self.runInteraction(
diff --git a/syweb/webclient/app.css b/syweb/webclient/app.css
index 403d615bf1..648388cdb9 100755
--- a/syweb/webclient/app.css
+++ b/syweb/webclient/app.css
@@ -812,6 +812,14 @@ textarea, input {
background-color: #eee;
}
+.recentsRoomUnread {
+ background-color: #fee;
+}
+
+.recentsRoomBing {
+ background-color: #eef;
+}
+
.recentsRoomName {
font-size: 16px;
padding-top: 7px;
diff --git a/syweb/webclient/app.js b/syweb/webclient/app.js
index 17b2bb6e8f..35190a71f4 100644
--- a/syweb/webclient/app.js
+++ b/syweb/webclient/app.js
@@ -31,6 +31,7 @@ var matrixWebClient = angular.module('matrixWebClient', [
'eventStreamService',
'eventHandlerService',
'notificationService',
+ 'recentsService',
'modelService',
'infinite-scroll',
'ui.bootstrap',
diff --git a/syweb/webclient/components/matrix/event-handler-service.js b/syweb/webclient/components/matrix/event-handler-service.js
index 7b2a75507d..6645d20374 100644
--- a/syweb/webclient/components/matrix/event-handler-service.js
+++ b/syweb/webclient/components/matrix/event-handler-service.js
@@ -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
diff --git a/syweb/webclient/components/matrix/matrix-call.js b/syweb/webclient/components/matrix/matrix-call.js
index b560cf7daa..a1c3aaa103 100644
--- a/syweb/webclient/components/matrix/matrix-call.js
+++ b/syweb/webclient/components/matrix/matrix-call.js
@@ -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;
}]);
diff --git a/syweb/webclient/components/matrix/matrix-service.js b/syweb/webclient/components/matrix/matrix-service.js
index c1264887c8..cfe8691f85 100644
--- a/syweb/webclient/components/matrix/matrix-service.js
+++ b/syweb/webclient/components/matrix/matrix-service.js
@@ -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
diff --git a/syweb/webclient/components/matrix/recents-service.js b/syweb/webclient/components/matrix/recents-service.js
new file mode 100644
index 0000000000..3d82b8218b
--- /dev/null
+++ b/syweb/webclient/components/matrix/recents-service.js
@@ -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);
+ }
+
+ };
+
+}]);
diff --git a/syweb/webclient/index.html b/syweb/webclient/index.html
index f6487f381d..4bca320e77 100644
--- a/syweb/webclient/index.html
+++ b/syweb/webclient/index.html
@@ -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>
diff --git a/syweb/webclient/recents/recents-controller.js b/syweb/webclient/recents/recents-controller.js
index 6f0be18f1a..41720d4cb0 100644
--- a/syweb/webclient/recents/recents-controller.js
+++ b/syweb/webclient/recents/recents-controller.js
@@ -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) );
+ };
}]);
diff --git a/syweb/webclient/recents/recents.html b/syweb/webclient/recents/recents.html
index 7297e23703..0b3a77ca11 100644
--- a/syweb/webclient/recents/recents.html
+++ b/syweb/webclient/recents/recents.html
@@ -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 }}
diff --git a/syweb/webclient/room/room-controller.js b/syweb/webclient/room/room-controller.js
index 6928754c5d..6670201707 100644
--- a/syweb/webclient/room/room-controller.js
+++ b/syweb/webclient/room/room-controller.js
@@ -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) {
diff --git a/syweb/webclient/room/room.html b/syweb/webclient/room/room.html
index 1f1cd9baef..b97b839b41 100644
--- a/syweb/webclient/room/room.html
+++ b/syweb/webclient/room/room.html
@@ -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') : '' "/>
diff --git a/syweb/webclient/test/karma.conf.js b/syweb/webclient/test/karma.conf.js
index 5f0642ca33..37a9eaf1c1 100644
--- a/syweb/webclient/test/karma.conf.js
+++ b/syweb/webclient/test/karma.conf.js
@@ -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,
diff --git a/syweb/webclient/test/unit/recents-service.spec.js b/syweb/webclient/test/unit/recents-service.spec.js
new file mode 100644
index 0000000000..a2f9ecbaf8
--- /dev/null
+++ b/syweb/webclient/test/unit/recents-service.spec.js
@@ -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({});
+ }));
+});
|