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 graph/*.dot
**/webclient/config.js **/webclient/config.js
webclient/test/environment-protractor.js **/webclient/test/coverage/
**/webclient/test/environment-protractor.js
uploads uploads

View File

@ -40,6 +40,8 @@ class FederationHandler(BaseHandler):
of the home server (including auth and state conflict resoultion) of the home server (including auth and state conflict resoultion)
b) converting events that were produced by local clients that may need b) converting events that were produced by local clients that may need
to be sent to remote home servers. to be sent to remote home servers.
c) doing the necessary dances to invite remote users and join remote
rooms.
""" """
def __init__(self, hs): def __init__(self, hs):
@ -102,6 +104,8 @@ class FederationHandler(BaseHandler):
logger.debug("Got event: %s", event.event_id) 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: if event.room_id in self.room_queues:
self.room_queues[event.room_id].append(pdu) self.room_queues[event.room_id].append(pdu)
return return
@ -187,6 +191,8 @@ class FederationHandler(BaseHandler):
@log_function @log_function
@defer.inlineCallbacks @defer.inlineCallbacks
def backfill(self, dest, room_id, limit): 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) extremities = yield self.store.get_oldest_events_in_room(room_id)
pdus = yield self.replication_layer.backfill( pdus = yield self.replication_layer.backfill(
@ -212,6 +218,10 @@ class FederationHandler(BaseHandler):
@defer.inlineCallbacks @defer.inlineCallbacks
def send_invite(self, target_host, event): 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( pdu = yield self.replication_layer.send_invite(
destination=target_host, destination=target_host,
context=event.room_id, context=event.room_id,
@ -229,6 +239,17 @@ class FederationHandler(BaseHandler):
@log_function @log_function
@defer.inlineCallbacks @defer.inlineCallbacks
def do_invite_join(self, target_host, room_id, joinee, content, snapshot): 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( pdu = yield self.replication_layer.make_join(
target_host, target_host,
room_id, room_id,
@ -313,6 +334,10 @@ class FederationHandler(BaseHandler):
@defer.inlineCallbacks @defer.inlineCallbacks
@log_function @log_function
def on_make_join_request(self, context, user_id): 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( event = self.event_factory.create_event(
etype=RoomMemberEvent.TYPE, etype=RoomMemberEvent.TYPE,
content={"membership": Membership.JOIN}, content={"membership": Membership.JOIN},
@ -335,6 +360,9 @@ class FederationHandler(BaseHandler):
@defer.inlineCallbacks @defer.inlineCallbacks
@log_function @log_function
def on_send_join_request(self, origin, pdu): 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 = self.pdu_codec.event_from_pdu(pdu)
event.outlier = False event.outlier = False
@ -403,6 +431,10 @@ class FederationHandler(BaseHandler):
@defer.inlineCallbacks @defer.inlineCallbacks
def on_invite_request(self, origin, pdu): 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 = self.pdu_codec.event_from_pdu(pdu)
event.outlier = True event.outlier = True

View File

@ -279,13 +279,14 @@ class DataStore(RoomMemberStore, RoomStore,
) )
if hasattr(event, "signatures"): if hasattr(event, "signatures"):
signatures = event.signatures.get(event.origin, {}) logger.debug("sigs: %s", event.signatures)
for name, sigs in event.signatures.items():
for key_id, signature_base64 in signatures.items(): for key_id, signature_base64 in sigs.items():
signature_bytes = decode_base64(signature_base64) signature_bytes = decode_base64(signature_base64)
self._store_event_origin_signature_txn( self._store_event_signature_txn(
txn, event.event_id, event.origin, key_id, signature_bytes, txn, event.event_id, name, key_id,
) signature_bytes,
)
for prev_event_id, prev_hashes in event.prev_events: for prev_event_id, prev_hashes in event.prev_events:
for alg, hash_base64 in prev_hashes.items(): 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 = ?" select_event_sql = "SELECT * FROM events WHERE event_id = ?"
for i, ev in enumerate(events): for i, ev in enumerate(events):
signatures = self._get_event_origin_signatures_txn( signatures = self._get_event_signatures_txn(
txn, ev.event_id, txn, ev.event_id,
) )
ev.signatures = { 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) prevs = self._get_prev_events_and_state(txn, ev.event_id)

View File

@ -23,6 +23,14 @@ logger = logging.getLogger(__name__)
class EventFederationStore(SQLBaseStore): 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): def get_auth_chain(self, event_id):
return self.runInteraction( return self.runInteraction(
@ -205,6 +213,8 @@ class EventFederationStore(SQLBaseStore):
return results return results
def get_min_depth(self, room_id): def get_min_depth(self, room_id):
""" For hte given room, get the minimum depth we have seen for it.
"""
return self.runInteraction( return self.runInteraction(
"get_min_depth", "get_min_depth",
self._get_min_depth_interaction, self._get_min_depth_interaction,
@ -240,6 +250,10 @@ class EventFederationStore(SQLBaseStore):
def _handle_prev_events(self, txn, outlier, event_id, prev_events, def _handle_prev_events(self, txn, outlier, event_id, prev_events,
room_id): room_id):
"""
For the given event, update the event edges table and forward and
backward extremities tables.
"""
for e_id, _ in prev_events: for e_id, _ in prev_events:
# TODO (erikj): This could be done as a bulk insert # TODO (erikj): This could be done as a bulk insert
self._simple_insert_txn( self._simple_insert_txn(
@ -267,8 +281,8 @@ class EventFederationStore(SQLBaseStore):
} }
) )
# We only insert as a forward extremity the new pdu if there are # We only insert as a forward extremity the new event if there are
# no other pdus that reference it as a prev pdu # no other events that reference it as a prev event
query = ( query = (
"INSERT OR IGNORE INTO %(table)s (event_id, room_id) " "INSERT OR IGNORE INTO %(table)s (event_id, room_id) "
"SELECT ?, ? WHERE NOT EXISTS (" "SELECT ?, ? WHERE NOT EXISTS ("
@ -284,7 +298,7 @@ class EventFederationStore(SQLBaseStore):
txn.execute(query, (event_id, room_id, event_id)) 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. # deleted in a second if they're incorrect anyway.
for e_id, _ in prev_events: for e_id, _ in prev_events:
# TODO (erikj): This could be done as a bulk insert # 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 # 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 = ( query = (
"DELETE FROM event_backward_extremities WHERE EXISTS (" "DELETE FROM event_backward_extremities WHERE EXISTS ("
"SELECT 1 FROM events " "SELECT 1 FROM events "
@ -311,17 +325,14 @@ class EventFederationStore(SQLBaseStore):
txn.execute(query) txn.execute(query)
def get_backfill_events(self, room_id, event_list, limit): def get_backfill_events(self, room_id, event_list, limit):
"""Get a list of Events for a given topic that occured before (and """Get a list of Events for a given topic that occurred before (and
including) the pdus in pdu_list. Return a list of max size `limit`. including) the events in event_list. Return a list of max size `limit`
Args: Args:
txn txn
room_id (str) room_id (str)
event_list (list) event_list (list)
limit (int) limit (int)
Return:
list: A list of PduTuples
""" """
return self.runInteraction( return self.runInteraction(
"get_backfill_events", "get_backfill_events",
@ -334,7 +345,6 @@ class EventFederationStore(SQLBaseStore):
room_id, repr(event_list), limit room_id, repr(event_list), limit
) )
# We seed the pdu_results with the things from the pdu_list.
event_results = event_list event_results = event_list
front = event_list front = event_list
@ -373,5 +383,4 @@ class EventFederationStore(SQLBaseStore):
front = new_front front = new_front
event_results += 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) 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, event_id TEXT,
origin TEXT, signature_name TEXT,
key_id TEXT, key_id TEXT,
signature BLOB, signature BLOB,
CONSTRAINT uniqueness UNIQUE (event_id, key_id) 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 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( CREATE TABLE IF NOT EXISTS event_to_state_groups(
event_id TEXT NOT NULL, event_id TEXT NOT NULL,
state_group INTEGER 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, or_ignore=True,
) )
def _get_event_signatures_txn(self, txn, event_id):
def _get_event_origin_signatures_txn(self, txn, event_id):
"""Get all the signatures for a given PDU. """Get all the signatures for a given PDU.
Args: Args:
txn (cursor): txn (cursor):
event_id (str): Id for the Event. event_id (str): Id for the Event.
Returns: Returns:
A dict of key_id -> signature_bytes. A dict of sig name -> dict(key_id -> signature_bytes)
""" """
query = ( query = (
"SELECT key_id, signature" "SELECT signature_name, key_id, signature"
" FROM event_origin_signatures" " FROM event_signatures"
" WHERE event_id = ? " " WHERE event_id = ? "
) )
txn.execute(query, (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): signature_bytes):
"""Store a signature from the origin server for a PDU. """Store a signature from the origin server for a PDU.
Args: Args:
@ -132,10 +138,10 @@ class SignatureStore(SQLBaseStore):
""" """
self._simple_insert_txn( self._simple_insert_txn(
txn, txn,
"event_origin_signatures", "event_signatures",
{ {
"event_id": event_id, "event_id": event_id,
"origin": origin, "signature_name": signature_name,
"key_id": key_id, "key_id": key_id,
"signature": buffer(signature_bytes), "signature": buffer(signature_bytes),
}, },

View File

@ -14,43 +14,71 @@
# limitations under the License. # limitations under the License.
from ._base import SQLBaseStore from ._base import SQLBaseStore
from twisted.internet import defer
class StateStore(SQLBaseStore): 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): def get_state_groups(self, event_ids):
groups = set() """ Get the state groups for the given list of event_ids
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 = {} The return value is a dict mapping group names to lists of events.
for group in groups: """
state_ids = yield self._simple_select_onecol(
table="state_groups_state", def f(txn):
keyvalues={"state_group": group}, groups = set()
retcol="event_id", for event_id in event_ids:
) group = self._simple_select_one_onecol_txn(
state = [] txn,
for state_id in state_ids: table="event_to_state_groups",
s = yield self.get_event( keyvalues={"event_id": event_id},
state_id, retcol="state_group",
allow_none=True, allow_none=True,
) )
if s: if group:
state.append(s) 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): def store_state_groups(self, event):
return self.runInteraction( return self.runInteraction(

View File

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

View File

@ -31,6 +31,7 @@ var matrixWebClient = angular.module('matrixWebClient', [
'eventStreamService', 'eventStreamService',
'eventHandlerService', 'eventHandlerService',
'notificationService', 'notificationService',
'recentsService',
'modelService', 'modelService',
'infinite-scroll', 'infinite-scroll',
'ui.bootstrap', '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]); 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) { var displayNotification = function(event) {
if (window.Notification && event.user_id != matrixService.config().user_id) { if (window.Notification && event.user_id != matrixService.config().user_id) {
var shouldBing = notificationService.containsBingWord( var shouldBing = containsBingWord(event);
matrixService.config().user_id,
matrixService.config().display_name,
matrixService.config().bingWords,
event.content.body
);
// Ideally we would notify only when the window is hidden (i.e. document.hidden = true). // 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); resetRoomMessages(room_id);
}, },
eventContainsBingWord: function(event) {
return containsBingWord(event);
},
/** /**
* Return the last message event of a room * Return the last message event of a room
* @param {String} room_id the room id * @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.getTurnServer();
MatrixCall.CALL_TIMEOUT = 60000; MatrixCall.CALL_TIMEOUT = 60000;
@ -92,7 +92,8 @@ angular.module('MatrixCall', [])
var pc; var pc;
if (window.mozRTCPeerConnection) { if (window.mozRTCPeerConnection) {
var iceServers = []; var iceServers = [];
if (MatrixCall.turnServer) { // https://github.com/EricssonResearch/openwebrtc/issues/85
if (MatrixCall.turnServer /*&& !this.isOpenWebRTC()*/) {
if (MatrixCall.turnServer.uris) { if (MatrixCall.turnServer.uris) {
for (var i = 0; i < MatrixCall.turnServer.uris.length; i++) { for (var i = 0; i < MatrixCall.turnServer.uris.length; i++) {
iceServers.push({ iceServers.push({
@ -110,7 +111,8 @@ angular.module('MatrixCall', [])
pc = new window.mozRTCPeerConnection({"iceServers":iceServers}); pc = new window.mozRTCPeerConnection({"iceServers":iceServers});
} else { } else {
var iceServers = []; var iceServers = [];
if (MatrixCall.turnServer) { // https://github.com/EricssonResearch/openwebrtc/issues/85
if (MatrixCall.turnServer /*&& !this.isOpenWebRTC()*/) {
if (MatrixCall.turnServer.uris) { if (MatrixCall.turnServer.uris) {
iceServers.push({ iceServers.push({
'urls': MatrixCall.turnServer.uris, 'urls': MatrixCall.turnServer.uris,
@ -492,6 +494,8 @@ angular.module('MatrixCall', [])
$timeout(function() { $timeout(function() {
var vel = self.getRemoteVideoElement(); var vel = self.getRemoteVideoElement();
if (vel.play) vel.play(); 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; 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; 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. well as attach common params (e.g. access_token) to requests.
*/ */
angular.module('matrixService', []) angular.module('matrixService', [])
.factory('matrixService', ['$http', '$q', '$rootScope', function($http, $q, $rootScope) { .factory('matrixService', ['$http', '$q', function($http, $q) {
/* /*
* Permanent storage of user information * 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-stream-service.js"></script>
<script src="components/matrix/event-handler-service.js"></script> <script src="components/matrix/event-handler-service.js"></script>
<script src="components/matrix/notification-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/model-service.js"></script>
<script src="components/matrix/presence-service.js"></script> <script src="components/matrix/presence-service.js"></script>
<script src="components/fileInput/file-input-directive.js"></script> <script src="components/fileInput/file-input-directive.js"></script>

View File

@ -17,18 +17,37 @@
'use strict'; 'use strict';
angular.module('RecentsController', ['matrixService', 'matrixFilter']) angular.module('RecentsController', ['matrixService', 'matrixFilter'])
.controller('RecentsController', ['$rootScope', '$scope', 'eventHandlerService', 'modelService', .controller('RecentsController', ['$rootScope', '$scope', 'eventHandlerService', 'modelService', 'recentsService',
function($rootScope, $scope, eventHandlerService, modelService) { function($rootScope, $scope, eventHandlerService, modelService, recentsService) {
// Expose the service to the view // Expose the service to the view
$scope.eventHandlerService = eventHandlerService; $scope.eventHandlerService = eventHandlerService;
// retrieve all rooms and expose them // retrieve all rooms and expose them
$scope.rooms = modelService.getRooms(); $scope.rooms = modelService.getRooms();
// $rootScope of the parent where the recents component is included can override this value // track the selected room ID: the html will use this
// in order to highlight a specific room in the list $scope.recentsSelectedRoomID = recentsService.getSelectedRoomId();
$rootScope.recentsSelectedRoomID; $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"> <div ng-controller="RecentsController">
<table class="recentsTable"> <table class="recentsTable">
<tbody ng-repeat="(index, room) in rooms | orderRecents" <tbody ng-repeat="(index, room) in rooms | orderRecents"
ng-click="goToPage('room/' + (room.room_alias ? room.room_alias : room.room_id) )" ng-click="selectRoom(room)"
class="recentsRoom" class="recentsRoom"
ng-class="{'recentsRoomSelected': (room.room_id === recentsSelectedRoomID)}"> ng-class="{'recentsRoomSelected': (room.room_id === recentsSelectedRoomID), 'recentsRoomBing': (unreadBings[room.room_id]), 'recentsRoomUnread': (unreadMessages[room.room_id])}">
<tr> <tr>
<td ng-class="room.current_room_state.state('m.room.join_rules').content.join_rule == 'public' ? 'recentsRoomName recentsPublicRoom' : 'recentsRoomName'"> <td ng-class="room.current_room_state.state('m.room.join_rules').content.join_rule == 'public' ? 'recentsRoomName recentsPublicRoom' : 'recentsRoomName'">
{{ room.room_id | mRoomName }} {{ room.room_id | mRoomName }}

View File

@ -15,21 +15,14 @@ limitations under the License.
*/ */
angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput', 'angular-peity']) 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', .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) { function($modal, $filter, $scope, $timeout, $routeParams, $location, $rootScope, matrixService, mPresence, eventHandlerService, mFileUpload, matrixPhoneService, MatrixCall, notificationService, modelService, recentsService) {
'use strict'; 'use strict';
var MESSAGES_PER_PAGINATION = 30; var MESSAGES_PER_PAGINATION = 30;
var THUMBNAIL_SIZE = 320; var THUMBNAIL_SIZE = 320;
// .html needs this // .html needs this
$scope.containsBingWord = function(content) { $scope.containsBingWord = eventHandlerService.eventContainsBingWord;
return notificationService.containsBingWord(
matrixService.config().user_id,
matrixService.config().display_name,
matrixService.config().bingWords,
content
);
};
// Room ids. Computed and resolved in onInit // Room ids. Computed and resolved in onInit
$scope.room_id = undefined; $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 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.members = {};
$scope.autoCompleting = false;
$scope.autoCompleteIndex = 0;
$scope.autoCompleteOriginal = "";
$scope.imageURLToSend = ""; $scope.imageURLToSend = "";
$scope.userIDToInvite = "";
// vars and functions for updating the name // 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) { $scope.$on(eventHandlerService.MSG_EVENT, function(ngEvent, event, isLive) {
if (isLive && event.room_id === $scope.room_id) { if (isLive && event.room_id === $scope.room_id) {
scrollToBottom(); scrollToBottom();
} }
}); });
@ -804,7 +792,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput', 'a
console.log("onInit3"); console.log("onInit3");
// Make recents highlight the current room // Make recents highlight the current room
$scope.recentsSelectedRoomID = $scope.room_id; recentsService.setSelectedRoomId($scope.room_id);
// Init the history for this room // Init the history for this room
history.init(); 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() { $scope.leaveRoom = function() {
@ -923,7 +898,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput', 'a
call.onError = $rootScope.onCallError; call.onError = $rootScope.onCallError;
call.onHangup = $rootScope.onCallHangup; call.onHangup = $rootScope.onCallHangup;
// remote video element is used for playing audio in voice calls // 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(); call.placeVoiceCall();
$rootScope.currentCall = call; $rootScope.currentCall = call;
}; };
@ -1091,6 +1066,21 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput', 'a
}) })
.controller('RoomInfoController', function($scope, $modalInstance, $filter, matrixService) { .controller('RoomInfoController', function($scope, $modalInstance, $filter, matrixService) {
console.log("Displaying room info."); 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) { $scope.submit = function(event) {
if (event.content) { if (event.content) {

View File

@ -203,7 +203,7 @@
<span ng-show='msg.content.msgtype === "m.text"' <span ng-show='msg.content.msgtype === "m.text"'
class="message" 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') ? 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.formatted_body | unsanitizedLinky) :
(msg.content.msgtype === 'm.text' && msg.type === 'm.room.message') ? (msg.content.body | linky:'_blank') : '' "/> (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 // preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: { 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 // test results reporter to use
// possible values: 'dots', 'progress' // possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter // available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['progress', 'junit'], reporters: ['progress', 'junit', 'coverage'],
junitReporter: { junitReporter: {
outputFile: 'test-results.xml', outputFile: 'test-results.xml',
suite: '' suite: ''
}, },
coverageReporter: {
type: 'cobertura',
dir: 'coverage/',
file: 'coverage.xml'
},
// web server port // web server port
port: 9876, 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({});
}));
});