diff --git a/webclient/app-controller.js b/webclient/app-controller.js
index 7d61207554..e4b7cd286f 100644
--- a/webclient/app-controller.js
+++ b/webclient/app-controller.js
@@ -53,7 +53,7 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
* Open a given page.
* @param {String} url url of the page
*/
- $scope.goToPage = function(url) {
+ $rootScope.goToPage = function(url) {
$location.url(url);
};
diff --git a/webclient/app.css b/webclient/app.css
index dc2d589c58..20a13aad81 100755
--- a/webclient/app.css
+++ b/webclient/app.css
@@ -416,7 +416,6 @@ textarea, input {
text-align: right;
float: right;
margin-top: 15px;
- width: 100%;
}
/*** Participant list ***/
diff --git a/webclient/components/matrix/event-handler-service.js b/webclient/components/matrix/event-handler-service.js
index b1580b3d8a..3b1354cdef 100644
--- a/webclient/components/matrix/event-handler-service.js
+++ b/webclient/components/matrix/event-handler-service.js
@@ -58,14 +58,29 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
var shouldBing = false;
// case-insensitive name check for user_id OR display_name if they exist
+ var userRegex = "";
var myUserId = matrixService.config().user_id;
if (myUserId) {
- myUserId = myUserId.toLocaleLowerCase();
+ var localpart = getLocalPartFromUserId(myUserId);
+ if (localpart) {
+ localpart = localpart.toLocaleLowerCase();
+ userRegex += "\\b" + localpart + "\\b";
+ }
}
var myDisplayName = matrixService.config().display_name;
if (myDisplayName) {
myDisplayName = myDisplayName.toLocaleLowerCase();
+ if (userRegex.length > 0) {
+ userRegex += "|";
+ }
+ userRegex += "\\b" + myDisplayName + "\\b";
}
+
+ var r = new RegExp(userRegex, 'i');
+ if (content.search(r) >= 0) {
+ shouldBing = true;
+ }
+
if ( (myDisplayName && content.toLocaleLowerCase().indexOf(myDisplayName) != -1) ||
(myUserId && content.toLocaleLowerCase().indexOf(myUserId) != -1) ) {
shouldBing = true;
@@ -84,6 +99,18 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
return shouldBing;
};
+ var getLocalPartFromUserId = function(user_id) {
+ if (!user_id) {
+ return null;
+ }
+ var localpartRegex = /@(.*):\w+/i
+ var results = localpartRegex.exec(user_id);
+ if (results && results.length == 2) {
+ return results[1];
+ }
+ return null;
+ };
+
var initialSyncDeferred;
var reset = function() {
@@ -172,6 +199,17 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
};
var handleMessage = function(event, isLiveEvent) {
+ // Check for empty event content
+ var hasContent = false;
+ for (var prop in event.content) {
+ hasContent = true;
+ break;
+ }
+ if (!hasContent) {
+ // empty json object is a redacted event, so ignore.
+ return;
+ }
+
if (isLiveEvent) {
if (event.user_id === matrixService.config().user_id &&
(event.content.msgtype === "m.text" || event.content.msgtype === "m.emote") ) {
@@ -238,6 +276,12 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
"body": message,
"icon": member ? member.avatar_url : undefined
});
+
+ notification.onclick = function() {
+ console.log("notification.onclick() room=" + event.room_id);
+ $rootScope.goToPage('room/' + (event.room_id));
+ };
+
$timeout(function() {
notification.close();
}, 5 * 1000);
@@ -329,6 +373,31 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
$rootScope.events.rooms[event.room_id].messages.push(event);
}
};
+
+ var handleRedaction = function(event, isLiveEvent) {
+ if (!isLiveEvent) {
+ // we have nothing to remove, so just ignore it.
+ console.log("Received redacted event: "+JSON.stringify(event));
+ return;
+ }
+
+ // we need to remove something possibly: do we know the redacted
+ // event ID?
+ if (eventMap[event.redacts]) {
+ // remove event from list of messages in this room.
+ var eventList = $rootScope.events.rooms[event.room_id].messages;
+ for (var i=0; i<eventList.length; i++) {
+ if (eventList[i].event_id === event.redacts) {
+ console.log("Removing event " + event.redacts);
+ eventList.splice(i, 1);
+ break;
+ }
+ }
+
+ // broadcast the redaction so controllers can nuke this
+ console.log("Redacted an event.");
+ }
+ }
/**
* Get the index of the event in $rootScope.events.rooms[room_id].messages
@@ -491,6 +560,9 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
case 'm.room.topic':
handleRoomTopic(event, isLiveEvent, isStateEvent);
break;
+ case 'm.room.redaction':
+ handleRedaction(event, isLiveEvent);
+ break;
default:
console.log("Unable to handle event type " + event.type);
console.log(JSON.stringify(event, undefined, 4));
diff --git a/webclient/components/matrix/matrix-filter.js b/webclient/components/matrix/matrix-filter.js
index e6f2acc5fd..3d64a569a1 100644
--- a/webclient/components/matrix/matrix-filter.js
+++ b/webclient/components/matrix/matrix-filter.js
@@ -47,7 +47,6 @@ angular.module('matrixFilter', [])
else if (room.members && !isPublicRoom) { // Do not rename public room
var user_id = matrixService.config().user_id;
-
// Else, build the name from its users
// Limit the room renaming to 1:1 room
if (2 === Object.keys(room.members).length) {
@@ -65,8 +64,16 @@ angular.module('matrixFilter', [])
var otherUserId;
- if (Object.keys(room.members)[0] && Object.keys(room.members)[0] !== user_id) {
+ if (Object.keys(room.members)[0]) {
otherUserId = Object.keys(room.members)[0];
+ // this could be an invite event (from event stream)
+ if (otherUserId === user_id &&
+ room.members[user_id].content.membership === "invite") {
+ // this is us being invited to this room, so the
+ // *user_id* is the other user ID and not the state
+ // key.
+ otherUserId = room.members[user_id].user_id;
+ }
}
else {
// it's got to be an invite, or failing that a self-chat;
diff --git a/webclient/components/matrix/matrix-service.js b/webclient/components/matrix/matrix-service.js
index a4f0568bce..1840cf46c0 100644
--- a/webclient/components/matrix/matrix-service.js
+++ b/webclient/components/matrix/matrix-service.js
@@ -438,6 +438,14 @@ angular.module('matrixService', [])
return this.sendMessage(room_id, msg_id, content);
},
+ redactEvent: function(room_id, event_id) {
+ var path = "/rooms/$room_id/redact/$event_id";
+ path = path.replace("$room_id", room_id);
+ path = path.replace("$event_id", event_id);
+ var content = {};
+ return doRequest("POST", path, undefined, content);
+ },
+
// get a snapshot of the members in a room.
getMemberList: function(room_id) {
// Like the cmd client, escape room ids
diff --git a/webclient/room/room-controller.js b/webclient/room/room-controller.js
index 78520a829d..841b5cccdd 100644
--- a/webclient/room/room-controller.js
+++ b/webclient/room/room-controller.js
@@ -133,7 +133,9 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
// Do not autoscroll to the bottom to display the new event if the user is not at the bottom.
// Exception: in case where the event is from the user, we want to force scroll to the bottom
var objDiv = document.getElementById("messageTableWrapper");
- if ((objDiv.offsetHeight + objDiv.scrollTop >= objDiv.scrollHeight) || force) {
+ // add a 10px buffer to this check so if the message list is not *quite*
+ // at the bottom it still scrolls since it basically is at the bottom.
+ if ((10 + objDiv.offsetHeight + objDiv.scrollTop >= objDiv.scrollHeight) || force) {
$timeout(function() {
objDiv.scrollTop = objDiv.scrollHeight;
@@ -983,10 +985,45 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
};
$scope.openJson = function(content) {
- console.log("Displaying modal dialog for " + JSON.stringify(content));
+ $scope.event_selected = content;
+ // scope this so the template can check power levels and enable/disable
+ // buttons
+ $scope.pow = matrixService.getUserPowerLevel;
+
var modalInstance = $modal.open({
- template: "<pre>" + angular.toJson(content, true) + "</pre>"
+ templateUrl: 'eventInfoTemplate.html',
+ controller: 'EventInfoController',
+ scope: $scope
+ });
+
+ modalInstance.result.then(function(action) {
+ if (action === "redact") {
+ var eventId = $scope.event_selected.event_id;
+ console.log("Redacting event ID " + eventId);
+ matrixService.redactEvent(
+ $scope.event_selected.room_id,
+ eventId
+ ).then(function(response) {
+ console.log("Redaction = " + JSON.stringify(response));
+ }, function(error) {
+ console.error("Failed to redact event: "+JSON.stringify(error));
+ if (error.data.error) {
+ $scope.feedback = error.data.error;
+ }
+ });
+ }
+ }, function() {
+ // any dismiss code
});
};
-}]);
+}])
+.controller('EventInfoController', function($scope, $modalInstance) {
+ console.log("Displaying modal dialog for >>>> " + JSON.stringify($scope.event_selected));
+ $scope.redact = function() {
+ console.log("User level = "+$scope.pow($scope.room_id, $scope.state.user_id)+
+ " Redact level = "+$scope.events.rooms[$scope.room_id]["m.room.ops_levels"].content.redact_level);
+ console.log("Redact event >> " + JSON.stringify($scope.event_selected));
+ $modalInstance.close("redact");
+ };
+});
diff --git a/webclient/room/room.html b/webclient/room/room.html
index e753b037fe..38b6d591ea 100644
--- a/webclient/room/room.html
+++ b/webclient/room/room.html
@@ -1,5 +1,18 @@
<div ng-controller="RoomController" data-ng-init="onInit()" class="room" style="height: 100%;">
+ <script type="text/ng-template" id="eventInfoTemplate.html">
+ <div class="modal-body">
+ <pre> {{event_selected | json}} </pre>
+ </div>
+ <div class="modal-footer">
+ <button ng-click="redact()" type="button" class="btn btn-danger"
+ 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"
+ title="Delete this event on all home servers. This cannot be undone.">
+ Redact
+ </button>
+ </div>
+ </script>
+
<div id="roomHeader">
<a href ng-click="goToPage('/')"><img src="img/logo-small.png" width="100" height="43" alt="[matrix]"/></a>
<div class="roomHeaderInfo">
@@ -83,11 +96,11 @@
ng-hide="events.rooms[room_id].messages[$index - 1].user_id === msg.user_id || msg.user_id === state.user_id"/>
</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'">
- <div class="bubble">
- <span ng-if="'join' === msg.content.membership && msg.changedKey === 'membership'" ng-click="openJson(msg)">
+ <div class="bubble" ng-click="openJson(msg)">
+ <span ng-if="'join' === msg.content.membership && msg.changedKey === 'membership'">
{{ members[msg.state_key].displayname || msg.state_key }} joined
</span>
- <span ng-if="'leave' === msg.content.membership && msg.changedKey === 'membership'" ng-click="openJson(msg)">
+ <span ng-if="'leave' === msg.content.membership && msg.changedKey === 'membership'">
<span ng-if="msg.user_id === msg.state_key">
{{ members[msg.state_key].displayname || msg.state_key }} left
</span>
@@ -101,7 +114,7 @@
</span>
</span>
<span ng-if="'invite' === msg.content.membership && msg.changedKey === 'membership' ||
- 'ban' === msg.content.membership && msg.changedKey === 'membership'" ng-click="openJson(msg)">
+ 'ban' === msg.content.membership && msg.changedKey === 'membership'">
{{ members[msg.user_id].displayname || msg.user_id }}
{{ {"invite": "invited", "ban": "banned"}[msg.content.membership] }}
{{ members[msg.state_key].displayname || msg.state_key }}
@@ -109,25 +122,24 @@
: {{ msg.content.reason }}
</span>
</span>
- <span ng-if="msg.changedKey === 'displayname'" ng-click="openJson(msg)">
+ <span ng-if="msg.changedKey === 'displayname'">
{{ msg.user_id }} changed their display name from {{ msg.prev_content.displayname }} to {{ msg.content.displayname }}
</span>
<span ng-show='msg.content.msgtype === "m.emote"'
ng-class="msg.echo_msg_state"
ng-bind-html="'* ' + (members[msg.user_id].displayname || msg.user_id) + ' ' + msg.content.body | linky:'_blank'"
- ng-click="openJson(msg)"/>
+ />
<span ng-show='msg.content.msgtype === "m.text"'
class="message"
- ng-click="openJson(msg)"
ng-class="containsBingWord(msg.content.body) && msg.user_id != state.user_id ? msg.echo_msg_state + ' messageBing' : msg.echo_msg_state"
ng-bind-html="(msg.content.msgtype === 'm.text' && msg.type === 'm.room.message' && msg.content.format === 'org.matrix.custom.html') ?
(msg.content.formatted_body | unsanitizedLinky) :
(msg.content.msgtype === 'm.text' && msg.type === 'm.room.message') ? (msg.content.body | linky:'_blank') : '' "/>
- <span ng-show='msg.type === "m.call.invite" && msg.user_id == state.user_id' ng-click="openJson(msg)">Outgoing Call{{ isWebRTCSupported ? '' : ' (But your browser does not support VoIP)' }}</span>
- <span ng-show='msg.type === "m.call.invite" && msg.user_id != state.user_id' ng-click="openJson(msg)">Incoming Call{{ isWebRTCSupported ? '' : ' (But your browser does not support VoIP)' }}</span>
+ <span ng-show='msg.type === "m.call.invite" && msg.user_id == state.user_id'>Outgoing Call{{ isWebRTCSupported ? '' : ' (But your browser does not support VoIP)' }}</span>
+ <span ng-show='msg.type === "m.call.invite" && msg.user_id != state.user_id'>Incoming Call{{ isWebRTCSupported ? '' : ' (But your browser does not support VoIP)' }}</span>
<div ng-show='msg.content.msgtype === "m.image"'>
<div ng-hide='msg.content.thumbnail_url' ng-style="msg.content.body.h && { 'height' : (msg.content.body.h < 320) ? msg.content.body.h : 320}">
@@ -135,15 +147,15 @@
</div>
<div ng-show='msg.content.thumbnail_url' ng-style="{ 'height' : msg.content.thumbnail_info.h }">
<img class="image mouse-pointer" ng-src="{{ msg.content.thumbnail_url }}"
- ng-click="$parent.fullScreenImageURL = msg.content.url"/>
+ ng-click="$parent.fullScreenImageURL = msg.content.url; $event.stopPropagation();"/>
</div>
</div>
- <span ng-if="'m.room.topic' === msg.type" ng-click="openJson(msg)">
+ <span ng-if="'m.room.topic' === msg.type">
{{ members[msg.user_id].displayname || msg.user_id }} changed the topic to: {{ msg.content.topic }}
</span>
- <span ng-if="'m.room.name' === msg.type" ng-click="openJson(msg)">
+ <span ng-if="'m.room.name' === msg.type">
{{ members[msg.user_id].displayname || msg.user_id }} changed the room name to: {{ msg.content.name }}
</span>
|