Merge pull request #11 from matrix-org/webclient-room-data-restructure

Webclient room data restructure
This commit is contained in:
Kegsay 2014-11-04 15:44:58 +00:00
commit 020fc15d98
15 changed files with 541 additions and 479 deletions

View File

@ -21,8 +21,8 @@ limitations under the License.
'use strict'; 'use strict';
angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'eventStreamService']) angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'eventStreamService'])
.controller('MatrixWebClientController', ['$scope', '$location', '$rootScope', '$timeout', '$animate', 'matrixService', 'mPresence', 'eventStreamService', 'eventHandlerService', 'matrixPhoneService', .controller('MatrixWebClientController', ['$scope', '$location', '$rootScope', '$timeout', '$animate', 'matrixService', 'mPresence', 'eventStreamService', 'eventHandlerService', 'matrixPhoneService', 'modelService',
function($scope, $location, $rootScope, $timeout, $animate, matrixService, mPresence, eventStreamService, eventHandlerService, matrixPhoneService) { function($scope, $location, $rootScope, $timeout, $animate, matrixService, mPresence, eventStreamService, eventHandlerService, matrixPhoneService, modelService) {
// Check current URL to avoid to display the logout button on the login page // Check current URL to avoid to display the logout button on the login page
$scope.location = $location.path(); $scope.location = $location.path();
@ -117,7 +117,7 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
return; return;
} }
var roomMembers = angular.copy($rootScope.events.rooms[$rootScope.currentCall.room_id].members); var roomMembers = angular.copy(modelService.getRoom($rootScope.currentCall.room_id).current_room_state.members);
delete roomMembers[matrixService.config().user_id]; delete roomMembers[matrixService.config().user_id];
$rootScope.currentCall.user_id = Object.keys(roomMembers)[0]; $rootScope.currentCall.user_id = Object.keys(roomMembers)[0];

View File

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

View File

@ -22,13 +22,12 @@ not care where the event came from, it only needs enough context to be able to
process them. Events may be coming from the event stream, the REST API (via process them. Events may be coming from the event stream, the REST API (via
direct GETs or via a pagination stream API), etc. direct GETs or via a pagination stream API), etc.
Typically, this service will store events or broadcast them to any listeners Typically, this service will store events and broadcast them to any listeners
(e.g. controllers) via $broadcast. Alternatively, it may update the $rootScope (e.g. controllers) via $broadcast.
if typically all the $on method would do is update its own $scope.
*/ */
angular.module('eventHandlerService', []) angular.module('eventHandlerService', [])
.factory('eventHandlerService', ['matrixService', '$rootScope', '$q', '$timeout', 'mPresence', 'notificationService', .factory('eventHandlerService', ['matrixService', '$rootScope', '$q', '$timeout', 'mPresence', 'notificationService', 'modelService',
function(matrixService, $rootScope, $q, $timeout, mPresence, notificationService) { function(matrixService, $rootScope, $q, $timeout, mPresence, notificationService, modelService) {
var ROOM_CREATE_EVENT = "ROOM_CREATE_EVENT"; var ROOM_CREATE_EVENT = "ROOM_CREATE_EVENT";
var MSG_EVENT = "MSG_EVENT"; var MSG_EVENT = "MSG_EVENT";
var MEMBER_EVENT = "MEMBER_EVENT"; var MEMBER_EVENT = "MEMBER_EVENT";
@ -44,94 +43,121 @@ function(matrixService, $rootScope, $q, $timeout, mPresence, notificationService
// of the app, given we never try to reap memory yet) // of the app, given we never try to reap memory yet)
var eventMap = {}; var eventMap = {};
// TODO: Remove this and replace with modelService.User objects.
$rootScope.presence = {}; $rootScope.presence = {};
var initialSyncDeferred; var initialSyncDeferred;
var reset = function() { var reset = function() {
initialSyncDeferred = $q.defer(); initialSyncDeferred = $q.defer();
$rootScope.events = {
rooms: {} // will contain roomId: { messages:[], members:{userid1: event} }
};
$rootScope.presence = {}; $rootScope.presence = {};
eventMap = {}; eventMap = {};
}; };
reset(); reset();
var initRoom = function(room_id, room) {
if (!(room_id in $rootScope.events.rooms)) {
console.log("Creating new rooms entry for " + room_id);
$rootScope.events.rooms[room_id] = {
room_id: room_id,
messages: [],
members: {},
// Pagination information
pagination: {
earliest_token: "END" // how far back we've paginated
}
};
}
if (room) { // we got an existing room object from initialsync, seemingly.
// Report all other metadata of the room object (membership, inviter, visibility, ...)
for (var field in room) {
if (!room.hasOwnProperty(field)) continue;
if (-1 === ["room_id", "messages", "state"].indexOf(field)) { // why indexOf - why not ===? --Matthew
$rootScope.events.rooms[room_id][field] = room[field];
}
}
$rootScope.events.rooms[room_id].membership = room.membership;
}
};
var resetRoomMessages = function(room_id) { var resetRoomMessages = function(room_id) {
if ($rootScope.events.rooms[room_id]) { var room = modelService.getRoom(room_id);
$rootScope.events.rooms[room_id].messages = []; room.events = [];
}
}; };
// Generic method to handle events data // Generic method to handle events data
var handleRoomDateEvent = function(event, isLiveEvent, addToRoomMessages) { var handleRoomStateEvent = function(event, isLiveEvent, addToRoomMessages) {
// Add topic changes as if they were a room message var room = modelService.getRoom(event.room_id);
if (addToRoomMessages) { if (addToRoomMessages) {
if (isLiveEvent) { // some state events are displayed as messages, so add them.
$rootScope.events.rooms[event.room_id].messages.push(event); room.addMessageEvent(event, !isLiveEvent);
}
else {
$rootScope.events.rooms[event.room_id].messages.unshift(event);
}
} }
// live events always update, but non-live events only update if the if (isLiveEvent) {
// ts is later. // update the current room state with the latest state
var latestData = true; room.current_room_state.storeStateEvent(event);
if (!isLiveEvent) { }
else {
var eventTs = event.origin_server_ts; var eventTs = event.origin_server_ts;
var storedEvent = $rootScope.events.rooms[event.room_id][event.type]; var storedEvent = room.current_room_state.getStateEvent(event.type, event.state_key);
if (storedEvent) { if (storedEvent) {
if (storedEvent.origin_server_ts > eventTs) { if (storedEvent.origin_server_ts < eventTs) {
// ignore it, we have a newer one already. // the incoming event is newer, use it.
latestData = false; room.current_room_state.storeStateEvent(event);
} }
} }
} }
if (latestData) { // TODO: handle old_room_state
$rootScope.events.rooms[event.room_id][event.type] = event;
}
}; };
var handleRoomCreate = function(event, isLiveEvent) { var handleRoomCreate = function(event, isLiveEvent) {
// For now, we do not use the event data. Simply signal it to the app controllers
$rootScope.$broadcast(ROOM_CREATE_EVENT, event, isLiveEvent); $rootScope.$broadcast(ROOM_CREATE_EVENT, event, isLiveEvent);
}; };
var handleRoomAliases = function(event, isLiveEvent) { var handleRoomAliases = function(event, isLiveEvent) {
matrixService.createRoomIdToAliasMapping(event.room_id, event.content.aliases[0]); matrixService.createRoomIdToAliasMapping(event.room_id, event.content.aliases[0]);
}; };
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
);
// Ideally we would notify only when the window is hidden (i.e. document.hidden = true).
//
// However, Chrome on Linux and OSX currently returns document.hidden = false unless the window is
// explicitly showing a different tab. So we need another metric to determine hiddenness - we
// simply use idle time. If the user has been idle enough that their presence goes to idle, then
// we also display notifs when things happen.
//
// This is far far better than notifying whenever anything happens anyway, otherwise you get spammed
// to death with notifications when the window is in the foreground, which is horrible UX (especially
// if you have not defined any bingers and so get notified for everything).
var isIdle = (document.hidden || matrixService.presence.unavailable === mPresence.getState());
// We need a way to let people get notifications for everything, if they so desire. The way to do this
// is to specify zero bingwords.
var bingWords = matrixService.config().bingWords;
if (bingWords === undefined || bingWords.length === 0) {
shouldBing = true;
}
if (shouldBing && isIdle) {
console.log("Displaying notification for "+JSON.stringify(event));
var member = modelService.getMember(event.room_id, event.user_id);
var displayname = getUserDisplayName(event.room_id, event.user_id);
var message = event.content.body;
if (event.content.msgtype === "m.emote") {
message = "* " + displayname + " " + message;
}
else if (event.content.msgtype === "m.image") {
message = displayname + " sent an image.";
}
var roomTitle = matrixService.getRoomIdToAliasMapping(event.room_id);
var theRoom = modelService.getRoom(event.room_id);
if (!roomTitle && theRoom.current_room_state.state("m.room.name") && theRoom.current_room_state.state("m.room.name").content) {
roomTitle = theRoom.current_room_state.state("m.room.name").content.name;
}
if (!roomTitle) {
roomTitle = event.room_id;
}
notificationService.showNotification(
displayname + " (" + roomTitle + ")",
message,
member ? member.avatar_url : undefined,
function() {
console.log("notification.onclick() room=" + event.room_id);
$rootScope.goToPage('room/' + event.room_id);
}
);
}
}
};
var handleMessage = function(event, isLiveEvent) { var handleMessage = function(event, isLiveEvent) {
// Check for empty event content // Check for empty event content
@ -144,135 +170,79 @@ function(matrixService, $rootScope, $q, $timeout, mPresence, notificationService
// empty json object is a redacted event, so ignore. // empty json object is a redacted event, so ignore.
return; return;
} }
if (isLiveEvent) { // =======================
if (event.user_id === matrixService.config().user_id &&
(event.content.msgtype === "m.text" || event.content.msgtype === "m.emote") ) { var room = modelService.getRoom(event.room_id);
// Assume we've already echoed it. So, there is a fake event in the messages list of the room
// Replace this fake event by the true one if (event.user_id !== matrixService.config().user_id) {
var index = getRoomEventIndex(event.room_id, event.event_id); room.addMessageEvent(event, !isLiveEvent);
if (index) { displayNotification(event);
$rootScope.events.rooms[event.room_id].messages[index] = event;
}
else {
$rootScope.events.rooms[event.room_id].messages.push(event);
}
}
else {
$rootScope.events.rooms[event.room_id].messages.push(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
);
// Ideally we would notify only when the window is hidden (i.e. document.hidden = true).
//
// However, Chrome on Linux and OSX currently returns document.hidden = false unless the window is
// explicitly showing a different tab. So we need another metric to determine hiddenness - we
// simply use idle time. If the user has been idle enough that their presence goes to idle, then
// we also display notifs when things happen.
//
// This is far far better than notifying whenever anything happens anyway, otherwise you get spammed
// to death with notifications when the window is in the foreground, which is horrible UX (especially
// if you have not defined any bingers and so get notified for everything).
var isIdle = (document.hidden || matrixService.presence.unavailable === mPresence.getState());
// We need a way to let people get notifications for everything, if they so desire. The way to do this
// is to specify zero bingwords.
var bingWords = matrixService.config().bingWords;
if (bingWords === undefined || bingWords.length === 0) {
shouldBing = true;
}
if (shouldBing && isIdle) {
console.log("Displaying notification for "+JSON.stringify(event));
var member = getMember(event.room_id, event.user_id);
var displayname = getUserDisplayName(event.room_id, event.user_id);
var message = event.content.body;
if (event.content.msgtype === "m.emote") {
message = "* " + displayname + " " + message;
}
else if (event.content.msgtype === "m.image") {
message = displayname + " sent an image.";
}
var roomTitle = matrixService.getRoomIdToAliasMapping(event.room_id);
var theRoom = $rootScope.events.rooms[event.room_id];
if (!roomTitle && theRoom && theRoom["m.room.name"] && theRoom["m.room.name"].content) {
roomTitle = theRoom["m.room.name"].content.name;
}
if (!roomTitle) {
roomTitle = event.room_id;
}
notificationService.showNotification(
displayname + " (" + roomTitle + ")",
message,
member ? member.avatar_url : undefined,
function() {
console.log("notification.onclick() room=" + event.room_id);
$rootScope.goToPage('room/' + event.room_id);
}
);
}
}
} }
else { else {
$rootScope.events.rooms[event.room_id].messages.unshift(event); // we may have locally echoed this, so we should replace the event
// instead of just adding.
room.addOrReplaceMessageEvent(event, !isLiveEvent);
} }
// TODO send delivery receipt if isLiveEvent // TODO send delivery receipt if isLiveEvent
// $broadcast this, as controllers may want to do funky things such as
// scroll to the bottom, etc which cannot be expressed via simple $scope
// updates.
$rootScope.$broadcast(MSG_EVENT, event, isLiveEvent); $rootScope.$broadcast(MSG_EVENT, event, isLiveEvent);
}; };
var handleRoomMember = function(event, isLiveEvent, isStateEvent) { var handleRoomMember = function(event, isLiveEvent, isStateEvent) {
var room = modelService.getRoom(event.room_id);
// add membership changes as if they were a room message if something interesting changed // did something change?
// Exception: Do not do this if the event is a room state event because such events already come var memberChanges = undefined;
// as room messages events. Moreover, when they come as room messages events, they are relatively ordered
// with other other room messages
if (!isStateEvent) { if (!isStateEvent) {
// could be a membership change, display name change, etc. // could be a membership change, display name change, etc.
// Find out which one. // Find out which one.
var memberChanges = undefined;
if ((event.prev_content === undefined && event.content.membership) || (event.prev_content && (event.prev_content.membership !== event.content.membership))) { if ((event.prev_content === undefined && event.content.membership) || (event.prev_content && (event.prev_content.membership !== event.content.membership))) {
memberChanges = "membership"; memberChanges = "membership";
} }
else if (event.prev_content && (event.prev_content.displayname !== event.content.displayname)) { else if (event.prev_content && (event.prev_content.displayname !== event.content.displayname)) {
memberChanges = "displayname"; memberChanges = "displayname";
} }
// mark the key which changed // mark the key which changed
event.changedKey = memberChanges; event.changedKey = memberChanges;
// If there was a change we want to display, dump it in the message
// list.
if (memberChanges) {
if (isLiveEvent) {
$rootScope.events.rooms[event.room_id].messages.push(event);
}
else {
$rootScope.events.rooms[event.room_id].messages.unshift(event);
}
}
} }
// Use data from state event or the latest data from the stream.
// Do not care of events that come when paginating back // modify state before adding the message so it points to the right thing.
// The events are copied to avoid referencing the same event when adding
// the message (circular json structures)
if (isStateEvent || isLiveEvent) { if (isStateEvent || isLiveEvent) {
$rootScope.events.rooms[event.room_id].members[event.state_key] = event; var newEvent = angular.copy(event);
newEvent.cnt = event.content;
room.current_room_state.storeStateEvent(newEvent);
} }
else if (!isLiveEvent) {
// mutate the old room state
var oldEvent = angular.copy(event);
oldEvent.cnt = event.content;
if (event.prev_content) {
// the m.room.member event we are handling is the NEW event. When
// we keep going back in time, we want the PREVIOUS value for displaying
// names/etc, hence the clobber here.
oldEvent.cnt = event.prev_content;
}
if (event.changedKey === "membership" && event.content.membership === "join") {
// join has a prev_content but it doesn't contain all the info unlike the join, so use that.
oldEvent.cnt = event.content;
}
room.old_room_state.storeStateEvent(oldEvent);
}
// If there was a change we want to display, dump it in the message
// list. This has to be done after room state is updated.
if (memberChanges) {
room.addMessageEvent(event, !isLiveEvent);
}
$rootScope.$broadcast(MEMBER_EVENT, event, isLiveEvent, isStateEvent); $rootScope.$broadcast(MEMBER_EVENT, event, isLiveEvent, isStateEvent);
}; };
@ -283,30 +253,28 @@ function(matrixService, $rootScope, $q, $timeout, mPresence, notificationService
}; };
var handlePowerLevels = function(event, isLiveEvent) { var handlePowerLevels = function(event, isLiveEvent) {
// Keep the latest data. Do not care of events that come when paginating back handleRoomStateEvent(event, isLiveEvent);
if (!$rootScope.events.rooms[event.room_id][event.type] || isLiveEvent) { $rootScope.$broadcast(POWERLEVEL_EVENT, event, isLiveEvent);
$rootScope.events.rooms[event.room_id][event.type] = event;
$rootScope.$broadcast(POWERLEVEL_EVENT, event, isLiveEvent);
}
}; };
var handleRoomName = function(event, isLiveEvent, isStateEvent) { var handleRoomName = function(event, isLiveEvent, isStateEvent) {
console.log("handleRoomName room_id: " + event.room_id + " - isLiveEvent: " + isLiveEvent + " - name: " + event.content.name); console.log("handleRoomName room_id: " + event.room_id + " - isLiveEvent: " + isLiveEvent + " - name: " + event.content.name);
handleRoomDateEvent(event, isLiveEvent, !isStateEvent); handleRoomStateEvent(event, isLiveEvent, !isStateEvent);
$rootScope.$broadcast(NAME_EVENT, event, isLiveEvent); $rootScope.$broadcast(NAME_EVENT, event, isLiveEvent);
}; };
var handleRoomTopic = function(event, isLiveEvent, isStateEvent) { var handleRoomTopic = function(event, isLiveEvent, isStateEvent) {
console.log("handleRoomTopic room_id: " + event.room_id + " - isLiveEvent: " + isLiveEvent + " - topic: " + event.content.topic); console.log("handleRoomTopic room_id: " + event.room_id + " - isLiveEvent: " + isLiveEvent + " - topic: " + event.content.topic);
handleRoomDateEvent(event, isLiveEvent, !isStateEvent); handleRoomStateEvent(event, isLiveEvent, !isStateEvent);
$rootScope.$broadcast(TOPIC_EVENT, event, isLiveEvent); $rootScope.$broadcast(TOPIC_EVENT, event, isLiveEvent);
}; };
var handleCallEvent = function(event, isLiveEvent) { var handleCallEvent = function(event, isLiveEvent) {
$rootScope.$broadcast(CALL_EVENT, event, isLiveEvent); $rootScope.$broadcast(CALL_EVENT, event, isLiveEvent);
if (event.type === 'm.call.invite') { if (event.type === 'm.call.invite') {
$rootScope.events.rooms[event.room_id].messages.push(event); var room = modelService.getRoom(event.room_id);
room.addMessageEvent(event, !isLiveEvent);
} }
}; };
@ -320,8 +288,9 @@ function(matrixService, $rootScope, $q, $timeout, mPresence, notificationService
// we need to remove something possibly: do we know the redacted // we need to remove something possibly: do we know the redacted
// event ID? // event ID?
if (eventMap[event.redacts]) { if (eventMap[event.redacts]) {
var room = modelService.getRoom(event.room_id);
// remove event from list of messages in this room. // remove event from list of messages in this room.
var eventList = $rootScope.events.rooms[event.room_id].messages; var eventList = room.events;
for (var i=0; i<eventList.length; i++) { for (var i=0; i<eventList.length; i++) {
if (eventList[i].event_id === event.redacts) { if (eventList[i].event_id === event.redacts) {
console.log("Removing event " + event.redacts); console.log("Removing event " + event.redacts);
@ -330,50 +299,9 @@ function(matrixService, $rootScope, $q, $timeout, mPresence, notificationService
} }
} }
// broadcast the redaction so controllers can nuke this
console.log("Redacted an event."); console.log("Redacted an event.");
} }
} }
/**
* Get the index of the event in $rootScope.events.rooms[room_id].messages
* @param {type} room_id the room id
* @param {type} event_id the event id to look for
* @returns {Number | undefined} the index. undefined if not found.
*/
var getRoomEventIndex = function(room_id, event_id) {
var index;
var room = $rootScope.events.rooms[room_id];
if (room) {
// Start looking from the tail since the first goal of this function
// is to find a messaged among the latest ones
for (var i = room.messages.length - 1; i > 0; i--) {
var message = room.messages[i];
if (event_id === message.event_id) {
index = i;
break;
}
}
}
return index;
};
/**
* Get the member object of a room member
* @param {String} room_id the room id
* @param {String} user_id the id of the user
* @returns {undefined | Object} the member object of this user in this room if he is part of the room
*/
var getMember = function(room_id, user_id) {
var member;
var room = $rootScope.events.rooms[room_id];
if (room) {
member = room.members[user_id];
}
return member;
};
/** /**
* Return the display name of an user acccording to data already downloaded * Return the display name of an user acccording to data already downloaded
@ -385,17 +313,17 @@ function(matrixService, $rootScope, $q, $timeout, mPresence, notificationService
var displayName; var displayName;
// Get the user display name from the member list of the room // Get the user display name from the member list of the room
var member = getMember(room_id, user_id); var member = modelService.getMember(room_id, user_id);
if (member && member.content.displayname) { // Do not consider null displayname if (member && member.content.displayname) { // Do not consider null displayname
displayName = member.content.displayname; displayName = member.content.displayname;
// Disambiguate users who have the same displayname in the room // Disambiguate users who have the same displayname in the room
if (user_id !== matrixService.config().user_id) { if (user_id !== matrixService.config().user_id) {
var room = $rootScope.events.rooms[room_id]; var room = modelService.getRoom(room_id);
for (var member_id in room.members) { for (var member_id in room.current_room_state.members) {
if (room.members.hasOwnProperty(member_id) && member_id !== user_id) { if (room.current_room_state.members.hasOwnProperty(member_id) && member_id !== user_id) {
var member2 = room.members[member_id]; var member2 = room.current_room_state.members[member_id];
if (member2.content.displayname && member2.content.displayname === displayName) { if (member2.content.displayname && member2.content.displayname === displayName) {
displayName = displayName + " (" + user_id + ")"; displayName = displayName + " (" + user_id + ")";
break; break;
@ -433,19 +361,9 @@ function(matrixService, $rootScope, $q, $timeout, mPresence, notificationService
reset(); reset();
$rootScope.$broadcast(RESET_EVENT); $rootScope.$broadcast(RESET_EVENT);
}, },
initRoom: function(room) {
initRoom(room.room_id, room);
},
handleEvent: function(event, isLiveEvent, isStateEvent) { handleEvent: function(event, isLiveEvent, isStateEvent) {
// FIXME: /initialSync on a particular room is not yet available
// So initRoom on a new room is not called. Make sure the room data is initialised here
if (event.room_id) {
initRoom(event.room_id);
}
// Avoid duplicated events // Avoid duplicated events
// Needed for rooms where initialSync has not been done. // Needed for rooms where initialSync has not been done.
// In this case, we do not know where to start pagination. So, it starts from the END // In this case, we do not know where to start pagination. So, it starts from the END
@ -504,11 +422,11 @@ function(matrixService, $rootScope, $q, $timeout, mPresence, notificationService
// displays on the Room Info screen. // displays on the Room Info screen.
if (typeof(event.state_key) === "string") { // incls. 0-len strings if (typeof(event.state_key) === "string") { // incls. 0-len strings
if (event.room_id) { if (event.room_id) {
handleRoomDateEvent(event, isLiveEvent, false); handleRoomStateEvent(event, isLiveEvent, false);
} }
} }
console.log("Unable to handle event type " + event.type); console.log("Unable to handle event type " + event.type);
console.log(JSON.stringify(event, undefined, 4)); // console.log(JSON.stringify(event, undefined, 4));
break; break;
} }
} }
@ -524,8 +442,6 @@ function(matrixService, $rootScope, $q, $timeout, mPresence, notificationService
// Handle messages from /initialSync or /messages // Handle messages from /initialSync or /messages
handleRoomMessages: function(room_id, messages, isLiveEvents, dir) { handleRoomMessages: function(room_id, messages, isLiveEvents, dir) {
initRoom(room_id);
var events = messages.chunk; var events = messages.chunk;
// Handles messages according to their time order // Handles messages according to their time order
@ -536,21 +452,67 @@ function(matrixService, $rootScope, $q, $timeout, mPresence, notificationService
} }
// Store how far back we've paginated // Store how far back we've paginated
$rootScope.events.rooms[room_id].pagination.earliest_token = messages.end; var room = modelService.getRoom(room_id);
room.old_room_state.pagination_token = messages.end;
} }
else { else {
// InitialSync returns messages in chronological order // InitialSync returns messages in chronological order, so invert
// it to get most recent > oldest
for (var i=events.length - 1; i>=0; i--) { for (var i=events.length - 1; i>=0; i--) {
this.handleEvent(events[i], isLiveEvents, isLiveEvents); this.handleEvent(events[i], isLiveEvents, isLiveEvents);
} }
// Store where to start pagination // Store where to start pagination
$rootScope.events.rooms[room_id].pagination.earliest_token = messages.start; var room = modelService.getRoom(room_id);
room.old_room_state.pagination_token = messages.start;
} }
}, },
handleInitialSyncDone: function(initialSyncData) { handleInitialSyncDone: function(response) {
console.log("# handleInitialSyncDone"); console.log("# handleInitialSyncDone");
initialSyncDeferred.resolve(initialSyncData);
var rooms = response.data.rooms;
for (var i = 0; i < rooms.length; ++i) {
var room = rooms[i];
// FIXME: This is ming: the HS should be sending down the m.room.member
// event for the invite in .state but it isn't, so fudge it for now.
if (room.inviter && room.membership === "invite") {
var me = matrixService.config().user_id;
var fakeEvent = {
event_id: "__FAKE__" + room.room_id,
user_id: room.inviter,
origin_server_ts: 0,
room_id: room.room_id,
state_key: me,
type: "m.room.member",
content: {
membership: "invite"
}
};
if (!room.state) {
room.state = [];
}
room.state.push(fakeEvent);
console.log("RECV /initialSync invite >> "+room.room_id);
}
var newRoom = modelService.getRoom(room.room_id);
newRoom.current_room_state.storeStateEvents(room.state);
newRoom.old_room_state.storeStateEvents(room.state);
// this should be done AFTER storing state events since these
// messages may make the old_room_state diverge.
if ("messages" in room) {
this.handleRoomMessages(room.room_id, room.messages, false);
newRoom.current_room_state.pagination_token = room.messages.end;
newRoom.old_room_state.pagination_token = room.messages.start;
}
}
var presence = response.data.presence;
this.handleEvents(presence, false);
initialSyncDeferred.resolve(response);
}, },
// Returns a promise that resolves when the initialSync request has been processed // Returns a promise that resolves when the initialSync request has been processed
@ -571,15 +533,13 @@ function(matrixService, $rootScope, $q, $timeout, mPresence, notificationService
getLastMessage: function(room_id, filterEcho) { getLastMessage: function(room_id, filterEcho) {
var lastMessage; var lastMessage;
var room = $rootScope.events.rooms[room_id]; var events = modelService.getRoom(room_id).events;
if (room) { for (var i = events.length - 1; i >= 0; i--) {
for (var i = room.messages.length - 1; i >= 0; i--) { var message = events[i];
var message = room.messages[i];
if (!filterEcho || undefined === message.echo_msg_state) { if (!filterEcho || undefined === message.echo_msg_state) {
lastMessage = message; lastMessage = message;
break; break;
}
} }
} }
@ -594,18 +554,15 @@ function(matrixService, $rootScope, $q, $timeout, mPresence, notificationService
getUsersCountInRoom: function(room_id) { getUsersCountInRoom: function(room_id) {
var memberCount; var memberCount;
var room = $rootScope.events.rooms[room_id]; var room = modelService.getRoom(room_id);
if (room) { memberCount = 0;
memberCount = 0; for (var i in room.current_room_state.members) {
if (!room.current_room_state.members.hasOwnProperty(i)) continue;
for (var i in room.members) { var member = room.current_room_state.members[i];
if (!room.members.hasOwnProperty(i)) continue;
var member = room.members[i]; if ("join" === member.content.membership) {
memberCount = memberCount + 1;
if ("join" === member.membership) {
memberCount = memberCount + 1;
}
} }
} }
@ -613,13 +570,24 @@ function(matrixService, $rootScope, $q, $timeout, mPresence, notificationService
}, },
/** /**
* Get the member object of a room member * Return the power level of an user in a particular room
* @param {String} room_id the room id * @param {String} room_id the room id
* @param {String} user_id the id of the user * @param {String} user_id the user id
* @returns {undefined | Object} the member object of this user in this room if he is part of the room * @returns {Number} a value between 0 and 10
*/ */
getMember: function(room_id, user_id) { getUserPowerLevel: function(room_id, user_id) {
return getMember(room_id, user_id); var powerLevel = 0;
var room = modelService.getRoom(room_id).current_room_state;
if (room.state("m.room.power_levels")) {
if (user_id in room.state("m.room.power_levels").content) {
powerLevel = room.state("m.room.power_levels").content[user_id];
}
else {
// Use the room default user power
powerLevel = room.state("m.room.power_levels").content["default"];
}
}
return powerLevel;
}, },
/** /**
@ -630,18 +598,6 @@ function(matrixService, $rootScope, $q, $timeout, mPresence, notificationService
*/ */
getUserDisplayName: function(room_id, user_id) { getUserDisplayName: function(room_id, user_id) {
return getUserDisplayName(room_id, user_id); return getUserDisplayName(room_id, user_id);
},
setRoomVisibility: function(room_id, visible) {
if (!visible) {
return;
}
initRoom(room_id);
var room = $rootScope.events.rooms[room_id];
if (room) {
room.visibility = visible;
}
} }
}; };
}]); }]);

View File

@ -109,25 +109,6 @@ angular.module('eventStreamService', [])
// without requiring to make an additional request // without requiring to make an additional request
matrixService.initialSync(30, false).then( matrixService.initialSync(30, false).then(
function(response) { function(response) {
var rooms = response.data.rooms;
for (var i = 0; i < rooms.length; ++i) {
var room = rooms[i];
eventHandlerService.initRoom(room);
if ("messages" in room) {
eventHandlerService.handleRoomMessages(room.room_id, room.messages, false);
}
if ("state" in room) {
eventHandlerService.handleEvents(room.state, false, true);
}
}
var presence = response.data.presence;
eventHandlerService.handleEvents(presence, false);
// Initial sync is done
eventHandlerService.handleInitialSyncDone(response); eventHandlerService.handleInitialSyncDone(response);
// Start event streaming from that point // Start event streaming from that point

View File

@ -46,7 +46,7 @@ var isWebRTCSupported = function () {
}; };
angular.module('MatrixCall', []) angular.module('MatrixCall', [])
.factory('MatrixCall', ['matrixService', 'matrixPhoneService', '$rootScope', '$timeout', function MatrixCallFactory(matrixService, matrixPhoneService, $rootScope, $timeout) { .factory('MatrixCall', ['matrixService', 'matrixPhoneService', 'modelService', '$rootScope', '$timeout', function MatrixCallFactory(matrixService, matrixPhoneService, modelService, $rootScope, $timeout) {
$rootScope.isWebRTCSupported = isWebRTCSupported(); $rootScope.isWebRTCSupported = isWebRTCSupported();
var MatrixCall = function(room_id) { var MatrixCall = function(room_id) {
@ -213,7 +213,7 @@ angular.module('MatrixCall', [])
var self = this; var self = this;
var roomMembers = $rootScope.events.rooms[this.room_id].members; var roomMembers = modelService.getRoom(this.room_id).current_room_state.members;
if (roomMembers[matrixService.config().user_id].membership != 'join') { if (roomMembers[matrixService.config().user_id].membership != 'join') {
console.log("We need to join the room before we can accept this call"); console.log("We need to join the room before we can accept this call");
matrixService.join(this.room_id).then(function() { matrixService.join(this.room_id).then(function() {

View File

@ -19,102 +19,72 @@
angular.module('matrixFilter', []) angular.module('matrixFilter', [])
// Compute the room name according to information we have // Compute the room name according to information we have
.filter('mRoomName', ['$rootScope', 'matrixService', 'eventHandlerService', function($rootScope, matrixService, eventHandlerService) { // TODO: It would be nice if this was stateless and had no dependencies. That would
// make the business logic here a lot easier to see.
.filter('mRoomName', ['$rootScope', 'matrixService', 'eventHandlerService', 'modelService',
function($rootScope, matrixService, eventHandlerService, modelService) {
return function(room_id) { return function(room_id) {
var roomName; var roomName;
// If there is an alias, use it // If there is an alias, use it
// TODO: only one alias is managed for now // TODO: only one alias is managed for now
var alias = matrixService.getRoomIdToAliasMapping(room_id); var alias = matrixService.getRoomIdToAliasMapping(room_id);
var room = modelService.getRoom(room_id).current_room_state;
var room_name_event = room.state("m.room.name");
var room = $rootScope.events.rooms[room_id]; // Determine if it is a public room
if (room) { var isPublicRoom = false;
// Get name from room state date if (room.state("m.room.join_rules") && room.state("m.room.join_rules").content) {
var room_name_event = room["m.room.name"]; isPublicRoom = ("public" === room.state("m.room.join_rules").content.join_rule);
}
// Determine if it is a public room if (room_name_event) {
var isPublicRoom = false; roomName = room_name_event.content.name;
if (room["m.room.join_rules"] && room["m.room.join_rules"].content) { }
isPublicRoom = ("public" === room["m.room.join_rules"].content.join_rule); else if (alias) {
} roomName = alias;
}
if (room_name_event) { else if (Object.keys(room.members).length > 0 && !isPublicRoom) { // Do not rename public room
roomName = room_name_event.content.name; var user_id = matrixService.config().user_id;
}
else if (alias) {
roomName = alias;
}
else if (room.members && !isPublicRoom) { // Do not rename public room
var user_id = matrixService.config().user_id; // this is a "one to one" room and should have the name of the other user.
// Else, build the name from its users if (Object.keys(room.members).length === 2) {
// Limit the room renaming to 1:1 room for (var i in room.members) {
if (2 === Object.keys(room.members).length) { if (!room.members.hasOwnProperty(i)) continue;
for (var i in room.members) {
if (!room.members.hasOwnProperty(i)) continue;
var member = room.members[i]; var member = room.members[i];
if (member.state_key !== user_id) { if (member.state_key !== user_id) {
roomName = eventHandlerService.getUserDisplayName(room_id, member.state_key); roomName = eventHandlerService.getUserDisplayName(room_id, member.state_key);
break; break;
}
} }
} }
else if (Object.keys(room.members).length <= 1) { }
else if (Object.keys(room.members).length === 1) {
var otherUserId; // this could be just us (self-chat) or could be the other person
// in a room if they have invited us to the room. Find out which.
if (Object.keys(room.members)[0]) { var otherUserId = Object.keys(room.members)[0];
otherUserId = Object.keys(room.members)[0]; if (otherUserId === user_id) {
// this could be an invite event (from event stream) // it's us, we may have been invited to this room or it could
if (otherUserId === user_id && // be a self chat.
room.members[user_id].content.membership === "invite") { if (room.members[otherUserId].content.membership === "invite") {
// this is us being invited to this room, so the // someone invited us, use the right ID.
// *user_id* is the other user ID and not the state roomName = eventHandlerService.getUserDisplayName(room_id, room.members[otherUserId].user_id);
// key.
otherUserId = room.members[user_id].user_id;
}
} }
else { else {
// it's got to be an invite, or failing that a self-chat; roomName = eventHandlerService.getUserDisplayName(room_id, otherUserId);
otherUserId = room.inviter || user_id;
/*
// XXX: This should all be unnecessary now thanks to using the /rooms/<room>/roomid API
// The other member may be in the invite list, get all invited users
var invitedUserIDs = [];
// XXX: *SURELY* we shouldn't have to trawl through the whole messages list to
// find invite - surely the other user should be in room.members with state invited? :/ --Matthew
for (var i in room.messages) {
var message = room.messages[i];
if ("m.room.member" === message.type && "invite" === message.content.membership) {
// Filter out the current user
var member_id = message.state_key;
if (member_id === user_id) {
member_id = message.user_id;
}
if (member_id !== user_id) {
// Make sure there is no duplicate user
if (-1 === invitedUserIDs.indexOf(member_id)) {
invitedUserIDs.push(member_id);
}
}
}
}
// For now, only 1:1 room needs to be renamed. It means only 1 invited user
if (1 === invitedUserIDs.length) {
otherUserId = invitedUserIDs[0];
}
*/
} }
}
// Get the user display name else { // it isn't us, so use their name if we know it.
roomName = eventHandlerService.getUserDisplayName(room_id, otherUserId); roomName = eventHandlerService.getUserDisplayName(room_id, otherUserId);
} }
} }
else if (Object.keys(room.members).length === 0) {
// this shouldn't be possible
console.error("0 members in room >> " + room_id);
}
} }
// Always show the alias in the room displayed name // Always show the alias in the room displayed name
if (roomName && alias && alias !== roomName) { if (roomName && alias && alias !== roomName) {
@ -124,14 +94,6 @@ angular.module('matrixFilter', [])
if (undefined === roomName) { if (undefined === roomName) {
// By default, use the room ID // By default, use the room ID
roomName = room_id; roomName = room_id;
// XXX: this is *INCREDIBLY* heavy logging for a function that calls every single
// time any kind of digest runs which refreshes a room name...
// commenting it out for now.
// Log some information that lead to this leak
// console.log("Room ID leak for " + room_id);
// console.log("room object: " + JSON.stringify(room, undefined, 4));
} }
return roomName; return roomName;

View File

@ -725,57 +725,30 @@ angular.module('matrixService', [])
//console.log("looking for roomId for " + alias + "; found: " + roomId); //console.log("looking for roomId for " + alias + "; found: " + roomId);
return roomId; return roomId;
}, },
/****** Power levels management ******/
/**
* Return the power level of an user in a particular room
* @param {String} room_id the room id
* @param {String} user_id the user id
* @returns {Number} a value between 0 and 10
*/
getUserPowerLevel: function(room_id, user_id) {
var powerLevel = 0;
var room = $rootScope.events.rooms[room_id];
if (room && room["m.room.power_levels"]) {
if (user_id in room["m.room.power_levels"].content) {
powerLevel = room["m.room.power_levels"].content[user_id];
}
else {
// Use the room default user power
powerLevel = room["m.room.power_levels"].content["default"];
}
}
return powerLevel;
},
/** /**
* Change or reset the power level of a user * Change or reset the power level of a user
* @param {String} room_id the room id * @param {String} room_id the room id
* @param {String} user_id the user id * @param {String} user_id the user id
* @param {Number} powerLevel a value between 0 and 10 * @param {Number} powerLevel The desired power level.
* If undefined, the user power level will be reset, ie he will use the default room user power level * If undefined, the user power level will be reset, ie he will use the default room user power level
* @param event The existing m.room.power_levels event if one exists.
* @returns {promise} an $http promise * @returns {promise} an $http promise
*/ */
setUserPowerLevel: function(room_id, user_id, powerLevel) { setUserPowerLevel: function(room_id, user_id, powerLevel, event) {
var content = {};
// Hack: currently, there is no home server API so do it by hand by updating if (event) {
// the current m.room.power_levels of the room and send it to the server // if there is an existing event, copy the content as it contains
var room = $rootScope.events.rooms[room_id]; // the power level values for other members which we do not want
if (room && room["m.room.power_levels"]) { // to modify.
var content = angular.copy(room["m.room.power_levels"].content); content = angular.copy(event.content);
content[user_id] = powerLevel;
var path = "/rooms/$room_id/state/m.room.power_levels";
path = path.replace("$room_id", encodeURIComponent(room_id));
return doRequest("PUT", path, undefined, content);
} }
content[user_id] = powerLevel;
// The room does not exist or does not contain power_levels data
var deferred = $q.defer(); var path = "/rooms/$room_id/state/m.room.power_levels";
deferred.reject({data:{error: "Invalid room: " + room_id}}); path = path.replace("$room_id", encodeURIComponent(room_id));
return deferred.promise;
return doRequest("PUT", path, undefined, content);
}, },
getTurnServer: function() { getTurnServer: function() {

View File

@ -0,0 +1,170 @@
/*
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 serves as the entry point for all models in the app. If access to
underlying data in a room is required, then this service should be used as the
dependency.
*/
// NB: This is more explicit than linking top-level models to $rootScope
// in that by adding this service as a dep you are clearly saying "this X
// needs access to the underlying data store", rather than polluting the
// $rootScope.
angular.module('modelService', [])
.factory('modelService', ['matrixService', function(matrixService) {
/***** Room Object *****/
var Room = function Room(room_id) {
this.room_id = room_id;
this.old_room_state = new RoomState();
this.current_room_state = new RoomState();
this.events = []; // events which can be displayed on the UI. TODO move?
};
Room.prototype = {
addMessageEvents: function addMessageEvents(events, toFront) {
for (var i=0; i<events.length; i++) {
this.addMessageEvent(events[i], toFront);
}
},
addMessageEvent: function addMessageEvent(event, toFront) {
// every message must reference the RoomMember which made it *at
// that time* so things like display names display correctly.
var stateAtTheTime = toFront ? this.old_room_state : this.current_room_state;
event.__room_member = stateAtTheTime.getStateEvent("m.room.member", event.user_id);
if (event.type === "m.room.member" && event.content.membership === "invite") {
// give information on both the inviter and invitee
event.__target_room_member = stateAtTheTime.getStateEvent("m.room.member", event.state_key);
}
if (toFront) {
this.events.unshift(event);
}
else {
this.events.push(event);
}
},
addOrReplaceMessageEvent: function addOrReplaceMessageEvent(event, toFront) {
// Start looking from the tail since the first goal of this function
// is to find a message among the latest ones
for (var i = this.events.length - 1; i >= 0; i--) {
var storedEvent = this.events[i];
if (storedEvent.event_id === event.event_id) {
// It's clobbering time!
this.events[i] = event;
return;
}
}
this.addMessageEvent(event, toFront);
},
leave: function leave() {
return matrixService.leave(this.room_id);
}
};
/***** Room State Object *****/
var RoomState = function RoomState() {
// list of RoomMember
this.members = {};
// state events, the key is a compound of event type + state_key
this.state_events = {};
this.pagination_token = "";
};
RoomState.prototype = {
// get a state event for this room from this.state_events. State events
// are unique per type+state_key tuple, with a lot of events using 0-len
// state keys. To make it not Really Annoying to access, this method is
// provided which can just be given the type and it will return the
// 0-len event by default.
state: function state(type, state_key) {
if (!type) {
return undefined; // event type MUST be specified
}
if (!state_key) {
return this.state_events[type]; // treat as 0-len state key
}
return this.state_events[type + state_key];
},
storeStateEvent: function storeState(event) {
this.state_events[event.type + event.state_key] = event;
if (event.type === "m.room.member") {
this.members[event.state_key] = event;
}
},
storeStateEvents: function storeState(events) {
if (!events) {
return;
}
for (var i=0; i<events.length; i++) {
this.storeStateEvent(events[i]);
}
},
getStateEvent: function getStateEvent(event_type, state_key) {
return this.state_events[event_type + state_key];
}
};
/***** Room Member Object *****/
var RoomMember = function RoomMember() {
this.event = {}; // the m.room.member event representing the RoomMember.
this.user = undefined; // the User
};
/***** User Object *****/
var User = function User() {
this.event = {}; // the m.presence event representing the User.
};
// rooms are stored here when they come in.
var rooms = {
// roomid: <Room>
};
console.log("Models inited.");
return {
getRoom: function(roomId) {
if(!rooms[roomId]) {
rooms[roomId] = new Room(roomId);
}
return rooms[roomId];
},
getRooms: function() {
return rooms;
},
/**
* Get the member object of a room member
* @param {String} room_id the room id
* @param {String} user_id the id of the user
* @returns {undefined | Object} the member object of this user in this room if he is part of the room
*/
getMember: function(room_id, user_id) {
var room = this.getRoom(room_id);
return room.current_room_state.members[user_id];
}
};
}]);

View File

@ -58,7 +58,6 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen
// Add room_alias & room_display_name members // Add room_alias & room_display_name members
angular.extend(room, matrixService.getRoomAliasAndDisplayName(room)); angular.extend(room, matrixService.getRoomAliasAndDisplayName(room));
eventHandlerService.setRoomVisibility(room.room_id, "public");
} }
} }
); );

View File

@ -13,7 +13,7 @@
<script type='text/javascript' src='js/jquery-1.8.3.min.js'></script> <script type='text/javascript' src='js/jquery-1.8.3.min.js'></script>
<script type="text/javascript" src="https://www.google.com/recaptcha/api/js/recaptcha_ajax.js"></script> <script type="text/javascript" src="https://www.google.com/recaptcha/api/js/recaptcha_ajax.js"></script>
<script src="js/angular.min.js"></script> <script src="js/angular.js"></script>
<script src="js/angular-route.min.js"></script> <script src="js/angular-route.min.js"></script>
<script src="js/angular-sanitize.min.js"></script> <script src="js/angular-sanitize.min.js"></script>
<script src="js/angular-animate.min.js"></script> <script src="js/angular-animate.min.js"></script>
@ -42,6 +42,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/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>
<script src="components/fileUpload/file-upload-service.js"></script> <script src="components/fileUpload/file-upload-service.js"></script>

View File

@ -17,11 +17,14 @@
'use strict'; 'use strict';
angular.module('RecentsController', ['matrixService', 'matrixFilter']) angular.module('RecentsController', ['matrixService', 'matrixFilter'])
.controller('RecentsController', ['$rootScope', '$scope', 'eventHandlerService', .controller('RecentsController', ['$rootScope', '$scope', 'eventHandlerService', 'modelService',
function($rootScope, $scope, eventHandlerService) { function($rootScope, $scope, eventHandlerService, modelService) {
// Expose the service to the view // Expose the service to the view
$scope.eventHandlerService = eventHandlerService; $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 // $rootScope of the parent where the recents component is included can override this value
// in order to highlight a specific room in the list // in order to highlight a specific room in the list

View File

@ -17,7 +17,7 @@
'use strict'; 'use strict';
angular.module('RecentsController') angular.module('RecentsController')
.filter('orderRecents', ["matrixService", "eventHandlerService", function(matrixService, eventHandlerService) { .filter('orderRecents', ["matrixService", "eventHandlerService", "modelService", function(matrixService, eventHandlerService, modelService) {
return function(rooms) { return function(rooms) {
var user_id = matrixService.config().user_id; var user_id = matrixService.config().user_id;
@ -25,26 +25,30 @@ angular.module('RecentsController')
// The key, room_id, is already in value objects // The key, room_id, is already in value objects
var filtered = []; var filtered = [];
angular.forEach(rooms, function(room, room_id) { angular.forEach(rooms, function(room, room_id) {
room.recent = {};
var meEvent = room.current_room_state.state("m.room.member", user_id);
// Show the room only if the user has joined it or has been invited // Show the room only if the user has joined it or has been invited
// (ie, do not show it if he has been banned) // (ie, do not show it if he has been banned)
var member = eventHandlerService.getMember(room_id, user_id); var member = modelService.getMember(room_id, user_id);
if (member && ("invite" === member.membership || "join" === member.membership)) { room.recent.me = member;
if (member && ("invite" === member.content.membership || "join" === member.content.membership)) {
if ("invite" === member.content.membership) {
room.recent.inviter = member.user_id;
}
// Count users here // Count users here
// TODO: Compute it directly in eventHandlerService // TODO: Compute it directly in eventHandlerService
room.numUsersInRoom = eventHandlerService.getUsersCountInRoom(room_id); room.recent.numUsersInRoom = eventHandlerService.getUsersCountInRoom(room_id);
filtered.push(room); filtered.push(room);
} }
else if ("invite" === room.membership) { else if (meEvent && "invite" === meEvent.content.membership) {
// The only information we have about the room is that the user has been invited // The only information we have about the room is that the user has been invited
filtered.push(room); filtered.push(room);
} }
}); });
// And time sort them // And time sort them
// The room with the lastest message at first // The room with the latest message at first
filtered.sort(function (roomA, roomB) { filtered.sort(function (roomA, roomB) {
var lastMsgRoomA = eventHandlerService.getLastMessage(roomA.room_id, true); var lastMsgRoomA = eventHandlerService.getLastMessage(roomA.room_id, true);

View File

@ -1,16 +1,16 @@
<div ng-controller="RecentsController"> <div ng-controller="RecentsController">
<table class="recentsTable"> <table class="recentsTable">
<tbody ng-repeat="(index, room) in events.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="goToPage('room/' + (room.room_alias ? room.room_alias : room.room_id) )"
class="recentsRoom" class="recentsRoom"
ng-class="{'recentsRoomSelected': (room.room_id === recentsSelectedRoomID)}"> ng-class="{'recentsRoomSelected': (room.room_id === recentsSelectedRoomID)}">
<tr> <tr>
<td ng-class="room['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 }}
</td> </td>
<td class="recentsRoomSummaryUsersCount"> <td class="recentsRoomSummaryUsersCount">
<span ng-show="undefined !== room.numUsersInRoom"> <span ng-show="undefined !== room.recent.numUsersInRoom">
{{ room.numUsersInRoom || '1' }} {{ room.numUsersInRoom == 1 ? 'user' : 'users' }} {{ room.recent.numUsersInRoom || '1' }} {{ room.recent.numUsersInRoom == 1 ? 'user' : 'users' }}
</span> </span>
</td> </td>
<td class="recentsRoomSummaryTS"> <td class="recentsRoomSummaryTS">
@ -27,11 +27,11 @@
<tr> <tr>
<td colspan="3" class="recentsRoomSummary"> <td colspan="3" class="recentsRoomSummary">
<div ng-show="room.membership === 'invite'"> <div ng-show="room.recent.me.content.membership === 'invite'">
{{ room.inviter | mUserDisplayName: room.room_id }} invited you {{ room.recent.inviter | mUserDisplayName: room.room_id }} invited you
</div> </div>
<div ng-hide="room.membership === 'invite'" ng-switch="lastMsg.type"> <div ng-hide="room.recent.me.membership === 'invite'" ng-switch="lastMsg.type">
<div ng-switch-when="m.room.member"> <div ng-switch-when="m.room.member">
<span ng-switch="lastMsg.changedKey"> <span ng-switch="lastMsg.changedKey">
<span ng-switch-when="membership"> <span ng-switch-when="membership">

View File

@ -15,8 +15,8 @@ limitations under the License.
*/ */
angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput']) angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
.controller('RoomController', ['$modal', '$filter', '$scope', '$timeout', '$routeParams', '$location', '$rootScope', 'matrixService', 'mPresence', 'eventHandlerService', 'mFileUpload', 'matrixPhoneService', 'MatrixCall', 'notificationService', .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) { function($modal, $filter, $scope, $timeout, $routeParams, $location, $rootScope, matrixService, mPresence, eventHandlerService, mFileUpload, matrixPhoneService, MatrixCall, notificationService, modelService) {
'use strict'; 'use strict';
var MESSAGES_PER_PAGINATION = 30; var MESSAGES_PER_PAGINATION = 30;
var THUMBNAIL_SIZE = 320; var THUMBNAIL_SIZE = 320;
@ -64,7 +64,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
return; return;
}; };
var nameEvent = $rootScope.events.rooms[$scope.room_id]['m.room.name']; var nameEvent = $scope.room.current_room_state.state_events['m.room.name'];
if (nameEvent) { if (nameEvent) {
$scope.name.newNameText = nameEvent.content.name; $scope.name.newNameText = nameEvent.content.name;
} }
@ -105,7 +105,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
console.log("Warning: Already editing topic."); console.log("Warning: Already editing topic.");
return; return;
} }
var topicEvent = $rootScope.events.rooms[$scope.room_id]['m.room.topic']; var topicEvent = $scope.room.current_room_state.state_events['m.room.topic'];
if (topicEvent) { if (topicEvent) {
$scope.topic.newTopicText = topicEvent.content.topic; $scope.topic.newTopicText = topicEvent.content.topic;
} }
@ -254,11 +254,11 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
$scope.state.paginating = true; $scope.state.paginating = true;
} }
console.log("paginateBackMessages from " + $rootScope.events.rooms[$scope.room_id].pagination.earliest_token + " for " + numItems); console.log("paginateBackMessages from " + $scope.room.old_room_state.pagination_token + " for " + numItems);
var originalTopRow = $("#messageTable>tbody>tr:first")[0]; var originalTopRow = $("#messageTable>tbody>tr:first")[0];
// Paginate events from the point in cache // Paginate events from the point in cache
matrixService.paginateBackMessages($scope.room_id, $rootScope.events.rooms[$scope.room_id].pagination.earliest_token, numItems).then( matrixService.paginateBackMessages($scope.room_id, $scope.room.old_room_state.pagination_token, numItems).then(
function(response) { function(response) {
eventHandlerService.handleRoomMessages($scope.room_id, response.data, false, 'b'); eventHandlerService.handleRoomMessages($scope.room_id, response.data, false, 'b');
@ -404,7 +404,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
var updateUserPowerLevel = function(user_id) { var updateUserPowerLevel = function(user_id) {
var member = $scope.members[user_id]; var member = $scope.members[user_id];
if (member) { if (member) {
member.powerLevel = matrixService.getUserPowerLevel($scope.room_id, user_id); member.powerLevel = eventHandlerService.getUserPowerLevel($scope.room_id, user_id);
normaliseMembersPowerLevels(); normaliseMembersPowerLevels();
} }
@ -492,7 +492,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
var room_id = matrixService.getAliasToRoomIdMapping(room_alias); var room_id = matrixService.getAliasToRoomIdMapping(room_alias);
console.log("joining " + room_alias + " id=" + room_id); console.log("joining " + room_alias + " id=" + room_id);
if ($rootScope.events.rooms[room_id]) { if ($scope.room) { // TODO actually check that you = join
// don't send a join event for a room you're already in. // don't send a join event for a room you're already in.
$location.url("room/" + room_alias); $location.url("room/" + room_alias);
} }
@ -576,7 +576,8 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
powerLevel = parseInt(matches[3]); powerLevel = parseInt(matches[3]);
} }
if (powerLevel !== NaN) { if (powerLevel !== NaN) {
promise = matrixService.setUserPowerLevel($scope.room_id, user_id, powerLevel); var powerLevelEvent = $scope.room.current_room_state.state("m.room.power_levels");
promise = matrixService.setUserPowerLevel($scope.room_id, user_id, powerLevel, powerLevelEvent);
} }
} }
} }
@ -591,7 +592,8 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
if (args) { if (args) {
var matches = args.match(/^(\S+)$/); var matches = args.match(/^(\S+)$/);
if (matches) { if (matches) {
promise = matrixService.setUserPowerLevel($scope.room_id, args, undefined); var powerLevelEvent = $scope.room.current_room_state.state("m.room.power_levels");
promise = matrixService.setUserPowerLevel($scope.room_id, args, undefined, powerLevelEvent);
} }
} }
@ -629,7 +631,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
}; };
$('#mainInput').val(''); $('#mainInput').val('');
$rootScope.events.rooms[$scope.room_id].messages.push(echoMessage); $scope.room.addMessageEvent(echoMessage);
scrollToBottom(); scrollToBottom();
} }
@ -717,6 +719,9 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
var onInit2 = function() { var onInit2 = function() {
console.log("onInit2"); console.log("onInit2");
// =============================
$scope.room = modelService.getRoom($scope.room_id);
// =============================
// Scroll down as soon as possible so that we point to the last message // Scroll down as soon as possible so that we point to the last message
// if it already exists in memory // if it already exists in memory
@ -729,9 +734,9 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
var needsToJoin = true; var needsToJoin = true;
// The room members is available in the data fetched by initialSync // The room members is available in the data fetched by initialSync
if ($rootScope.events.rooms[$scope.room_id]) { if ($scope.room) {
var messages = $rootScope.events.rooms[$scope.room_id].messages; var messages = $scope.room.events;
if (0 === messages.length if (0 === messages.length
|| (1 === messages.length && "m.room.member" === messages[0].type && "invite" === messages[0].content.membership && $scope.state.user_id === messages[0].state_key)) { || (1 === messages.length && "m.room.member" === messages[0].type && "invite" === messages[0].content.membership && $scope.state.user_id === messages[0].state_key)) {
@ -743,7 +748,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
$scope.state.first_pagination = false; $scope.state.first_pagination = false;
} }
var members = $rootScope.events.rooms[$scope.room_id].members; var members = $scope.room.current_room_state.members;
// Update the member list // Update the member list
for (var i in members) { for (var i in members) {
@ -999,10 +1004,15 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
}; };
$scope.openJson = function(content) { $scope.openJson = function(content) {
$scope.event_selected = content; $scope.event_selected = angular.copy(content);
// FIXME: Pre-calculated event data should be stripped in a nicer way.
$scope.event_selected.__room_member = undefined;
$scope.event_selected.__target_room_member = undefined;
// scope this so the template can check power levels and enable/disable // scope this so the template can check power levels and enable/disable
// buttons // buttons
$scope.pow = matrixService.getUserPowerLevel; $scope.pow = eventHandlerService.getUserPowerLevel;
var modalInstance = $modal.open({ var modalInstance = $modal.open({
templateUrl: 'eventInfoTemplate.html', templateUrl: 'eventInfoTemplate.html',
@ -1039,8 +1049,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
state_key: "" state_key: ""
}; };
var stateFilter = $filter("stateEventsFilter"); var stateEvents = $scope.room.current_room_state.state_events;
var stateEvents = stateFilter($scope.events.rooms[$scope.room_id]);
// The modal dialog will 2-way bind this field, so we MUST make a deep // The modal dialog will 2-way bind this field, so we MUST make a deep
// copy of the state events else we will be *actually adjusing our view // copy of the state events else we will be *actually adjusing our view
// of the world* when fiddling with the JSON!! Apparently parse/stringify // of the world* when fiddling with the JSON!! Apparently parse/stringify
@ -1059,7 +1068,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
console.log("Displaying modal dialog for >>>> " + JSON.stringify($scope.event_selected)); console.log("Displaying modal dialog for >>>> " + JSON.stringify($scope.event_selected));
$scope.redact = function() { $scope.redact = function() {
console.log("User level = "+$scope.pow($scope.room_id, $scope.state.user_id)+ console.log("User level = "+$scope.pow($scope.room_id, $scope.state.user_id)+
" Redact level = "+$scope.events.rooms[$scope.room_id]["m.room.ops_levels"].content.redact_level); " Redact level = "+$scope.room.current_room_state.state_events["m.room.ops_levels"].content.redact_level);
console.log("Redact event >> " + JSON.stringify($scope.event_selected)); console.log("Redact event >> " + JSON.stringify($scope.event_selected));
$modalInstance.close("redact"); $modalInstance.close("redact");
}; };

View File

@ -6,7 +6,7 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button ng-click="redact()" type="button" class="btn btn-danger" <button ng-click="redact()" type="button" class="btn btn-danger"
ng-disabled="!events.rooms[room_id]['m.room.ops_levels'].content.redact_level || !pow(room_id, state.user_id) || pow(room_id, state.user_id) < events.rooms[room_id]['m.room.ops_levels'].content.redact_level" ng-disabled="!room.current_room_state.state('m.room.ops_levels').content.redact_level || !pow(room_id, state.user_id) || pow(room_id, state.user_id) < room.current_room_state.state('m.room.ops_levels').content.redact_level"
title="Delete this event on all home servers. This cannot be undone."> title="Delete this event on all home servers. This cannot be undone.">
Redact Redact
</button> </button>
@ -18,7 +18,8 @@
<table class="room-info"> <table class="room-info">
<tr ng-repeat="(key, event) in roomInfo.stateEvents" class="room-info-event"> <tr ng-repeat="(key, event) in roomInfo.stateEvents" class="room-info-event">
<td class="room-info-event-meta" width="30%"> <td class="room-info-event-meta" width="30%">
<span class="monospace">{{ key }}</span> <span class="monospace">{{ event.type }}</span>
<span ng-show="event.state_key" class="monospace"> ({{event.state_key}})</span>
<br/> <br/>
{{ (event.origin_server_ts) | date:'MMM d HH:mm' }} {{ (event.origin_server_ts) | date:'MMM d HH:mm' }}
<br/> <br/>
@ -68,13 +69,13 @@
</div> </div>
<div class="roomTopicSection"> <div class="roomTopicSection">
<button ng-hide="events.rooms[room_id]['m.room.topic'].content.topic || topic.isEditing" <button ng-hide="room.current_room_state.state_events['m.room.topic'].content.topic || topic.isEditing"
ng-click="topic.editTopic()" class="roomTopicSetNew"> ng-click="topic.editTopic()" class="roomTopicSetNew">
Set Topic Set Topic
</button> </button>
<div ng-show="events.rooms[room_id]['m.room.topic'].content.topic || topic.isEditing"> <div ng-show="room.current_room_state.state_events['m.room.topic'].content.topic || topic.isEditing">
<div ng-hide="topic.isEditing" ng-dblclick="topic.editTopic()" id="roomTopic"> <div ng-hide="topic.isEditing" ng-dblclick="topic.editTopic()" id="roomTopic">
{{ events.rooms[room_id]['m.room.topic'].content.topic | limitTo: 200}} {{ room.current_room_state.state_events['m.room.topic'].content.topic | limitTo: 200}}
</div> </div>
<form ng-submit="topic.updateTopic()" ng-show="topic.isEditing" class="roomTopicForm"> <form ng-submit="topic.updateTopic()" ng-show="topic.isEditing" class="roomTopicForm">
<input ng-model="topic.newTopicText" ng-blur="topic.cancelEdit()" class="roomTopicInput" placeholder="Topic"/> <input ng-model="topic.newTopicText" ng-blur="topic.cancelEdit()" class="roomTopicInput" placeholder="Topic"/>
@ -123,32 +124,34 @@
ng-style="{ 'visibility': state.messages_visibility }" ng-style="{ 'visibility': state.messages_visibility }"
keep-scroll> keep-scroll>
<table id="messageTable" infinite-scroll="paginateMore()"> <table id="messageTable" infinite-scroll="paginateMore()">
<tr ng-repeat="msg in events.rooms[room_id].messages" <tr ng-repeat="msg in room.events"
ng-class="(events.rooms[room_id].messages[$index + 1].user_id !== msg.user_id ? 'differentUser' : '') + (msg.user_id === state.user_id ? ' mine' : '')" scroll-item> ng-class="(room.events[$index + 1].user_id !== msg.user_id ? 'differentUser' : '') + (msg.user_id === state.user_id ? ' mine' : '')" scroll-item>
<td class="leftBlock"> <td class="leftBlock">
<div class="sender" ng-hide="events.rooms[room_id].messages[$index - 1].user_id === msg.user_id"> {{ msg.user_id | mUserDisplayName: room_id }}</div> <div class="sender" ng-hide="room.events[$index - 1].user_id === msg.user_id"> {{ msg.__room_member.cnt.displayname || msg.user_id | mUserDisplayName: room_id }}</div>
<div class="timestamp" <div class="timestamp"
ng-class="msg.echo_msg_state"> ng-class="msg.echo_msg_state">
{{ (msg.origin_server_ts) | date:'MMM d HH:mm' }} {{ (msg.origin_server_ts) | date:'MMM d HH:mm' }}
</div> </div>
</td> </td>
<td class="avatar"> <td class="avatar">
<img class="avatarImage" ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.png' }}" width="32" height="32" title="{{msg.user_id}}" <!-- msg.__room_member.avatar_url is just backwards compat, and can be removed in the future. -->
ng-hide="events.rooms[room_id].messages[$index - 1].user_id === msg.user_id || msg.user_id === state.user_id"/> <img class="avatarImage" ng-src="{{ msg.__room_member.cnt.avatar_url || msg.__room_member.avatar_url || 'img/default-profile.png' }}" width="32" height="32" title="{{msg.user_id}}"
ng-hide="room.events[$index - 1].user_id === msg.user_id || msg.user_id === state.user_id"/>
</td> </td>
<td ng-class="(!msg.content.membership && ('m.room.topic' !== msg.type && 'm.room.name' !== msg.type))? (msg.content.msgtype === 'm.emote' ? 'emote text' : 'text') : 'membership text'"> <td ng-class="(!msg.content.membership && ('m.room.topic' !== msg.type && 'm.room.name' !== msg.type))? (msg.content.msgtype === 'm.emote' ? 'emote text' : 'text') : 'membership text'">
<div class="bubble" ng-dblclick="openJson(msg)"> <div class="bubble" ng-dblclick="openJson(msg)">
<span ng-if="'join' === msg.content.membership && msg.changedKey === 'membership'"> <span ng-if="'join' === msg.content.membership && msg.changedKey === 'membership'">
{{ members[msg.state_key].displayname || msg.state_key }} joined {{ msg.content.displayname || members[msg.state_key].displayname || msg.state_key }} joined
</span> </span>
<span ng-if="'leave' === msg.content.membership && msg.changedKey === 'membership'"> <span ng-if="'leave' === msg.content.membership && msg.changedKey === 'membership'">
<span ng-if="msg.user_id === msg.state_key"> <span ng-if="msg.user_id === msg.state_key">
{{ members[msg.state_key].displayname || msg.state_key }} left <!-- FIXME: This seems like a synapse bug that the 'leave' content doesn't give the displayname... -->
{{ msg.__room_member.cnt.displayname || members[msg.state_key].displayname || msg.state_key }} left
</span> </span>
<span ng-if="msg.user_id !== msg.state_key && msg.prev_content"> <span ng-if="msg.user_id !== msg.state_key && msg.prev_content">
{{ members[msg.user_id].displayname || msg.user_id }} {{ msg.content.displayname || members[msg.user_id].displayname || msg.user_id }}
{{ {"invite": "kicked", "join": "kicked", "ban": "unbanned"}[msg.prev_content.membership] }} {{ {"invite": "kicked", "join": "kicked", "ban": "unbanned"}[msg.prev_content.membership] }}
{{ members[msg.state_key].displayname || msg.state_key }} {{ msg.__target_room_member.content.displayname || msg.state_key }}
<span ng-if="'join' === msg.prev_content.membership && msg.content.reason"> <span ng-if="'join' === msg.prev_content.membership && msg.content.reason">
: {{ msg.content.reason }} : {{ msg.content.reason }}
</span> </span>
@ -156,9 +159,9 @@
</span> </span>
<span ng-if="'invite' === msg.content.membership && msg.changedKey === 'membership' || <span ng-if="'invite' === msg.content.membership && msg.changedKey === 'membership' ||
'ban' === msg.content.membership && msg.changedKey === 'membership'"> 'ban' === msg.content.membership && msg.changedKey === 'membership'">
{{ members[msg.user_id].displayname || msg.user_id }} {{ msg.__room_member.cnt.displayname || msg.user_id }}
{{ {"invite": "invited", "ban": "banned"}[msg.content.membership] }} {{ {"invite": "invited", "ban": "banned"}[msg.content.membership] }}
{{ members[msg.state_key].displayname || msg.state_key }} {{ msg.__target_room_member.cnt.displayname || msg.state_key }}
<span ng-if="msg.prev_content && 'ban' === msg.prev_content.membership && msg.content.reason"> <span ng-if="msg.prev_content && 'ban' === msg.prev_content.membership && msg.content.reason">
: {{ msg.content.reason }} : {{ msg.content.reason }}
</span> </span>
@ -204,7 +207,7 @@
</td> </td>
<td class="rightBlock"> <td class="rightBlock">
<img class="avatarImage" ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.png' }}" width="32" height="32" <img class="avatarImage" ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.png' }}" width="32" height="32"
ng-hide="events.rooms[room_id].messages[$index - 1].user_id === msg.user_id || msg.user_id !== state.user_id"/> ng-hide="room.events[$index - 1].user_id === msg.user_id || msg.user_id !== state.user_id"/>
</td> </td>
</tr> </tr>
</table> </table>