diff --git a/webclient/room/room-controller.js b/webclient/room/room-controller.js
new file mode 100644
index 0000000000..f3836c536b
--- /dev/null
+++ b/webclient/room/room-controller.js
@@ -0,0 +1,214 @@
+angular.module('RoomController', [])
+.controller('RoomController', ['$scope', '$http', '$timeout', '$routeParams', '$location', 'matrixService',
+ function($scope, $http, $timeout, $routeParams, $location, matrixService) {
+ 'use strict';
+ $scope.room_id = $routeParams.room_id;
+ $scope.room_alias = matrixService.getRoomIdToAliasMapping($scope.room_id);
+ $scope.state = {
+ user_id: matrixService.config().user_id,
+ events_from: "START"
+ };
+ $scope.messages = [];
+ $scope.members = {};
+ $scope.stopPoll = false;
+
+ $scope.userIDToInvite = "";
+
+ var shortPoll = function() {
+ $http.get(matrixService.config().homeserver + matrixService.prefix + "/events", {
+ "params": {
+ "access_token": matrixService.config().access_token,
+ "from": $scope.state.events_from,
+ "timeout": 5000
+ }})
+ .then(function(response) {
+ console.log("Got response from "+$scope.state.events_from+" to "+response.data.end);
+ $scope.state.events_from = response.data.end;
+
+ for (var i = 0; i < response.data.chunk.length; i++) {
+ var chunk = response.data.chunk[i];
+ if (chunk.room_id == $scope.room_id && chunk.type == "m.room.message") {
+ if ("membership_target" in chunk.content) {
+ chunk.user_id = chunk.content.membership_target;
+ }
+ $scope.messages.push(chunk);
+ $timeout(function() {
+ window.scrollTo(0, document.body.scrollHeight);
+ },0);
+ }
+ else if (chunk.room_id == $scope.room_id && chunk.type == "m.room.member") {
+ updateMemberList(chunk);
+ }
+ else if (chunk.type === "m.presence") {
+ updatePresence(chunk);
+ }
+ }
+ if ($scope.stopPoll) {
+ console.log("Stopping polling.");
+ }
+ else {
+ $timeout(shortPoll, 0);
+ }
+ }, function(response) {
+ $scope.feedback = "Can't stream: " + JSON.stringify(response);
+ if ($scope.stopPoll) {
+ console.log("Stopping polling.");
+ }
+ else {
+ $timeout(shortPoll, 2000);
+ }
+ });
+ };
+
+ var updateMemberList = function(chunk) {
+ var isNewMember = !(chunk.target_user_id in $scope.members);
+ if (isNewMember) {
+ $scope.members[chunk.target_user_id] = chunk;
+ // get their display name and profile picture and set it to their
+ // member entry in $scope.members. We HAVE to use $timeout with 0 delay
+ // to make this function run AFTER the current digest cycle, else the
+ // response may update a STALE VERSION of the member list (manifesting
+ // as no member names appearing, or appearing sporadically).
+ $scope.$evalAsync(function() {
+ matrixService.getDisplayName(chunk.target_user_id).then(
+ function(response) {
+ var member = $scope.members[chunk.target_user_id];
+ if (member !== undefined) {
+ console.log("Updated displayname "+chunk.target_user_id+" to " + response.displayname);
+ member.displayname = response.displayname;
+ }
+ }
+ );
+ matrixService.getProfilePictureUrl(chunk.target_user_id).then(
+ function(response) {
+ var member = $scope.members[chunk.target_user_id];
+ if (member !== undefined) {
+ console.log("Updated image for "+chunk.target_user_id+" to " + response.avatar_url);
+ member.avatar_url = response.avatar_url;
+ }
+ }
+ );
+ });
+ }
+ else {
+ // selectively update membership else it will nuke the picture and displayname too :/
+ var member = $scope.members[chunk.target_user_id];
+ member.content.membership = chunk.content.membership;
+ }
+ }
+
+ var updatePresence = function(chunk) {
+ if (!(chunk.content.user_id in $scope.members)) {
+ console.log("updatePresence: Unknown member for chunk " + JSON.stringify(chunk));
+ return;
+ }
+ var member = $scope.members[chunk.content.user_id];
+
+ if ("state" in chunk.content) {
+ var ONLINE = 2;
+ var AWAY = 1;
+ var OFFLINE = 0;
+ if (chunk.content.state === ONLINE) {
+ member.presenceState = "online";
+ }
+ else if (chunk.content.state === OFFLINE) {
+ member.presenceState = "offline";
+ }
+ else if (chunk.content.state === AWAY) {
+ member.presenceState = "away";
+ }
+ }
+
+ // this may also contain a new display name or avatar url, so check.
+ if ("displayname" in chunk.content) {
+ member.displayname = chunk.content.displayname;
+ }
+
+ if ("avatar_url" in chunk.content) {
+ member.avatar_url = chunk.content.avatar_url;
+ }
+ }
+
+ $scope.send = function() {
+ if ($scope.textInput == "") {
+ return;
+ }
+
+ // Send the text message
+ var promise;
+ // FIXME: handle other commands too
+ if ($scope.textInput.indexOf("/me") == 0) {
+ promise = matrixService.sendEmoteMessage($scope.room_id, $scope.textInput.substr(4));
+ }
+ else {
+ promise = matrixService.sendTextMessage($scope.room_id, $scope.textInput);
+ }
+
+ promise.then(
+ function() {
+ console.log("Sent message");
+ $scope.textInput = "";
+ },
+ function(reason) {
+ $scope.feedback = "Failed to send: " + reason;
+ });
+ };
+
+ $scope.onInit = function() {
+ // $timeout(function() { document.getElementById('textInput').focus() }, 0);
+ console.log("onInit");
+
+ // Join the room
+ matrixService.join($scope.room_id).then(
+ function() {
+ console.log("Joined room");
+ // Now start reading from the stream
+ $timeout(shortPoll, 0);
+
+ // Get the current member list
+ matrixService.getMemberList($scope.room_id).then(
+ function(response) {
+ for (var i = 0; i < response.chunk.length; i++) {
+ var chunk = response.chunk[i];
+ updateMemberList(chunk);
+ }
+ },
+ function(reason) {
+ $scope.feedback = "Failed get member list: " + reason;
+ }
+ );
+ },
+ function(reason) {
+ $scope.feedback = "Can't join room: " + reason;
+ });
+ };
+
+ $scope.inviteUser = function(user_id) {
+
+ matrixService.invite($scope.room_id, user_id).then(
+ function() {
+ console.log("Invited.");
+ $scope.feedback = "Request for invitation succeeds";
+ },
+ function(reason) {
+ $scope.feedback = "Failure: " + reason;
+ });
+ };
+
+ $scope.leaveRoom = function() {
+
+ matrixService.leave($scope.room_id).then(
+ function(response) {
+ console.log("Left room ");
+ $location.path("rooms");
+ },
+ function(reason) {
+ $scope.feedback = "Failed to leave room: " + reason;
+ });
+ };
+
+ $scope.$on('$destroy', function(e) {
+ console.log("onDestroyed: Stopping poll.");
+ $scope.stopPoll = true;
+ });
+}]);
diff --git a/webclient/room/room.html b/webclient/room/room.html
new file mode 100644
index 0000000000..3439f1a786
--- /dev/null
+++ b/webclient/room/room.html
@@ -0,0 +1,76 @@
+<div ng-controller="RoomController" data-ng-init="onInit()" class="room">
+
+ <div class="page">
+
+ <div class="roomName">
+ {{ room_alias || room_id }}
+ </div>
+
+ <table class="usersTable">
+ <tr ng-repeat="(name, info) in members">
+ <td class="userAvatar">
+ <img class="userAvatarImage" ng-src="{{info.avatar_url || 'img/default-profile.jpg'}}" width="80" height="80"/>
+ <img class="userAvatarGradient" src="img/gradient.png" width="80" height="24"/>
+ <div class="userName">{{ info.displayname || name }}</div>
+ </td>
+ <td class="userPresence" ng-class="info.presenceState === 'online' ? 'online' : (info.presenceState === 'away' ? 'away' : '')" />
+ </table>
+
+ <div class="messageTableWrapper">
+ <table class="messageTable">
+ <tr ng-repeat="msg in messages" ng-class="msg.user_id === state.user_id ? 'mine' : ''">
+ <td class="leftBlock">
+ <div class="sender" ng-hide="messages[$index - 1].user_id === msg.user_id">{{ members[msg.user_id].displayname || msg.user_id }}</div>
+ <div class="timestamp">{{ msg.content.hsob_ts | date:'HH:mm:ss' }}</div>
+ </td>
+ <td class="avatar">
+ <img ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.jpg' }}" width="32" height="32"
+ ng-hide="messages[$index - 1].user_id === msg.user_id || msg.user_id === state.user_id"/>
+ </td>
+ <td ng-class="!msg.content.membership_target ? (msg.content.msgtype === 'm.emote' ? 'emote text' : 'text') : ''">
+ <div class="bubble">
+ {{ msg.content.msgtype === "m.emote" ? ("* " + (members[msg.user_id].displayname || msg.user_id) + " ") : "" }}
+ {{ msg.content.body }}
+ </div>
+ </td>
+ <td class="rightBlock">
+ <img ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.jpg' }}" width="32" height="32"
+ ng-hide="messages[$index - 1].user_id === msg.user_id || msg.user_id !== state.user_id"/>
+ </td>
+ </tr>
+ </table>
+ </div>
+
+ </div>
+
+ <div class="controlPanel">
+ <div class="controls">
+ <table class="inputBarTable">
+ <tr>
+ <td width="1">
+ {{ state.user_id }}
+ </td>
+ <td width="*">
+ <input class="mainInput" ng-model="textInput" ng-enter="send()" ng-focus="true"/>
+ </td>
+ <td width="1">
+ <button ng-click="send()">Send</button>
+ </td>
+ <td width="1">
+ {{ feedback }}
+ </td>
+ </tr>
+ </table>
+
+ <span>
+ Invite a user:
+ <input ng-model="userIDToInvite" size="32" type="text" placeholder="User ID (ex:@user:homeserver)"/>
+ <button ng-click="inviteUser(userIDToInvite)">Invite</button>
+ </span>
+ <button ng-click="leaveRoom()">Leave</button>
+ </div>
+ </div>
+
+
+
+ </div>
|