diff --git a/CHANGES.rst b/CHANGES.rst
index 0853c0312c..4e536bc4de 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,5 +1,32 @@
-Latest
-======
+Changes in synapse 0.3.0 (2014-09-18)
+=====================================
+See UPGRADE for information about changes to the client server API, including
+breaking backwards compatibility with VoIP calls and registration API.
+
+Homeserver:
+ * When a user changes their displayname or avatar the server will now update
+ all their join states to reflect this.
+ * The server now adds "age" key to events to indicate how old they are. This
+ is clock independent, so at no point does any server or webclient have to
+ assume their clock is in sync with everyone else.
+ * Fix bug where we didn't correctly pull in missing PDUs.
+ * Fix bug where prev_content key wasn't always returned.
+ * Add support for password resets.
+
+Webclient:
+ * Improve page content loading.
+ * Join/parts now trigger desktop notifications.
+ * Always show room aliases in the UI if one is present.
+ * No longer show user-count in the recents side panel.
+ * Add up & down arrow support to the text box for message sending to step
+ through your sent history.
+ * Don't display notifications for our own messages.
+ * Emotes are now formatted correctly in desktop notifications.
+ * The recents list now differentiates between public & private rooms.
+ * Fix bug where when switching between rooms the pagination flickered before
+ the view jumped to the bottom of the screen.
+ * Add support for password resets.
+ * Add bing word support.
Registration API:
* The registration API has been overhauled to function like the login API. In
diff --git a/UPGRADE.rst b/UPGRADE.rst
index 44c0af7282..713fb9ae83 100644
--- a/UPGRADE.rst
+++ b/UPGRADE.rst
@@ -1,4 +1,4 @@
-Upgrading to Latest
+Upgrading to v0.3.0
===================
This registration API now closely matches the login API. This introduces a bit
@@ -20,6 +20,14 @@ to the next stage. There is a new login type: ``m.login.email.identity`` which
contains the ``threepidCreds`` key which were previously sent in the original
register request. For more information on this, see the specification.
+Web Client
+----------
+
+The VoIP specification has changed between v0.2.0 and v0.3.0. Users should
+refresh any browser tabs to get the latest web client code. Users on
+v0.2.0 of the web client will not be able to call those on v0.3.0 and
+vice versa.
+
Upgrading to v0.2.0
===================
diff --git a/VERSION b/VERSION
index 7179039691..0d91a54c7d 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.2.3
+0.3.0
diff --git a/docs/freenode.txt b/docs/freenode.txt
new file mode 100644
index 0000000000..84fdf6d523
--- /dev/null
+++ b/docs/freenode.txt
@@ -0,0 +1 @@
+NCjcRSEG
diff --git a/synapse/__init__.py b/synapse/__init__.py
index d60267ebe4..8ef176ea6f 100644
--- a/synapse/__init__.py
+++ b/synapse/__init__.py
@@ -16,4 +16,4 @@
""" This is a reference implementation of a synapse home server.
"""
-__version__ = "0.2.3"
+__version__ = "0.3.0"
diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py
index b63863e5b2..14fae689f2 100644
--- a/synapse/handlers/message.py
+++ b/synapse/handlers/message.py
@@ -268,6 +268,9 @@ class MessageHandler(BaseHandler):
user, pagination_config, None
)
+ public_rooms = yield self.store.get_rooms(is_public=True)
+ public_room_ids = [r["room_id"] for r in public_rooms]
+
limit = pagin_config.limit
if not limit:
limit = 10
@@ -276,6 +279,8 @@ class MessageHandler(BaseHandler):
d = {
"room_id": event.room_id,
"membership": event.membership,
+ "visibility": ("public" if event.room_id in
+ public_room_ids else "private"),
}
if event.membership == Membership.INVITE:
diff --git a/synapse/handlers/profile.py b/synapse/handlers/profile.py
index 023d8c0cf2..dab9b03f04 100644
--- a/synapse/handlers/profile.py
+++ b/synapse/handlers/profile.py
@@ -15,9 +15,9 @@
from twisted.internet import defer
-from synapse.api.errors import SynapseError, AuthError
-
-from synapse.api.errors import CodeMessageException
+from synapse.api.errors import SynapseError, AuthError, CodeMessageException
+from synapse.api.constants import Membership
+from synapse.api.events.room import RoomMemberEvent
from ._base import BaseHandler
@@ -97,6 +97,8 @@ class ProfileHandler(BaseHandler):
}
)
+ yield self._update_join_states(target_user)
+
@defer.inlineCallbacks
def get_avatar_url(self, target_user):
if target_user.is_mine:
@@ -144,6 +146,8 @@ class ProfileHandler(BaseHandler):
}
)
+ yield self._update_join_states(target_user)
+
@defer.inlineCallbacks
def collect_presencelike_data(self, user, state):
if not user.is_mine:
@@ -180,3 +184,39 @@ class ProfileHandler(BaseHandler):
)
defer.returnValue(response)
+
+ @defer.inlineCallbacks
+ def _update_join_states(self, user):
+ if not user.is_mine:
+ return
+
+ joins = yield self.store.get_rooms_for_user_where_membership_is(
+ user.to_string(),
+ [Membership.JOIN],
+ )
+
+ for j in joins:
+ snapshot = yield self.store.snapshot_room(
+ j.room_id, j.state_key, RoomMemberEvent.TYPE,
+ j.state_key
+ )
+
+ content = {
+ "membership": j.content["membership"],
+ "prev": j.content["membership"],
+ }
+
+ yield self.distributor.fire(
+ "collect_presencelike_data", user, content
+ )
+
+ new_event = self.event_factory.create_event(
+ etype=j.type,
+ room_id=j.room_id,
+ state_key=j.state_key,
+ content=content,
+ user_id=j.state_key,
+ )
+
+ yield self.state_handler.handle_new_event(new_event, snapshot)
+ yield self._on_new_room_event(new_event, snapshot)
diff --git a/tests/handlers/test_presencelike.py b/tests/handlers/test_presencelike.py
index 72c55b3667..047752ad68 100644
--- a/tests/handlers/test_presencelike.py
+++ b/tests/handlers/test_presencelike.py
@@ -65,6 +65,8 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
"is_presence_visible",
"set_profile_displayname",
+
+ "get_rooms_for_user_where_membership_is",
]),
handlers=None,
resource_for_federation=Mock(),
@@ -132,6 +134,10 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
# Remote user
self.u_potato = hs.parse_userid("@potato:remote")
+ self.mock_get_joined = (
+ self.datastore.get_rooms_for_user_where_membership_is
+ )
+
@defer.inlineCallbacks
def test_set_my_state(self):
self.presence_list = [
@@ -152,6 +158,11 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
@defer.inlineCallbacks
def test_push_local(self):
+ def get_joined(*args):
+ return defer.succeed([])
+
+ self.mock_get_joined.side_effect = get_joined
+
self.presence_list = [
{"observed_user_id": "@banana:test"},
{"observed_user_id": "@clementine:test"},
diff --git a/tests/handlers/test_profile.py b/tests/handlers/test_profile.py
index f95ddd7018..5dc9b456e1 100644
--- a/tests/handlers/test_profile.py
+++ b/tests/handlers/test_profile.py
@@ -22,6 +22,7 @@ from mock import Mock
from synapse.api.errors import AuthError
from synapse.server import HomeServer
from synapse.handlers.profile import ProfileHandler
+from synapse.api.constants import Membership
from tests.utils import SQLiteMemoryDbPool
diff --git a/webclient/app.css b/webclient/app.css
index 704cd83947..736aea660c 100755
--- a/webclient/app.css
+++ b/webclient/app.css
@@ -603,6 +603,10 @@ a:active { color: #000; }
width: auto;
}
+.recentsPublicRoom {
+ font-weight: bold;
+}
+
.recentsRoomSummaryUsersCount, .recentsRoomSummaryTS {
color: #888;
font-size: 12px;
diff --git a/webclient/components/matrix/event-handler-service.js b/webclient/components/matrix/event-handler-service.js
index 258de9a31e..ad69d297fa 100644
--- a/webclient/components/matrix/event-handler-service.js
+++ b/webclient/components/matrix/event-handler-service.js
@@ -200,11 +200,17 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
if (member) {
displayname = member.displayname;
}
+
+ var message = event.content.body;
+ if (event.content.msgtype === "m.emote") {
+ message = "* " + displayname + " " + message;
+ }
+
var notification = new window.Notification(
(displayname || event.user_id) +
" (" + (matrixService.getRoomIdToAliasMapping(event.room_id) || event.room_id) + ")", // FIXME: don't leak room_ids here
{
- "body": event.content.body,
+ "body": message,
"icon": member ? member.avatar_url : undefined
});
$timeout(function() {
@@ -237,8 +243,9 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
// add membership changes as if they were a room message if something interesting changed
// Exception: Do not do this if the event is a room state event because such events already come
// as room messages events. Moreover, when they come as room messages events, they are relatively ordered
- // with other other room messages
- if (event.content.prev !== event.content.membership && !isStateEvent) {
+ // with other other room messages XXX This is no longer true, you only get a single event, not a room message event.
+ // FIXME: This possibly reintroduces multiple join messages.
+ if (event.content.prev !== event.content.membership) { // && !isStateEvent
if (isLiveEvent) {
$rootScope.events.rooms[event.room_id].messages.push(event);
}
@@ -369,6 +376,7 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
handleMessage(event, isLiveEvent);
break;
case "m.room.member":
+ isStateEvent = true;
handleRoomMember(event, isLiveEvent, isStateEvent);
break;
case "m.presence":
@@ -398,6 +406,8 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
// isLiveEvents determines whether notifications should be shown, whether
// messages get appended to the start/end of lists, etc.
handleEvents: function(events, isLiveEvents, isStateEvents) {
+ // XXX FIXME TODO: isStateEvents is being left as undefined sometimes. It makes no sense
+ // to have isStateEvents as an arg, since things like m.room.member are ALWAYS state events.
for (var i=0; i<events.length; i++) {
this.handleEvent(events[i], isLiveEvents, isStateEvents);
}
@@ -413,6 +423,7 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
if (dir && 'b' === dir) {
// paginateBackMessages requests messages to be in reverse chronological order
for (var i=0; i<events.length; i++) {
+ // FIXME: Being live != being state
this.handleEvent(events[i], isLiveEvents, isLiveEvents);
}
@@ -422,6 +433,7 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
else {
// InitialSync returns messages in chronological order
for (var i=events.length - 1; i>=0; i--) {
+ // FIXME: Being live != being state
this.handleEvent(events[i], isLiveEvents, isLiveEvents);
}
// Store where to start pagination
@@ -505,6 +517,18 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
member = room.members[user_id];
}
return member;
+ },
+
+ setRoomVisibility: function(room_id, visible) {
+ if (!visible) {
+ return;
+ }
+ initRoom(room_id);
+
+ var room = $rootScope.events.rooms[room_id];
+ if (room) {
+ room.visibility = visible;
+ }
}
};
}]);
diff --git a/webclient/components/matrix/event-stream-service.js b/webclient/components/matrix/event-stream-service.js
index 6f92332246..5af1ab2911 100644
--- a/webclient/components/matrix/event-stream-service.js
+++ b/webclient/components/matrix/event-stream-service.js
@@ -120,6 +120,8 @@ angular.module('eventStreamService', [])
if ("state" in room) {
eventHandlerService.handleEvents(room.state, false, true);
}
+
+ eventHandlerService.setRoomVisibility(room.room_id, room.visibility);
}
var presence = response.data.presence;
diff --git a/webclient/components/matrix/matrix-call.js b/webclient/components/matrix/matrix-call.js
index bf1e61ad7e..2ecb8b05ff 100644
--- a/webclient/components/matrix/matrix-call.js
+++ b/webclient/components/matrix/matrix-call.js
@@ -59,7 +59,7 @@ angular.module('MatrixCall', [])
var stunServer = 'stun:stun.l.google.com:19302';
var pc;
if (window.mozRTCPeerConnection) {
- pc = window.mozRTCPeerConnection({'url': stunServer});
+ pc = new window.mozRTCPeerConnection({'url': stunServer});
} else {
pc = new window.RTCPeerConnection({"iceServers":[{"urls":"stun:stun.l.google.com:19302"}]});
}
diff --git a/webclient/components/matrix/matrix-filter.js b/webclient/components/matrix/matrix-filter.js
index 015a88bcad..8b168cdedb 100644
--- a/webclient/components/matrix/matrix-filter.js
+++ b/webclient/components/matrix/matrix-filter.js
@@ -26,72 +26,74 @@ angular.module('matrixFilter', [])
// If there is an alias, use it
// TODO: only one alias is managed for now
var alias = matrixService.getRoomIdToAliasMapping(room_id);
- if (alias) {
- roomName = alias;
- }
-
- if (undefined === roomName) {
- var room = $rootScope.events.rooms[room_id];
- if (room) {
- // Get name from room state date
- var room_name_event = room["m.room.name"];
- if (room_name_event) {
- roomName = room_name_event.content.name;
- }
- else if (room.members) {
- // Else, build the name from its users
- // FIXME: Is it still required?
- // Limit the room renaming to 1:1 room
- if (2 === Object.keys(room.members).length) {
- for (var i in room.members) {
- var member = room.members[i];
- if (member.state_key !== matrixService.config().user_id) {
+ var room = $rootScope.events.rooms[room_id];
+ if (room) {
+ // Get name from room state date
+ var room_name_event = room["m.room.name"];
+ if (room_name_event) {
+ roomName = room_name_event.content.name;
+ }
+ else if (alias) {
+ roomName = alias;
+ }
+ else if (room.members) {
+ // Else, build the name from its users
+ // FIXME: Is it still required?
+ // Limit the room renaming to 1:1 room
+ if (2 === Object.keys(room.members).length) {
+ for (var i in room.members) {
+ var member = room.members[i];
+ if (member.state_key !== matrixService.config().user_id) {
- if (member.state_key in $rootScope.presence) {
- // If the user is available in presence, use the displayname there
- // as it is the most uptodate
- roomName = $rootScope.presence[member.state_key].content.displayname;
- }
- else if (member.content.displayname) {
- roomName = member.content.displayname;
- }
- else {
- roomName = member.state_key;
- }
+ if (member.state_key in $rootScope.presence) {
+ // If the user is available in presence, use the displayname there
+ // as it is the most uptodate
+ roomName = $rootScope.presence[member.state_key].content.displayname;
}
- }
- }
- else if (1 === Object.keys(room.members).length) {
- // The other member may be in the invite list, get all invited users
- var invitedUserIDs = [];
- for (var i in room.messages) {
- var message = room.messages[i];
- if ("m.room.member" === message.type && "invite" === message.membership) {
- // Make sure there is no duplicate user
- if (-1 === invitedUserIDs.indexOf(message.state_key)) {
- invitedUserIDs.push(message.state_key);
- }
- }
- }
-
- // For now, only 1:1 room needs to be renamed. It means only 1 invited user
- if (1 === invitedUserIDs.length) {
- var userID = invitedUserIDs[0];
-
- // Try to resolve his displayname in presence global data
- if (userID in $rootScope.presence) {
- roomName = $rootScope.presence[userID].content.displayname;
+ else if (member.content.displayname) {
+ roomName = member.content.displayname;
}
else {
- roomName = userID;
+ roomName = member.state_key;
}
}
}
}
+ else if (1 === Object.keys(room.members).length) {
+ // The other member may be in the invite list, get all invited users
+ var invitedUserIDs = [];
+ for (var i in room.messages) {
+ var message = room.messages[i];
+ if ("m.room.member" === message.type && "invite" === message.membership) {
+ // Make sure there is no duplicate user
+ if (-1 === invitedUserIDs.indexOf(message.state_key)) {
+ invitedUserIDs.push(message.state_key);
+ }
+ }
+ }
+
+ // For now, only 1:1 room needs to be renamed. It means only 1 invited user
+ if (1 === invitedUserIDs.length) {
+ var userID = invitedUserIDs[0];
+
+ // Try to resolve his displayname in presence global data
+ if (userID in $rootScope.presence) {
+ roomName = $rootScope.presence[userID].content.displayname;
+ }
+ else {
+ roomName = userID;
+ }
+ }
+ }
}
}
+ // Always show the alias in the room displayed name
+ if (roomName && alias && alias !== roomName) {
+ roomName += " (" + alias + ")";
+ }
+
if (undefined === roomName) {
// By default, use the room ID
roomName = room_id;
diff --git a/webclient/home/home-controller.js b/webclient/home/home-controller.js
index 8ba817ca68..e35219bebb 100644
--- a/webclient/home/home-controller.js
+++ b/webclient/home/home-controller.js
@@ -53,6 +53,8 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen
// Add room_alias & room_display_name members
angular.extend(room, matrixService.getRoomAliasAndDisplayName(room));
+
+ eventHandlerService.setRoomVisibility(room.room_id, "public");
}
}
);
diff --git a/webclient/recents/recents-filter.js b/webclient/recents/recents-filter.js
index 2fd4dbe98b..d948205e19 100644
--- a/webclient/recents/recents-filter.js
+++ b/webclient/recents/recents-filter.js
@@ -35,9 +35,8 @@ angular.module('RecentsController')
// Count users here
// TODO: Compute it directly in eventHandlerService
room.numUsersInRoom = eventHandlerService.getUsersCountInRoom(room_id);
-
- filtered.push(room);
}
+ filtered.push(room);
});
// And time sort them
@@ -61,4 +60,4 @@ angular.module('RecentsController')
});
return filtered;
};
-}]);
\ No newline at end of file
+}]);
diff --git a/webclient/recents/recents.html b/webclient/recents/recents.html
index e783d3a6b4..edfc1677eb 100644
--- a/webclient/recents/recents.html
+++ b/webclient/recents/recents.html
@@ -5,7 +5,7 @@
class ="recentsRoom"
ng-class="{'recentsRoomSelected': (room.room_id === recentsSelectedRoomID)}">
<tr>
- <td class="recentsRoomName">
+ <td ng-class="room['m.room.join_rules'].content.join_rule == 'public' ? 'recentsRoomName recentsPublicRoom' : 'recentsRoomName'">
{{ room.room_id | mRoomName }}
</td>
<td class="recentsRoomSummaryUsersCount">
diff --git a/webclient/room/room-controller.js b/webclient/room/room-controller.js
index 6e1d83a23d..de50058743 100644
--- a/webclient/room/room-controller.js
+++ b/webclient/room/room-controller.js
@@ -32,7 +32,8 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
can_paginate: false, // this is toggled off when we are not ready yet to paginate or when we run out of items
paginating: false, // used to avoid concurrent pagination requests pulling in dup contents
stream_failure: undefined, // the response when the stream fails
- waiting_for_joined_event: false // true when the join request is pending. Back to false once the corresponding m.room.member event is received
+ waiting_for_joined_event: false, // true when the join request is pending. Back to false once the corresponding m.room.member event is received
+ messages_visibility: "hidden" // In order to avoid flickering when scrolling down the message table at the page opening, delay the message table display
};
$scope.members = {};
$scope.autoCompleting = false;
@@ -53,8 +54,13 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
return;
};
- // Use the filter applied in html to set the input value
- $scope.name.newNameText = $filter('mRoomName')($scope.room_id);
+ var nameEvent = $rootScope.events.rooms[$scope.room_id]['m.room.name'];
+ if (nameEvent) {
+ $scope.name.newNameText = nameEvent.content.name;
+ }
+ else {
+ $scope.name.newNameText = "";
+ }
// Force focus to the input
$timeout(function() {
@@ -131,6 +137,13 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
$timeout(function() {
objDiv.scrollTop = objDiv.scrollHeight;
+
+ // Show the message table once the first scrolldown is done
+ if ("visible" !== $scope.state.messages_visibility) {
+ $timeout(function() {
+ $scope.state.messages_visibility = "visible";
+ }, 0);
+ }
}, 0);
}
};
@@ -404,12 +417,15 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
};
$scope.send = function() {
- if ($scope.textInput === "") {
+ if (undefined === $scope.textInput || $scope.textInput === "") {
return;
}
scrollToBottom(true);
-
+
+ // Store the command in the history
+ history.push($scope.textInput);
+
var promise;
var cmd;
var args;
@@ -735,7 +751,10 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
// Make recents highlight the current room
$scope.recentsSelectedRoomID = $scope.room_id;
- // Get the up-to-date the current member list
+ // Init the history for this room
+ history.init();
+
+ // Get the up-to-date the current member list
matrixService.getMemberList($scope.room_id).then(
function(response) {
for (var i = 0; i < response.data.chunk.length; i++) {
@@ -847,4 +866,82 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
$rootScope.currentCall = call;
};
+ // Manage history of typed messages
+ // History is saved in sessionStoratge so that it survives when the user
+ // navigates through the rooms and when it refreshes the page
+ var history = {
+ // The list of typed messages. Index 0 is the more recents
+ data: [],
+
+ // The position in the history currently displayed
+ position: -1,
+
+ // The message the user has started to type before going into the history
+ typingMessage: undefined,
+
+ // Init/load data for the current room
+ init: function() {
+ var data = sessionStorage.getItem("history_" + $scope.room_id);
+ if (data) {
+ this.data = JSON.parse(data);
+ }
+ },
+
+ // Store a message in the history
+ push: function(message) {
+ this.data.unshift(message);
+
+ // Update the session storage
+ sessionStorage.setItem("history_" + $scope.room_id, JSON.stringify(this.data));
+
+ // Reset history position
+ this.position = -1;
+ this.typingMessage = undefined;
+ },
+
+ // Move in the history
+ go: function(offset) {
+
+ if (-1 === this.position) {
+ // User starts to go to into the history, save the current line
+ this.typingMessage = $scope.textInput;
+ }
+ else {
+ // If the user modified this line in history, keep the change
+ this.data[this.position] = $scope.textInput;
+ }
+
+ // Bounds the new position to valid data
+ var newPosition = this.position + offset;
+ newPosition = Math.max(-1, newPosition);
+ newPosition = Math.min(newPosition, this.data.length - 1);
+ this.position = newPosition;
+
+ if (-1 !== this.position) {
+ // Show the message from the history
+ $scope.textInput = this.data[this.position];
+ }
+ else if (undefined !== this.typingMessage) {
+ // Go back to the message the user started to type
+ $scope.textInput = this.typingMessage;
+ }
+ }
+ };
+
+ // Make history singleton methods available from HTML
+ $scope.history = {
+ goUp: function($event) {
+ if ($scope.room_id) {
+ history.go(1);
+ }
+ $event.preventDefault();
+ },
+ goDown: function($event) {
+ if ($scope.room_id) {
+ history.go(-1);
+ }
+ $event.preventDefault();
+ }
+ };
+
}]);
diff --git a/webclient/room/room.html b/webclient/room/room.html
index 886c2afe64..44a0e34d9f 100644
--- a/webclient/room/room.html
+++ b/webclient/room/room.html
@@ -9,7 +9,7 @@
{{ room_id | mRoomName }}
</div>
<form ng-submit="name.updateName()" ng-show="name.isEditing" class="roomNameForm">
- <input ng-model="name.newNameText" ng-blur="name.cancelEdit()" class="roomNameInput" />
+ <input ng-model="name.newNameText" ng-blur="name.cancelEdit()" class="roomNameInput" placeholder="Room name"/>
</form>
</div>
@@ -23,7 +23,7 @@
{{ events.rooms[room_id]['m.room.topic'].content.topic | limitTo: 200}}
</div>
<form ng-submit="topic.updateTopic()" ng-show="topic.isEditing" class="roomTopicForm">
- <input ng-model="topic.newTopicText" ng-blur="topic.cancelEdit()" class="roomTopicInput" />
+ <input ng-model="topic.newTopicText" ng-blur="topic.cancelEdit()" class="roomTopicInput" placeholder="Topic"/>
</form>
</div>
</div>
@@ -56,7 +56,10 @@
</table>
</div>
- <div id="messageTableWrapper" ng-hide="state.permission_denied" keep-scroll>
+ <div id="messageTableWrapper"
+ ng-hide="state.permission_denied"
+ ng-style="{ 'visibility': state.messages_visibility }"
+ keep-scroll>
<!-- FIXME: need to have better timestamp semantics than the (msg.content.hsob_ts || msg.ts) hack below -->
<table id="messageTable" infinite-scroll="paginateMore()">
<tr ng-repeat="msg in events.rooms[room_id].messages"
@@ -156,7 +159,8 @@
<td width="*">
<textarea id="mainInput" rows="1" ng-model="textInput" ng-enter="send()"
ng-disabled="state.permission_denied"
- ng-focus="true" autocomplete="off" tab-complete/>
+ ng-keydown="(38 === $event.which) ? history.goUp($event) : ((40 === $event.which) ? history.goDown($event) : 0)"
+ ng-focus="true" autocomplete="off" tab-complete/>
</td>
<td id="buttonsCell">
<button ng-click="send()" ng-disabled="state.permission_denied">Send</button>
|