summary refs log tree commit diff
path: root/webclient/room
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--webclient/room/room-controller.js214
-rw-r--r--webclient/room/room.html76
-rw-r--r--webclient/rooms/rooms-controller.js195
-rw-r--r--webclient/rooms/rooms.html80
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>