diff options
Diffstat (limited to '')
-rw-r--r-- | webclient/room/room-controller.js | 214 | ||||
-rw-r--r-- | webclient/room/room.html | 76 | ||||
-rw-r--r-- | webclient/rooms/rooms-controller.js | 195 | ||||
-rw-r--r-- | webclient/rooms/rooms.html | 80 |
4 files changed, 565 insertions, 0 deletions
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> diff --git a/webclient/rooms/rooms-controller.js b/webclient/rooms/rooms-controller.js new file mode 100644 index 0000000000..58420e0eb2 --- /dev/null +++ b/webclient/rooms/rooms-controller.js @@ -0,0 +1,195 @@ +'use strict'; + +angular.module('RoomsController', ['matrixService']) +.controller('RoomsController', ['$scope', '$location', 'matrixService', + function($scope, $location, matrixService) { + + $scope.rooms = []; + $scope.public_rooms = []; + $scope.newRoomId = ""; + $scope.feedback = ""; + + $scope.newRoom = { + room_id: "", + private: false + }; + + $scope.goToRoom = { + room_id: "", + }; + + $scope.newProfileInfo = { + name: matrixService.config().displayName, + avatar: matrixService.config().avatarUrl + }; + + $scope.linkedEmails = { + linkNewEmail: "", // the email entry box + emailBeingAuthed: undefined, // to populate verification text + authTokenId: undefined, // the token id from the IS + emailCode: "", // the code entry box + linkedEmailList: matrixService.config().emailList // linked email list + }; + + var assignRoomAliases = function(data) { + for (var i=0; i<data.length; i++) { + var alias = matrixService.getRoomIdToAliasMapping(data[i].room_id); + if (alias) { + data[i].room_alias = alias; + } + else { + data[i].room_alias = data[i].room_id; + } + } + return data; + }; + + $scope.refresh = function() { + // List all rooms joined or been invited to + $scope.rooms = matrixService.rooms(); + matrixService.rooms().then( + function(data) { + data = assignRoomAliases(data); + $scope.feedback = "Success"; + $scope.rooms = data; + }, + function(reason) { + $scope.feedback = "Failure: " + reason; + }); + + matrixService.publicRooms().then( + function(data) { + $scope.public_rooms = assignRoomAliases(data.chunk); + } + ); + }; + + $scope.createNewRoom = function(room_id, isPrivate) { + + var visibility = "public"; + if (isPrivate) { + visibility = "private"; + } + + matrixService.create(room_id, visibility).then( + function(response) { + // This room has been created. Refresh the rooms list + console.log("Created room " + response.room_alias + " with id: "+ + response.room_id); + matrixService.createRoomIdToAliasMapping( + response.room_id, response.room_alias); + $scope.refresh(); + }, + function(reason) { + $scope.feedback = "Failure: " + reason; + }); + }; + + // Go to a room + $scope.goToRoom = function(room_id) { + // Simply open the room page on this room id + //$location.path("room/" + room_id); + matrixService.join(room_id).then( + function(response) { + if (response.hasOwnProperty("room_id")) { + if (response.room_id != room_id) { + $location.path("room/" + response.room_id); + return; + } + } + + $location.path("room/" + room_id); + }, + function(reason) { + $scope.feedback = "Can't join room: " + reason; + } + ); + }; + + $scope.setDisplayName = function(newName) { + matrixService.setDisplayName(newName).then( + function(response) { + $scope.feedback = "Updated display name."; + var config = matrixService.config(); + config.displayName = newName; + matrixService.setConfig(config); + matrixService.saveConfig(); + }, + function(reason) { + $scope.feedback = "Can't update display name: " + reason; + } + ); + }; + + $scope.setAvatar = function(newUrl) { + console.log("Updating avatar to "+newUrl); + matrixService.setProfilePictureUrl(newUrl).then( + function(response) { + console.log("Updated avatar"); + $scope.feedback = "Updated avatar."; + var config = matrixService.config(); + config.avatarUrl = newUrl; + matrixService.setConfig(config); + matrixService.saveConfig(); + }, + function(reason) { + $scope.feedback = "Can't update avatar: " + reason; + } + ); + }; + + $scope.linkEmail = function(email) { + matrixService.linkEmail(email).then( + function(response) { + if (response.success === true) { + $scope.linkedEmails.authTokenId = response.tokenId; + $scope.emailFeedback = "You have been sent an email."; + $scope.linkedEmails.emailBeingAuthed = email; + } + else { + $scope.emailFeedback = "Failed to send email."; + } + }, + function(reason) { + $scope.emailFeedback = "Can't send email: " + reason; + } + ); + }; + + $scope.submitEmailCode = function(code) { + var tokenId = $scope.linkedEmails.authTokenId; + if (tokenId === undefined) { + $scope.emailFeedback = "You have not requested a code with this email."; + return; + } + matrixService.authEmail(matrixService.config().user_id, tokenId, code).then( + function(response) { + if ("success" in response && response.success === false) { + $scope.emailFeedback = "Failed to authenticate email."; + return; + } + var config = matrixService.config(); + var emailList = {}; + if ("emailList" in config) { + emailList = config.emailList; + } + emailList[response.address] = response; + // save the new email list + config.emailList = emailList; + matrixService.setConfig(config); + matrixService.saveConfig(); + // invalidate the email being authed and update UI. + $scope.linkedEmails.emailBeingAuthed = undefined; + $scope.emailFeedback = ""; + $scope.linkedEmails.linkedEmailList = emailList; + $scope.linkedEmails.linkNewEmail = ""; + $scope.linkedEmails.emailCode = ""; + }, + function(reason) { + $scope.emailFeedback = "Failed to auth email: " + reason; + } + ); + }; + + $scope.refresh(); +}]); diff --git a/webclient/rooms/rooms.html b/webclient/rooms/rooms.html new file mode 100644 index 0000000000..04b153982e --- /dev/null +++ b/webclient/rooms/rooms.html @@ -0,0 +1,80 @@ +<div ng-controller="RoomsController" class="rooms"> + + <div class="page"> + + <div> + <form> + <input size="40" ng-model="newProfileInfo.name" ng-enter="setDisplayName(newProfileInfo.name)" /> + <button ng-disabled="!newProfileInfo.name" ng-click="setDisplayName(newProfileInfo.name)">Update Name</button> + </form> + </div> + <div> + <form> + <input size="40" ng-model="newProfileInfo.avatar" ng-enter="setAvatar(newProfileInfo.avatar)" /> + <button ng-disabled="!newProfileInfo.avatar" ng-click="setAvatar(newProfileInfo.avatar)">Update Avatar</button> + </form> + </div> + <br/> + + <div> + <form> + <input size="40" ng-model="linkedEmails.linkNewEmail" ng-enter="linkEmail(linkedEmails.linkNewEmail)" /> + <button ng-disabled="!linkedEmails.linkNewEmail" ng-click="linkEmail(linkedEmails.linkNewEmail)"> + Link Email + </button> + {{ emailFeedback }} + </form> + <form ng-hide="!linkedEmails.emailBeingAuthed"> + Enter validation token for {{ linkedEmails.emailBeingAuthed }}: + <br /> + <input size="20" ng-model="linkedEmails.emailCode" ng-enter="submitEmailCode(linkedEmails.emailCode)" /> + <button ng-disabled="!linkedEmails.emailCode || !linkedEmails.linkNewEmail" ng-click="submitEmailCode(linkedEmails.emailCode)"> + Submit Code + </button> + </form> + Linked emails: + <table> + <tr ng-repeat="(address, info) in linkedEmails.linkedEmailList"> + <td>{{address}}</td> + </tr> + </table> + </div> + <br/> + + <h3>My rooms</h3> + + <div class="rooms" ng-repeat="room in rooms"> + <div> + <a href="#/room/{{ room.room_id }}" >{{ room.room_alias }}</a> + </div> + </div> + <br/> + + <h3>Public rooms</h3> + + <div class="public_rooms" ng-repeat="room in public_rooms"> + <div> + <a href="#/room/{{ room.room_id }}" >{{ room.room_alias }}</a> + </div> + </div> + <br/> + + <div> + <form> + <input size="40" ng-model="newRoom.room_id" ng-enter="createNewRoom(newRoom.room_id, newRoom.private)" placeholder="(e.g. foo_channel)"/> + <input type="checkbox" ng-model="newRoom.private">private + <button ng-disabled="!newRoom.room_id" ng-click="createNewRoom(newRoom.room_id, newRoom.private)">Create room</button> + </form> + </div> + <div> + <form> + <input size="40" ng-model="goToRoom.room_id" ng-enter="goToRoom(goToRoom.room_id)" placeholder="(e.g. #foo_channe:example.org)"/> + <button ng-disabled="!goToRoom.room_id" ng-click="goToRoom(goToRoom.room_id)">Go to room</button> + </form> + </div> + <br/> + + {{ feedback }} + + </div> +</div> |