diff --git a/package-lock.json b/package-lock.json
index ad5c28cf..9c931b4c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1020,6 +1020,14 @@
"tslib": "^1.9.0"
}
},
+ "@angular/upgrade": {
+ "version": "8.2.2",
+ "resolved": "https://registry.npmjs.org/@angular/upgrade/-/upgrade-8.2.2.tgz",
+ "integrity": "sha512-ED5F/cPfFGi72yDaB2FutUgPPIZBsc62HTkpcFNt9m6JX2z4J7XxGKcQFUN/9cMzdbbPjpOJ5LFga046Nv47bQ==",
+ "requires": {
+ "tslib": "^1.9.0"
+ }
+ },
"@babel/code-frame": {
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz",
@@ -1219,12 +1227,24 @@
"semver-intersect": "1.4.0"
}
},
+ "@types/angular": {
+ "version": "1.6.56",
+ "resolved": "https://registry.npmjs.org/@types/angular/-/angular-1.6.56.tgz",
+ "integrity": "sha512-HxtqilvklZ7i6XOaiP7uIJIrFXEVEhfbSY45nfv2DeBRngncI58Y4ZOUMiUkcT8sqgLL1ablmbfylChUg7A3GA==",
+ "dev": true
+ },
"@types/events": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
"integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==",
"dev": true
},
+ "@types/file-saver": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.1.tgz",
+ "integrity": "sha512-g1QUuhYVVAamfCifK7oB7G3aIl4BbOyzDOqVyUfEr4tfBKrXfeH+M+Tg7HKCXSrbzxYdhyCP7z9WbKo0R2hBCw==",
+ "dev": true
+ },
"@types/glob": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz",
@@ -3980,6 +4000,11 @@
}
}
},
+ "file-saver": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.2.tgz",
+ "integrity": "sha512-Wz3c3XQ5xroCxd1G8b7yL0Ehkf0TC9oYC6buPFkNnU9EnaPlifeAFCyCh+iewXTyFRcg0a6j3J7FmJsIhlhBdw=="
+ },
"fileset": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz",
diff --git a/package.json b/package.json
index f577e066..3ce9caf8 100644
--- a/package.json
+++ b/package.json
@@ -19,18 +19,22 @@
"@angular/platform-browser": "~8.2.0",
"@angular/platform-browser-dynamic": "~8.2.0",
"@angular/router": "~8.2.0",
+ "@angular/upgrade": "~8.2.0",
"rxjs": "~6.4.0",
"tslib": "^1.10.0",
- "zone.js": "~0.9.1"
+ "zone.js": "~0.9.1",
+ "file-saver": "latest"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.802.2",
"@angular/cli": "~8.2.2",
"@angular/compiler-cli": "~8.2.0",
"@angular/language-service": "~8.2.0",
- "@types/node": "~8.9.4",
+ "@types/angular": "^1.6.56",
"@types/jasmine": "~3.3.8",
"@types/jasminewd2": "~2.0.3",
+ "@types/node": "~8.9.4",
+ "@types/file-saver": "^2.0.1",
"codelyzer": "^5.0.0",
"jasmine-core": "~3.4.0",
"jasmine-spec-reporter": "~4.2.1",
diff --git a/src/.idea/encodings.xml b/src/.idea/encodings.xml
new file mode 100644
index 00000000..15a15b21
--- /dev/null
+++ b/src/.idea/encodings.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/.idea/misc.xml b/src/.idea/misc.xml
new file mode 100644
index 00000000..28a804d8
--- /dev/null
+++ b/src/.idea/misc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/.idea/modules.xml b/src/.idea/modules.xml
new file mode 100644
index 00000000..f669a0e5
--- /dev/null
+++ b/src/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/.idea/src.iml b/src/.idea/src.iml
new file mode 100644
index 00000000..d6ebd480
--- /dev/null
+++ b/src/.idea/src.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/.idea/vcs.xml b/src/.idea/vcs.xml
new file mode 100644
index 00000000..6c0b8635
--- /dev/null
+++ b/src/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/.idea/workspace.xml b/src/.idea/workspace.xml
new file mode 100644
index 00000000..764c38ed
--- /dev/null
+++ b/src/.idea/workspace.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/angularjs/app/app.ts b/src/angularjs/app/app.ts
new file mode 100755
index 00000000..48ce899a
--- /dev/null
+++ b/src/angularjs/app/app.ts
@@ -0,0 +1,48 @@
+import * as angular from 'angular'
+
+import '../app/controllers'
+import '../app/directives'
+import '../app/filters'
+import '../app/services'
+import {SetBy} from "../services/config";
+
+// Declare app level module which depends on filters, and services
+export let myApp = angular.module('myApp',
+ [
+ 'myApp.filters',
+ 'myApp.services',
+ 'myApp.directives',
+ 'myApp.controllers',
+ require('angular-sanitize'),
+ ]
+).config(['$sceDelegateProvider', '$provide', ($sceDelegateProvider, $provide) => {
+ $sceDelegateProvider.resourceUrlWhitelist([
+ // Allow same origin resource loads.
+ 'self',
+ // Allow loading from our assets domain. Notice the difference between * and **.
+ 'https://chatcatio.firebaseapp.com/partials/**',
+ 'https://chatcatio-test.firebaseapp.com/partials/**',
+ 'https://chatcat.firebaseapp.com/partials/**',
+ 'http://chatcatio.firebaseapp.com/partials/**',
+ 'http://chatcatio-test.firebaseapp.com/partials/**',
+ 'http://chatcat.firebaseapp.com/partials/**',
+ 'http://chatcat/dist_test/partials/**',
+ 'http://chatcat/dist/partials/**'
+ // TODO: Put this back in
+ // 'https://' + ChatSDKOptions.firebaseConfig.authDomain + '/partials/**'
+ ]);
+
+ $provide.decorator('$browser', ['$delegate', ($delegate) => {
+ $delegate.onUrlChange = () => {};
+ $delegate.url = () => {
+ return "";
+ };
+ return $delegate;
+ }]);
+}]).run(['Config', 'Environment', (Config, Environment) => {
+ Config.setConfig(SetBy.Include, Environment.config());
+}]);
+
+angular.bootstrap(document.getElementById("cc-app"), ['myApp']);
+
+
diff --git a/src/angularjs/app/config.ts b/src/angularjs/app/config.ts
new file mode 100755
index 00000000..7db23a8f
--- /dev/null
+++ b/src/angularjs/app/config.ts
@@ -0,0 +1,57 @@
+const FirebaseConfig = {
+ apiKey: "AIzaSyASm9RYrr3u_Bc22eglk0OtsC2GnnTQp_c",
+ authDomain: "chat-sdk-v4.firebaseapp.com",
+ databaseURL: "https://chat-sdk-v4.firebaseio.com",
+ projectId: "chat-sdk-v4",
+ storageBucket: "chat-sdk-v4.appspot.com",
+ messagingSenderId: "1088435112418"
+};
+
+export const ChatSDKConfig = {
+
+ firebaseConfig: FirebaseConfig,
+
+ rootPath: '111_web_aug_19',
+
+ facebookAppID: '735373466519297',
+
+ cloudImageToken: 'cag084en',
+
+ // This defaults to 5 minutes min 2 minutes, max 15 minutes
+ inactivityTimeout: 5,
+
+ // 1) 24hour - show time in 24 hour format
+ clockType: '24hour',
+
+ // Users can create public chat rooms?
+ // If this is true users will be able to setup new
+ // public rooms
+ usersCanCreatePublicRooms: true,
+
+ // Allow anonymous login?
+ anonymousLoginEnabled: true,
+
+ // Enable social login - please email us to get your domain whitelisted
+ socialLoginEnabled: false,
+
+ // The URL to contact for single sign on
+ singleSignOnURL: '',
+
+ environment: 'test',
+
+ imageMessagesEnabled: true,
+
+ fileMessagesEnabled: true,
+
+ hideMainBox: false,
+
+ // Comma separated list of paths. If set, chat will
+ // only display on these paths
+ showOnPaths: null,
+
+ // If set, partials will be loaded from this URL. Otherwise
+ // they will be loaded from the current url in test mode or
+ // the Firebase hosting URL if live
+ resourceRootURL: null,
+};
+
diff --git a/src/angularjs/app/controllers.ts b/src/angularjs/app/controllers.ts
new file mode 100755
index 00000000..d922f880
--- /dev/null
+++ b/src/angularjs/app/controllers.ts
@@ -0,0 +1,23 @@
+import * as angular from 'angular'
+
+angular.module('myApp.controllers', []);
+
+import '../controllers/app'
+import '../controllers/chat'
+import '../controllers/chat-bar'
+import '../controllers/chat-embed'
+import '../controllers/chat-settings'
+import '../controllers/create-room'
+import '../controllers/draggable-user'
+import '../controllers/emoji'
+import '../controllers/error-box'
+import '../controllers/inbox-rooms-list'
+import '../controllers/login'
+import '../controllers/main-box'
+import '../controllers/notification'
+import '../controllers/online-users-list'
+import '../controllers/profile-settings'
+import '../controllers/public-rooms-list'
+import '../controllers/room-list-box'
+import '../controllers/user-list'
+import '../controllers/user-profile-box'
\ No newline at end of file
diff --git a/src/angularjs/app/directives.ts b/src/angularjs/app/directives.ts
new file mode 100755
index 00000000..c34f29e4
--- /dev/null
+++ b/src/angularjs/app/directives.ts
@@ -0,0 +1,29 @@
+import * as angular from 'angular'
+
+angular.module('myApp.directives', []).
+ directive('appVersion', ['version', function(version) {
+ return function(scope, elm, attrs) {
+ elm.text(version);
+ };
+ }]);
+
+import '../directives/animate-room'
+import '../directives/cc-flash'
+import '../directives/cc-focus'
+import '../directives/cc-uncloak'
+import '../directives/center-mouse-y'
+import '../directives/consume-event'
+import '../directives/disable-drag'
+import '../directives/draggable-room'
+import '../directives/draggable-user'
+import '../directives/enter-submit'
+import '../directives/fit-text'
+import '../directives/infinite-scroll'
+import '../directives/on-edit-message'
+import '../directives/on-file-change'
+import '../directives/pikaday'
+import '../directives/resize-room'
+import '../directives/scroll-glue'
+import '../directives/social-iframe'
+import '../directives/stop-shake'
+import '../directives/user-drop-location'
diff --git a/src/angularjs/app/filters.ts b/src/angularjs/app/filters.ts
new file mode 100755
index 00000000..1cc25a98
--- /dev/null
+++ b/src/angularjs/app/filters.ts
@@ -0,0 +1,10 @@
+import * as angular from 'angular'
+
+angular.module('myApp.filters', []);
+
+import '../filters/interpolate'
+import '../filters/new-line'
+import '../filters/emoji-filter'
+
+
+
diff --git a/src/angularjs/app/services.ts b/src/angularjs/app/services.ts
new file mode 100755
index 00000000..73165ad0
--- /dev/null
+++ b/src/angularjs/app/services.ts
@@ -0,0 +1,52 @@
+import * as angular from 'angular'
+
+angular.module('myApp.services', []).value('version', '0.1');
+
+// Entity
+import '../entities/message'
+import '../entities/room'
+import '../entities/user'
+
+// Services
+import '../services/array-utils'
+import '../services/before-unload'
+import '../services/cloud-image'
+import '../services/config'
+import '../entities/entity'
+import '../services/environment'
+import '../services/log'
+import '../services/marquee'
+import '../services/partials'
+import '../services/path-analyser'
+import '../services/room-open-queue'
+import '../services/room-position-manager'
+import '../services/screen'
+import '../services/sound-effects'
+import '../services/state-manager'
+import '../services/time'
+import '../services/utils'
+import '../services/visibility'
+import '../services/emoji'
+
+// Persistence
+import '../persistence/cache'
+import '../persistence/local-storage'
+import '../persistence/room-store'
+import '../persistence/user-store'
+import '../persistence/web-storage'
+
+// Network
+import '../network/auth'
+import '../network/auto-login'
+import '../network/credential'
+import '../network/firebase-upload-handler'
+import '../network/network-manager'
+import '../network/paths'
+import '../network/presence'
+import '../network/single-sign-on'
+import '../network/abstract-authentication-handler'
+
+// Connectors
+import '../connectors/friend-connector'
+import '../connectors/online-connector'
+import '../connectors/public-rooms-connector'
\ No newline at end of file
diff --git a/src/angularjs/connectors/friend-connector.ts b/src/angularjs/connectors/friend-connector.ts
new file mode 100755
index 00000000..210f316a
--- /dev/null
+++ b/src/angularjs/connectors/friend-connector.ts
@@ -0,0 +1,115 @@
+import * as angular from 'angular'
+import {Utils} from "../services/utils";
+import {N} from "../keys/notification-keys";
+import {IUser} from "../entities/user";
+
+export interface IFriendsConnector {
+ on(uid: string): void
+ off(uid: string): void
+ isFriend(user: IUser): boolean
+}
+
+angular.module('myApp.services').factory('FriendsConnector', ['$rootScope', 'User', 'UserStore', 'Paths', function ($rootScope, User, UserStore, Paths) {
+ return {
+
+ friends: {},
+
+ on: function (uid: string): void {
+ let friendsRef = Paths.userFriendsRef(uid);
+
+ friendsRef.on('child_added', (snapshot) => {
+
+ if(snapshot && snapshot.val()) {
+ this.impl_friendAdded(snapshot);
+ }
+
+ });
+
+ friendsRef.on('child_removed', (snapshot) => {
+
+ if(snapshot && snapshot.val()) {
+ this.impl_friendRemoved(snapshot);
+ }
+
+ });
+ },
+
+ off: function (uid: string): void {
+ let friendsRef = Paths.userFriendsRef(uid);
+
+ friendsRef.off('child_added');
+ friendsRef.off('child_removed');
+
+ this.friends = {};
+ },
+
+ /**
+ * Friends
+ */
+
+ impl_friendAdded: function (snapshot) {
+
+ let uid = snapshot.val().uid;
+ if(uid) {
+ let user = UserStore.getOrCreateUserWithID(uid);
+
+ user.removeFriend = function () {
+ snapshot.ref.remove();
+ };
+ this.addFriend(user);
+ }
+
+ },
+
+ impl_friendRemoved: function (snapshot) {
+ this.removeFriendWithID(snapshot.val().uid);
+ },
+
+ addFriendsFromConfig: function (friends) {
+ for(let i = 0; i < friends.length; i++) {
+ let uid = friends[i];
+
+ let user = UserStore.getOrCreateUserWithID(uid);
+ user.ssoFriend = true;
+
+ this.addFriend(user);
+ }
+ },
+
+ addFriend: function (user) {
+ if(user && user.uid()) {
+ this.friends[user.uid()] = user;
+ user.friend = true;
+ $rootScope.$broadcast(N.FriendAdded);
+ }
+ },
+
+ isFriend: function (user: IUser): boolean {
+ if(user && user.uid()) {
+ return this.isFriendUID(user.uid());
+ }
+ return false;
+ },
+
+ isFriendUID: function(uid) {
+ return !Utils.unORNull(this.friends[uid]);
+ },
+
+ removeFriend: function (user) {
+ if(user && user.uid()) {
+ this.removeFriendWithID(user.uid());
+ }
+ },
+
+ removeFriendWithID: function (uid) {
+ if(uid) {
+ let user = this.friends[uid];
+ if(user) {
+ user.friend = false;
+ delete this.friends[uid];
+ $rootScope.$broadcast(N.FriendRemoved);
+ }
+ }
+ }
+ }
+}]);
\ No newline at end of file
diff --git a/src/angularjs/connectors/online-connector.ts b/src/angularjs/connectors/online-connector.ts
new file mode 100755
index 00000000..af836641
--- /dev/null
+++ b/src/angularjs/connectors/online-connector.ts
@@ -0,0 +1,145 @@
+import * as angular from 'angular'
+import * as firebase from 'firebase';
+
+import {DEBUG} from "../keys/defines";
+import {N} from "../keys/notification-keys";
+import {IUser} from "../entities/user";
+import {IPaths} from "../network/paths";
+import {IUserStore} from "../persistence/user-store";
+import {IRootScope} from "../controllers/app";
+
+export interface IOnlineConnector {
+ on(): void
+ off(): void
+}
+
+class OnlineConnector implements IOnlineConnector {
+
+ isOn = false;
+ onlineUsers = {};
+
+ static $inject = ['$rootScope', 'UserStore', 'Paths'];
+
+ constructor (
+ private $rootScope: IRootScope,
+ private UserStore: IUserStore,
+ private Paths: IPaths) {}
+
+
+ on(): void {
+
+ if(this.isOn) {
+ return;
+ }
+ this.isOn = true;
+
+ let onlineUsersRef = this.Paths.onlineUsersRef();
+
+ onlineUsersRef.on("child_added", (snapshot: firebase.database.DataSnapshot) => {
+
+ if(DEBUG) console.log('Online: ' + snapshot.key);
+
+ // Get the UID of the added user
+ let uid = null;
+ if (snapshot && snapshot.val()) {
+ uid = snapshot.key;
+
+ let user = this.UserStore.getOrCreateUserWithID(uid);
+
+ if(this.addOnlineUser(user)) {
+ // Update the user's rooms
+ this.$rootScope.$broadcast(N.UserOnlineStateChanged, user);
+ }
+ }
+
+ }, (error) => {
+ console.log(error.message);
+ });
+
+ onlineUsersRef.on("child_removed", (snapshot: firebase.database.DataSnapshot) => {
+
+ console.log('Offline: ' + snapshot.key);
+
+ let user = this.UserStore.getOrCreateUserWithID(snapshot.key);
+
+ user.off();
+
+ if (user) {
+ this.removeOnlineUser(user);
+ }
+
+ this.$rootScope.$broadcast(N.UserOnlineStateChanged, user);
+
+ }, (error) => {
+ console.log(error.message);
+ });
+ }
+
+ off(): void {
+
+ this.isOn = false;
+
+ //this.onlineUsers = {};
+ // having the user.blocked is useful because it means
+ // that the partials don't have to call a function
+ // however when you logout you want the flags to be reset
+ for(let key in this.onlineUsers) {
+ if(this.onlineUsers.hasOwnProperty(key)) {
+ this.onlineUsers[key].blocked = false;
+ this.onlineUsers[key].friend = false;
+ }
+ }
+ this.onlineUsers = {};
+
+ let onlineUsersRef = this.Paths.onlineUsersRef();
+
+ onlineUsersRef.off('child_added');
+ onlineUsersRef.off('child_removed');
+ }
+
+ /**
+ * Online users
+ */
+
+ addOnlineUser(user: IUser) {
+ if(user && user.uid()) {
+ if(!user.isMe()) {
+ user.online = true;
+ this.onlineUsers[user.uid()] = user;
+ this.$rootScope.$broadcast(N.OnlineUserAdded);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ removeOnlineUser(user) {
+ if(user && user.meta && user.uid()) {
+ this.removeOnlineUserWithID(user.uid());
+ }
+ }
+
+ removeOnlineUserWithID(uid) {
+ if(uid) {
+ let user = this.onlineUsers[uid];
+ if(user) {
+ user.online = false;
+ delete this.onlineUsers[uid];
+ this.$rootScope.$broadcast(N.OnlineUserRemoved);
+ }
+ }
+ }
+
+ onlineUserCount() {
+ let i = 0;
+ for(let key in this.onlineUsers) {
+ if(this.onlineUsers.hasOwnProperty(key)) {
+ i++;
+ }
+ }
+ return i;
+ }
+
+}
+
+angular.module('myApp.services').service('OnlineConnector', OnlineConnector);
\ No newline at end of file
diff --git a/src/angularjs/connectors/public-rooms-connector.ts b/src/angularjs/connectors/public-rooms-connector.ts
new file mode 100755
index 00000000..44f65a02
--- /dev/null
+++ b/src/angularjs/connectors/public-rooms-connector.ts
@@ -0,0 +1,57 @@
+import * as angular from 'angular'
+import {N} from "../keys/notification-keys";
+import {IPaths} from "../network/paths";
+import {IRoomStore} from "../persistence/room-store";
+
+export interface IPublicRoomsConnector {
+ off(): void
+ on(): void
+}
+
+class PublicRoomsConnector implements IPublicRoomsConnector{
+
+ static $inject = ['$rootScope', 'RoomStore', 'Paths'];
+
+ $rootScope;
+ Paths: IPaths;
+ RoomStore: IRoomStore;
+
+ constructor ($rootScope, RoomStore, Paths) {
+ this.$rootScope = $rootScope;
+ this.Paths = Paths;
+ this.RoomStore = RoomStore;
+ }
+
+ on(): void {
+ const publicRoomsRef = this.Paths.publicRoomsRef();
+
+ // Start listening to Firebase
+ publicRoomsRef.on('child_added', (snapshot) => {
+
+ const rid = snapshot.key;
+ if(rid) {
+ const room = this.RoomStore.getOrCreateRoomWithID(rid);
+
+ room.on().then(() => {
+ this.$rootScope.$broadcast(N.PublicRoomAdded, room);
+ });
+ }
+
+ });
+
+ publicRoomsRef.on('child_removed', (snapshot) => {
+
+ const room = this.RoomStore.getOrCreateRoomWithID(snapshot.key);
+ this.$rootScope.$broadcast(N.PublicRoomRemoved, room);
+ });
+ }
+
+ off(): void {
+ const publicRoomsRef = this.Paths.publicRoomsRef();
+
+ publicRoomsRef.off('child_added');
+ publicRoomsRef.off('child_removed');
+ }
+}
+
+angular.module('myApp.services').service('PublicRoomsConnector', PublicRoomsConnector);
\ No newline at end of file
diff --git a/src/angularjs/controllers/app.ts b/src/angularjs/controllers/app.ts
new file mode 100755
index 00000000..925de31d
--- /dev/null
+++ b/src/angularjs/controllers/app.ts
@@ -0,0 +1,581 @@
+import * as angular from 'angular'
+
+import {N} from "../keys/notification-keys";
+import * as RoomType from "../keys/room-type";
+import * as Defines from "../keys/defines";
+import {Dimensions} from "../keys/dimensions";
+import {MessageType} from "../keys/message-type";
+import {IUser} from "../entities/user";
+import {Utils} from "../services/utils";
+import {IRoom} from "../entities/room";
+import {Log} from "../services/log";
+
+export interface IRootScope extends ng.IRootScopeService{
+ user: IUser
+}
+
+angular.module('myApp.controllers').controller('AppController', [
+ '$rootScope', '$scope','$timeout', '$window', '$sce', 'PathAnalyser', 'OnlineConnector', 'FriendsConnector', 'Cache', 'UserStore', 'RoomStore','$document', 'Presence', 'LocalStorage', 'RoomCreator', 'Config', 'Partials', 'RoomPositionManager', 'Paths', 'Auth', 'StateManager', 'RoomOpenQueue', 'NetworkManager', 'Environment',
+ function($rootScope, $scope, $timeout, $window, $sce, PathAnalyser, OnlineConnector, FriendsConnector, Cache, UserStore, RoomStore, $document, Presence, LocalStorage, RoomCreator, Config, Partials, RoomPositionManager, Paths, Auth, StateManager, RoomOpenQueue, NetworkManager, Environment) {
+
+ $scope.totalUserCount = 0;
+ $scope.friendsEnabled = true;
+
+ console.log("Start controller!");
+
+ // Used to hide chat box
+ $scope.hidden = Environment.config().hideMainBox;
+
+ $rootScope.messageTypeText = MessageType.Text;
+ $rootScope.messageTypeImage = MessageType.Image;
+ $rootScope.messageTypeFile = MessageType.File;
+
+ $scope.init = function () {
+
+ // Check to see if the user wants the chat to
+ // load on this page. We look at the showOnPaths variable
+ // in the options
+ //CC_OPTIONS.showOnPaths = "*ccwp, *p*";
+ if(Environment.showOnPaths()) {
+ let paths = Environment.showOnPaths();
+ if(!PathAnalyser.shouldShowChatOnPath(paths)) {
+ return;
+ }
+ }
+
+ Paths.setCID(Environment.rootPath());
+
+ // Start the config listener to get the current
+ // settings from Firebase
+ Config.startConfigListener().then(() => {
+
+ });
+
+ Partials.load();
+
+ //API.getOnlineUserCount().then(function (count) {
+ // $scope.totalUserCount = count;
+ //});
+
+ // Show the waiting overlay
+ $scope.notification = {
+ show: false
+ };
+
+ if(LocalStorage.isOffline()) {
+ $scope.on = false;
+ Presence.goOffline();
+ }
+ else {
+ $scope.on = true;
+ }
+
+ $rootScope.websiteName = $window.location.host;
+
+ /**
+ * Single Sign on
+ */
+
+ let loginURL = Config.loginURL;
+ if(loginURL && loginURL.length > 0) {
+ $rootScope.loginURL = loginURL;
+ }
+
+ let registerURL = Config.registerURL;
+ if(registerURL && registerURL.length > 0) {
+ $rootScope.registerURL = registerURL;
+ }
+
+ /**
+ * Anonymous login and social login
+ */
+
+ $scope.setupImages();
+ $scope.setupFileIcons();
+
+ $scope.setMainBoxMinimized(LocalStorage.getProperty(LocalStorage.mainMinimizedKey));
+
+ $scope.$on(N.UserOnlineStateChanged, function () {
+ Log.notification(N.UserOnlineStateChanged, "AppController");
+ $scope.updateTotalUserCount();
+ $timeout(() => {
+ $scope.$digest();
+ });
+ });
+
+ };
+
+ /**
+ * The images in the partials should be pointed at the correct
+ * server
+ */
+ $scope.setupImages = function () {
+ $rootScope.img_30_minimize = Environment.imagesURL() + 'cc-30-minimize.png';
+ $rootScope.img_30_resize = Environment.imagesURL() + 'cc-30-resize.png';
+ $rootScope.img_20_cross = Environment.imagesURL() + 'cc-20-cross.png';
+ $rootScope.img_30_cross = Environment.imagesURL() + 'cc-30-cross.png';
+ $rootScope.img_40_cross = Environment.imagesURL() + 'cc-40-cross.png';
+ $rootScope.img_40_tick = Environment.imagesURL() + 'cc-40-tick.png';
+ $rootScope.img_30_shutdown = Environment.imagesURL() + 'cc-30-shutdown_on.png';
+ $rootScope.img_30_shutdown_on = Environment.imagesURL() + 'cc-30-shutdown.png';
+ $rootScope.img_30_plus = Environment.imagesURL() + 'cc-30-plus.png';
+ $rootScope.img_30_profile_pic = Environment.imagesURL() + 'cc-30-profile-pic.png';
+ $rootScope.img_30_gear = Environment.imagesURL() + 'cc-30-gear.png';
+ $rootScope.img_loader = Environment.imagesURL() + 'loader.gif';
+ $rootScope.img_20_user = Environment.imagesURL() + 'cc-20-user.png';
+ $rootScope.img_20_friend = Environment.imagesURL() + 'cc-20-friend.png';
+ $rootScope.img_30_logout = Environment.imagesURL() + 'cc-30-logout.png';
+ $rootScope.img_30_emojis = Environment.imagesURL() + 'cc-30-emojis.png';
+ $rootScope.img_30_maximize = Environment.imagesURL() + 'cc-30-maximize.png';
+ $rootScope.img_30_sound_on = Environment.imagesURL() + 'cc-30-sound-on.png';
+ $rootScope.img_30_sound_off = Environment.imagesURL() + 'cc-30-sound-off.png';
+ $rootScope.img_30_clear_cache = Environment.imagesURL() + 'cc-30-clear-cache.png';
+ $rootScope.img_30_cache_cleared = Environment.imagesURL() + 'cc-30-cache-cleared.png';
+ $rootScope.img_24_save = Environment.imagesURL() + 'cc-24-save.png';
+ $rootScope.img_30_save = Environment.imagesURL() + 'cc-30-save.png';
+ $rootScope.img_24_copy = Environment.imagesURL() + 'cc-24-copy.png';
+ $rootScope.img_24_cross = Environment.imagesURL() + 'cc-24-cross.png';
+ $rootScope.img_30_image = Environment.imagesURL() + 'cc-30-image.png';
+ $rootScope.img_20_flag = Environment.imagesURL() + 'cc-20-flag.png';
+ $rootScope.img_20_flagged = Environment.imagesURL() + 'cc-20-flagged.png';
+ $rootScope.img_30_powered_by = Environment.imagesURL() + 'cc-30-powered-by.png';
+ $rootScope.img_30_start_chatting = Environment.imagesURL() + 'cc-30-start-chatting.png';
+ };
+
+ $scope.setupFileIcons = function () {
+ $rootScope.img_file = Environment.imagesURL() + 'file.png';
+ $rootScope.img_file_download = Environment.imagesURL() + 'file-download.png';
+ $rootScope.img_file_aac = Environment.imagesURL() + 'file-type-aac.png';
+ $rootScope.img_file_acc = Environment.imagesURL() + 'file-type-acc.png';
+ $rootScope.img_file_ai = Environment.imagesURL() + 'file-type-ai.png';
+ $rootScope.img_file_avi = Environment.imagesURL() + 'file-type-avi.png';
+ $rootScope.img_file_bmp = Environment.imagesURL() + 'file-type-bmp.png';
+ $rootScope.img_file_f4a = Environment.imagesURL() + 'file-type-f4a.png';
+ $rootScope.img_file_gif = Environment.imagesURL() + 'file-type-gif.png';
+ $rootScope.img_file_html = Environment.imagesURL() + 'file-type-html.png';
+ $rootScope.img_file_jpeg = Environment.imagesURL() + 'file-type-jpeg.png';
+ $rootScope.img_file_jpg = Environment.imagesURL() + 'file-type-jpg.png';
+ $rootScope.img_file_jpp = Environment.imagesURL() + 'file-type-jpp.png';
+ $rootScope.img_file_json = Environment.imagesURL() + 'file-type-json.png';
+ $rootScope.img_file_m4a = Environment.imagesURL() + 'file-type-m4a.png';
+ $rootScope.img_file_midi = Environment.imagesURL() + 'file-type-midi.png';
+ $rootScope.img_file_mov = Environment.imagesURL() + 'file-type-mov.png';
+ $rootScope.img_file_mp3 = Environment.imagesURL() + 'file-type-mp3.png';
+ $rootScope.img_file_mp4 = Environment.imagesURL() + 'file-type-mp4.png';
+ $rootScope.img_file_oga = Environment.imagesURL() + 'file-type-oga.png';
+ $rootScope.img_file_ogg = Environment.imagesURL() + 'file-type-ogg.png';
+ $rootScope.img_file_pdf = Environment.imagesURL() + 'file-type-pdf.png';
+ $rootScope.img_file_psd = Environment.imagesURL() + 'file-type-psd.png';
+ $rootScope.img_file_rtf = Environment.imagesURL() + 'file-type-rtf.png';
+ $rootScope.img_file_svg = Environment.imagesURL() + 'file-type-svg.png';
+ $rootScope.img_file_tif = Environment.imagesURL() + 'file-type-tif.png';
+ $rootScope.img_file_tiff = Environment.imagesURL() + 'file-type-tiff.png';
+ $rootScope.img_file_txt = Environment.imagesURL() + 'file-type-txt.png';
+ $rootScope.img_file_wav = Environment.imagesURL() + 'file-type-wav.png';
+ $rootScope.img_file_wma = Environment.imagesURL() + 'file-type-wma.png';
+ $rootScope.img_file_xml = Environment.imagesURL() + 'file-type-xml.png';
+ $rootScope.img_file_zip = Environment.imagesURL() + 'file-type-zip.png';
+ };
+
+ $rootScope.imgForFileType = function (type) {
+ return $rootScope['img_file_' + type] || $rootScope['img_file'];
+ };
+
+ $scope.getUser = function (): IUser {
+ return UserStore.currentUser();
+ };
+
+ /**
+ * Show the login box
+ */
+ $scope.showLoginBox = function (mode?) {
+ $rootScope.loginMode = mode ? mode : Auth.mode;
+ $scope.activeBox = Defines.LoginBox;
+ $timeout(() => {
+ $scope.$digest();
+ });
+ };
+
+ /**
+ * Show the profile settings
+ */
+ $scope.showProfileSettingsBox = function () {
+ $scope.activeBox = Defines.ProfileSettingsBox;
+
+ // This will allow us to setup validation after the user
+ // has been loaded
+ $scope.$broadcast(Defines.ShowProfileSettingsBox);
+ };
+
+ /**
+ * Show the main box
+ */
+ $scope.showMainBox = function () {
+ $scope.activeBox = Defines.MainBox;
+ };
+
+ $scope.showErrorBox = function (message) {
+ $scope.activeBox = Defines.ErrorBox;
+ $scope.errorBoxMessage = message;
+ $timeout(() => {
+ $scope.$digest();
+ });
+ };
+
+ /**
+ * Show the create public room box
+ */
+ $scope.showCreateRoomBox = function () {
+ $scope.activeBox = Defines.CreateRoomBox;
+ $scope.$broadcast(Defines.ShowCreateChatBox);
+ };
+
+ $scope.toggleMainBoxVisibility = function() {
+ $scope.setMainBoxMinimized(!$scope.mainBoxMinimized);
+ };
+
+ $scope.minimizeMainBox = function () {
+ $scope.setMainBoxMinimized(true);
+ };
+
+ $scope.setMainBoxMinimized = function (minimized) {
+ $scope.mainBoxMinimized = minimized;
+ LocalStorage.setProperty(LocalStorage.mainMinimizedKey, minimized);
+ };
+
+// $scope.saveRoomSlotToUser = function (room) {
+// $scope.getUser().updateRoomSlot(room, room.slot);
+// };
+
+ /**
+ * Show the floating profile box
+ * when the user's mouse leaves the box
+ * we wait a small amount of time before
+ * hiding the box - this gives the mouse
+ * time to go from the list to inside the
+ * box before the box disappears
+ */
+ $scope.showProfileBox = function (uid, duration) {
+
+ if(Config.disableUserInfoPopup) {
+ return;
+ }
+
+ $scope.friendsEnabled = Config.friendsEnabled;
+
+ $scope.profileBoxStyle = {
+ right: 250,
+ width: Dimensions.ProfileBoxWidth,
+ 'border-top-left-radius': 4,
+ 'border-bottom-left-radius': 4,
+ 'border-top-right-radius': 0,
+ 'border-bottom-right-radius': 0
+ };
+
+ if(!uid) {
+ if(duration === 0) {
+ $scope.currentUser = null;
+ }
+ else {
+ $scope.profileHideTimeoutPromise = $timeout(() => {
+ $scope.currentUser = null;
+ }, duration ? duration : 100);
+ }
+ }
+ else {
+ $scope.cancelTimer();
+ $scope.currentUser = UserStore.getUserWithID(uid);
+ let profileHTML = $scope.currentUser.getProfileHTML();
+ $scope.currentUserHTML = !profileHTML ? null : $sce.trustAsHtml(profileHTML);
+ }
+ };
+
+
+ $scope.cancelTimer = function () {
+ $timeout.cancel($scope.profileHideTimeoutPromise);
+ };
+
+ $scope.addRemoveFriend = function(user) {
+ if($scope.isFriend(user)) {
+ $scope.getUser().removeFriend(user);
+ }
+ else {
+ $scope.getUser().addFriend(user);
+ }
+ };
+
+ $scope.isFriend = function (user) {
+ return FriendsConnector.isFriend(user);
+ };
+
+ $scope.blockUnblockUser = function(user) {
+ if($scope.isBlocked(user)) {
+ $scope.getUser().unblockUser(user);
+ }
+ else {
+ $scope.getUser().blockUser(user);
+ }
+ };
+
+ $scope.isBlocked = function (user) {
+ if(user) {
+ return !Utils.unORNull(Cache.blockedUsers[user.uid()]);
+ }
+ return false;
+ };
+
+ $scope.buttonClassForUser = function (user) {
+ if(user) {
+ if($scope.isBlocked(user)) {
+ return 'uk-button-danger';
+ }
+ else if(!$scope.isOnline(user)) {
+ return null;
+ }
+ else {
+ return 'uk-button-success';
+ }
+ }
+ };
+
+ $scope.buttonTextForUser = function (user) {
+ if(user) {
+ if($scope.isBlocked(user)) {
+ return "Unblock";
+ }
+ else if(!$scope.isOnline(user)) {
+ return 'Offline';
+ }
+ else {
+ return 'Chat';
+ }
+ }
+ };
+
+ $scope.isOnline = function (user) {
+ return user.online;
+ };
+
+ /**
+ * @return number of online users
+ */
+ $scope.updateTotalUserCount = function () {
+ $scope.totalUserCount = OnlineConnector.onlineUserCount();
+ };
+
+ $scope.userClicked = function (user) {
+
+ // Is the user blocked?
+ if (Cache.isBlockedUser(user.uid())) {
+ $scope.getUser().unblockUser(user);
+ }
+ else {
+ // Check to see if there's an open room with the two users
+ let rooms = Cache.getPrivateRoomsWithUsers(UserStore.currentUser(), user);
+ if (rooms.length) {
+ let r = rooms[0];
+ if(r.getType() == RoomType.RoomType.OneToOne) {
+ r.flashHeader();
+ // The room is already open! Do nothing
+ return;
+ }
+ }
+ else {
+ rooms = RoomStore.getPrivateRoomsWithUsers(UserStore.currentUser(), user);
+ if(rooms.length) {
+ let room = rooms[0];
+ room.open(0, 300);
+ return;
+ }
+ }
+ RoomCreator.createPrivateRoom([user]).then((room: IRoom) => {
+ RoomOpenQueue.addRoomWithID(room.rid());
+ //let room = RoomStore.getOrCreateRoomWithID(rid);
+ }, (error) => {
+ console.log(error);
+ });
+ }
+ };
+
+ /**
+ *
+ */
+ $scope.logout = function () {
+
+ // Now we need to
+ Presence.goOffline();
+
+ //
+ Presence.stop();
+
+ if(UserStore.currentUser()) {
+ StateManager.userOff(UserStore.currentUser().uid());
+ }
+
+ StateManager.off();
+
+ // TODO: Should we set all rooms off?
+
+ RoomPositionManager.closeAllRooms();
+
+ NetworkManager.auth.setCurrentUserID(null);
+ $rootScope.user = null;
+
+ // Clear the cache down
+ Cache.clear();
+
+
+ // Allow the user to log back in
+ // Handled by callback
+ //$scope.showLoginBox();
+
+ // Set all current rooms off
+
+ $scope.hideNotification();
+
+ $scope.email = "";
+ $scope.password = "";
+
+ $rootScope.$broadcast(N.Logout);
+
+ LocalStorage.clearToken();
+
+ Auth.logout();
+
+ $timeout(() => {
+ $rootScope.$digest();
+ });
+ };
+
+ $scope.shutdown = function ($event) {
+
+ if (typeof $event.stopPropagation != "undefined") {
+ $event.stopPropagation();
+ } else {
+ $event.cancelBubble = true;
+ }
+
+ $scope.on = !$scope.on;
+ if($scope.on) {
+ LocalStorage.setOffline(false);
+ Presence.goOnline();
+ }
+ else {
+ Presence.goOffline();
+ LocalStorage.setOffline(true);
+ }
+ };
+
+ $scope.shutdownImage = function () {
+ if($scope.on) {
+ return $scope.img_30_shutdown_on;
+ }
+ else {
+ return $scope.img_30_shutdown;
+ }
+ };
+
+ // File uploads
+ $scope.onFileSelect = function($files) {
+
+ $scope.uploadingFile = false;
+ $scope.uploadProgress = 0;
+
+ let f = $files[0];
+ if(!f) {
+ return;
+ }
+
+ if(f.type == "image/png" || f.type == 'image/jpeg') {
+
+ }
+ else {
+ $scope.showNotification(Defines.NotificationTypeAlert, 'File error', 'Only image files can be uploaded', 'ok');
+ return;
+ }
+
+ if($files.length > 0) {
+ NetworkManager.upload.uploadFile($files[0]).then((path) => {
+ $scope.getUser().updateImageURL(path);
+ });
+
+ //Parse.uploadFile($files[0]).then((function(r) {
+ //
+ // if(r.data && r.data.url) {
+ //
+ // $scope.getUser().updateImageURL(r.data.url);
+ // }
+ //
+ //}).bind(this), (function (error) {
+ //
+ //}).bind(this));
+ }
+
+ let reader = new FileReader();
+
+ // Load the image into the canvas immediately - so the user
+ // doesn't have to wait for it to upload
+ reader.onload = (() => {
+ return function(e) {
+
+ let image = new Image();
+
+ image.onload = function () {
+
+ // Resize the image
+ let canvas = document.createElement('canvas'),
+ max_size = 100,
+ width = image.width,
+ height = image.height;
+
+ let x = 0;
+ let y = 0;
+
+ if (width > height) {
+ x = (width - height)/2;
+
+ } else {
+ y = (height - width)/2;
+ }
+
+ //let size = width - 2 * x;
+
+ // First rescale the image to be square
+ canvas.width = max_size;
+ canvas.height = max_size;
+ canvas.getContext('2d').drawImage(image, x, y, width - 2 * x, height - 2 * y, 0, 0, max_size, max_size);
+
+ let imageDataURL = canvas.toDataURL('image/jpeg');
+
+ // Set the user's image
+ $scope.$apply(() => {
+ $scope.getUser().setImage(imageDataURL, true);
+ });
+
+ };
+ image.src = e.target.result;
+ };
+ });
+
+ reader.readAsDataURL(f);
+
+ };
+
+ $scope.hideNotification = function () {
+ $scope.notification.show = false;
+ };
+
+ $scope.showNotification = function (type, title, message, button) {
+ $scope.notification.title = title;
+ $scope.notification.message = message;
+ $scope.notification.type = type;
+ $scope.notification.button = button;
+ $scope.notification.show = true;
+ $timeout(() => {
+ $scope.$digest();
+ });
+ };
+
+ $scope.init();
+
+ }]);
diff --git a/src/angularjs/controllers/chat-bar.ts b/src/angularjs/controllers/chat-bar.ts
new file mode 100755
index 00000000..99cbb49d
--- /dev/null
+++ b/src/angularjs/controllers/chat-bar.ts
@@ -0,0 +1,51 @@
+import * as angular from 'angular'
+import {N} from "../keys/notification-keys";
+import {IRoomListScope} from "./room-list-box";
+import {ICache} from "../persistence/cache";
+import {IRoom} from "../entities/room";
+import {Log} from "../services/log";
+
+export interface IChatBarController {
+ rooms: IRoom []
+ updateList(): void
+}
+
+class ChatBarController implements IChatBarController{
+
+ static $inject = ['$scope', '$timeout', 'Cache'];
+
+ public rooms: IRoom [] = [];
+
+ constructor (
+ private $scope: IRoomListScope,
+ private $timeout: ng.ITimeoutService,
+ private Cache: ICache) {
+
+ const updateList = () => {
+ this.updateList();
+ };
+
+ $scope.$on(N.RoomOpened, updateList);
+ $scope.$on(N.RoomClosed, updateList);
+ $scope.$on(N.Logout, updateList);
+
+ $scope.$on(N.UpdateRoomActiveStatus, () => {
+ Log.notification(N.UpdateRoomActiveStatus, 'ChatBarController');
+ updateList();
+ });
+ }
+
+ updateList(): void {
+ Log.notification(N.RoomOpened + "/" + N.RoomClosed, 'ChatBarController');
+
+ // Only include rooms that are active
+ this.rooms = this.Cache.activeRooms();
+
+ this.$timeout(() => {
+ this.$scope.$digest();
+ })
+ }
+
+}
+
+angular.module('myApp.controllers').controller('ChatBarController', ChatBarController);
\ No newline at end of file
diff --git a/src/angularjs/controllers/chat-embed.ts b/src/angularjs/controllers/chat-embed.ts
new file mode 100755
index 00000000..e88bb5c4
--- /dev/null
+++ b/src/angularjs/controllers/chat-embed.ts
@@ -0,0 +1,60 @@
+import * as angular from 'angular'
+import {RoomType} from "../keys/room-type";
+import {UserStatus} from "../keys/user-status";
+import {N} from "../keys/notification-keys";
+
+angular.module('myApp.controllers').controller('ChatEmbedController', ['$scope', '$timeout', '$rootScope', 'RoomStore', function($scope, $timeout, $rootScope, RoomStore) {
+
+ $scope.rooms = [];
+
+ $scope.init = (rid, width, height) => {
+ $scope.rid = rid;
+ $scope.width = width;
+ $scope.height = height;
+
+ // When login is complete setup this room
+ $scope.$on(N.LoginComplete, () => {
+
+ let rid = $scope.rid;
+ let room = RoomStore.getOrCreateRoomWithID(rid);
+ room.on().then(() => {
+
+ room.width = $scope.width;
+ room.height = $scope.height;
+
+ let open = () => {
+
+ // Start listening to message updates
+ room.messagesOn(room.deletedTimestamp);
+
+ // Start listening to typing indicator updates
+ room.typingOn();
+
+ };
+
+ switch (room.getType()) {
+ case RoomType.Public:
+ room.join(UserStatus.Member).then(() => {
+ open();
+ }, (error) => {
+ console.log(error);
+ });
+ break;
+ case RoomType.Group:
+ case RoomType.OneToOne:
+ open();
+ }
+
+ $scope.rooms = [
+ room
+ ];
+
+ $timeout(() => {
+ $rootScope.$digest();
+ });
+
+ });
+
+ });
+ };
+}]);
\ No newline at end of file
diff --git a/src/angularjs/controllers/chat-settings.ts b/src/angularjs/controllers/chat-settings.ts
new file mode 100755
index 00000000..1e029dba
--- /dev/null
+++ b/src/angularjs/controllers/chat-settings.ts
@@ -0,0 +1,40 @@
+import * as angular from 'angular'
+import {DEBUG} from "../keys/defines";
+import * as FileSaver from 'file-saver'
+import {IRoomScope} from "./chat";
+import {IRoom} from "../entities/room";
+
+
+export interface IChatSettingsController {
+ $scope: IRoomScope
+ saveTranscript()
+ copyTranscript()
+}
+
+class ChatSettingsController implements IChatSettingsController {
+
+ static $inject = ['$scope'];
+
+ public room: IRoom;
+ public $scope: IRoomScope;
+
+ constructor ($scope: IRoomScope) {
+ this.$scope = $scope;
+ }
+
+ copyTranscript() {
+ window.prompt("Copy to clipboard: Ctrl+C, Enter", this.$scope.room.transcript());
+ };
+
+ saveTranscript() {
+
+ let t = this.$scope.room.transcript();
+
+ if(DEBUG) console.log(t);
+
+ FileSaver.saveAs(new Blob([t], {type: "text/plain;charset=utf-8"}), this.$scope.room.name + "-transcript.txt");
+
+ };
+}
+
+angular.module('myApp.controllers').controller('ChatSettingsController', ChatSettingsController);
\ No newline at end of file
diff --git a/src/angularjs/controllers/chat.ts b/src/angularjs/controllers/chat.ts
new file mode 100755
index 00000000..b4217d34
--- /dev/null
+++ b/src/angularjs/controllers/chat.ts
@@ -0,0 +1,360 @@
+import * as angular from 'angular'
+
+import {N} from "../keys/notification-keys";
+import * as Defines from "../keys/defines";
+import * as TabKeys from "../keys/tab-keys";
+import {IRoom} from "../entities/room";
+import {Dimensions} from "../keys/dimensions";
+import {Utils} from "../services/utils";
+import {ArrayUtils} from "../services/array-utils";
+import {Log} from "../services/log";
+
+export interface IRoomScope extends ng.IScope {
+ room: IRoom
+ resizing: any
+ dragging: any
+ startDrag: any
+ wasDragged: any
+ inputHeight: any
+ input: any
+ emojis: string[]
+ autoScroll: boolean
+ leaveRoom()
+}
+
+export interface MessageScope extends ng.IScope {
+ message: any,
+}
+
+angular.module('myApp.controllers').controller('ChatController', ['$scope', '$timeout', '$window', '$sce', 'Config', 'Auth', 'Screen', 'RoomPositionManager', 'NetworkManager',
+ function ($scope, $timeout, $window, $sce, Config, Auth, Screen, RoomPositionManager, NetworkManager) {
+
+ $scope.showEmojis = false;
+ $scope.showMessageOptions = false;
+
+ //$scope.headerColor = $scope.config.headerColor;
+ $scope.loginIframeURL = $sce.trustAsResourceUrl('http://ccwp/social.html');
+
+ $scope.init = function (room) {
+
+ // let room = RoomStore.getRoomWithID(rid);
+
+ $scope.input = {};
+ $scope.room = room;
+
+ $scope.hideChat = false;
+
+ $scope.tabClicked('messages');
+
+ // The height of the bottom message input bar
+ $scope.inputHeight = 26;
+
+ let digest = function (callback) {
+ $timeout(() => {
+ $scope.$digest();
+ if(callback) {
+ callback();
+ }
+ });
+ };
+
+ // When the user value changes update the user interface
+ $scope.$on(N.UserValueChanged, (event, user) => {
+ Log.notification(N.UserValueChanged, 'ChatController');
+ if($scope.room.containsUser(user)) {
+ digest(null);
+ }
+ });
+
+ $scope.$on(N.RoomPositionUpdated, (event, room) => {
+ Log.notification(N.RoomPositionUpdated, 'ChatController');
+ if($scope.room == room) {
+ // Update the room's active status
+ digest(null);
+ }
+ });
+ $scope.$on(N.RoomSizeUpdated, (event, room) => {
+ Log.notification(N.RoomSizeUpdated, 'ChatController');
+ if($scope.room == room) {
+ digest(null);
+ }
+ });
+ $scope.$on(N.LazyLoadedMessages, (event, room) => {
+ Log.notification(N.LazyLoadedMessages, 'ChatController');
+ if($scope.room == room) {
+ digest(null);
+ }
+ });
+ $scope.$on(N.ChatUpdated, (event, room) => {
+ Log.notification(N.ChatUpdated, 'CreateRoomController');
+ if($scope.room == room) {
+ digest(null);
+ }
+ });
+ };
+
+ $scope.enabledMessageOptions = function () {
+ let list = [];
+ if (Config.fileMessagesEnabled) {
+ list.push('fileMessagesEnabled');
+ }
+ if (Config.imageMessagesEnabled) {
+ list.push('imageMessagesEnabled');
+ }
+ return list;
+ };
+
+ $scope.enabledMessageOptionsCount = function () {
+ return $scope.enabledMessageOptions().length;
+ };
+
+ $scope.onSelectImage = function (room) {
+ $scope.showMessageOptions = false;
+ $scope.uploadingFile = true;
+ this.sendImageMessage($window.event.target.files, room)
+ };
+
+ $scope.onSelectFile = function (room) {
+ $scope.showMessageOptions = false;
+ $scope.uploadingFile = true;
+ this.sendFileMessage($window.event.target.files, room)
+ };
+
+ $scope.imageUploadFinished = function () {
+ $scope.uploadingFile = false;
+ $scope.sendingImage = false;
+ };
+
+ $scope.fileUploadFinished = function () {
+ $scope.uploadingFile = false;
+ $scope.sendingFile = false;
+ };
+
+ $scope.sendImageMessage = function ($files, room) {
+
+ if ($scope.sendingImage || $files.length === 0) {
+ this.imageUploadFinished();
+ return;
+ }
+
+ let f = $files[0];
+
+ if (f.type == 'image/png' || f.type == 'image/jpeg') {
+ $scope.sendingImage = true;
+ }
+ else {
+ $scope.showNotification(Defines.NotificationTypeAlert, 'File error', 'Only image files can be uploaded', 'ok');
+ this.imageUploadFinished();
+ return;
+ }
+
+ NetworkManager.upload.uploadFile(f).then((r) => {
+ let url = (typeof r === 'string' ? r : r.data && r.data.url);
+ if (typeof url === 'string' && url.length > 0) {
+ let reader = new FileReader();
+
+ // Load the image into the canvas immediately to get the dimensions
+ reader.onload = () => {
+ return (e) => {
+ let image = new Image();
+ image.onload = () => {
+ room.sendImageMessage($scope.getUser(), url, image.width, image.height);
+ };
+ image.src = e.target.result;
+ };
+ };
+ reader.readAsDataURL(f);
+ }
+ this.imageUploadFinished();
+
+ }, (error) => {
+ $scope.showNotification(Defines.NotificationTypeAlert, 'Image error', 'The image could not be sent', 'ok');
+ this.imageUploadFinished();
+ });
+ };
+
+ $scope.sendFileMessage = function ($files, room) {
+
+ if ($scope.sendingFile || $files.length === 0) {
+ this.fileUploadFinished();
+ return;
+ }
+
+ let f = $files[0];
+
+ if (f.type == 'image/png' || f.type == 'image/jpeg') {
+ this.sendImageMessage($files, room);
+ return;
+ }
+ else {
+ $scope.sendingFile = true;
+ }
+
+ NetworkManager.upload.uploadFile(f).then((r) => {
+ let url = (typeof r === 'string' ? r : r.data && r.data.url)
+ if (typeof url === 'string' && url.length > 0) {
+ room.sendFileMessage($scope.getUser(), f.name, f.type, url);
+ }
+ this.fileUploadFinished();
+
+ }, (error) => {
+ $scope.showNotification(Defines.NotificationTypeAlert, 'File error', 'The file could not be sent', 'ok');
+ this.fileUploadFinished();
+ });
+ };
+
+ $scope.getZIndex = function () {
+ // Make sure windows further to the right have a higher index
+ let z = $scope.room.zIndex ? $scope.room.zIndex : 100 * (1 - $scope.room.offset/Screen.screenWidth);
+ return parseInt(z);
+ };
+
+ $scope.sendMessage = function () {
+ console.log('sendMessage()');
+ let user = $scope.getUser();
+
+ $scope.showEmojis = false;
+ $scope.showMessageOptions = false;
+
+ $scope.room.sendTextMessage(user, $scope.input.text);
+ $scope.input.text = "";
+ };
+
+ $scope.loadMoreMessages = function(): Promise {
+ return $scope.room.loadMoreMessages();
+ };
+
+ $scope.tabClicked = function (tab) {
+ $scope.activeTab = tab;
+ if (tab == TabKeys.MessagesTab) {
+ $scope.showEmojis = false;
+ $scope.showMessageOptions = false;
+ }
+ };
+
+ $scope.chatBoxStyle = function () {
+ return $scope.hideChat ? 'style="0px"' : "";
+ };
+
+ $scope.toggleVisibility = function () {
+ if($scope.boxWasDragged) {
+ return;
+ }
+ $scope.setMinimized(!$scope.room.minimized);
+ $scope.room.badge = null;
+ };
+
+ $scope.toggleEmoticons = function () {
+ $scope.showMessageOptions = false;
+ $scope.showEmojis = !$scope.showEmojis;
+ };
+
+ $scope.toggleMessageOptions = function () {
+ $scope.showEmojis = false;
+ $scope.showMessageOptions = !$scope.showMessageOptions;
+ };
+
+ // Save the super class
+ $scope.superShowProfileBox = $scope.showProfileBox;
+ $scope.showProfileBox = function (uid) {
+
+ $scope.superShowProfileBox(uid);
+
+ // Work out the x position
+ let x = $scope.room.offset + $scope.room.width;
+
+ let facesLeft = true;
+ if ($scope.room.offset + Dimensions.ProfileBoxWidth + $scope.room.width > Screen.screenWidth) {
+ facesLeft = false;
+ x = $scope.room.offset - Dimensions.ProfileBoxWidth;
+ }
+
+ $scope.profileBoxStyle.right = x;
+ $scope.profileBoxStyle['border-top-left-radius'] = facesLeft ? 4 : 0;
+ $scope.profileBoxStyle['border-bottom-left-radius'] = facesLeft ? 4 : 0;
+ $scope.profileBoxStyle['border-top-right-radius'] = facesLeft ? 0 : 4;
+ $scope.profileBoxStyle['border-bottom-right-radius'] = facesLeft ? 0 : 4;
+ };
+
+ $scope.acceptInvitation = function () {
+ $scope.room.acceptInvitation();
+ };
+
+ $scope.minimize = function () {
+ $scope.setMinimized(true);
+ };
+
+ $scope.setMinimized = function (minimized) {
+ $scope.room.minimized = minimized;
+ $scope.chatBoxStyle = minimized ? {height: 0} : {};
+ RoomPositionManager.setDirty();
+ RoomPositionManager.updateRoomPositions($scope.room, 0);
+ RoomPositionManager.updateAllRoomActiveStatus();
+ };
+
+ $scope.startDrag = function () {
+ $scope.dragStarted = true;
+ $scope.boxWasDragged = false;
+ };
+
+ $scope.wasDragged = function () {
+ // We don't want the chat crossing the min point
+ if($scope.room.offset < $scope.mainBoxWidth + Dimensions.ChatRoomSpacing) {
+ $scope.room.setOffset($scope.mainBoxWidth + Dimensions.ChatRoomSpacing);
+ }
+ $scope.boxWasDragged = true;
+ };
+
+ $scope.getAllUsers = function () {
+ if (!Utils.unORNull($scope.room)) {
+ return ArrayUtils.objectToArray($scope.room.getUsers());
+ } else {
+ return [];
+ }
+ };
+
+ $scope.searchKeyword = function () {
+ return null;
+ };
+
+// $scope.getUsers = function () {
+//
+// let users = $scope.room.getUsers();
+// // Add the users to an array
+// let array = [];
+// for(let key in users) {
+// if(users.hasOwnProperty(key)) {
+// array.push(users[key]);
+// }
+// }
+// // Sort the array
+// array.sort(function (a, b) {
+// a = Utils.unORNull(a.online) ? false : a.online;
+// b = Utils.unORNull(b.online) ? false : b.online;
+//
+// if(a == b) {
+// return 0;
+// }
+// else {
+// return a == true ? -1 : 1;
+// }
+// });
+//
+// return array;
+// };
+
+ $scope.setTyping = function (typing) {
+ if(typing) {
+ $scope.room.startTyping($scope.getUser());
+ }
+ else {
+ $scope.room.finishTyping($scope.getUser());
+ }
+ };
+
+ $scope.leaveRoom = function () {
+ $scope.room.close();
+ $scope.room.leave();
+ }
+
+ }]);
diff --git a/src/angularjs/controllers/create-room.ts b/src/angularjs/controllers/create-room.ts
new file mode 100755
index 00000000..2d5df14a
--- /dev/null
+++ b/src/angularjs/controllers/create-room.ts
@@ -0,0 +1,65 @@
+import * as angular from 'angular'
+import {ShowCreateChatBox} from "../keys/defines";
+import {RoomType} from "../keys/room-type";
+import {IRoomCreator} from "../entities/room";
+import {IRoomOpenQueue} from "../services/room-open-queue";
+import {Log} from "../services/log";
+
+class CreateRoomController {
+ static $inject = ['$scope', 'RoomCreator', 'RoomOpenQueue'];
+
+ constructor (
+ private $scope,
+ private RoomCreator: IRoomCreator,
+ private RoomOpenQueue: IRoomOpenQueue,
+ ){
+ this.clearForm();
+
+ $scope.$on(ShowCreateChatBox , () =>{
+ Log.notification(ShowCreateChatBox, 'CreateRoomController');
+ $scope.focusName = true;
+ });
+ }
+
+ createRoom() {
+
+ let promise;
+
+ // Is this a public room?
+ if(this.$scope.public) {
+ promise = this.RoomCreator.createPublicRoom(
+ this.$scope.room.name,
+ this.$scope.room.description
+ );
+ }
+ else {
+ promise = this.RoomCreator.createRoom(
+ this.$scope.room.name,
+ this.$scope.room.description,
+ this.$scope.room.invitesEnabled,
+ RoomType.OneToOne
+ );
+ }
+
+ promise.then((rid) => {
+ this.RoomOpenQueue.addRoomWithID(rid);
+ });
+
+ this.back();
+ };
+
+ back() {
+ this.clearForm();
+ this.$scope.showMainBox();
+ };
+
+ clearForm() {
+ this.$scope.room = {
+ invitesEnabled: false,
+ name: null,
+ description: null
+ };
+ };
+}
+
+angular.module('myApp.controllers').controller('CreateRoomController', CreateRoomController);
diff --git a/src/angularjs/controllers/draggable-user.ts b/src/angularjs/controllers/draggable-user.ts
new file mode 100755
index 00000000..c0f125cb
--- /dev/null
+++ b/src/angularjs/controllers/draggable-user.ts
@@ -0,0 +1,8 @@
+import * as angular from 'angular'
+
+angular.module('myApp.controllers').controller('DraggableUserController', ['$scope', function($scope) {
+ $scope.init = function () {
+
+ };
+ $scope.init();
+}]);
\ No newline at end of file
diff --git a/src/angularjs/controllers/emoji.ts b/src/angularjs/controllers/emoji.ts
new file mode 100755
index 00000000..14e9c574
--- /dev/null
+++ b/src/angularjs/controllers/emoji.ts
@@ -0,0 +1,54 @@
+import * as angular from 'angular'
+import {Emoji} from "../services/emoji";
+import {IRoomScope} from "./chat";
+
+// module app.controllers {
+
+export interface IEmojiController {
+ addEmoji(text: string): void
+ getEmojis(): string[]
+}
+
+class EmojiController implements IEmojiController{
+
+ static $inject = ['Emoji', '$scope'];
+
+ private emoji: Emoji;
+ private $scope: IRoomScope;
+ private emojis: string[];
+
+ constructor (public Emoji: Emoji, $scope: IRoomScope) {
+ this.emoji = Emoji;
+ this.$scope = $scope;
+ this.emojis = this.getEmojis()
+ }
+
+ addEmoji(text: string): void {
+ if(!this.$scope.input.text) {
+ this.$scope.input.text = ""
+ }
+ this.$scope.input.text += text
+ }
+
+ getEmojis(): string[] {
+ return this.emoji.getEmojis()
+ }
+
+}
+
+// TODO: Finish this
+class EmojiComponent implements ng.IComponentOptions {
+
+ public bindings:any;
+ public controller:any;
+ public templateUrl:'emojis.html';
+
+ constructor () {
+ this.controller = EmojiController
+ }
+
+}
+
+angular.module('myApp.controllers').controller('EmojiController', EmojiController);
+
+
diff --git a/src/angularjs/controllers/error-box.ts b/src/angularjs/controllers/error-box.ts
new file mode 100755
index 00000000..41422a34
--- /dev/null
+++ b/src/angularjs/controllers/error-box.ts
@@ -0,0 +1,6 @@
+import * as angular from 'angular'
+
+angular.module('myApp.controllers').controller('ErrorBoxController', ['$scope', function($scope) {
+
+
+}]);
\ No newline at end of file
diff --git a/src/angularjs/controllers/inbox-rooms-list.ts b/src/angularjs/controllers/inbox-rooms-list.ts
new file mode 100755
index 00000000..e9bb2eb7
--- /dev/null
+++ b/src/angularjs/controllers/inbox-rooms-list.ts
@@ -0,0 +1,56 @@
+import * as angular from 'angular'
+import {N} from "../keys/notification-keys";
+import {ArrayUtils} from "../services/array-utils";
+import {Log} from "../services/log";
+
+angular.module('myApp.controllers').controller('InboxRoomsListController', ['$scope', '$timeout', 'RoomStore', function($scope, $timeout, RoomStore) {
+
+ $scope.rooms = [];
+ $scope.allRooms = [];
+
+ $scope.init = function () {
+
+ $scope.$on(N.RoomAdded, () => {
+ Log.notification(N.RoomAdded, 'InboxRoomsListController');
+ $scope.updateList();
+
+ });
+
+ $scope.$on(N.RoomRemoved, () => {
+ Log.notification(N.RoomRemoved, 'InboxRoomsListController');
+ $scope.updateList();
+ });
+
+ $scope.$on(N.LoginComplete, () => {
+ Log.notification(N.LoginComplete, 'InboxRoomsListController');
+ RoomStore.loadPrivateRoomsToMemory();
+ $scope.updateList();
+ });
+
+ // Update the list if the user count on a room changes
+ $scope.$on(N.RoomUpdated, $scope.updateList);
+
+ $scope.$on(N.Logout, $scope.updateList);
+
+ $scope.$watchCollection('search', $scope.updateList);
+
+ };
+
+ $scope.updateList = function () {
+
+ $scope.allRooms = RoomStore.getPrivateRooms();
+
+ $scope.allRooms = ArrayUtils.roomsSortedByMostRecent($scope.allRooms);
+
+ $scope.rooms = ArrayUtils.filterByKey($scope.allRooms, $scope.search[$scope.activeTab], (room) => {
+ return room.meta.name;
+ });
+
+ $timeout(() => {
+ $scope.$digest();
+ });
+ };
+
+ $scope.init();
+
+}]);
diff --git a/src/angularjs/controllers/login.ts b/src/angularjs/controllers/login.ts
new file mode 100755
index 00000000..5275a218
--- /dev/null
+++ b/src/angularjs/controllers/login.ts
@@ -0,0 +1,256 @@
+import * as angular from 'angular'
+import * as firebase from 'firebase';
+
+import {NotificationTypeAlert, NotificationTypeWaiting} from "../keys/defines";
+import {N} from "../keys/notification-keys";
+import {Utils} from "../services/utils";
+import {LoginMode} from "../keys/login-mode-keys";
+
+angular.module('myApp.controllers').controller('LoginController', ['$rootScope', '$scope', '$timeout', 'FriendsConnector', 'Cache', 'Presence', 'SingleSignOn','OnlineConnector', 'Paths', 'LocalStorage', 'StateManager', 'RoomPositionManager', 'Config', 'Auth', 'Credential', 'AutoLogin',
+ function($rootScope, $scope, $timeout, FriendsConnector, Cache, Presence, SingleSignOn, OnlineConnector, Paths, LocalStorage, StateManager, RoomPositionManager, Config, Auth, Credential, AutoLogin) {
+
+ /**
+ * Initialize the login controller
+ * Add listeners to AngularFire login, logout and error broadcasts
+ * Setup the auth variable and try to authenticate
+ */
+ $scope.init = function () {
+
+ $scope.rememberMe = true;
+
+ $scope.showLoginBox(LoginMode.Authenticating);
+
+ if (AutoLogin.autoLoginEnabled()) {
+ const _ = firebase.auth().signOut();
+ }
+
+ firebase.auth().onAuthStateChanged((authData) => {
+ if (!Auth.isAuthenticating()) {
+ $scope.authenticate(null);
+ }
+ });
+
+
+ // Auth.setAuthListener((function (authData) {
+ // $scope.authenticate(null);
+ // }).bind(this));
+
+ };
+
+ $scope.startChatting = function() {
+ LocalStorage.setLastVisited();
+ $scope.authenticate(null);
+ };
+
+ $scope.authenticate = function (credential) {
+ $scope.showLoginBox(LoginMode.Authenticating);
+
+ Auth.authenticate(credential).then((authUser) => {
+ $scope.handleAuthData(authUser);
+ }).catch((error) => {
+ if (!Utils.unORNull(error)) {
+ $scope.handleLoginError(error);
+ } else {
+ $scope.showLoginBox($scope.getLoginMode());
+ }
+ });
+ };
+
+ $scope.getLoginMode = function () {
+
+ let loginMode = LoginMode.Simple;
+ let lastVisited = LocalStorage.getLastVisited();
+
+ // We don't want to load the messenger straightaway to save bandwidth.
+ // This will check when they last accessed the chat. If it was less than the timeout time ago,
+ // then the click to chat box will be displayed. Clicking that will reset the timer
+ if(Utils.unORNull(lastVisited) || (new Date().getTime() - lastVisited)/1000 > Config.clickToChatTimeout && Config.clickToChatTimeout > 0) {
+ loginMode = LoginMode.ClickToChat;
+ }
+ return loginMode;
+ };
+
+ $scope.handleAuthData = function (authData) {
+ $rootScope.loginMode = Auth.mode;
+
+ console.log(authData);
+
+ $rootScope.auth = authData;
+ if(authData) {
+ $scope.handleLoginComplete(authData, false);
+ }
+ else {
+ $scope.showLoginBox();
+ }
+ };
+
+ $scope.setError = function (message) {
+ $scope.showError = !Utils.unORNull(message);
+ $scope.errorMessage = message;
+ };
+
+ $scope.loginWithPassword = function () {
+ $scope.login(new Credential().emailAndPassword($scope.email, $scope.password));
+ };
+
+ $scope.loginWithFacebook = function () {
+ $scope.login(new Credential().facebook());
+ };
+
+ $scope.loginWithTwitter = function () {
+ $scope.login(new Credential().twitter());
+ };
+
+ $scope.loginWithGoogle = function () {
+ $scope.login(new Credential().google());
+ };
+
+ $scope.loginWithGithub = function () {
+ $scope.login(new Credential().github());
+ };
+
+ $scope.loginWithAnonymous = function () {
+ $scope.login(new Credential().anonymous());
+ };
+
+ /**
+ * Log the user in using the appropriate login method
+ * @param method - the login method: facebook, twitter etc...
+ * @param options - hash of options: remember me etc...
+ */
+ $scope.login = function (credential) {
+
+ // TODO: Move this to a service!
+ // Re-establish a connection with Firebase
+ Presence.goOnline();
+
+ // Reset any error messages
+ $scope.showError = false;
+
+ // Hide the overlay
+ $scope.showNotification(NotificationTypeWaiting, "Logging in", "For social login make sure to enable popups!");
+
+ Auth.authenticate(credential).then((authData) => {
+ $scope.handleAuthData(authData);
+ }).catch((error) => {
+ $scope.hideNotification();
+ $scope.handleLoginError(error);
+
+ $timeout(() =>{
+ $scope.$digest();
+ });
+ });
+ };
+
+ $scope.forgotPassword = function (email) {
+
+ Auth.resetPasswordByEmail(email).then(() => {
+ $scope.showNotification(NotificationTypeAlert, "Email sent",
+ "Instructions have been sent. Please check your Junk folder!", "ok");
+ $scope.setError(null);
+ }).catch((error) => {
+ $scope.handleLoginError(error);
+ });
+ };
+
+ /**
+ * Create a new account
+ * @param email - user's email
+ * @param password - user's password
+ */
+ $scope.signUp = function (email, password) {
+
+ // Re-establish connection with Firebase
+ Presence.goOnline();
+
+ $scope.showError = false;
+
+ $scope.showNotification(NotificationTypeWaiting, "Registering...");
+
+ // First create the super
+
+ Auth.signUp(email, password).then(() => {
+ $scope.email = email;
+ $scope.password = password;
+ $scope.loginWithPassword();
+ }).catch((error) => {
+ $scope.handleLoginError(error);
+ });
+ };
+
+ /**
+ * Bind the user to Firebase
+ * Using the user's authentcation information create
+ * a three way binding to the user property
+ * @param userData - User object from Firebase authentication
+ * @param firstLogin - Has the user just signed up?
+ */
+
+ $scope.handleLoginComplete = function (userData, firstLogin) {
+
+ // Write a record to the firebase to record this API key
+ $scope.showNotification(NotificationTypeWaiting, "Opening Chat...");
+
+ // Load friends from config
+ if(Config.friends) {
+ FriendsConnector.addFriendsFromConfig(Config.friends);
+ }
+
+ // This allows us to clear the cache remotely
+ LocalStorage.clearCacheWithTimestamp(Config.clearCacheTimestamp);
+
+ // We have the user's ID so we can get the user's object
+ if(firstLogin) {
+ $scope.showProfileSettingsBox();
+ }
+ else {
+ $scope.showMainBox();
+ }
+
+ $rootScope.$broadcast(N.LoginComplete);
+ $scope.hideNotification();
+
+ };
+
+ /**
+ * Handle a login error
+ * Show a red warning box in the UI with the
+ * error message
+ * @param error - error returned from Firebase
+ */
+ $scope.handleLoginError = function (error) {
+
+ // The login failed - display a message to the user
+ $scope.hideNotification();
+
+ let message = "An unknown error occurred";
+
+ if (error.code == 'AUTHENTICATION_DISABLED') {
+ message = "This authentication method is currently disabled.";
+ }
+ if (error.code == 'EMAIL_TAKEN') {
+ message = "Email address unavailable.";
+ }
+ if (error.code == 'INVALID_EMAIL') {
+ message = "Please enter a valid email.";
+ }
+ if (error.code == 'INVALID_ORIGIN') {
+ message = "Login is not available from this domain.";
+ }
+ if (error.code == 'INVALID_PASSWORD') {
+ message = "Please enter a valid password.";
+ }
+ if (error.code == 'INVALID_USER') {
+ message = "Invalid email or password.";
+ }
+ if (error.code == 'INVALID_USER') {
+ message = "Invalid email or password.";
+ }
+
+ $scope.setError(message);
+
+ };
+
+ $scope.init();
+
+ }]);
diff --git a/src/angularjs/controllers/main-box.ts b/src/angularjs/controllers/main-box.ts
new file mode 100755
index 00000000..4eb6a38c
--- /dev/null
+++ b/src/angularjs/controllers/main-box.ts
@@ -0,0 +1,177 @@
+import * as angular from 'angular'
+import {N} from "../keys/notification-keys";
+import {FriendsTab, InboxTab, RoomsTab, UsersTab} from "../keys/tab-keys";
+import {NotificationTypeWaiting} from "../keys/defines";
+import {Dimensions} from "../keys/dimensions";
+import {ArrayUtils} from "../services/array-utils";
+import {Log} from "../services/log";
+
+angular.module('myApp.controllers').controller('MainBoxController', ['$scope', '$timeout', 'Auth', 'FriendsConnector', 'Config', 'Screen', 'RoomPositionManager', 'RoomStore',
+ function($scope, $timeout, Auth, FriendsConnector, Config, Screen, RoomPositionManager, RoomStore) {
+
+ $scope.inboxCount = 0;
+
+ $scope.usersTabEnabled = true;
+ $scope.roomsTabEnabled = true;
+ $scope.friendsTabEnabled = true;
+ $scope.tabCount = 0;
+ $scope.config = Config;
+
+ $scope.init = function () {
+
+ // Work out how many tabs there are
+ $scope.$on(N.ConfigUpdated , () =>{
+ $scope.updateConfig();
+ });
+ $scope.updateConfig();
+
+ // Setup the search variable - if we don't do this
+ // Angular can't set search.text
+ $scope.search = {};
+ $scope.search[UsersTab] = "";
+ $scope.search[RoomsTab] = "";
+ $scope.search[FriendsTab] = "";
+
+ // This is used by sub views for their layouts
+ $scope.boxWidth = Dimensions.MainBoxWidth;
+
+ // We don't want people deleting rooms from this view
+ $scope.canCloseRoom = false;
+
+ // When the user value changes update the user interface
+ $scope.$on(N.UserValueChanged , () =>{
+ Log.notification(N.UserValueChanged, "MainBoxController");
+ $timeout(() => {
+ $scope.$digest();
+ });
+ });
+
+ $scope.updateMainBoxSize();
+ $scope.$on(N.ScreenSizeChanged , () =>{
+ Log.notification(N.ScreenSizeChanged, "MainBoxController");
+ $scope.updateMainBoxSize();
+ });
+
+ $scope.$on(N.RoomBadgeChanged , () =>{
+ Log.notification(N.RoomBadgeChanged, "MainBoxController");
+ $scope.updateInboxCount();
+ });
+
+ $scope.$on(N.LoginComplete , () =>{
+ Log.notification(N.RoomRemoved, 'InboxRoomsListController');
+ $scope.updateInboxCount();
+ });
+
+ };
+
+ $scope.updateConfig = function () {
+ $scope.usersTabEnabled = Config.onlineUsersEnabled;
+ $scope.roomsTabEnabled = Config.publicRoomsEnabled;
+ $scope.friendsTabEnabled = Config.friendsEnabled;
+
+ $scope.tabCount = $scope.numberOfTabs();
+
+ // Make the users tab start clicked
+ if(Config.onlineUsersEnabled) {
+ $scope.tabClicked(UsersTab);
+ }
+ else if(Config.publicRoomsEnabled) {
+ $scope.tabClicked(RoomsTab);
+ }
+ else {
+ $scope.tabClicked(InboxTab);
+ }
+
+ $timeout(() => {
+ $scope.$digest();
+ })
+ };
+
+ $scope.numberOfTabs = function () {
+ let tabs = 1;
+ if(Config.onlineUsersEnabled) {
+ tabs++;
+ }
+ if(Config.publicRoomsEnabled) {
+ tabs++;
+ }
+ if(Config.friendsEnabled) {
+ tabs++;
+ }
+ return tabs;
+ };
+
+ $scope.updateInboxCount = function () {
+ $scope.inboxCount = RoomStore.inboxBadgeCount();
+ $timeout(() => {
+ $scope.$digest();
+ });
+ };
+
+ $scope.updateMainBoxSize = function () {
+ $scope.mainBoxHeight = Math.max(Screen.screenHeight * 0.5, Dimensions.MainBoxHeight);
+ $scope.mainBoxWidth = Dimensions.MainBoxWidth;
+ $timeout(() => {
+ $scope.$digest();
+ });
+ };
+
+ $scope.profileBoxDisabled = function () {
+ return Config.disableProfileBox;
+ };
+
+ $scope.showOverlay = function (message) {
+ $scope.notification.show = true;
+ $scope.type = NotificationTypeWaiting;
+ $scope.notification.message = message;
+ };
+
+ $scope.tabClicked = function (tab) {
+ $scope.activeTab = tab;
+
+ // Save current search text
+ //$scope.search
+
+ if(tab == UsersTab) {
+ $scope.title = "Who's online";
+ }
+ if(tab == RoomsTab) {
+ $scope.title = "Chat rooms";
+ }
+ if(tab == FriendsTab) {
+ $scope.title = "My friends";
+ }
+ if(tab == InboxTab) {
+ $scope.title = "Inbox";
+ }
+ };
+
+ /**
+ * Return a list of friends filtered by the search box
+ * @return A list of users who's names meet the search text
+ */
+ $scope.getAllUsers = function () {
+ return ArrayUtils.objectToArray(FriendsConnector.friends);
+ };
+
+ $scope.searchKeyword = function () {
+ return $scope.search[$scope.activeTab];
+ };
+
+ $scope.roomClicked = function (room) {
+
+ // Trim the messages array in case it gets too long
+ // we only need to store the last 200 messages!
+ room.trimMessageList();
+
+ // Messages on is called by when we add the room to the user
+ // If the room is already open do nothing!
+ if(room.flashHeader()) {
+ return;
+ }
+
+ room.open(0, 300);
+ };
+
+ $scope.init();
+ }]);
\ No newline at end of file
diff --git a/src/angularjs/controllers/notification.ts b/src/angularjs/controllers/notification.ts
new file mode 100755
index 00000000..5f06b568
--- /dev/null
+++ b/src/angularjs/controllers/notification.ts
@@ -0,0 +1,7 @@
+import * as angular from 'angular'
+
+angular.module('myApp.controllers').controller('NotificationController', ['$scope', function($scope) {
+ $scope.submit = function () {
+ $scope.notification.show = false;
+ };
+}]);
\ No newline at end of file
diff --git a/src/angularjs/controllers/online-users-list.ts b/src/angularjs/controllers/online-users-list.ts
new file mode 100755
index 00000000..dfb4464b
--- /dev/null
+++ b/src/angularjs/controllers/online-users-list.ts
@@ -0,0 +1,65 @@
+import * as angular from 'angular'
+import {N} from "../keys/notification-keys";
+import {ArrayUtils} from "../services/array-utils";
+import {Log} from "../services/log";
+
+angular.module('myApp.controllers').controller('OnlineUsersListController', ['$scope', '$timeout', 'OnlineConnector', function($scope, $timeout, OnlineConnector) {
+
+ $scope.users = [];
+ $scope.allUsers = [];
+
+ $scope.init = function () {
+
+ $scope.$on(N.OnlineUserAdded, () => {
+ Log.notification(N.OnlineUserAdded, 'OnlineUsersListController');
+ $scope.updateList();
+
+ });
+
+ $scope.$on(N.OnlineUserRemoved, () => {
+ Log.notification(N.OnlineUserRemoved, 'OnlineUsersListController');
+ $scope.updateList();
+ });
+
+ $scope.$on(N.UserBlocked, () => {
+ Log.notification(N.UserBlocked, 'OnlineUsersListController');
+ $scope.updateList();
+ });
+
+ $scope.$on(N.UserUnblocked, () => {
+ Log.notification(N.UserUnblocked, 'OnlineUsersListController');
+ $scope.updateList();
+ });
+
+ $scope.$on(N.FriendAdded, () => {
+ Log.notification(N.FriendAdded, 'OnlineUsersListController');
+ $scope.updateList();
+ });
+
+ $scope.$on(N.FriendRemoved, () => {
+ Log.notification(N.FriendAdded, 'OnlineUsersListController');
+ $scope.updateList();
+ });
+
+ $scope.$on(N.Logout, $scope.updateList);
+
+ $scope.$watchCollection('search', $scope.updateList);
+
+ };
+
+ $scope.updateList = function () {
+
+ // Filter online users to remove users that are blocking us
+ $scope.allUsers = ArrayUtils.objectToArray(OnlineConnector.onlineUsers);
+ $scope.users = ArrayUtils.filterByKey($scope.allUsers, $scope.search[$scope.activeTab], (user) => {
+ return user.getName();
+ });
+
+ $timeout(() => {
+ $scope.$digest();
+ });
+ };
+
+ $scope.init();
+
+}]);
\ No newline at end of file
diff --git a/src/angularjs/controllers/profile-settings.ts b/src/angularjs/controllers/profile-settings.ts
new file mode 100755
index 00000000..4ebc8042
--- /dev/null
+++ b/src/angularjs/controllers/profile-settings.ts
@@ -0,0 +1,187 @@
+import * as angular from 'angular'
+import {NotificationTypeAlert, ShowProfileSettingsBox} from "../keys/defines";
+import {UserKeys} from "../keys/user-keys";
+import {Utils} from "../services/utils";
+
+angular.module('myApp.controllers').controller('ProfileSettingsController', ['$scope', 'Auth', 'Config', 'SoundEffects', 'LocalStorage',
+ function($scope, Auth, Config, SoundEffects, LocalStorage) {
+
+ $scope.ref = null;
+ $scope.muted = false;
+ $scope.nameChangeDummy = null;
+ $scope.dirty = false;
+
+ $scope.init = function () {
+
+ // Listen for validation errors
+ $scope.muted = SoundEffects.muted;
+
+ $scope.validation = {};
+
+ $scope.validation[UserKeys.Name] = {
+ minLength: 2,
+ maxLength: 50,
+ valid: true
+ };
+
+ // $scope.validation[UserKeys.Location] = {
+ // minLength: 0,
+ // maxLength: 50,
+ // valid: true
+ // };
+
+ $scope.validation[UserKeys.ProfileLink] = {
+ minLength: 0,
+ maxLength: 100,
+ valid: true
+ };
+
+ $scope.$watchCollection('user.meta', () => {
+ $scope.dirty = true;
+ });
+
+ // When the box will be opened we need to add a listener to the
+ // user
+ $scope.$on(ShowProfileSettingsBox, () => {
+
+ console.log("Show Profile");
+
+ });
+ };
+
+ $scope.validateLocation = function () {
+ return true;
+ // return $scope.validation[UserKeys.Location].valid;
+ };
+
+ $scope.validateProfileLink = function () {
+ return $scope.validation[UserKeys.ProfileLink].valid;
+ };
+
+ $scope.validateName= function () {
+ return $scope.validation[UserKeys.Name].valid;
+ };
+
+ $scope.toggleMuted = function () {
+ $scope.muted = SoundEffects.toggleMuted();
+ };
+
+ $scope.clearCache = function () {
+ if(!$scope.cacheCleared) {
+ LocalStorage.clearCache();
+ }
+ $scope.cacheCleared = true;
+ };
+
+ $scope.isValidURL = function(url) {// wrapped in self calling function to prevent global pollution
+
+ //URL pattern based on rfc1738 and rfc3986
+ let rg_pctEncoded = "%[0-9a-fA-F]{2}";
+ let rg_protocol = "(http|https):\\/\\/";
+
+ let rg_userinfo = "([a-zA-Z0-9$\\-_.+!*'(),;:&=]|" + rg_pctEncoded + ")+" + "@";
+
+ let rg_decOctet = "(25[0-5]|2[0-4][0-9]|[0-1][0-9][0-9]|[1-9][0-9]|[0-9])"; // 0-255
+ let rg_ipv4address = "(" + rg_decOctet + "(\\." + rg_decOctet + "){3}" + ")";
+ let rg_hostname = "([a-zA-Z0-9\\-\\u00C0-\\u017F]+\\.)+([a-zA-Z]{2,})";
+ let rg_port = "[0-9]+";
+
+ let rg_hostport = "(" + rg_ipv4address + "|localhost|" + rg_hostname + ")(:" + rg_port + ")?";
+
+ // chars sets
+ // safe = "$" | "-" | "_" | "." | "+"
+ // extra = "!" | "*" | "'" | "(" | ")" | ","
+ // hsegment = *[ alpha | digit | safe | extra | ";" | ":" | "@" | "&" | "=" | escape ]
+ let rg_pchar = "a-zA-Z0-9$\\-_.+!*'(),;:@&=";
+ let rg_segment = "([" + rg_pchar + "]|" + rg_pctEncoded + ")*";
+
+ let rg_path = rg_segment + "(\\/" + rg_segment + ")*";
+ let rg_query = "\\?" + "([" + rg_pchar + "/?]|" + rg_pctEncoded + ")*";
+ let rg_fragment = "\\#" + "([" + rg_pchar + "/?]|" + rg_pctEncoded + ")*";
+
+ let rgHttpUrl = new RegExp(
+ "^"
+ + rg_protocol
+ + "(" + rg_userinfo + ")?"
+ + rg_hostport
+ + "(\\/"
+ + "(" + rg_path + ")?"
+ + "(" + rg_query + ")?"
+ + "(" + rg_fragment + ")?"
+ + ")?"
+ + "$"
+ );
+
+ // export public function
+ if (rgHttpUrl.test(url)) {
+ return true;
+ } else {
+ return false;
+ }
+ };
+
+ $scope.validate = function () {
+
+ let user = $scope.getUser();
+
+ // Validate the user
+ let nameValid = $scope.validateString(UserKeys.Name, user.getName());
+
+ let profileLinkValid = !Config.userProfileLinkEnabled || $scope.validateString(UserKeys.ProfileLink, user.getProfileLink());
+
+ return nameValid && profileLinkValid;
+ };
+
+ $scope.validateString = function (key, string) {
+ let valid = true;
+
+ if(Utils.unORNull(string)) {
+ valid = false;
+ }
+
+ else if(string.length < $scope.validation[key].minLength) {
+ valid = false;
+ }
+
+ else if(string.length > $scope.validation[key].maxLength) {
+ valid = false;
+ }
+
+ $scope.validation[key].valid = valid;
+ return valid;
+
+ };
+
+ /**
+ * This is called when the user confirms changes to their user
+ * profile
+ */
+ $scope.done = function () {
+
+ // Is the name valid?
+ if($scope.validate()) {
+
+ $scope.showMainBox();
+
+ // Did the user update any values?
+ if($scope.dirty) {
+ $scope.getUser().pushMeta();
+ $scope.dirty = false;
+ }
+ }
+ else {
+ if(!$scope.validation[UserKeys.Name].valid) {
+ $scope.showNotification(NotificationTypeAlert, "Validation failed", "The name must be between "+$scope.validation[UserKeys.Name].minLength+" - "+$scope.validation[UserKeys.Name].maxLength+" characters long ", "Ok");
+ }
+ // if(!$scope.validation[UserKeys.Location].valid) {
+ // $scope.showNotification(NotificationTypeAlert, "Validation failed", "The location must be between "+$scope.validation[UserKeys.Location].minLength+" - "+$scope.validation[UserKeys.Location].maxLength+" characters long", "Ok");
+ // }
+ if(!$scope.validation[UserKeys.ProfileLink].valid) {
+ $scope.showNotification(NotificationTypeAlert, "Validation failed", "The profile link must be a valid URL", "Ok");
+ }
+ }
+ };
+
+ $scope.init();
+
+ }]);
\ No newline at end of file
diff --git a/src/angularjs/controllers/public-rooms-list.ts b/src/angularjs/controllers/public-rooms-list.ts
new file mode 100755
index 00000000..47126c18
--- /dev/null
+++ b/src/angularjs/controllers/public-rooms-list.ts
@@ -0,0 +1,94 @@
+import * as angular from 'angular'
+import {N} from "../keys/notification-keys";
+import {Utils} from "../services/utils";
+import {ArrayUtils} from "../services/array-utils";
+import {Log} from "../services/log";
+
+angular.module('myApp.controllers').controller('PublicRoomsListController', ['$scope', '$timeout', function($scope, $timeout) {
+
+ $scope.rooms = [];
+ $scope.allRooms = [];
+
+ $scope.init = function () {
+
+ $scope.$on(N.PublicRoomAdded, (event, room) => {
+ Log.notification(N.PublicRoomAdded, 'PublicRoomsListController');
+ // Add the room and sort the list
+ if(!ArrayUtils.contains($scope.allRooms, room)) {
+ $scope.allRooms.push(room);
+ }
+ $scope.updateList();
+
+ });
+
+ $scope.$on(N.PublicRoomRemoved, (event, room) => {
+ Log.notification(N.PublicRoomRemoved, 'PublicRoomsListController');
+
+ ArrayUtils.remove($scope.allRooms, room);
+ $scope.updateList();
+
+ });
+
+ // Update the list if the user count on a room changes
+ $scope.$on(N.RoomUpdated, $scope.updateList);
+
+ $scope.$on(N.Logout, $scope.updateList);
+
+ $scope.$watchCollection('search', $scope.updateList);
+ };
+
+
+ $scope.updateList = function () {
+
+ Log.notification(N.Logout, 'PublicRoomsListController');
+
+ $scope.allRooms.sort((a, b) => {
+
+ let au = Utils.unORNull(a.meta.userCreated) ? false : a.meta.userCreated;
+ let bu = Utils.unORNull(b.meta.userCreated) ? false : b.meta.userCreated;
+
+ if(au != bu) {
+ return au ? 1 : -1;
+ }
+
+ // Weight
+ let aw = Utils.unORNull(a.meta.weight) ? 100 : a.meta.weight;
+ let bw = Utils.unORNull(b.meta.weight) ? 100 : b.meta.weight;
+
+ if(aw != bw) {
+ return aw - bw;
+ }
+ else {
+
+ let ac = a.getOnlineUserCount();
+ let bc = b.getOnlineUserCount();
+
+ //console.log("1: " + ac + ", 2: " + bc);
+
+ if(ac != bc) {
+ return bc - ac;
+ }
+ else {
+ return a.name < b.name ? -1 : 1;
+ }
+ }
+
+ });
+
+ $scope.rooms = ArrayUtils.filterByKey($scope.allRooms, $scope.search[$scope.activeTab], (room) => {
+ return room.meta.name;
+ });
+
+ $timeout(() =>{
+ $scope.$digest();
+ });
+ };
+
+// $scope.getRooms = function() {
+// // Filter rooms by search text
+// return Utilities.filterByName(Cache.getPublicRooms(), $scope.search[$scope.activeTab]);
+// };
+
+ $scope.init();
+
+}]);
\ No newline at end of file
diff --git a/src/angularjs/controllers/room-list-box.ts b/src/angularjs/controllers/room-list-box.ts
new file mode 100755
index 00000000..b9be9181
--- /dev/null
+++ b/src/angularjs/controllers/room-list-box.ts
@@ -0,0 +1,124 @@
+import * as angular from 'angular'
+import {N} from "../keys/notification-keys";
+import {IRoom} from "../entities/room";
+import {Dimensions} from "../keys/dimensions";
+import {Log} from "../services/log";
+
+export interface IRoomListScope extends ng.IScope {
+ rooms: IRoom [],
+ updateList(): void,
+}
+
+angular.module('myApp.controllers').controller('RoomListBoxController', ['$scope', '$rootScope', '$timeout', 'Auth', 'Cache', 'LocalStorage', 'RoomPositionManager',
+ function($scope, $rootScope, $timeout, Auth, Cache, LocalStorage, RoomPositionManager) {
+
+ $scope.rooms = [];
+ $scope.moreChatsMinimized = true;
+ $scope.roomBackgroundColor = '#FFF';
+
+ $scope.init = function () {
+ $scope.boxWidth = Dimensions.RoomListBoxWidth;
+ $scope.boxHeight = Dimensions.RoomListBoxHeight;
+ $scope.canCloseRoom = true;
+
+ // Is the more box minimized?
+ $scope.setMoreBoxMinimized(LocalStorage.getProperty(LocalStorage.moreMinimizedKey));
+
+ // Update the list when a room changes
+ $scope.$on(N.UpdateRoomActiveStatus, $scope.updateList);
+ $scope.$on(N.RoomUpdated, $scope.updateList);
+ $scope.$on(N.Logout, $scope.updateList);
+
+
+ };
+
+ $scope.updateList = function () {
+
+ Log.notification(N.UpdateRoomActiveStatus, 'RoomListBoxController');
+
+ $scope.rooms = Cache.inactiveRooms();
+
+ // Sort rooms by the number of unread messages
+ $scope.rooms.sort((a, b) => {
+ // First order by number of unread messages
+ // Badge can be null
+ let ab = a.badge ? a.badge : 0;
+ let bb = b.badge ? b.badge : 0;
+
+ if(ab != bb) {
+ return bb - ab;
+ }
+ // Otherwise sort them by number of users
+ else {
+ return b.onlineUserCount - a.onlineUserCount;
+ }
+ });
+
+ $scope.moreChatsMinimized = $scope.rooms.length == 0;
+
+ $timeout(() =>{
+ $scope.$digest();
+ });
+ };
+
+ $scope.roomClicked = function(room) {
+
+ // Get the left most room
+ let rooms = RoomPositionManager.getRooms();
+
+ // Get the last box that's active
+ for(let i = rooms.length - 1; i >= 0; i--) {
+ if(rooms[i].active) {
+
+ // Get the details of the final room
+ let offset = rooms[i].offset;
+ let width = rooms[i].width;
+ let height = rooms[i].height;
+ let slot = rooms[i].slot;
+
+ // Update the old room with the position of the new room
+ rooms[i].setOffset(room.offset);
+ rooms[i].width = room.width;
+ rooms[i].height = room.height;
+ //rooms[i].active = false;
+ rooms[i].setActive(false);
+ rooms[i].slot = room.slot;
+
+ // Update the new room
+ room.setOffset(offset);
+ room.width = width;
+ room.height = height;
+
+ //room.setSizeToDefault();
+ room.setActive(true);
+ room.badge = null;
+ room.minimized = false;
+ room.slot = slot;
+
+// RoomPositionManager.setDirty();
+// RoomPositionManager.updateRoomPositions(room, 0);
+// RoomPositionManager.updateAllRoomActiveStatus();
+
+ break;
+ }
+ }
+ $rootScope.$broadcast(N.UpdateRoomActiveStatus);
+
+ };
+
+ $scope.minimize = function () {
+ $scope.setMoreBoxMinimized(true);
+ };
+
+ $scope.toggle = function () {
+ $scope.setMoreBoxMinimized(!$scope.hideRoomList);
+ };
+
+ $scope.setMoreBoxMinimized = function (minimized) {
+ $scope.hideRoomList = minimized;
+ LocalStorage.setProperty(LocalStorage.moreMinimizedKey, minimized);
+ };
+
+ $scope.init();
+
+ }]);
\ No newline at end of file
diff --git a/src/angularjs/controllers/user-list.ts b/src/angularjs/controllers/user-list.ts
new file mode 100755
index 00000000..72805572
--- /dev/null
+++ b/src/angularjs/controllers/user-list.ts
@@ -0,0 +1,90 @@
+import * as angular from 'angular'
+import {N} from "../keys/notification-keys";
+import {ArrayUtils} from "../services/array-utils";
+import {Log} from "../services/log";
+
+export interface UserListScope extends ng.IScope {
+ aUser: any,
+}
+
+angular.module('myApp.controllers').controller('UserListController', ['$scope', '$timeout', 'OnlineConnector', function($scope, $timeout, OnlineConnector) {
+
+ $scope.users = [];
+ $scope.allUsers = [];
+
+ $scope.init = function () {
+
+ $scope.$on(N.FriendAdded,() => {
+ Log.notification(N.FriendAdded, 'UserListController');
+ $scope.updateList();
+ });
+
+ $scope.$on(N.FriendRemoved,() =>{
+ Log.notification(N.FriendAdded, 'UserListController');
+ $scope.updateList();
+ });
+
+ $scope.$on(N.UserBlocked, () => {
+ Log.notification(N.UserBlocked, 'UserListController');
+ $scope.updateList();
+ });
+
+ $scope.$on(N.UserUnblocked , () =>{
+ Log.notification(N.UserUnblocked, 'UserListController');
+ $scope.updateList();
+ });
+
+ // TODO: A bit hacky
+ $scope.$on(N.RoomUpdated, (event, room) => {
+ Log.notification(N.RoomUpdated, 'UserListController');
+ if(room === $scope.room) {
+ $scope.updateList();
+ }
+ });
+
+ $scope.$on(N.Logout, $scope.updateList);
+
+ $scope.$watchCollection('search', $scope.updateList);
+
+ };
+
+ $scope.updateList = function () {
+
+ // Filter online users to remove users that are blocking us
+ $scope.allUsers = $scope.getAllUsers();
+
+ if($scope.searchKeyword()) {
+ $scope.users = ArrayUtils.filterByKey($scope.allUsers, $scope.searchKeyword(), (user) => {
+ return user.getName();
+ });
+ }
+ else {
+ $scope.users = $scope.allUsers;
+ }
+
+ // Sort the array first by who's online
+ // then alphabetically
+ $scope.users.sort((user1, user2) => {
+ // Sort by who's online first then alphabetcially
+ let aOnline = OnlineConnector.onlineUsers[user1.uid()];
+ let bOnline = OnlineConnector.onlineUsers[user2.uid()];
+
+ if(aOnline !== bOnline) {
+ return aOnline ? 1 : -1;
+ }
+ else {
+ if(user1.getName() !== user2.getName()) {
+ return user1.getName() > user2.getName() ? 1 : -1;
+ }
+ return 0;
+ }
+ });
+
+ $timeout(() => {
+ $scope.$digest();
+ });
+ };
+
+ $scope.init();
+
+}]);
\ No newline at end of file
diff --git a/src/angularjs/controllers/user-profile-box.ts b/src/angularjs/controllers/user-profile-box.ts
new file mode 100755
index 00000000..582735ef
--- /dev/null
+++ b/src/angularjs/controllers/user-profile-box.ts
@@ -0,0 +1,33 @@
+import * as angular from 'angular'
+import {IUser} from "../entities/user";
+
+export interface IProfileBoxScope extends ng.IScope {
+ hover: any,
+ currentUser: any,
+}
+
+export interface IUserProfileBoxController {
+
+}
+
+class UserProfileBoxController implements IUserProfileBoxController{
+
+ static $inject = ['$scope'];
+
+ private $scope;
+
+ constructor($scope) {
+ this.$scope = $scope;
+ }
+
+ copyUserID() {
+
+ // Get the ID
+ const id = this.$scope.currentUser.uid();
+
+ window.prompt("Copy to clipboard: Ctrl+C, Enter", id);
+ };
+
+}
+
+angular.module('myApp.controllers').controller('UserProfileBoxController', UserProfileBoxController);
\ No newline at end of file
diff --git a/src/angularjs/directives/animate-room.ts b/src/angularjs/directives/animate-room.ts
new file mode 100755
index 00000000..567f4845
--- /dev/null
+++ b/src/angularjs/directives/animate-room.ts
@@ -0,0 +1,52 @@
+import * as angular from 'angular'
+import * as $ from 'jquery'
+
+import {N} from "../keys/notification-keys";
+import {IRoomScope} from "../controllers/chat";
+import {Utils} from "../services/utils";
+import {Log} from "../services/log";
+
+angular.module('myApp.directives').directive('animateRoom', ['$timeout', 'RoomPositionManager', function ($timeout, RoomPositionManager) {
+ return function (scope: IRoomScope, elm) {
+
+ scope.$on(N.AnimateRoom, (event, args) => {
+
+ Log.notification(N.AnimateRoom, 'animateRoom');
+
+ if(args.room == scope.room) {
+
+ if(!Utils.unORNull(args.slot)) {
+ scope.room.slot = args.slot;
+ }
+
+ // Get the final offset
+ const toOffset = RoomPositionManager.offsetForSlot(scope.room.slot);
+
+ // Stop the previous animation
+ $(elm).stop(true, false);
+
+ let completion = function () {
+ scope.room.setOffset(toOffset);
+
+ scope.room.zIndex = null;
+
+ RoomPositionManager.updateAllRoomActiveStatus();
+
+ $timeout(() => {
+ scope.$digest();
+ });
+ };
+
+ if(!Utils.unORNull(args.duration) && args.duration == 0) {
+ completion();
+ }
+ else {
+ // Animate the chat room into position
+ $(elm).animate({right: toOffset}, !Utils.unORNull(args.duration) ? args.duration : 300 , () =>{
+ completion();
+ });
+ }
+ }
+ });
+ };
+}]);
\ No newline at end of file
diff --git a/src/angularjs/directives/cc-flash.ts b/src/angularjs/directives/cc-flash.ts
new file mode 100755
index 00000000..121fb3df
--- /dev/null
+++ b/src/angularjs/directives/cc-flash.ts
@@ -0,0 +1,36 @@
+import * as angular from 'angular'
+import {N} from "../keys/notification-keys";
+import {IRoomScope} from "../controllers/chat";
+
+angular.module('myApp.directives').directive('ccFlash', ['$timeout', 'Config', function ($timeout, Config) {
+ return function (scope: IRoomScope, element, attr) {
+
+ let originalColor = element.css('background-color');
+ let originalTag = element.attr('cc-flash');
+ let animating = false;
+
+ scope.$on(N.RoomFlashHeader, (event, room, color, period, tag) => {
+ if(scope.room == room && color && period && !animating) {
+ if(!tag || tag == originalTag) {
+ animating = true;
+
+ element.css('background-color', color);
+
+ $timeout(() => {
+ scope.$digest();
+ });
+
+ // Set another timeout
+ $timeout(() => {
+ if(tag == "room-header") {
+ originalColor = Config.headerColor;
+ }
+ element.css('background-color', originalColor);
+ scope.$digest();
+ animating = false;
+ }, period);
+ }
+ }
+ });
+ };
+}]);
\ No newline at end of file
diff --git a/src/angularjs/directives/cc-focus.ts b/src/angularjs/directives/cc-focus.ts
new file mode 100755
index 00000000..e9e6864b
--- /dev/null
+++ b/src/angularjs/directives/cc-focus.ts
@@ -0,0 +1,14 @@
+import * as angular from 'angular'
+
+angular.module('myApp.directives').directive('ccFocus', function () {
+ return {
+ restrict: 'A',
+ link: function (scope, element, attr) {
+ scope.$watch(attr.ccFocus, (n, o) => {
+ if (n !== 0 && n) {
+ element[0].focus();
+ }
+ });
+ }
+ };
+});
diff --git a/src/angularjs/directives/cc-uncloak.ts b/src/angularjs/directives/cc-uncloak.ts
new file mode 100755
index 00000000..3d0e3186
--- /dev/null
+++ b/src/angularjs/directives/cc-uncloak.ts
@@ -0,0 +1,7 @@
+import * as angular from 'angular'
+
+angular.module('myApp.directives').directive('ccUncloak', function () {
+ return function (scope, element, attr) {
+ element.removeAttr('style');
+ };
+});
\ No newline at end of file
diff --git a/src/angularjs/directives/center-mouse-y.ts b/src/angularjs/directives/center-mouse-y.ts
new file mode 100755
index 00000000..4280f198
--- /dev/null
+++ b/src/angularjs/directives/center-mouse-y.ts
@@ -0,0 +1,24 @@
+// This is used by the profile box - to keep it centered on the
+// mouse's y axis until we move into it
+import * as $ from 'jquery'
+import * as angular from 'angular'
+import {IProfileBoxScope} from "../controllers/user-profile-box";
+
+angular.module('myApp.directives').directive('centerMouseY', ['$document', 'Screen', function ($document, Screen) {
+ return function (scope: IProfileBoxScope, elm) {
+
+ $(elm).hover(() => {
+ scope.hover = true;
+ }, () => {
+ scope.hover = false;
+ });
+
+ $document.mousemove((e) => {
+ //!elm.is(":hover")
+ if(scope.currentUser && !scope.hover) {
+ // Keep the center of this box level with the mouse y
+ elm.css({bottom: Screen.screenHeight - e.clientY - $(elm).height()/2});
+ }
+ });
+ };
+}]);
diff --git a/src/angularjs/directives/consume-event.ts b/src/angularjs/directives/consume-event.ts
new file mode 100755
index 00000000..32558e3b
--- /dev/null
+++ b/src/angularjs/directives/consume-event.ts
@@ -0,0 +1,12 @@
+import * as angular from 'angular'
+import * as $ from 'jquery'
+import {Utils} from "../services/utils";
+
+angular.module('myApp.directives').directive('consumeEvent', [function () {
+ return function (scope, elm, attrs) {
+ $(elm).mousedown((e) => {
+ Utils.stopDefault(e);
+ return false;
+ });
+ };
+}]);
diff --git a/src/angularjs/directives/disable-drag.ts b/src/angularjs/directives/disable-drag.ts
new file mode 100755
index 00000000..d1a3065b
--- /dev/null
+++ b/src/angularjs/directives/disable-drag.ts
@@ -0,0 +1,15 @@
+import * as angular from 'angular'
+import * as $ from 'jquery'
+
+angular.module('myApp.directives').directive('disableDrag', ['$rootScope','$document', function ($rootScope, $document) {
+ return function (scope, elm, attrs) {
+
+ $(elm).mousedown((e) => {
+ $rootScope.disableDrag = true;
+ });
+
+ $document.mouseup((e) => {
+ $rootScope.disableDrag = false;
+ });
+ };
+}]);
diff --git a/src/angularjs/directives/draggable-room.ts b/src/angularjs/directives/draggable-room.ts
new file mode 100755
index 00000000..b089c955
--- /dev/null
+++ b/src/angularjs/directives/draggable-room.ts
@@ -0,0 +1,92 @@
+import * as angular from 'angular'
+import * as $ from 'jquery'
+import {N} from "../keys/notification-keys";
+import {IRoomScope} from "../controllers/chat";
+import {Dimensions} from "../keys/dimensions";
+import {Utils} from "../services/utils";
+
+angular.module('myApp.directives').directive('draggableRoom', ['$rootScope', '$document', '$timeout', 'RoomPositionManager', function ($rootScope, $document, $timeout, RoomPositionManager) {
+
+ return function (scope: IRoomScope, elm, attrs) {
+
+ let lastClientX = 0;
+
+ // Set the room as draggable - this will interact
+ // with the layout manager i.e. draggable rooms
+ // will be animated to position whereas non-draggable
+ // rooms will be moved position manually
+ scope.room.draggable = true;
+
+ $(elm).mousedown((e) => {
+
+ // If the user clicked in the text box
+ // then don't drag
+ if($rootScope.disableDrag) {
+ return true;
+ }
+
+ if(scope.resizing) {
+ return;
+ }
+
+ scope.room.zIndex = 1000;
+
+ $(elm).stop(true, false);
+
+ scope.startDrag();
+
+ scope.dragging = true;
+ lastClientX = e.clientX;
+
+ return false;
+ });
+
+ $document.mousemove((e) => {
+
+ if(scope.dragging && !$rootScope.disableDrag) {
+
+ Utils.stopDefault(e);
+
+ let dx = lastClientX - e.clientX;
+
+ // We must be moving in either a positive direction
+ if(dx === 0) {
+ return false;
+ }
+
+ // Modify the chat's offset
+ scope.room.dragDirection = dx;
+ scope.room.setOffset(scope.room.offset + dx);
+
+ lastClientX = e.clientX;
+
+ // Apply constraints
+ scope.room.offset = Math.max(scope.room.offset, Dimensions.MainBoxWidth + Dimensions.ChatRoomSpacing);
+ scope.room.offset = Math.min(scope.room.offset, RoomPositionManager.effectiveScreenWidth() - scope.room.width - Dimensions.ChatRoomSpacing);
+
+ scope.wasDragged();
+
+ RoomPositionManager.roomDragged(scope.room);
+
+ // Apply the change
+ $timeout(() => {
+ scope.$digest();
+ });
+
+ return false;
+ }
+ });
+
+ $document.mouseup((e) => {
+ if(scope.dragging) {
+
+ scope.dragging = false;
+
+ $rootScope.$broadcast(N.AnimateRoom, {
+ room: scope.room
+ });
+
+ }
+ });
+ };
+}]);
\ No newline at end of file
diff --git a/src/angularjs/directives/draggable-user.ts b/src/angularjs/directives/draggable-user.ts
new file mode 100755
index 00000000..b991d1f0
--- /dev/null
+++ b/src/angularjs/directives/draggable-user.ts
@@ -0,0 +1,65 @@
+import * as angular from 'angular'
+import * as $ from 'jquery'
+import {UserListScope} from "../controllers/user-list";
+import {Utils} from "../services/utils";
+
+angular.module('myApp.directives').directive('draggableUser', ['$rootScope','$document', '$timeout', 'Screen', function ($rootScope, $document, $timeout, Screen) {
+ return function (scope: UserListScope, elm, attrs) {
+
+ $rootScope.userDrag = {};
+
+ $(elm).mousedown((e) => {
+ // Set the current user
+ $rootScope.userDrag = {
+ user: scope.aUser,
+ x:0,
+ y:0,
+ dragging: true,
+ dropLoc:false,
+ visible: false
+ };
+
+ Utils.stopDefault(e);
+
+ return false;
+
+ });
+
+ $document.mousemove((e) => {
+
+ if($rootScope.userDrag.dragging) {
+
+ $rootScope.userDrag.visible = true;
+
+ $rootScope.userDrag.x = e.clientX - 10;
+ $rootScope.userDrag.y = e.clientY - 10;
+
+ // TODO: Don't hardcode these values
+ // for some reason .width() isn't working
+ // Stop the dragged item going off the screen
+ $rootScope.userDrag.x = Math.max($rootScope.userDrag.x, 0);
+ $rootScope.userDrag.x = Math.min($rootScope.userDrag.x, Screen.screenWidth - 200);
+
+ $rootScope.userDrag.y = Math.max($rootScope.userDrag.y, 0);
+ $rootScope.userDrag.y = Math.min($rootScope.userDrag.y, Screen.screenHeight - 30);
+
+ // If we're in the drop loc
+ $timeout(() => {
+ scope.$apply();
+ });
+ }
+
+ });
+
+ $document.mouseup((e) => {
+ if($rootScope.userDrag.dragging) {
+ $rootScope.userDrag.dragging = false;
+ $rootScope.userDrag.visible = false;
+
+ $timeout(() => {
+ scope.$apply();
+ });
+ }
+ });
+ };
+}]);
\ No newline at end of file
diff --git a/src/angularjs/directives/enter-submit.ts b/src/angularjs/directives/enter-submit.ts
new file mode 100755
index 00000000..b7dc4e73
--- /dev/null
+++ b/src/angularjs/directives/enter-submit.ts
@@ -0,0 +1,24 @@
+import * as angular from 'angular'
+
+angular.module('myApp.directives').directive('enterSubmit', function () {
+ return {
+ restrict: 'A',
+ link: function (scope, elem, attrs) {
+
+ elem.bind('keydown', function(event) {
+ let code = event.keyCode || event.which;
+
+ if (code === 13) {
+ if (!event.shiftKey) {
+ event.preventDefault();
+ scope.$apply(attrs.enterSubmit);
+
+ // Scroll down on enter too
+ scope.$broadcast('enterScrollDown');
+
+ }
+ }
+ });
+ }
+ };
+});
diff --git a/src/angularjs/directives/fit-text.ts b/src/angularjs/directives/fit-text.ts
new file mode 100755
index 00000000..078eb28c
--- /dev/null
+++ b/src/angularjs/directives/fit-text.ts
@@ -0,0 +1,37 @@
+import * as angular from 'angular'
+import {IRoomScope} from "../controllers/chat";
+
+angular.module('myApp.directives').directive('fitText', function () {
+
+ return function(scope: IRoomScope, element, attr) {
+
+ element.bind('keyup', function(e) {
+ //jQuery(element).height(0);
+ //let height = jQuery(element)[0].scrollHeight;
+ let height = element.prop('scrollHeight');
+
+ // 8 is for the padding
+ if (height < 26) {
+ height = 26;
+ }
+
+ // If we go over the max height
+ let maxHeight = eval(attr.fitText);
+ if(height > maxHeight) {
+ height = maxHeight;
+ element.css({overflow: 'auto'});
+ }
+ else {
+ element.css({overflow: 'hidden'});
+ }
+
+ scope.$apply(() => {
+ scope.inputHeight = height;
+ });
+
+ element.css({'max-height': height});
+ element.css({'height': height});
+
+ });
+ };
+});
\ No newline at end of file
diff --git a/src/angularjs/directives/infinite-scroll.ts b/src/angularjs/directives/infinite-scroll.ts
new file mode 100755
index 00000000..d1ad5a77
--- /dev/null
+++ b/src/angularjs/directives/infinite-scroll.ts
@@ -0,0 +1,142 @@
+import * as $ from 'jquery'
+import * as angular from 'angular'
+import {IRoomScope} from "../controllers/chat";
+import {IRoom} from "../entities/room";
+
+class InfiniteScroll implements ng.IDirective{
+
+ static $inject = ["$timeout"];
+
+ private loading = false;
+ private scrollHeight: number;
+ private scrollTop: number;
+ private lastScrollTop: number;
+ private height: number;
+ private top: number;
+ private bottom: number;
+ private timer: any;
+ private silent = false;
+
+ constructor (
+ private $timeout: ng.ITimeoutService
+ ) {}
+
+ public link = (scope: IRoomScope, elem) => {
+ elem.on('scroll', () => {
+ this.onScroll(scope, elem)
+ });
+
+ scope.$on('$destroy', () => {
+ return elem.off('scroll', () => {
+ this.onScroll(scope, elem)
+ });
+ });
+ };
+
+ loadMoreMessages(scope: IRoomScope, elem) {
+ this.loading = true;
+
+ const $elem = $(elem);
+
+ console.log("2 - Height: "+ this.height +", Scroll height: " + this.scrollHeight + ", top: " + this.scrollTop);
+
+ const scrollHeight = this.scrollHeight;
+
+ // this.silent = true;
+
+ scope.room.loadMoreMessages().then(messages => {
+ this.$timeout(() => {
+ let top = elem.prop('scrollHeight');
+
+ // console.log("3 - Height: " + $elem.height() + ", Scroll height: " + top + ", top: " + $elem.scrollTop());
+
+ if (messages && messages.length && $elem.scrollTop() == 0) {
+ $elem.stop();
+ this.setScrollTopSilent(elem, top - scrollHeight);
+ }
+
+ this.loading = false;
+ });
+ });
+
+ }
+
+ onScroll(scope: IRoomScope, elem): void {
+
+ const $elem = $(elem);
+
+ if (this.silent) {
+ this.silent = false;
+ return;
+ }
+
+ if (this.timer) {
+ this.$timeout.cancel(this.timer);
+ this.timer = null;
+ }
+
+ this.timer = this.$timeout(() => {
+ this.onScrollFinished(scope, elem)
+ }, 100);
+
+ this.scrollHeight = elem.prop('scrollHeight');
+ this.scrollTop = $elem.scrollTop();
+ this.height = $elem.height();
+
+ this.top = this.scrollTop;
+ this.bottom = this.scrollHeight - this.scrollTop - this.height;
+
+
+ // Only load more messages when we're scrolling up
+ if (this.scrollTop < this.lastScrollTop) {
+ if(this.top / this.scrollHeight < 0.5 && !this.loading) {
+ this.loadMoreMessages(scope, elem);
+ }
+ }
+
+ // For Mobile or negative scrolling
+ this.lastScrollTop = this.scrollTop <= 0 ? 0 : this.scrollTop;
+ };
+
+ onScrollFinished(scope: IRoomScope, elem): void {
+ this.$timeout.cancel(this.timer);
+
+ const $elem = $(elem);
+
+ // this.silent = false;
+ if ($elem.scrollTop() == 0) {
+
+ this.loadMoreMessages(scope, elem);
+
+ //
+ // const scrollHeight = elem.prop('scrollHeight');
+ //
+ // scope.room.loadMoreMessages().then(messages => {
+ // this.$timeout(() => {
+ // if (messages && messages.length) {
+ // const top = elem.prop('scrollHeight') - scrollHeight;
+ //
+ // console.log("Set scroll top: " + top);
+ //
+ // this.silent = true;
+ // $elem.scrollTop(top)
+ // }
+ // })
+ // })
+ //
+ // // this.silent = true;
+ // // this.loadMoreMessages(scope, elem)
+ }
+ }
+
+ setScrollTopSilent(elem, position: number): void {
+ this.silent = true;
+ $(elem).scrollTop(position)
+ }
+
+ static factory (): ng.IDirectiveFactory {
+ return ($timeout: ng.ITimeoutService) => new InfiniteScroll($timeout)
+ }
+}
+
+angular.module('myApp.directives').directive('infiniteScroll', InfiniteScroll.factory());
\ No newline at end of file
diff --git a/src/angularjs/directives/on-edit-message.ts b/src/angularjs/directives/on-edit-message.ts
new file mode 100755
index 00000000..41924a78
--- /dev/null
+++ b/src/angularjs/directives/on-edit-message.ts
@@ -0,0 +1,13 @@
+import * as angular from 'angular'
+import {MessageScope} from "../controllers/chat";
+import {N} from "../keys/notification-keys";
+
+angular.module('myApp.directives').directive('onEditMessage', function () {
+ return function (scope: MessageScope, element, attr) {
+ scope.$on(N.EditMessage, (event, mid, newText) => {
+ if(mid == scope.message.meta.mid) {
+ element.text(newText);
+ }
+ });
+ };
+});
diff --git a/src/angularjs/directives/on-file-change.ts b/src/angularjs/directives/on-file-change.ts
new file mode 100755
index 00000000..78b9f952
--- /dev/null
+++ b/src/angularjs/directives/on-file-change.ts
@@ -0,0 +1,12 @@
+import * as angular from 'angular'
+
+angular.module('myApp.directives').directive('onFileChange', function () {
+ return {
+ restrict: 'A',
+ link: function (scope, element, attrs) {
+ element.bind('change' , () =>{
+ scope.$eval(attrs.onFileChange);
+ });
+ }
+ };
+});
diff --git a/src/angularjs/directives/pikaday.ts b/src/angularjs/directives/pikaday.ts
new file mode 100755
index 00000000..9053eb59
--- /dev/null
+++ b/src/angularjs/directives/pikaday.ts
@@ -0,0 +1,19 @@
+import * as angular from 'angular'
+import * as Pikaday from 'Pikaday'
+
+// Not used now - was used to show date of birth picker
+angular.module('myApp.directives').directive('pikaday', ["$rootScope", function ($rootScope) {
+ return function (scope, element, attr) {
+
+ new Pikaday({
+ field: element[0],
+ firstDay: 1,
+ minDate: new Date('1920-01-01'),
+ maxDate: new Date(),
+ defaultDate: new Date('1990-01-01'),
+ onSelect: function (date) {
+ }
+ });
+
+ };
+}]);
diff --git a/src/angularjs/directives/resize-room.ts b/src/angularjs/directives/resize-room.ts
new file mode 100755
index 00000000..0e131f21
--- /dev/null
+++ b/src/angularjs/directives/resize-room.ts
@@ -0,0 +1,76 @@
+import * as angular from 'angular'
+import * as $ from 'jquery'
+
+import {IRoomScope} from "../controllers/chat";
+import {Dimensions} from "../keys/dimensions";
+import {Utils} from "../services/utils";
+import {N} from "../keys/notification-keys";
+
+angular.module('myApp.directives').directive('resizeRoom',['$rootScope', '$timeout', '$document', 'Screen', 'RoomPositionManager', function ($rootScope, $timeout, $document, Screen, RoomPositionManager) {
+ return function (scope: IRoomScope, elm, attrs) {
+
+ let lastClientX = 0;
+ let lastClientY = 0;
+
+ // $.element(elm).mousedown
+
+ $(elm).mousedown((e) => {
+ Utils.stopDefault(e);
+ scope.resizing = true;
+ lastClientX = e.clientX;
+ lastClientY = e.clientY;
+ });
+
+ $document.mousemove((e) => {
+ if(scope.resizing) {
+
+ Utils.stopDefault(e);
+
+ // Min width
+ scope.room.width += lastClientX - e.clientX;
+ scope.room.width = Math.max(scope.room.width, Dimensions.ChatRoomWidth);
+ scope.room.width = Math.min(scope.room.width, RoomPositionManager.effectiveScreenWidth() - scope.room.offset - Dimensions.ChatRoomSpacing);
+
+ lastClientX = e.clientX;
+
+ // Min height
+ scope.room.height += lastClientY - e.clientY;
+ scope.room.height = Math.max(scope.room.height, Dimensions.ChatRoomHeight);
+ scope.room.height = Math.min(scope.room.height, Screen.screenHeight - Dimensions.ChatRoomTopMargin);
+
+ lastClientY = e.clientY;
+
+ // Update the room's scope
+ $timeout(() => {
+ scope.$digest();
+ });
+
+ // We've changed the room's size so we need to re-calculate
+ RoomPositionManager.setDirty();
+
+ // Update the rooms to the left
+ let rooms = RoomPositionManager.getRooms();
+
+ // Only loop from this room's position onwards
+ let room;
+ for(let i = rooms.indexOf(scope.room); i < rooms.length; i++) {
+ room = rooms[i];
+ if(room != scope.room) {
+ room.setOffset(RoomPositionManager.offsetForSlot(i));
+ $rootScope.$broadcast(N.RoomPositionUpdated, room);
+ RoomPositionManager.updateAllRoomActiveStatus();
+ }
+ }
+
+ return false;
+ }
+ });
+
+ $document.mouseup((e) => {
+ if(scope.resizing) {
+ scope.resizing = false;
+ }
+ });
+
+ };
+}]);
\ No newline at end of file
diff --git a/src/angularjs/directives/scroll-glue.ts b/src/angularjs/directives/scroll-glue.ts
new file mode 100755
index 00000000..964f8744
--- /dev/null
+++ b/src/angularjs/directives/scroll-glue.ts
@@ -0,0 +1,47 @@
+import * as angular from 'angular'
+import {IRoomScope} from "../controllers/chat";
+
+angular.module('myApp.directives').directive('scrollGlue', function(){
+ return {
+ priority: 1,
+ require: ['?ngModel'],
+ restrict: 'A',
+ link: function(scope: IRoomScope, $el, attrs, ctrls){
+ const el = $el[0];
+
+ let didScroll = false;
+
+ const scrollToBottom = () => {
+ el.scrollTop = el.scrollHeight;
+ };
+
+ const shouldActivateAutoScroll = () => {
+ // + 1 catches off by one errors in chrome
+ return el.scrollTop + el.clientHeight + 1 >= el.scrollHeight;
+ };
+
+ scope.$watch(() => {
+ if(scope.autoScroll){
+ scrollToBottom();
+ }
+ if (!didScroll) {
+ scrollToBottom();
+ }
+ });
+
+ $el.bind('scroll', () => {
+ didScroll = true;
+ let activate = shouldActivateAutoScroll();
+ if (activate !== scope.autoScroll) {
+ scope.autoScroll = activate;
+ }
+ });
+
+ // If they press enter scroll down
+ scope.$on('enterScrollDown' , () =>{
+ scrollToBottom();
+ });
+
+ }
+ };
+});
\ No newline at end of file
diff --git a/src/angularjs/directives/social-iframe.ts b/src/angularjs/directives/social-iframe.ts
new file mode 100755
index 00000000..5a5a8938
--- /dev/null
+++ b/src/angularjs/directives/social-iframe.ts
@@ -0,0 +1,38 @@
+import * as $ from 'jquery'
+import * as angular from 'angular'
+import {N} from "../keys/notification-keys";
+
+angular.module('myApp.directives').directive('socialIframe', ["$rootScope", "$document", "$window", "Paths", function ($rootScope, $document, $window, Paths) {
+ return function (scope, element, attr) {
+
+ $rootScope.$on(N.StartSocialLogin, (event, data, callback) => {
+
+ //element.load(function () {
+
+// let data = {
+// action: 'github',
+// path: Paths.firebase().toString()
+// };
+
+ // Add the event listener
+ let eventMethod = $window.addEventListener ? "addEventListener" : "attachEvent";
+ let eventer = $window[eventMethod];
+ let messageEvent = eventMethod == "attachEvent" ? "onmessage" : "message";
+
+ eventer(messageEvent, (e) => {
+ if (e.data) {
+ let data = JSON.parse(e.data);
+ if(data.provider == 'chatcat') {
+ callback(data);
+ }
+ }
+// else {
+// callback(null);
+// }
+ });
+
+ $(element).get(0).contentWindow.postMessage(JSON.stringify(data), "*");
+ //});
+ });
+ };
+}]);
\ No newline at end of file
diff --git a/src/angularjs/directives/stop-shake.ts b/src/angularjs/directives/stop-shake.ts
new file mode 100755
index 00000000..3e91b265
--- /dev/null
+++ b/src/angularjs/directives/stop-shake.ts
@@ -0,0 +1,23 @@
+import * as angular from 'angular'
+import * as $ from 'jquery'
+
+/**
+ * #54
+ * This directive is used for scrollbars when the component can
+ * also be dragged horizontally. If the user has shaky hands then
+ * the chat will shake while they're scrolling. To prevent this
+ * we add a listener to hear when they're scrolling.
+ */
+angular.module('myApp.directives').directive('stopShake', ['$rootScope', '$document',function ($rootScope, $document) {
+ return function (scope, elm, attrs) {
+
+ $(elm).scroll(() => {
+ $rootScope.disableDrag = true;
+ });
+
+ // Allow dragging again on mouse up
+ $document.mouseup((e) => {
+ $rootScope.disableDrag = false;
+ });
+ };
+}]);
\ No newline at end of file
diff --git a/src/angularjs/directives/user-drop-location.ts b/src/angularjs/directives/user-drop-location.ts
new file mode 100755
index 00000000..e7d0aee7
--- /dev/null
+++ b/src/angularjs/directives/user-drop-location.ts
@@ -0,0 +1,37 @@
+import * as angular from 'angular'
+import {NotificationTypeAlert} from "../keys/defines";
+import * as $ from 'jquery'
+import {IRoomScope} from "../controllers/chat";
+import {UserStatus} from "../keys/user-status";
+
+angular.module('myApp.directives').directive('userDropLocation', ['$rootScope', 'RoomFactory', function ($rootScope, RoomFactory) {
+ return function (scope: IRoomScope, elm, attrs) {
+
+ $(elm).mouseenter((e) => {
+ if($rootScope.userDrag && $rootScope.userDrag.dragging) {
+ $rootScope.userDrag.dropLoc = true;
+ }
+ });
+
+ $(elm).mouseleave((e) => {
+ if($rootScope.userDrag && $rootScope.userDrag.dragging) {
+ $rootScope.userDrag.dropLoc = false;
+ }
+ });
+
+ $(elm).mouseup((e) => {
+ // Add the user to this chat
+ if($rootScope.userDrag && $rootScope.userDrag.dragging) {
+ // Is the user already a member of this room?
+
+ // This isn't really needed since it's handled with security rules
+ RoomFactory.addUserToRoom(scope.room.rid(), $rootScope.userDrag.user, UserStatus.Member).then(() => {
+ // Update the room's type
+ scope.room.updateType();
+ }, (error) => {
+ $rootScope.showNotification(NotificationTypeAlert, "Error", error.message, "Ok");
+ });
+ }
+ });
+ };
+}]);
\ No newline at end of file
diff --git a/src/angularjs/entities/entity.ts b/src/angularjs/entities/entity.ts
new file mode 100755
index 00000000..07fd9bf8
--- /dev/null
+++ b/src/angularjs/entities/entity.ts
@@ -0,0 +1,224 @@
+import * as angular from 'angular'
+import * as firebase from 'firebase';
+
+import * as PathKeys from "../keys/path-keys";
+import * as Defines from "../keys/defines";
+import {IPaths} from "../network/paths";
+import {Emoji} from "../services/emoji";
+import {Utils} from "../services/utils";
+
+export interface IEntity {
+ serialize()
+ setMeta(meta: any): void
+ getMetaObject (): {}
+ getMeta(): Map
+ updateState(key: string): Promise
+ removeOnDisconnect (path: string): Promise
+}
+
+export class Entity implements IEntity {
+
+ protected meta = new Map();
+ protected _path: string;
+ protected _id: string;
+ pathIsOn = {};
+ state = {};
+
+ // static $inject = ['$q', 'Paths'];
+ constructor (
+ protected Paths: IPaths,
+ path: string,
+ id: string) {
+ this._path = path;
+ this._id = id;
+ }
+
+ /**
+ * Start listening to a path
+ * This method first adds a listener to the state ref. It then only
+ * adds a listener to the data path if the saved state is out of date
+ *
+ * @param key - the data key i.e. meta, blocked, friends, messages etc...
+ * @param callback - The callback is called each time the value changes
+ * @returns promise - the promise is resolved when the it's confirmed that
+ * the state of the local data is up to date
+ */
+ pathOn(key, callback): Promise {
+ return new Promise((resolve, reject) => {
+ // Check to see if this path has already
+ // been turned on
+ if(this.pathIsOn[key]) {
+ resolve();
+ } else {
+ this.pathIsOn[key] = true;
+
+ // Start listening to the state
+ let stateRef = this.stateRef(key);
+
+ stateRef.off('value');
+
+ // TODO: There seems to be a bug with Firebase
+ // when we call push this method is called twice
+ stateRef.on('value', (snapshot) => {
+
+ let time = snapshot.val();
+
+ if(Defines.DEBUG) console.log('Entity ID: ' + this._id + ' Key: ' + key);
+ if(Defines.DEBUG) console.log('Date: ' + time + ' State time: ' + this.state[key]);
+
+ // If the state isn't set either locally or remotely
+ // or if it is set but the timestamp is lower than the remove value
+ // add a listener to the value
+ if((!time || !this.state[key]) || (time && time > this.state[key])) {
+
+ // Assume that the we will be able to get
+ // the latest version of the data so prevent any
+ // new requests
+ this.state[key] = time;
+
+ // Get the ref
+ let ref = this.pathRef(key);
+
+ // Add the value listener
+ // TODO: Check this
+ resolve(ref.once('value', (snapshot) => {
+ if (callback) {
+ callback(snapshot.val());
+ resolve();
+ }
+ }));
+ }
+ else {
+ resolve();
+ }
+ }, (error) => {
+ console.log(error.message);
+ reject(error);
+ });
+ }
+ });
+ }
+
+ // This method strips the root of the Firebase reference to give a relative
+ // path for batch writes
+ relativeFirebasePath (ref: firebase.database.Reference): string {
+ return ref.toString().replace(this.Paths.firebase().toString(), "");
+ }
+
+ pathOff(key) {
+
+ this.pathIsOn[key] = false;
+
+ this.stateRef(key).off('value');
+ this.pathRef(key).off('value');
+ }
+
+ ref() {
+ return this.Paths.firebase().child(this._path).child(this._id);
+ }
+
+ removeOnDisconnect (path: string): Promise {
+ return this.ref().child(path).onDisconnect().remove();
+ }
+
+ pathRef(path) {
+ return this.ref().child(path);
+ }
+
+ stateRef(key) {
+ return this.ref().child(PathKeys.UpdatedPath).child(key);
+ }
+
+ updateState(key: string): Promise {
+ const ref = this.stateRef(key);
+ return ref.set(firebase.database.ServerValue.TIMESTAMP);
+ }
+
+ setMeta(meta: any): void {
+ if (meta instanceof Map) {
+ this.meta = meta;
+ } else {
+ this.meta = new Map(Object.entries(meta));
+ }
+ };
+
+ getMetaObject (): {} {
+ return Utils.toObject(this.meta);
+ }
+
+ getMeta(): Map {
+ return this.meta;
+ }
+
+ metaValue(key) {
+ if(this.getMeta()) {
+ return this.getMeta().get(key);
+ }
+ return null;
+ };
+
+ getMetaValue(key) {
+ return this.metaValue(key);
+ };
+
+ setMetaValue(key: string, value: any) {
+ this.getMeta().set(key, value);
+ };
+
+ serialize(): {} {
+ return {
+ _path: this._path,
+ _id: this._id,
+ state: this.state,
+ meta: this.getMetaObject()
+ }
+ }
+
+ deserialize(se) {
+ if(se) {
+ this._path = se._path;
+ this._id = se._id;
+ this.state = se.state ? se.state : {};
+ this.setMeta(se.meta);
+ }
+ }
+
+}
+
+export class EntityFactory {
+
+ static $inject = ['$q', 'Paths'];
+ constructor (protected $q: ng.IQService, protected Paths: IPaths) {}
+
+ ref(path, id) {
+ return this.Paths.firebase().child(path).child(id);
+ }
+
+ stateRef(path, id, key) {
+ return this.ref(path, id).child(PathKeys.UpdatedPath).child(key);
+ }
+
+ updateState(path, id, key) {
+
+ const deferred = this.$q.defer();
+
+ const ref = this.stateRef(path, id, key);
+ ref.set(firebase.database.ServerValue.TIMESTAMP, (error) => {
+ if(!error) {
+ deferred.resolve();
+ }
+ else {
+ deferred.reject();
+ }
+ });
+
+ return deferred.promise;
+ }}
+
+angular.module('myApp.services')
+ .service('Entity', ['Paths', function(Paths) {
+ // we can ask for more parameters if needed
+ return function entityFactory(path: string, id: string) { // return a factory instead of a new talker
+ return new Entity(Paths, path, id);
+ }}])
+ .service('EntityFactory', EntityFactory);
diff --git a/src/angularjs/entities/message.ts b/src/angularjs/entities/message.ts
new file mode 100755
index 00000000..b25b42e1
--- /dev/null
+++ b/src/angularjs/entities/message.ts
@@ -0,0 +1,301 @@
+import * as angular from 'angular'
+import * as firebase from 'firebase';
+
+import {IUser} from "./user";
+import {MessageType} from "../keys/message-type";
+import {MessageKeys} from "../keys/message-keys";
+import {Utils} from "../services/utils";
+
+export interface IMessage {
+ hideName: boolean
+ hideTime: boolean
+ mid: string
+ user: IUser
+ meta: {}
+ nextMessage: IMessage
+ previousMessage: IMessage
+ read: boolean
+ time()
+ date()
+ markRead(uid?: string): void
+ type(): MessageType
+ sender(): IUser
+ text(): string
+ updateDisplay(): void
+ uid(): string
+}
+
+class Message implements IMessage {
+
+ public read = false;
+ public flagged = false;
+ public side: MessageSide;
+ public user: IUser;
+ public imageURL: string;
+ public thumbnailURL: string;
+ public fileURL: string;
+ public timeString: string;
+ public nextMessage: IMessage;
+ public previousMessage: IMessage;
+
+ hideName = false;
+ hideTime = false;
+
+ constructor (
+ private $rootScope,
+ private Time,
+ private UserStore,
+ private Config,
+ private CloudImage,
+ public mid: string,
+ public meta: Map
+ ) {
+ if (!meta) {
+ this.meta = new Map();
+ }
+
+ if(meta) {
+
+ if(!this.type()) {
+ this.setType(MessageType.Text);
+ }
+
+ if(this.type() == MessageType.Image || this.type() == MessageType.File) {
+ // Get the image and thumbnail URLs
+ let json = meta[MessageKeys.JSONv2];
+
+ if(json) {
+ if(this.type() == MessageType.Image) {
+ this.thumbnailURL = this.CloudImage.cloudImage(json[MessageKeys.ImageURL], 200, 200);
+ this.imageURL = json[MessageKeys.ImageURL];
+ }
+ if(this.type() == MessageType.File) {
+ this.fileURL = json[MessageKeys.FileURL];
+ }
+ }
+ }
+
+ // Our messages are on the right - other user's messages are
+ // on the left
+ this.side = this.uid() == this.UserStore.currentUser().uid() ? MessageSide.Right : MessageSide.Left;
+
+ this.timeString = this.Time.formatTimestamp(this.time(), this.Config.clockType);
+
+ // Set the user
+ if(this.uid()) {
+
+ // We need to set the user here
+ if(this.uid() == this.UserStore.currentUser().uid()) {
+ this.user = this.UserStore.currentUser();
+ }
+ else {
+ this.user = this.UserStore.getOrCreateUserWithID(this.uid());
+ }
+ }
+ }
+ }
+
+ markRead(uid?: string): void {
+ this.read = true;
+ }
+
+
+ serialize() {
+ return {
+ meta: this.meta,
+ mid: this.mid,
+ read: this.read,
+ flagged: this.flagged,
+ hideTime: this.hideTime,
+ hideName: this.hideName,
+ side: this.side,
+ }
+ }
+
+ updateDisplay(): void {
+
+ let hideName = this.sender().isMe();
+ let hideDate = true;
+
+ if (this.nextMessage) {
+ hideName = hideName || this.uid() == this.nextMessage.uid();
+ }
+ if (this.previousMessage) {
+ hideDate = Utils.sameMinute(this.date(), this.previousMessage.date());
+ }
+
+ this.hideName = hideName;
+ this.hideTime = hideDate;
+
+ //console.log("Message: " + this.text() + ", user: " + this.uid() + ", h: " + this.date().getHours() + " m:" + this.date().getMinutes() + " hideName: " + hideName + ", hideDate: " + hideDate);
+ }
+
+ deserialize(sm) {
+ this.mid = sm.mid;
+ this.meta = sm.meta;
+ this.read = sm.read;
+ this.flagged = sm.flagged;
+ this.hideTime = sm.hideTime;
+ this.hideName = sm.hideName;
+ this.side = sm.side;
+ }
+
+ setTime(time) {
+ this.setValue(MessageKeys.Date, time);
+ }
+
+ time() {
+ return this.getValue(MessageKeys.Date);
+ }
+
+ date(): Date {
+ return new Date(this.time());
+ }
+
+ setText(text) {
+ this.setMetaValue(MessageKeys.Text, text);
+ }
+
+ text(): string {
+ if (this.type() == MessageType.Text) {
+ return this.getMetaValue(MessageKeys.Text);
+ }
+ else if (this.type() == MessageType.Image) {
+ return "Image";
+ }
+ else if (this.type() == MessageType.File) {
+ return "File";
+ }
+ else if (this.type() == MessageType.Location) {
+ return "Location";
+ }
+ return "";
+ }
+
+ sender(): IUser {
+ return this.user;
+ }
+
+ getMetaValue(key) {
+ return this.getValue(MessageKeys.Meta)[key];
+ }
+
+ getValue(key) {
+ return this.meta[key];
+ }
+
+ setValue(key, value) {
+ this.meta[key] = value;
+ }
+
+ type(): MessageType {
+ return this.getValue(MessageKeys.Type);
+ }
+
+ setType(type) {
+ this.setValue(MessageKeys.Type, type);
+ }
+
+ uid(): string {
+ let from = this.getValue(MessageKeys.From);
+ if (!from) {
+ from = this.getValue(MessageKeys.UID);
+ }
+ return from;
+ }
+
+ setUID(uid) {
+ this.setValue(MessageKeys.From, uid);
+ this.setValue(MessageKeys.UID, uid);
+ }
+
+ metaValue(key) {
+ return this.getMetaValue(key);
+ }
+
+ setMetaValue(key, value) {
+ this.meta[MessageKeys.Meta][key] = value;
+ }
+
+ setMID(mid) {
+ this.mid = mid;
+ }
+}
+
+export enum MessageSide {
+ Right = 'right',
+ Left = 'left'
+}
+
+export interface IMessageFactory {
+}
+
+class MessageFactory implements IMessageFactory {
+
+ constructor() {}
+
+
+ buildFileMeta (fileName: string, mimeType: string, fileURL: string): Map {
+ const map = new Map();
+ map.set(MessageKeys.Text, fileName);
+ map.set(MessageKeys.MimeType, mimeType);
+ map.set(MessageKeys.FileURL, fileURL);
+ return map;
+ }
+
+ buildTextMeta (text: string): Map {
+ const map = new Map();
+ map.set(MessageKeys.Text, text);
+ return map;
+ }
+
+ buildImageMeta (url: string, width: number, height: number): Map {
+ const map = new Map();
+ map.set(MessageKeys.ImageURL, url);
+ map.set(MessageKeys.ImageWidth, width);
+ map.set(MessageKeys.ImageHeight, height);
+ return map;
+ }
+
+ buildMessage(from: string, to: Array, type: MessageType, meta: Map) {
+ const message = {};
+
+ message[MessageKeys.From] = from;
+ message[MessageKeys.UserFirebaseID] = from;
+
+ message[MessageKeys.To] = to;
+
+ const metaObject = Utils.toObject(meta);
+
+ message[MessageKeys.Meta] = metaObject;
+ message[MessageKeys.JSONv2] = metaObject;
+
+ message[MessageKeys.Date] = firebase.database.ServerValue.TIMESTAMP;
+ message[MessageKeys.Type] = type;
+
+ let read = new Map();
+
+ for (let i = 0; i < to.length; i++) {
+ const status = {};
+ status[MessageKeys.Status] = 0;
+ read.set(to[i], status);
+ }
+
+ // Set my read status to read
+ const status = {};
+ status[MessageKeys.Status] = 2;
+ read.set(from, status);
+
+ message[MessageKeys.Read] = Utils.toObject(read);
+
+ return message;
+ }
+}
+
+angular.module('myApp.services')
+ .service('Message', ['$rootScope', 'Time', 'UserStore', 'Config', 'CloudImage', function($rootScope, Time, UserStore, Config, CloudImage) {
+ // we can ask for more parameters if needed
+ return function messageFactory(mid: string, meta: Map) { // return a factory instead of a new talker
+ return new Message($rootScope, Time, UserStore, Config, CloudImage, mid, meta);
+ }}])
+ .service('MessageFactory', MessageFactory);
diff --git a/src/angularjs/entities/room.ts b/src/angularjs/entities/room.ts
new file mode 100755
index 00000000..e5c03882
--- /dev/null
+++ b/src/angularjs/entities/room.ts
@@ -0,0 +1,1673 @@
+import * as angular from 'angular'
+import * as firebase from 'firebase';
+
+import * as PathKeys from "../keys/path-keys";
+import {N} from "../keys/notification-keys";
+import * as RoomNameKeys from "../keys/room-name-keys";
+import * as Keys from "../keys/keys";
+import * as Defines from "../keys/defines";
+import {Entity, EntityFactory, IEntity} from "./entity";
+import {RoomKeys} from "../keys/room-keys";
+import {IMessage} from "./message";
+import {RoomType} from "../keys/room-type";
+import {MessageKeys} from "../keys/message-keys";
+import {MessageType} from "../keys/message-type";
+import {Dimensions} from "../keys/dimensions";
+import {IUser} from "./user";
+import {Utils} from "../services/utils";
+import {UserStatus} from "../keys/user-status";
+import {UserKeys} from "../keys/user-keys";
+import {IRootScope} from "../controllers/app";
+import {Log} from "../services/log";
+import {IFirebaseReference} from "../network/paths";
+
+export interface IRoom extends IEntity {
+ name: string
+ isOpen: boolean
+ slot: number
+ height: number
+ width: number
+ offset: number
+ type: RoomType
+ dragDirection: number
+ zIndex: number
+ draggable: boolean
+ invitedBy: IUser
+ deleted: boolean
+ transcript(): string
+ setOffset(offset: number)
+ updateOffsetFromSlot()
+ updateType()
+ loadMoreMessages(numberOfMessages?: number): Promise>
+ rid(): string
+ on(): Promise
+ off(): void
+ created()
+ getUserStatus(user: IUser): UserStatus
+ getType(): RoomType
+ addUserUpdate(user: IUser, status: UserStatus): {}
+ removeUserUpdate(user: IUser): {}
+ open(slot: number, duration: number): void
+ close(): void
+ deserialize(sr): void
+ lastMessage (): IMessage
+ lastMessageTime()
+}
+
+export interface IRoomFactory {
+
+}
+
+class Room extends Entity implements IRoom {
+
+ users = {};
+ usersMeta = {};
+ onlineUserCount = 0;
+ messages = [];
+ typing = {};
+ typingMessage = "";
+ badge = 0;
+ isOn = false;
+ draggable: boolean;
+ type: RoomType;
+
+ // Layout
+ offset: number; // The x offset
+ dragDirection = 0; // drag direction +ve / -ve
+
+ width = Dimensions.ChatRoomWidth;
+ height = Dimensions.ChatRoomHeight;
+ zIndex = null;
+ active = true; // in side list or not
+ minimized = false;
+ loadingMoreMessages = false;
+ loadingTimer = null;
+ muted = false;
+ invitedBy: IUser;
+
+ // Has the room been deleted?
+ deleted = false;
+ // When was the room deleted?
+ deletedTimestamp = null;
+
+ isOpen = false;
+ readTimestamp = 0; // When was the thread last read?
+
+ thumbnail = this.Environment.defaultRoomPictureURL();
+ showImage = false;
+
+ // The room associated with this use
+ // this is used to make sure that if a user logs out
+ // the next user who logs in doesn't see their
+ // inbox
+ associatedUserID = null;
+
+ // TODO: Check this
+ name = "";
+
+ slot: number;
+ unreadMessages: Array;
+
+ userOnlineStateChangedNotificationOff;
+
+ messagesAreOn: boolean;
+
+ constructor (
+ private $rootScope: IRootScope,
+ private $timeout,
+ private $window,
+ private Presence,
+ Paths,
+ private Config,
+ private Message,
+ private MessageFactory,
+ private Cache,
+ private UserStore,
+ private User,
+ private RoomPositionManager,
+ private SoundEffects,
+ private Visibility,
+ private Time,
+ private CloudImage,
+ private Marquee,
+ private Environment,
+ private RoomFactory,
+ private NetworkManager,
+ rid: string,
+ meta?: Map,
+ ) {
+ super(Paths, PathKeys.RoomsPath, rid);
+ if(meta) {
+ this.setMeta(meta);
+ }
+ }
+
+ /***********************************
+ * GETTERS AND SETTERS
+ */
+
+ getRID() {
+ return this.rid();
+ };
+
+ getUserCreated() {
+ return this.metaValue(RoomKeys.UserCreated);
+ };
+
+ /***********************************
+ * UPDATE METHOD
+ */
+
+ /**
+ * If silent is true then this will not broadcast to update the UI.
+ * Primarily this is used when deserializing
+ *
+ * @param silent
+ */
+ update(silent = false) {
+ this.updateName();
+ // TODO: Check
+ this.setImage(this.metaValue(RoomKeys.Image));
+ this.updateOnlineUserCount();
+ if(!silent) {
+ this.$rootScope.$broadcast(N.RoomUpdated, this);
+ }
+ };
+
+ updateTyping() {
+
+ let i = 0;
+ let name = null;
+ for(let key in this.typing) {
+ if(this.typing.hasOwnProperty(key)) {
+ if(key == this.UserStore.currentUser().uid()) {
+ continue;
+ }
+ name = this.typing[key];
+ i++;
+ }
+ }
+
+ let typing = null;
+ if (i == 1) {
+ typing = name + "...";
+ }
+ else if (i > 1) {
+ typing = i + " people typing";
+ }
+
+ this.typingMessage = typing;
+ };
+
+ updateOnlineUserCount() {
+ this.onlineUserCount = this.getOnlineUserCount();
+ };
+
+ updateName() {
+
+ // If the room already has a name
+ // use it
+ const name = this.metaValue(RoomKeys.Name);
+ if(name && name.length) {
+ this.name = name;
+ return;
+ }
+
+ // Otherwise build a room based on the users' names
+ this.name = "";
+ for(let key in this.users) {
+ if(this.users.hasOwnProperty(key)) {
+ let user = this.users[key];
+ if(!user.isMe() && user.getName() && user.getName().length) {
+ this.name += user.getName() + ", ";
+ }
+ }
+ }
+ if(this.name.length >= 2) {
+ this.name = this.name.substring(0, this.name.length - 2);
+ }
+
+ // Private chat x users
+ // Ben Smiley
+ if(!this.name || !this.name.length) {
+ if (this.isPublic()) {
+ this.name = RoomNameKeys.RoomDefaultNamePublic;
+ }
+ else if (this.userCount() == 1) {
+ this.name = RoomNameKeys.RoomDefaultNameEmpty;
+ }
+ else if (this.getType() == RoomType.Group) {
+ this.name = RoomNameKeys.RoomDefaultNameGroup;
+ }
+ else {
+ this.name = RoomNameKeys.RoomDefaultName1To1;
+ }
+ }
+
+ };
+
+ /***********************************
+ * LIFECYCLE: on -> open -> closed -> off
+ */
+
+ on(): Promise {
+ return new Promise((resolve, reject) => {
+ if(!this.isOn && this.rid()) {
+ this.isOn = true;
+
+ let on = () => {
+ // When a user changes online state update the room
+ this.userOnlineStateChangedNotificationOff = this.$rootScope.$on(N.UserOnlineStateChanged, (event, user: IUser) => {
+ Log.notification(N.UserOnlineStateChanged, 'Room');
+ // If the user is a member of this room, update the room
+ if(this.containsUser(user)) {
+ this.update();
+ }
+ });
+ this.$rootScope.$on(N.UserValueChanged, (event, user: IUser) => {
+ if(this.containsUser(user)) {
+ this.update();
+ }
+ });
+
+
+ this.usersMetaOn();
+ this.messagesOn(this.deletedTimestamp);
+
+ resolve();
+ };
+
+ // First get the meta
+ this.metaOn().then(() => {
+
+ switch (this.getType()) {
+ case RoomType.OneToOne:
+ this.deleted = false;
+ this.userDeletedDate().then((timestamp) => {
+ if(timestamp) {
+ this.deleted = true;
+ this.deletedTimestamp = timestamp;
+ }
+ on();
+ });
+ break;
+ case RoomType.Public:
+ case RoomType.Group:
+ on();
+ break;
+ default:
+ resolve();
+ }
+
+ });
+ } else {
+ resolve();
+ }
+ });
+ };
+
+ open(slot: number, duration: number): void {
+
+ let open = () => {
+
+ // Add the room to the UI
+ this.RoomPositionManager.insertRoom(this, slot, duration);
+
+ // Start listening to message updates
+ this.messagesOn(this.deletedTimestamp);
+
+ // Start listening to typing indicator updates
+ this.typingOn();
+
+ // Update the interface
+ this.$rootScope.$broadcast(N.RoomAdded);
+
+ };
+
+ switch (this.getType()) {
+ case RoomType.Public:
+ this.join(UserStatus.Member).then(() => open(), (error) => {
+ console.log(error);
+ });
+ break;
+ case RoomType.Group:
+ case RoomType.OneToOne:
+ open();
+ }
+ };
+
+ /**
+ * Removes the room from the display
+ * and leaves the room
+ */
+ close(): void {
+
+ this.typingOff();
+ this.messagesOff();
+
+ let type = this.getType();
+
+ switch (type) {
+ case RoomType.Public:
+ {
+ this.RoomFactory.removeUserFromRoom(this.UserStore.currentUser(), this);
+ }
+ }
+
+ this.RoomPositionManager.closeRoom(this);
+ };
+
+ leave(): Promise {
+ this.deleteMessages();
+ this.$rootScope.$broadcast(N.RoomRemoved);
+ this.deleted = true;
+ return this.RoomFactory.removeUserFromRoom(this.UserStore.currentUser(), this).then(()=>{
+ this.off();
+ });
+ };
+
+ off(): void {
+
+ this.isOn = false;
+
+ if(this.userOnlineStateChangedNotificationOff) {
+ this.userOnlineStateChangedNotificationOff();
+ }
+
+ this.metaOff();
+ this.usersMetaOff();
+
+ };
+
+ getType(): RoomType {
+ let type = parseInt(this.metaValue(RoomKeys.Type));
+ if(!type) {
+ type = parseInt(this.metaValue(RoomKeys.Type_v4));
+ }
+ return type;
+ };
+
+ calculatedType() {
+
+ let type = null;
+
+ if(this.isPublic()) {
+ type = RoomType.Public;
+ }
+ else {
+ if(this.userCount() <= 1) {
+ type = RoomType.Invalid;
+ }
+ else if (this.userCount() == 2) {
+ type = RoomType.OneToOne;
+ }
+ else {
+ type = RoomType.Group;
+ }
+ }
+
+ return type;
+ };
+
+ updateType() {
+ const type = this.calculatedType();
+ if(type != this.getType()) {
+ // One important thing is that we can't go from group -> 1to1
+ if(this.getType() != RoomType.Group) {
+ this.RoomFactory.updateRoomType(this.rid(), type);
+ }
+ }
+ };
+
+ /**
+ * Message flagging
+ */
+
+ toggleMessageFlag(message) {
+ if(message.flagged) {
+ return this.unflagMessage(message);
+ }
+ else {
+ return this.flagMessage(message);
+ }
+ };
+
+ flagMessage(message) {
+
+ message.flagged = true;
+
+ const ref = this.Paths.flaggedMessageRef(message.mid);
+
+ const data = {};
+
+ data[Keys.Creator] = this.UserStore.currentUser().uid();
+ data[Keys.CreatorEntityID] = data[Keys.Creator];
+
+ data[Keys.From] = message.metaValue(MessageKeys.UserFirebaseID);
+ data[Keys.SenderEntityID] = data[Keys.From];
+
+ data[Keys.MessageKey] = message.text();
+ data[Keys.ThreadKey] = message.rid;
+ data[Keys.DateKey] = firebase.database.ServerValue.TIMESTAMP;
+
+ return ref.set(data).then(() => {
+ message.flagged = false;
+ this.$rootScope.$broadcast(N.ChatUpdated, this);
+ });
+ };
+
+ unflagMessage(message) {
+
+ message.flagged = false;
+
+ const ref = this.Paths.flaggedMessageRef(message.mid);
+ return ref.remove().then(() => {
+ message.flagged = true;
+ this.$rootScope.$broadcast(N.ChatUpdated, this);
+ });
+ };
+
+ isPublic() {
+ return this.getType() == RoomType.Public;
+ };
+
+ rid() {
+ return this._id;
+ };
+
+ created() {
+ return this.metaValue(RoomKeys.Created);
+ };
+
+ lastMessageExists(): boolean {
+ return this.messages.length > 0;
+ };
+
+ lastMessageType(): MessageType {
+ if(this.lastMessageExists()) {
+ this.lastMessage().type();
+ }
+ return null;
+ };
+
+ lastMessage(): IMessage {
+ if(this.lastMessageExists()) {
+ return this.messages[this.messages.length - 1];
+ }
+ return null;
+ }
+
+ lastMessageUserName(): string {
+ if(this.lastMessageExists()) {
+ return this.lastMessage().user.getName();
+ }
+ return null;
+ };
+
+ lastMessageTime() {
+ if(this.lastMessageExists()) {
+ return this.lastMessage().time();
+ }
+ return null;
+ };
+
+ lastMessageDate() {
+ if(this.lastMessageExists()) {
+ return this.lastMessage().date();
+ }
+ return null;
+ };
+
+ lastMessageText(): string {
+ if(this.lastMessageExists()) {
+ return this.lastMessage().text();
+ }
+ return null;
+ };
+
+ /**
+ * Add the user to the room and add the room to the
+ * user in Firebase
+ * @param status
+ */
+ join(status): Promise {
+ return this.RoomFactory.addUserToRoom(this.UserStore.currentUser(), this, status);
+ };
+
+ setActive(active) {
+ if(active) {
+ this.markRead();
+ }
+ this.active = active;
+ };
+
+ setSizeToDefault() {
+ this.width = Dimensions.ChatRoomWidth;
+ this.height = Dimensions.ChatRoomHeight;
+ };
+
+ flashHeader() {
+ // TODO: Implement this
+ // Ideally if the chat is in the side bar then bring it
+ // to the front
+ // Or flash the side bar
+ if(this.RoomPositionManager.roomIsOpen(this)) {
+ this.$rootScope.$broadcast(N.RoomFlashHeader, this, '#555', 500, 'room-header');
+ this.$rootScope.$broadcast(N.RoomFlashHeader, this, '#CCC', 500, 'room-list');
+ return true;
+ }
+ return false;
+ };
+
+ /***********************************
+ * USERS
+ */
+
+ getUserInfoWithUID(uid) {
+ // This could be called from the UI so it's important
+ // to wait until users has been populated
+ if(this.usersMeta) {
+ return this.usersMeta[uid];
+ }
+ return null;
+ };
+
+ getUserInfo(user) {
+ // This could be called from the UI so it's important
+ // to wait until users has been populated
+ if(user && user.meta) {
+ return this.getUserInfoWithUID(user.uid());
+ }
+ return null;
+ };
+
+ getUserStatus(user: IUser): UserStatus {
+ let info = this.getUserInfo(user);
+ return info ? info[UserKeys.Status] : null;
+ };
+
+ getUsers() {
+ let users = {};
+ for(let key in this.users) {
+ if(this.users.hasOwnProperty(key)) {
+ let user = this.users[key];
+ if(user.meta && this.UserStore.currentUser() && this.UserStore.currentUser().meta) {
+ if(user.uid() != this.UserStore.currentUser().uid()) {
+ users[user.uid()] = user;
+ }
+ }
+ }
+ }
+ return users;
+ };
+
+ getUserIDs():Array {
+ const users = new Array();
+ for(let key in this.users) {
+ if(this.users.hasOwnProperty(key)) {
+ users.push(key);
+ }
+ }
+ return users;
+ };
+
+ // userIsActiveWithUID(uid) {
+ // let info = this.getUserInfo(uid);
+ // return this.RoomFactory.userIsActiveWithInfo(info);
+ // };
+
+ getOwner() {
+ // get the owner's ID
+ let data = null;
+
+ for(let key in this.usersMeta) {
+ if(this.usersMeta.hasOwnProperty(key)) {
+ data = this.usersMeta[key];
+ if(data.status == UserStatus.Owner) {
+ break;
+ }
+ }
+ }
+ if(data) {
+ return this.UserStore.getOrCreateUserWithID(data.uid);
+ }
+ return null;
+ };
+
+// isClosed() {
+// return this.getUserStatus(this.UserStore.currentUser()) == UserStatusClosed;
+// };
+
+ containsUser(user) {
+ return this.users[user.uid()] != null;
+ };
+
+ // Update the timestamp on the user status
+ // updateUserStatusTime(user): Promise {
+ //
+ // let data = {
+ // time: firebase.database.ServerValue.TIMESTAMP
+ // };
+ //
+ // let ref = this.Paths.roomUsersRef(this.rid());
+ // return ref.child(user.uid()).update(data);
+ // };
+
+ /***********************************
+ * ROOM INFORMATION
+ */
+
+ getOnlineUserCount() {
+ let i = 0;
+ for(let key in this.usersMeta) {
+ if(this.usersMeta.hasOwnProperty(key)) {
+ let user = this.usersMeta[key];
+ if(this.UserStore.currentUser() && this.UserStore.currentUser().meta) {
+ if((this.UserStore.users[user.uid].online || this.UserStore.currentUser().uid() == user.uid)) {
+ i++;
+ }
+ }
+ }
+ }
+ return i;
+ };
+
+ userCount() {
+ let i = 0;
+ for(let key in this.users) {
+ if(this.users.hasOwnProperty(key)) {
+ i++;
+ }
+ }
+ return i;
+ };
+
+ containsOnlyUsers(users) {
+ let usersInRoom = 0;
+ const totalUsers = this.userCount();
+
+ for(let i = 0; i < users.length; i++) {
+ if(this.users[users[i].uid()]) {
+ usersInRoom++;
+ }
+ }
+ return usersInRoom == users.length && usersInRoom == totalUsers;
+ };
+
+ /***********************************
+ * LAYOUT
+ */
+
+ // If the room is animating then
+ // return the destination
+ getOffset() {
+ return this.offset;
+ };
+
+ getCenterX() {
+ return this.getOffset() + this.width / 2;
+ };
+
+ getMinX() {
+ return this.getOffset();
+ };
+
+ getMaxX() {
+ return this.getOffset() + this.width;
+ };
+
+ updateOffsetFromSlot() {
+ this.setOffset(this.RoomPositionManager.offsetForSlot(this.slot));
+ };
+
+ setOffset(offset: number) : void {
+ this.offset = offset;
+ };
+
+ setSlot(slot) {
+ this.slot = slot;
+ };
+
+ /***********************************
+ * MESSAGES
+ */
+
+ sendImageMessage(user: IUser, url: string, width: number, height: number): Promise {
+ const meta = this.MessageFactory.buildImageMeta(url, width, height);
+ const messageMeta = this.MessageFactory.buildMessage(user.uid(), this.getUserIDs(), MessageType.Image, meta);
+ return this.sendMessage(messageMeta, user);
+ };
+
+ sendFileMessage(user: IUser, fileName: string, mimeType: string, fileURL: string): Promise {
+ const meta = this.MessageFactory.buildFileMeta(fileName, mimeType, fileURL);
+ const messageMeta = this.MessageFactory.buildMessage(user.uid(), this.getUserIDs(), MessageType.File, meta);
+ return this.sendMessage(messageMeta, user);
+ };
+
+ sendTextMessage(user: IUser, text: string): Promise {
+ if(!text || text.length === 0) {
+ return;
+ }
+ const meta = this.MessageFactory.buildTextMeta(text);
+ const messageMeta = this.MessageFactory.buildMessage(user.uid(), this.getUserIDs(), MessageType.Text, meta);
+ return this.sendMessage(messageMeta, user);
+ };
+
+ sendMessage(messageMeta: {}, user): Promise {
+ let innerSendMessage = ((message, user) => {
+
+ // Get a ref to the room
+ const ref = this.Paths.roomMessagesRef(this.rid());
+
+ // Add the message
+ const newRef = ref.push() as IFirebaseReference;
+
+ const p1 = newRef.setWithPriority(messageMeta, firebase.database.ServerValue.TIMESTAMP);
+
+ // The user's been active so update their status
+ // with the current time
+ // this.updateUserStatusTime(user);
+
+ // Avoid a clash..
+ const p2 = this.updateState(PathKeys.MessagesPath);
+
+ return Promise.all([
+ p1, p2
+ ]);
+
+ });
+
+ return innerSendMessage(messageMeta, user).catch((error) => {
+ this.Presence.update().then(() => {
+ return innerSendMessage(messageMeta, user);
+ });
+ });
+ };
+
+ addMessagesFromSerialization (sm) {
+ for(let i = 0; i < sm.length; i++) {
+ this.addMessageFromSerialization(sm[i]);
+ }
+ // Now update all the message displays
+
+ }
+
+ addMessageFromSerialization (sm) {
+ const message = this.getMessageFromMeta(sm.mid, sm.meta);
+ message.deserialize(sm);
+ this.addMessageToEnd(message, true);
+ }
+
+ getMessageFromMeta (mid: string, metaValue) {
+ return this.Message(mid, metaValue);
+ }
+
+ getMessagesNewerThan(date: Date = null, number: number = null): Array {
+ const messages = new Array();
+ for (let i = 0; i < this.messages.length; i++) {
+ const message = this.messages[i];
+ if (!date || message.date() > date) {
+ messages.push(this.messages[i]);
+ }
+ }
+ return messages;
+ }
+
+ addMessageToStart (message: IMessage, silent = true): void {
+ if (this.messages.length) {
+ const nextMessage = this.messages[0];
+ nextMessage.previousMessage = message;
+ message.nextMessage = nextMessage;
+ message.updateDisplay();
+ nextMessage.updateDisplay();
+ }
+ this.messages.unshift(message);
+ this.update(silent);
+ }
+
+ addMessageToEnd (message: IMessage, silent = false): void {
+ if (this.messages.length) {
+ const previousMessage = this.messages[this.messages.length - 1];
+ previousMessage.nextMessage = message;
+ message.previousMessage = previousMessage;
+ message.updateDisplay();
+ previousMessage.updateDisplay();
+ }
+ this.updateBadgeForMessage(message);
+ this.messages.push(message);
+
+ if (message.user && !silent) {
+ this.Marquee.startWithMessage(message.user.getName() + ': ' + message.text());
+ }
+
+ this.update(silent);
+ }
+
+ updateBadgeForMessage(message: IMessage): void {
+ if(this.shouldIncrementUnreadMessageBadge() && !message.read && (message.time() > this.readTimestamp || !this.readTimestamp)) {
+
+ if(!this.unreadMessages) {
+ this.unreadMessages = [];
+ }
+
+ this.unreadMessages.push(message);
+ }
+ else {
+ // Is the room active? If it is then mark the message
+ // as seen
+ message.markRead();
+ }
+ }
+
+ getMessagesOlderThan(date: Date = null, number: number = null): Array {
+ const messages = new Array();
+ for (let i = 0; i < this.messages.length; i++) {
+ const message = this.messages[i];
+ if (!date || message.date() < date) {
+ messages.push(this.messages[i]);
+ }
+ }
+ return messages;
+ }
+
+ loadLocalMessages (fromDate: Date, number: number): Array {
+ const messages = new Array();
+
+ return messages;
+ }
+
+ // Load m
+ loadMessagesOlderThan (date: Date = null, number: number): Promise> {
+
+ let ref = this.Paths.roomMessagesRef(this.rid());
+ let query = ref.orderByChild(MessageKeys.Date).limitToLast(number);
+
+ if (date) {
+ query = query.endAt(date.getTime() - 1, MessageKeys.Date);
+ }
+
+ return >> query.once('value').then((snapshot: firebase.database.DataSnapshot) => {
+ const val = snapshot.val();
+ const messages = new Array();
+ if(val) {
+ Object.keys(val).forEach(key => {
+ messages.push(this.Message(key, val[key]));
+ });
+ }
+ return messages;
+ }).catch((e) => {
+ console.log(e.message);
+ });
+ }
+
+ loadMoreMessages(numberOfMessages: number = 10): Promise> {
+
+ if(this.loadingMoreMessages) {
+ return Promise.resolve([]);
+ }
+ this.loadingMoreMessages = true;
+
+ let date = null;
+ if (this.messages.length) {
+ date = this.messages[0].date();
+ }
+
+ return this.loadMessagesOlderThan(date, numberOfMessages).then(messages => {
+
+ const len = messages.length - 1;
+ for (let i = 0; i < messages.length; i++) {
+ this.addMessageToStart(messages[len - i]);
+ }
+
+
+ // Add messages to front of global list
+ // Ignore the last message - it's a duplicate
+ // let lastMessage = null;
+ // for(let i = messages.length - 2; i >= 0; i--) {
+ // if(this.messages.length > 0) {
+ // lastMessage = this.messages[0];
+ // }
+ // this.messages.unshift(messages[i]);
+ // if(lastMessage) {
+ // lastMessage.hideName = lastMessage.shouldHideUser(messages[i]);
+ // lastMessage.hideTime = lastMessage.shouldHideDate(messages[i]);
+ // }
+ // }
+
+ this.loadingMoreMessages = false;
+
+ this.$rootScope.$broadcast(N.LazyLoadedMessages, this);
+
+ return messages;
+ });
+ };
+
+ sortMessages() {
+ // Now we should sort all messages
+ this.sortMessageArray(this.messages);
+ };
+
+ deduplicateMessages() {
+ let uniqueMessages = [];
+
+ // Deduplicate list
+ let lastMID = null;
+ for(let i = 0; i < this.messages.length; i++) {
+ if(this.messages[i].mid != lastMID) {
+ uniqueMessages.push(this.messages[i]);
+ }
+ lastMID = this.messages[i].mid;
+ }
+
+ this.messages = uniqueMessages;
+
+ };
+
+ deleteMessages() {
+ this.messages.length = 0;
+ if(this.unreadMessages) {
+ this.unreadMessages.length = 0;
+ }
+ };
+
+ sortMessageArray(messages) {
+ messages.sort((a, b) => {
+ return a.time() - b.time();
+ });
+ };
+
+ markRead() {
+
+ let messages = this.unreadMessages;
+
+ if(messages && messages.length > 0) {
+
+ for(let i in messages) {
+ if(messages.hasOwnProperty(i)) {
+ messages[i].markRead();
+ }
+ }
+
+ // Clear the messages array
+ while(messages.length > 0) {
+ messages.pop();
+ }
+ }
+ this.badge = 0;
+ this.sendBadgeChangedNotification();
+
+ // Mark the date when the thread was read
+ if(!this.isPublic())
+ this.UserStore.currentUser().markRoomReadTime(this.rid());
+
+ };
+
+ updateImageURL(imageURL) {
+ // Compare to the old URL
+ let imageChanged = imageURL != this.metaValue(RoomKeys.Image);
+ if(imageChanged) {
+ this.setMetaValue(RoomKeys.Image, imageURL);
+ this.setImage(imageURL, false);
+ return this.pushMeta();
+ }
+ };
+
+ setImage(image, isData = false) {
+
+ this.showImage = this.getType() == RoomType.Public;
+
+ if(!image) {
+ image = this.Environment.defaultRoomPictureURL();
+ }
+ else {
+ if(isData || image == this.Environment.defaultRoomPictureURL()) {
+ this.thumbnail = image;
+ }
+ else {
+ this.thumbnail = this.CloudImage.cloudImage(image, 30, 30);
+ }
+ }
+ };
+
+ pushMeta(): Promise {
+ const ref = this.Paths.roomMetaRef(this.rid());
+ return ref.update(this.getMetaObject()).then(() => {
+ return this.updateState(Keys.DetailsKey);
+ });
+ };
+
+ sendBadgeChangedNotification() {
+ this.$rootScope.$broadcast(N.LazyLoadedMessages, this);
+ };
+
+ transcript() : string {
+
+ let transcript: string = "";
+
+ for(let i in this.messages) {
+ if(this.messages.hasOwnProperty(i)) {
+ let m = this.messages[i];
+ transcript += this.Time.formatTimestamp(m.time()) + " " + m.user.getName() + ": " + m.text() + "\n";
+ }
+ }
+
+ return transcript;
+ };
+
+ /***********************************
+ * TYPING INDICATOR
+ */
+
+ startTyping(user): Promise {
+ // The user is typing...
+ const ref = this.Paths.roomTypingRef(this.rid()).child(user.uid());
+ const promise = ref.set({name: user.getName()});
+
+ // If the user disconnects, tidy up by removing the typing
+ // indicator
+ ref.onDisconnect().remove();
+ return promise;
+ };
+
+ finishTyping(user): Promise {
+ const ref = this.Paths.roomTypingRef(this.rid()).child(user.uid());
+ return ref.remove();
+ };
+
+ /***********************************
+ * SERIALIZATION
+ */
+
+ serialize(): {} {
+ const superData = super.serialize();
+
+ const m = [];
+ for(let i = 0; i < this.messages.length; i++) {
+ m.push(this.messages[i].serialize());
+ }
+ const data = {
+ minimized: this.minimized,
+ width: this.width,
+ height: this.height,
+ //offset: this.offset,
+ messages: m,
+ usersMeta: this.usersMeta,
+ deleted: this.deleted,
+ isOpen: this.isOpen,
+ //badge: this.badge,
+ associatedUserID: this.associatedUserID,
+ offset: this.offset,
+ readTimestamp: this.readTimestamp,
+ };
+ return {...superData, ...data};
+ };
+
+ deserialize(sr): void {
+ if(sr) {
+ super.deserialize(sr);
+ this.minimized = sr.minimized;
+ this.width = sr.width;
+ this.height = sr.height;
+ this.deleted = sr.deleted;
+ this.isOpen = sr.isOpen;
+ //this.badge = sr.badge;
+ this.associatedUserID = sr.associatedUserID;
+ this.offset = sr.offset;
+ this.readTimestamp = sr.readTimestamp;
+
+ //this.setUsersMeta(sr.usersMeta);
+
+ for(let key in sr.usersMeta) {
+ if(sr.usersMeta.hasOwnProperty(key)) {
+ this.addUserMeta(sr.usersMeta[key]);
+ }
+ }
+ //this.offset = sr.offset;
+
+ this.addMessagesFromSerialization(sr.messages);
+
+ }
+ };
+
+ /***********************************
+ * FIREBASE
+ */
+
+ /**
+ * Start listening to updates in the
+ * room meta data
+ */
+ metaOn() {
+ return this.pathOn(Keys.DetailsKey, (val) => {
+ if(val) {
+ this.setMeta(val);
+ this.update();
+ }
+ });
+ };
+
+ metaOff() {
+ this.pathOff(Keys.DetailsKey);
+ };
+
+ addUserMeta(meta) {
+ // We only display users who have been active
+ // recently
+ // if(this.RoomFactory.userIsActiveWithInfo(meta)) {
+ this.usersMeta[meta[Keys.userUID]] = meta;
+
+ // Add the user object
+ let user = this.UserStore.getOrCreateUserWithID(meta[Keys.userUID]);
+ this.users[user.uid()] = user;
+
+ this.update(false);
+ // }
+ };
+
+ removeUserMeta(meta) {
+ delete this.usersMeta[meta[Keys.userUID]];
+ delete this.users[meta[Keys.userUID]];
+ this.update(false);
+ };
+
+ usersMetaOn() {
+
+ let roomUsersRef = this.Paths.roomUsersRef(this.rid());
+
+ roomUsersRef.on('child_added', (snapshot) => {
+ if(snapshot.val() && snapshot.val()) {
+ let meta = snapshot.val();
+ meta.uid = snapshot.key;
+ this.addUserMeta(meta);
+ }
+ });
+
+ roomUsersRef.on('child_removed', (snapshot) => {
+ if(snapshot.val()) {
+ let meta = snapshot.val();
+ meta.uid = snapshot.key;
+ this.removeUserMeta(meta);
+ }
+ });
+ };
+
+ usersMetaOff() {
+ this.Paths.roomUsersRef(this.rid()).off();
+ };
+
+ userDeletedDate(): Promise {
+ const ref = this.Paths.roomUsersRef(this.rid()).child(this.UserStore.currentUser().uid());
+ return ref.once('value').then((snapshot) => {
+ let val = snapshot.val();
+ if(val && val.status == UserStatus.Closed) {
+ return val.time;
+ }
+ return null;
+ });
+ };
+
+ /**
+ * Start listening to messages being added
+ */
+
+ updateUnreadMessageCounter(messageMeta) {
+ if(this.shouldIncrementUnreadMessageBadge() && (messageMeta[MessageKeys.Date] > this.readTimestamp || !this.readTimestamp)) {
+ // If this is the first badge then this.badge will
+ // undefined - so set it to one
+ if (!this.badge) {
+ this.badge = 1;
+ }
+ else {
+ this.badge = Math.min(this.badge + 1, 99);
+ }
+ this.sendBadgeChangedNotification();
+ }
+ };
+
+ shouldIncrementUnreadMessageBadge() {
+ return (!this.active || this.minimized || !this.RoomPositionManager.roomIsOpen(this));// && !this.isPublic();
+ };
+
+ messagesOn(timestamp) {
+
+ // Make sure the room is valid
+ if(this.messagesAreOn || !this.rid()) {
+ return;
+ }
+ this.messagesAreOn = true;
+
+ // Also get the messages from the room
+ let ref: firebase.database.Query = this.Paths.roomMessagesRef(this.rid());
+
+ let startDate = timestamp;
+ if(Utils.unORNull(startDate)) {
+ // If we already have a message then only listen for new
+ // messages
+ const lastMessageTime = this.lastMessageTime();
+ if(lastMessageTime) {
+ startDate = lastMessageTime + 1;
+ }
+ }
+ else {
+ startDate++;
+ }
+
+ if(startDate) {
+ // Start 1 thousandth of a second after the last message
+ // so we don't get a duplicate
+ ref = ref.startAt(startDate);
+ }
+ ref = ref.limitToLast(this.Config.maxHistoricMessages);
+
+ // Add listen to messages added to this thread
+ ref.on('child_added', (snapshot) => {
+
+ if(this.Cache.isBlockedUser(snapshot.val()[MessageKeys.UID])) {
+ return;
+ }
+
+ const message = this.getMessageFromMeta(snapshot.key, snapshot.val());
+ this.addMessageToEnd(message);
+
+ // if(this.addMessageFromMeta(snapshot.key, snapshot.val())) {
+ // Trim the room to make sure the message count isn't growing
+ // out of control
+ this.trimMessageList();
+
+ // Is the window visible?
+ // Play the sound
+ if (!this.muted) {
+ if (this.Visibility.getIsHidden()) {
+ // Only make a sound for messages that were received less than
+ // 30 seconds ago
+ if (Defines.DEBUG) console.log("Now: " + new Date().getTime() + ", Date now: " + this.Time.now() + ", Message: " + snapshot.val()[MessageKeys.Date]);
+ if (Defines.DEBUG) console.log("Diff: " + Math.abs(this.Time.now() - snapshot.val().time));
+ if (Math.abs(this.Time.now() - snapshot.val()[MessageKeys.Date]) / 1000 < 30) {
+ this.SoundEffects.messageReceived();
+ }
+ }
+ }
+ // }
+
+ });
+
+ ref.on('child_removed', (snapshot) => {
+ if(snapshot.val()) {
+ for(let i = 0; i < this.messages.length; i++) {
+ let message = this.messages[i];
+ if(message.mid == snapshot.key) {
+ this.messages.splice(i, 1);
+ break;
+ }
+ }
+ //this.$rootScope.$broadcast(DeleteMessageNotification, snapshot.val().meta.mid);
+ this.update(false);
+ }
+ });
+
+ };
+
+ trimMessageList() {
+ this.sortMessages();
+ this.deduplicateMessages();
+
+ let toRemove = this.messages.length - 100;
+ if(toRemove > 0) {
+ for(let j = 0; j < toRemove; j++) {
+ this.messages.shift();
+
+ }
+ }
+ };
+
+ messagesOff() {
+
+ this.messagesAreOn = false;
+
+ // Get the room meta data
+ if(this.rid()) {
+ this.Paths.roomMessagesRef(this.rid()).off();
+ }
+ };
+
+ typingOn() {
+
+ // Handle typing
+ let ref = this.Paths.roomTypingRef(this.rid());
+
+ ref.on('child_added', (snapshot) => {
+ this.typing[snapshot.key] = snapshot.val().name;
+
+ this.updateTyping();
+
+ // Send a notification to the chat room
+ this.$rootScope.$broadcast(N.ChatUpdated, this);
+ });
+
+ ref.on('child_removed', (snapshot) => {
+ delete this.typing[snapshot.key];
+
+ this.updateTyping();
+
+ // Send a notification to the chat room
+ this.$rootScope.$broadcast(N.ChatUpdated, this);
+ });
+
+ };
+
+ typingOff() {
+ this.Paths.roomTypingRef(this.rid()).off();
+ };
+
+ // lastMessageOn() {
+ // let lastMessageRef = this.Paths.roomLastMessageRef(this.rid());
+ // lastMessageRef.on('value', (snapshot) => {
+ // if(snapshot.val()) {
+ //
+ // this.setLastMessage(snapshot.val(), );
+ //
+ // // If the message comes in then we should make sure
+ // // the room is un deleted
+ // if(!this.Cache.isBlockedUser(this.lastMessage.user.uid())) {
+ // if(this.deleted) {
+ // this.deleted = false;
+ // this.$rootScope.$broadcast(N.RoomAdded, this);
+ // }
+ // }
+ //
+ // this.updateUnreadMessageCounter(this.lastMessage.meta);
+ // this.update(false);
+ //
+ // }
+ // });
+ // };
+
+ // lastMessageOff() {
+ // this.Paths.roomLastMessageRef(this.rid()).off();
+ // };
+
+ /**
+ * Remove a public room
+ * @returns {promise}
+ */
+ removeFromPublicRooms(): Promise {
+ const ref = this.Paths.publicRoomRef(this.getRID());
+ return ref.remove();
+ };
+
+ userIsMember(user) {
+ let userStatus = this.getUserStatus(user);
+ return userStatus == UserStatus.Member || userStatus == UserStatus.Owner;
+ };
+
+ addUserUpdate(user: IUser, status: UserStatus): {} {
+ const update = {};
+ const path = this.relativeFirebasePath(this.Paths.roomUsersRef(this.rid()).child(user.uid()).child(UserKeys.Status));
+ update[path] = status;
+ return update;
+ }
+
+ removeUserUpdate(user: IUser): {} {
+ const update = {};
+ let data = null;
+ if (this.getType() == RoomType.OneToOne) {
+ data = {};
+ data[RoomKeys.Deleted] = firebase.database.ServerValue.TIMESTAMP;
+ data[RoomKeys.Name] = user.getName();
+ }
+ update[this.relativeFirebasePath(this.usersRef().child(user.uid()))] = data;
+ return update;
+ }
+
+ usersRef(): firebase.database.Reference {
+ return this.Paths.roomUsersRef(this.rid());
+ }
+
+
+}
+
+class RoomFactory implements IRoomFactory {
+
+ static $inject = [
+ '$rootScope',
+ 'Time',
+ 'UserStore',
+ 'EntityFactory',
+ 'Paths',
+ ];
+
+ constructor (
+ private $rootScope,
+ private Time,
+ private UserStore,
+ private EntityFactory,
+ private Paths) {}
+
+ // **********************
+ // *** Static methods ***
+ // **********************
+
+
+
+ // Group chats should be handled separately to
+ // private chats
+ updateRoomType(rid, type) {
+
+ const ref = this.Paths.roomMetaRef(rid);
+ const data = {};
+ data[Keys.TypeKey] = type;
+
+ return ref.update(data);
+ }
+
+ removeUserFromRoom (user: IUser, room: IRoom): Promise {
+ const updates = {...room.removeUserUpdate(user), ...user.removeRoomUpdate(room)};
+ return this.Paths.firebase().update(updates);
+ }
+
+ addUserToRoom(user: IUser, room: IRoom, status: UserStatus): Promise {
+
+ const updates = {...user.addRoomUpdate(room), ...room.addUserUpdate(user, status)};
+
+ return this.Paths.firebase().update(updates).then(() => {
+ if (room.getType() == RoomType.Public) {
+ user.removeOnDisconnect(PathKeys.RoomsPath + '/' + room.rid());
+ room.removeOnDisconnect(PathKeys.UsersPath + '/' + user.uid())
+ }
+ return Promise.all([
+ room.updateState(PathKeys.UsersPath),
+ user.updateState(PathKeys.RoomsPath)
+ ]);
+ }).catch((error) => {
+ console.log(error);
+ });
+ };
+
+ roomMeta(rid, name, description, userCreated, invitesEnabled, type, weight) {
+
+ const m = {};
+ // TODO: Is this used?
+ // m[RoomKeys.RID] = rid ? rid : null;
+ m[RoomKeys.Name] = name ? name : null;
+ m[RoomKeys.InvitesEnabled] = !Utils.unORNull(invitesEnabled) ? invitesEnabled : true;
+ m[RoomKeys.Description] = description ? description : null;
+ m[RoomKeys.UserCreated] = !Utils.unORNull(userCreated) ? userCreated : true;
+ m[RoomKeys.Created] = firebase.database.ServerValue.TIMESTAMP;
+ m[RoomKeys.Weight] = weight ? weight : 0;
+
+ m[RoomKeys.Type] = type;
+ m[RoomKeys.Type_v4] = type; // Deprecated
+
+ return m;
+ };
+
+ // userIsActiveWithInfo(info) {
+ // // TODO: For the time being assume that users that
+ // // don't have this information are active
+ // if(info && info.status && info.time) {
+ // if(info.status != UserStatus.Closed) {
+ // return this.Time.secondsSince(info.time) < 60 * 60 * 24;
+ // }
+ // }
+ // return true;
+ // };
+
+}
+
+export interface IRoomCreator {
+ createRoomWithRID(rid: string, name: string, description: string, invitesEnabled: boolean, type: RoomType, userCreated: boolean, weight: number): Promise
+ createPublicRoom(name: string, description: string, weight?): Promise
+ createRoom(name: string, description: string, invitesEnabled: boolean, type: RoomType, weight?): Promise
+}
+
+class RoomCreator implements IRoomCreator {
+
+ static $inject = ['Room', 'UserStore', 'Paths', 'RoomFactory', 'EntityFactory'];
+
+ constructor(private Room, private UserStore, private Paths, private RoomFactory, private EntityFactory) {
+
+ }
+
+ createRoom(name: string, description: string, invitesEnabled: boolean, type: RoomType, weight = 0): Promise {
+ return this.createRoomWithRID(null, name, description, invitesEnabled, type, true, weight);
+ }
+
+ createRoomWithRID(rid: string, name: string, description: string, invitesEnabled: boolean, type: RoomType, userCreated: boolean, weight: number): Promise {
+
+ if(Utils.unORNull(rid)) {
+ rid = this.Paths.roomsRef().push().key;
+ }
+ const roomMeta = this.RoomFactory.roomMeta(rid, name, description, true, invitesEnabled, type, weight);
+
+ const room = this.Room(rid, roomMeta);
+
+ roomMeta[RoomKeys.Creator] = this.UserStore.currentUser().uid();
+ roomMeta[RoomKeys.CreatorEntityID] = roomMeta[RoomKeys.Creator];
+
+ const roomMetaRef = this.Paths.roomMetaRef(rid);
+
+ // Add the room to Firebase
+ return roomMetaRef.set(roomMeta).then(() => {
+ return this.RoomFactory.addUserToRoom(this.UserStore.currentUser(), room, UserStatus.Owner).then(() => {
+ if(type == RoomType.Public) {
+ const ref = this.Paths.publicRoomRef(rid);
+
+ const data = {};
+
+ data[RoomKeys.Created] = firebase.database.ServerValue.TIMESTAMP;
+ data[RoomKeys.RID] = rid;
+ data[RoomKeys.UserCreated] = true;
+
+ return ref.set(data);
+ }
+ }).then(() => {
+ const _ = this.EntityFactory.updateState(PathKeys.RoomsPath, rid, Keys.DetailsKey);
+ return room;
+ });
+ });
+ }
+
+ createPublicRoom(name: string, description: string, weight = 0): Promise {
+ return this.createRoom(name, description, true, RoomType.Public, weight);
+ }
+
+ createPrivateRoom(users: [IUser]): Promise {
+
+ // Since we're calling create room we will be added automatically
+ return this.createRoom(
+ null,
+ null,
+ true,
+ users.length == 1 ? RoomType.OneToOne : RoomType.Group
+ ).then((room: IRoom) => {
+
+ let promises = [];
+
+ for (let i = 0; i < users.length; i++) {
+ promises.push(
+ this.RoomFactory.addUserToRoom(users[i], room, UserStatus.Member)
+ );
+ }
+
+ return Promise.all(promises).then(() => {
+ return room;
+ });
+ });
+ }
+
+}
+
+angular.module('myApp.services')
+ .service('Room', [
+ '$rootScope',
+ '$timeout',
+ '$window',
+ 'Presence',
+ 'Paths',
+ 'Config',
+ 'Message',
+ 'MessageFactory',
+ 'Cache',
+ 'UserStore',
+ 'User',
+ 'RoomPositionManager',
+ 'SoundEffects',
+ 'Visibility',
+ 'Time',
+ 'CloudImage',
+ 'Marquee',
+ 'Environment',
+ 'RoomFactory',
+ 'NetworkManager'
+ ,function(
+ $rootScope,
+ $timeout,
+ $window,
+ Presence,
+ Paths,
+ Config,
+ Message,
+ MessageFactory,
+ Cache,
+ UserStore,
+ User,
+ RoomPositionManager,
+ SoundEffects,
+ Visibility,
+ Time,
+ CloudImage,
+ Marquee,
+ Environment,
+ RoomFactory,
+ NetworkManager
+ ) {
+ // we can ask for more parameters if needed
+ return function roomFactory(rid: string, meta?: Map) { // return a factory instead of a new talker
+ return new Room(
+ $rootScope,
+ $timeout,
+ $window,
+ Presence,
+ Paths,
+ Config,
+ Message,
+ MessageFactory,
+ Cache,
+ UserStore,
+ User,
+ RoomPositionManager,
+ SoundEffects,
+ Visibility,
+ Time,
+ CloudImage,
+ Marquee,
+ Environment,
+ RoomFactory,
+ NetworkManager,
+ rid,
+ meta,
+ );
+ }}])
+ .service('RoomFactory', RoomFactory)
+ .service('RoomCreator', RoomCreator);
+
diff --git a/src/angularjs/entities/user.ts b/src/angularjs/entities/user.ts
new file mode 100755
index 00000000..4cc20ec0
--- /dev/null
+++ b/src/angularjs/entities/user.ts
@@ -0,0 +1,455 @@
+import * as angular from 'angular'
+import * as firebase from 'firebase';
+
+import * as PathKeys from "../keys/path-keys";
+import {N} from "../keys/notification-keys";
+import * as Keys from "../keys/keys";
+import {Entity, IEntity} from "./entity";
+import {userUID} from "../keys/keys";
+import {UserKeys} from "../keys/user-keys";
+import {UserAllowInvites} from "../keys/allow-invite-type";
+import {Utils} from "../services/utils";
+import {IRoom} from "./room";
+import {RoomKeys} from "../keys/room-keys";
+import {IRootScope} from "../controllers/app";
+
+export interface IUser extends IEntity {
+ online: boolean
+ meta: Map
+ unblock: () => void
+
+ uid(): string
+ isMe(): boolean
+
+ setName(name): void
+ getName(): string
+ name(value): string
+
+ getImageURL(): string
+ setImageURL(imageURL): void
+
+ setImage(image, isData?): void
+
+ setProfileHTML(profileHTML): void
+ hasImage(): boolean
+ addRoomUpdate(room: IRoom): {}
+ removeRoomUpdate(room: IRoom): {}
+ updateImageURL(imageURL): Promise
+ pushMeta(): Promise
+ on(): Promise
+ off(): void
+ unblockUser(block): void
+ canBeInvitedByUser(invitingUser: IUser): boolean
+ allowInvitesFrom(type): boolean
+ deserialize(su): void
+}
+
+class User extends Entity implements IUser {
+
+ public meta = new Map();
+ public online: boolean;
+ private image;
+ unblock: () => void = null;
+
+ constructor (
+ private $rootScope: IRootScope,
+ private $timeout,
+ Paths,
+ private CloudImage,
+ private Environment,
+ private NetworkManager,
+ uid: string) {
+ super(Paths, PathKeys.UsersPath, uid);
+
+ this.setImageURL(Environment.defaultProfilePictureURL());
+ this.setUID(uid);
+ this.setAllowInvites(UserAllowInvites.Everyone);
+
+ }
+
+ getName() {
+ return this.getMetaValue(UserKeys.Name);
+ };
+
+ setName(name): void {
+ return this.setMetaValue(UserKeys.Name, name);
+ };
+
+ name(value): string {
+ if (Utils.unORNull(value)) {
+ return this.getName();
+ } else {
+ this.setName(value);
+ }
+ };
+
+ getStatus() {
+ return this.getMetaValue(UserKeys.Status);
+ };
+
+ setStatus(status) {
+ return this.setMetaValue(UserKeys.Status, status);
+ };
+
+ // For Angular getterSetter binding
+ status(value) {
+ if (Utils.unORNull(value)) {
+ return this.getStatus();
+ } else {
+ this.setStatus(value);
+ }
+ };
+
+ getLocation() {
+ return this.getMetaValue(UserKeys.Location);
+ };
+
+ setLocation(location) {
+ return this.setMetaValue(UserKeys.Location, location);
+ };
+
+ location(value) {
+ if (Utils.unORNull(value)) {
+ return this.getLocation();
+ } else {
+ this.setLocation(value);
+ }
+ };
+
+ getCountryCode() {
+ return this.getMetaValue(UserKeys.CountryCode);
+ };
+
+ setCountryCode(countryCode) {
+ return this.setMetaValue(UserKeys.CountryCode, countryCode);
+ };
+
+ countryCode(value) {
+ if (Utils.unORNull(value)) {
+ return this.getCountryCode();
+ } else {
+ this.setCountryCode(value);
+ }
+ };
+
+ getGender() {
+ return this.getMetaValue(UserKeys.Gender);
+ };
+
+ setGender(gender) {
+ return this.setMetaValue(UserKeys.Gender, gender);
+ };
+
+ gender(value) {
+ if (Utils.unORNull(value)) {
+ return this.getGender();
+ } else {
+ this.setGender(value);
+ }
+ };
+
+ getProfileLink() {
+ return this.getMetaValue(UserKeys.ProfileLink);
+ };
+
+ setProfileLink(profileLink) {
+ return this.setMetaValue(UserKeys.ProfileLink, profileLink);
+ };
+
+ profileLink(value) {
+ if (Utils.unORNull(value)) {
+ return this.getProfileLink();
+ } else {
+ this.setProfileLink(value);
+ }
+ };
+
+ getHomepageLink() {
+ return this.getMetaValue(UserKeys.HomepageLink);
+ };
+
+ setHomepageLink(homepageLink) {
+ return this.setMetaValue(UserKeys.HomepageLink, homepageLink);
+ };
+
+ homepageLink(value) {
+ if (Utils.unORNull(value)) {
+ return this.getHomepageLink();
+ } else {
+ this.setHomepageLink(value);
+ }
+ };
+
+ getHomepageText() {
+ return this.getMetaValue(UserKeys.HomepageText);
+ };
+
+ setHomepageText(homepageText) {
+ return this.setMetaValue(UserKeys.HomepageText, homepageText);
+ };
+
+ homepageText(value) {
+ if (Utils.unORNull(value)) {
+ return this.getHomepageText();
+ } else {
+ this.setHomepageText(value);
+ }
+ };
+
+ getProfileHTML() {
+ return this.getMetaValue(UserKeys.ProfileHTML);
+ };
+
+ setProfileHTML(profileHTML): void {
+ return this.setMetaValue(UserKeys.ProfileHTML, profileHTML);
+ };
+
+ profileHTML(value) {
+ if (Utils.unORNull(value)) {
+ return this.getProfileHTML();
+ } else {
+ this.setProfileHTML(value);
+ }
+ };
+
+ getAllowInvites() {
+ return this.getMetaValue(UserKeys.AllowInvites);
+ };
+
+ setAllowInvites(allowInvites) {
+ return this.setMetaValue(UserKeys.AllowInvites, allowInvites);
+ };
+
+ allowInvites(value = null) {
+ if (Utils.unORNull(value)) {
+ return this.getAllowInvites();
+ } else {
+ this.setAllowInvites(value);
+ }
+ };
+
+ getImageURL(): string {
+ return this.getMetaValue(UserKeys.ImageURL);
+ };
+
+ setImageURL(imageURL): void {
+ this.setMetaValue(UserKeys.ImageURL, imageURL);
+ };
+
+ getThumbnail() {
+ return this.CloudImage.cloudImage(this.getImageURL(), 100, 100);
+ }
+
+ imageURL(value = null) {
+ if (Utils.unORNull(value)) {
+ return this.getImageURL();
+ } else {
+ this.setImageURL(value);
+ }
+ };
+
+ on(): Promise {
+
+ if(this.pathIsOn[Keys.MetaKey]) {
+ return;
+ }
+
+ const ref = this.Paths.userOnlineRef(this.uid());
+ ref.on('value', (snapshot) => {
+ if(!Utils.unORNull(snapshot.val())) {
+ this.online = snapshot.val();
+ if(this.online) {
+ this.$rootScope.$broadcast(N.OnlineUserAdded);
+ }
+ else {
+ this.$rootScope.$broadcast(N.OnlineUserRemoved);
+ }
+ }
+ });
+
+ return this.pathOn(Keys.MetaKey, (val) => {
+ if(val) {
+ this.setMeta(val);
+
+ // Update the user's thumbnail
+ this.setImage(this.imageURL());
+
+ // Here we want to update the
+ // - Main box
+ // - Every chat room that includes the user
+ // - User settings popup
+ this.$rootScope.$broadcast(N.UserValueChanged, this);
+ }
+ });
+ };
+
+ // Stop listening to the Firebase location
+ off(): void {
+ this.pathOff(Keys.MetaKey);
+ this.Paths.userOnlineRef(this.uid()).off();
+ };
+
+ pushMeta(): Promise {
+ const ref = this.Paths.userMetaRef(this.uid());
+ return ref.update(this.getMetaObject()).then(() => {
+ return this.updateState(Keys.MetaKey);
+ }).catch((e) => {
+ console.log("PushMeta");
+ });
+ };
+
+ canBeInvitedByUser(invitingUser: IUser): boolean {
+
+ // This function should only ever be called on the root user
+ if(!this.isMe()) {
+ console.log("Can be invited should only be called on the root user");
+ return false;
+ }
+
+ if(invitingUser.isMe()) {
+ return true;
+ }
+
+ let allowInvites = this.allowInvites();
+ return Utils.unORNull(allowInvites) || allowInvites == UserAllowInvites.Everyone;
+ };
+
+ allowInvitesFrom(type): boolean {
+ return this.allowInvites() == type;
+ };
+
+ updateImageURL(imageURL): Promise {
+ // Compare to the old URL
+ let imageChanged = imageURL != this.imageURL();
+ if(imageChanged) {
+ this.setMetaValue(UserKeys.ImageURL, imageURL);
+ this.setImageURL(imageURL);
+ this.setImage(imageURL, false);
+ return this.pushMeta();
+ }
+ };
+
+ setImage(image, isData = false): void {
+ if(image === undefined) {
+ // TODO: Improve this
+ this.image = this.Environment.defaultProfilePictureURL();
+ }
+ else if(isData || image == this.Environment.defaultProfilePictureURL()) {
+ this.image = image;
+ }
+ else {
+ this.image = this.CloudImage.cloudImage(image, 100, 100);
+ }
+ };
+
+ isMe(): boolean {
+ return this.uid() === this.NetworkManager.auth.currentUserID();
+ };
+
+ getAvatar() {
+ if(Utils.unORNull(this.image)) {
+ return this.Environment.defaultProfilePictureURL();
+ }
+ return this.image;
+ };
+
+ hasImage(): boolean {
+ return this.image && this.image != this.Environment.defaultProfilePictureURL;
+ };
+
+ addRoomUpdate(room: IRoom): {} {
+ const update = {};
+ const path = this.relativeFirebasePath(this.roomsRef().child(room.rid()).child(RoomKeys.InvitedBy));
+ update[path] = this.NetworkManager.auth.currentUserID();
+ return update;
+ }
+
+ removeRoomUpdate(room: IRoom): {} {
+ const update = {};
+ update[this.relativeFirebasePath(this.roomsRef().child(room.rid()))] = null;
+ return update;
+ }
+
+ roomsRef(): firebase.database.Reference {
+ return this.Paths.userRoomsRef(this.uid());
+ }
+
+ addFriend(friend) {
+ if(friend && friend.meta && friend.uid()) {
+ return this.addFriendWithUID(friend.uid());
+ }
+ };
+
+ addFriendWithUID(uid) {
+ let ref = this.Paths.userFriendsRef(this.uid());
+ let data = {};
+ data[uid] = {uid: uid};
+
+ return ref.update(data, ).then(() => {
+ return this.updateState(PathKeys.FriendsPath);
+ });
+ };
+
+ uid() {
+ return this._id;
+ };
+
+ setUID(uid) {
+ return this.setMetaValue(userUID, uid);
+ };
+
+ removeFriend(friend) {
+ // This method is added to the object when the friend is
+ // added initially
+ friend.removeFriend();
+ friend.removeFriend = null;
+ this.updateState(PathKeys.FriendsPath);
+ };
+
+ blockUserWithUID(uid) {
+ const ref = this.Paths.userBlockedRef(this.uid());
+ const data = {};
+ data[uid] = {uid: uid};
+
+ ref.update(data).then(() => {
+ return this.updateState(PathKeys.BlockedPath);
+ });
+ };
+
+ markRoomReadTime(rid) {
+ const ref = this.Paths.userRoomsRef(this.uid()).child(rid);
+ const data = {};
+ data[Keys.ReadKey] = firebase.database.ServerValue.TIMESTAMP;
+ return ref.update(data);
+ };
+
+ blockUser(block) {
+ if(block && block.meta && block.uid()) {
+ this.blockUserWithUID(block.uid());
+ }
+ };
+
+ unblockUser(block): void {
+ block.unblock();
+ block.unblock = null;
+ const _ = this.updateState(PathKeys.BlockedPath);
+ };
+
+ serialize(): {} {
+ return super.serialize();
+ };
+
+ deserialize(su): void {
+ if(su) {
+ super.deserialize(su._super);
+ this.setImage(su.meta[UserKeys.ImageURL]);
+ }
+ };
+}
+
+angular.module('myApp.services')
+ .service('User', ['$rootScope', '$timeout', 'Paths', 'CloudImage', 'Environment', 'NetworkManager', function($rootScope, $timeout, Paths, CloudImage, Environment, NetworkManager) {
+ // we can ask for more parameters if needed
+ return function messageFactory(uid: string) { // return a factory instead of a new talker
+ return new User($rootScope, $timeout, Paths, CloudImage, Environment, NetworkManager, uid);
+ }}]);
diff --git a/src/angularjs/filters/emoji-filter.ts b/src/angularjs/filters/emoji-filter.ts
new file mode 100755
index 00000000..3e7ff3ea
--- /dev/null
+++ b/src/angularjs/filters/emoji-filter.ts
@@ -0,0 +1,75 @@
+import * as angular from 'angular'
+import {AllEmojis} from "../keys/all-emojis";
+
+angular.module('myApp.filters').filter("emoji", function () {
+ return function (input) {
+
+ if(!input) {
+ input = "";
+ }
+
+ let replaceAll = (target, search, replacement) => {
+ return target.split(search).join(replacement);
+ };
+
+ input = input + " ";
+
+ input = replaceAll(input, ':s ', ':confused:');
+ input = replaceAll(input, ':S ', ':confused:');
+ input = replaceAll(input, ':-s ', ':confused:');
+ input = replaceAll(input, ':-S ', ':confused:');
+
+ input = replaceAll(input, ':o ', ':open_mouth:');
+ input = replaceAll(input, ':O ', ':open_mouth:');
+ input = replaceAll(input, ':-o ', ':open_mouth:');
+ input = replaceAll(input, ':-O ', ':open_mouth:');
+
+ input = replaceAll(input, ':) ', ':smile:');
+ input = replaceAll(input, ':-) ', ':smile:');
+
+ input = replaceAll(input, '<3 ', ':heart:');
+
+
+ input = replaceAll(input, ';) ', ':wink:');
+ input = replaceAll(input, ';-) ', ':wink:');
+
+ input = replaceAll(input, ":'( ", ':cry:');
+
+ input = replaceAll(input, ':-( ', ':frowning:');
+
+ input = replaceAll(input, ':p ', ':stuck_out_tongue:');
+ input = replaceAll(input, ':P ', ':stuck_out_tongue:');
+ input = replaceAll(input, ':-p ', ':stuck_out_tongue:');
+ input = replaceAll(input, ':-P ', ':stuck_out_tongue:');
+
+ input = replaceAll(input, ';P ', ':stuck_out_tongue_winking_eye:');
+ input = replaceAll(input, ';P ', ':stuck_out_tongue_winking_eye:');
+ input = replaceAll(input, ';-p ', ':stuck_out_tongue_winking_eye:');
+ input = replaceAll(input, ';-P ', ':stuck_out_tongue_winking_eye:');
+
+ input = replaceAll(input, '(h) ', ':sunglasses:');
+ input = replaceAll(input, '(H) ', ':sunglasses:');
+
+ input = replaceAll(input, '(a) ', ':angel:');
+ input = replaceAll(input, '(A) ', ':angel:');
+
+ input = replaceAll(input, ':# ', ':no_mouth:');
+ input = replaceAll(input, ':-# ', ':no_mouth:');
+
+ input = replaceAll(input, ':d ', ':grin:');
+ input = replaceAll(input, ':D ', ':grin:');
+ input = replaceAll(input, ':-d ', ':grin:');
+ input = replaceAll(input, ':-D ', ':grin:');
+
+ input = replaceAll(input, ':* ', ':kissing:');
+ input = replaceAll(input, ':-* ', ':kissing:');
+
+ input = replaceAll(input, '(kiss) ', ':kiss:');
+
+ input = input.trim();
+
+ return input.replace(AllEmojis, (match, text) => {
+ return "" + text + "";
+ });
+ };
+});
diff --git a/src/angularjs/filters/interpolate.ts b/src/angularjs/filters/interpolate.ts
new file mode 100755
index 00000000..35239828
--- /dev/null
+++ b/src/angularjs/filters/interpolate.ts
@@ -0,0 +1,6 @@
+import * as angular from 'angular'
+angular.module('myApp.filters').filter('interpolate', ['version', function(version) {
+ return function(text) {
+ return String(text).replace(/\%VERSION\%/mg, version);
+ };
+}]);
\ No newline at end of file
diff --git a/src/angularjs/filters/new-line.ts b/src/angularjs/filters/new-line.ts
new file mode 100755
index 00000000..5eaddb53
--- /dev/null
+++ b/src/angularjs/filters/new-line.ts
@@ -0,0 +1,11 @@
+import * as angular from 'angular'
+angular.module('myApp.filters').filter('newline', function () {
+ return function(text) {
+ text = String(text);
+ text = text.split("\r").join("
");
+ text = text.split("\r\n").join("
");
+ text = text.split("\n").join("
");
+ text = text.split("
").join("
");
+ return text;
+ };
+});
\ No newline at end of file
diff --git a/src/angularjs/keys/all-emojis.ts b/src/angularjs/keys/all-emojis.ts
new file mode 100755
index 00000000..97edf166
--- /dev/null
+++ b/src/angularjs/keys/all-emojis.ts
@@ -0,0 +1,165 @@
+export const AllEmojiNames = [
+ "bowtie", "smile", "laughing", "blush", "smiley", "relaxed",
+ "smirk", "heart_eyes", "kissing_heart", "kissing_closed_eyes", "flushed",
+ "relieved", "satisfied", "grin", "wink", "stuck_out_tongue_winking_eye",
+ "stuck_out_tongue_closed_eyes", "grinning", "kissing",
+ "kissing_smiling_eyes", "stuck_out_tongue", "sleeping", "worried",
+ "frowning", "anguished", "open_mouth", "grimacing", "confused", "hushed",
+ "expressionless", "unamused", "sweat_smile", "sweat",
+ "disappointed_relieved", "weary", "pensive", "disappointed", "confounded",
+ "fearful", "cold_sweat", "persevere", "cry", "sob", "joy", "astonished",
+ "scream", "neckbeard", "tired_face", "angry", "rage", "triumph", "sleepy",
+ "yum", "mask", "sunglasses", "dizzy_face", "imp", "smiling_imp",
+ "neutral_face", "no_mouth", "innocent", "alien", "yellow_heart",
+ "blue_heart", "purple_heart", "heart", "green_heart", "broken_heart",
+ "heartbeat", "heartpulse", "two_hearts", "revolving_hearts", "cupid",
+ "sparkling_heart", "sparkles", "star", "star2", "dizzy", "boom",
+ "collision", "anger", "exclamation", "question", "grey_exclamation",
+ "grey_question", "zzz", "dash", "sweat_drops", "notes", "musical_note",
+ "fire", "hankey", "poop", "shit", "\\+1", "thumbsup", "-1", "thumbsdown",
+ "ok_hand", "punch", "facepunch", "fist", "v", "wave", "hand", "raised_hand",
+ "open_hands", "point_up", "point_down", "point_left", "point_right",
+ "raised_hands", "pray", "point_up_2", "clap", "muscle", "metal", "fu",
+ "walking", "runner", "running", "couple", "family", "two_men_holding_hands",
+ "two_women_holding_hands", "dancer", "dancers", "ok_woman", "no_good",
+ "information_desk_person", "raising_hand", "bride_with_veil",
+ "person_with_pouting_face", "person_frowning", "bow", "couplekiss",
+ "couple_with_heart", "massage", "haircut", "nail_care", "boy", "girl",
+ "woman", "man", "baby", "older_woman", "older_man",
+ "person_with_blond_hair", "man_with_gua_pi_mao", "man_with_turban",
+ "construction_worker", "cop", "angel", "princess", "smiley_cat",
+ "smile_cat", "heart_eyes_cat", "kissing_cat", "smirk_cat", "scream_cat",
+ "crying_cat_face", "joy_cat", "pouting_cat", "japanese_ogre",
+ "japanese_goblin", "see_no_evil", "hear_no_evil", "speak_no_evil",
+ "guardsman", "skull", "feet", "lips", "kiss", "droplet", "ear", "eyes",
+ "nose", "tongue", "love_letter", "bust_in_silhouette",
+ "busts_in_silhouette", "speech_balloon", "thought_balloon", "feelsgood",
+ "finnadie", "goberserk", "godmode", "hurtrealbad", "rage1", "rage2",
+ "rage3", "rage4", "suspect", "trollface", "sunny", "umbrella", "cloud",
+ "snowflake", "snowman", "zap", "cyclone", "foggy", "ocean", "cat", "dog",
+ "mouse", "hamster", "rabbit", "wolf", "frog", "tiger", "koala", "bear",
+ "pig", "pig_nose", "cow", "boar", "monkey_face", "monkey", "horse",
+ "racehorse", "camel", "sheep", "elephant", "panda_face", "snake", "bird",
+ "baby_chick", "hatched_chick", "hatching_chick", "chicken", "penguin",
+ "turtle", "bug", "honeybee", "ant", "beetle", "snail", "octopus",
+ "tropical_fish", "fish", "whale", "whale2", "dolphin", "cow2", "ram", "rat",
+ "water_buffalo", "tiger2", "rabbit2", "dragon", "goat", "rooster", "dog2",
+ "pig2", "mouse2", "ox", "dragon_face", "blowfish", "crocodile",
+ "dromedary_camel", "leopard", "cat2", "poodle", "paw_prints", "bouquet",
+ "cherry_blossom", "tulip", "four_leaf_clover", "rose", "sunflower",
+ "hibiscus", "maple_leaf", "leaves", "fallen_leaf", "herb", "mushroom",
+ "cactus", "palm_tree", "evergreen_tree", "deciduous_tree", "chestnut",
+ "seedling", "blossom", "ear_of_rice", "shell", "globe_with_meridians",
+ "sun_with_face", "full_moon_with_face", "new_moon_with_face", "new_moon",
+ "waxing_crescent_moon", "first_quarter_moon", "waxing_gibbous_moon",
+ "full_moon", "waning_gibbous_moon", "last_quarter_moon",
+ "waning_crescent_moon", "last_quarter_moon_with_face",
+ "first_quarter_moon_with_face", "moon", "earth_africa", "earth_americas",
+ "earth_asia", "volcano", "milky_way", "partly_sunny", "octocat", "squirrel",
+ "bamboo", "gift_heart", "dolls", "school_satchel", "mortar_board", "flags",
+ "fireworks", "sparkler", "wind_chime", "rice_scene", "jack_o_lantern",
+ "ghost", "santa", "christmas_tree", "gift", "bell", "no_bell",
+ "tanabata_tree", "tada", "confetti_ball", "balloon", "crystal_ball", "cd",
+ "dvd", "floppy_disk", "camera", "video_camera", "movie_camera", "computer",
+ "tv", "iphone", "phone", "telephone", "telephone_receiver", "pager", "fax",
+ "minidisc", "vhs", "sound", "speaker", "mute", "loudspeaker", "mega",
+ "hourglass", "hourglass_flowing_sand", "alarm_clock", "watch", "radio",
+ "satellite", "loop", "mag", "mag_right", "unlock", "lock",
+ "lock_with_ink_pen", "closed_lock_with_key", "key", "bulb", "flashlight",
+ "high_brightness", "low_brightness", "electric_plug", "battery", "calling",
+ "email", "mailbox", "postbox", "bath", "bathtub", "shower", "toilet",
+ "wrench", "nut_and_bolt", "hammer", "seat", "moneybag", "yen", "dollar",
+ "pound", "euro", "credit_card", "money_with_wings", "e-mail", "inbox_tray",
+ "outbox_tray", "envelope", "incoming_envelope", "postal_horn",
+ "mailbox_closed", "mailbox_with_mail", "mailbox_with_no_mail", "door",
+ "smoking", "bomb", "gun", "hocho", "pill", "syringe", "page_facing_up",
+ "page_with_curl", "bookmark_tabs", "bar_chart", "chart_with_upwards_trend",
+ "chart_with_downwards_trend", "scroll", "clipboard", "calendar", "date",
+ "card_index", "file_folder", "open_file_folder", "scissors", "pushpin",
+ "paperclip", "black_nib", "pencil2", "straight_ruler", "triangular_ruler",
+ "closed_book", "green_book", "blue_book", "orange_book", "notebook",
+ "notebook_with_decorative_cover", "ledger", "books", "bookmark",
+ "name_badge", "microscope", "telescope", "newspaper", "football",
+ "basketball", "soccer", "baseball", "tennis", "8ball", "rugby_football",
+ "bowling", "golf", "mountain_bicyclist", "bicyclist", "horse_racing",
+ "snowboarder", "swimmer", "surfer", "ski", "spades", "hearts", "clubs",
+ "diamonds", "gem", "ring", "trophy", "musical_score", "musical_keyboard",
+ "violin", "space_invader", "video_game", "black_joker",
+ "flower_playing_cards", "game_die", "dart", "mahjong", "clapper", "memo",
+ "pencil", "book", "art", "microphone", "headphones", "trumpet", "saxophone",
+ "guitar", "shoe", "sandal", "high_heel", "lipstick", "boot", "shirt",
+ "tshirt", "necktie", "womans_clothes", "dress", "running_shirt_with_sash",
+ "jeans", "kimono", "bikini", "ribbon", "tophat", "crown", "womans_hat",
+ "mans_shoe", "closed_umbrella", "briefcase", "handbag", "pouch", "purse",
+ "eyeglasses", "fishing_pole_and_fish", "coffee", "tea", "sake",
+ "baby_bottle", "beer", "beers", "cocktail", "tropical_drink", "wine_glass",
+ "fork_and_knife", "pizza", "hamburger", "fries", "poultry_leg",
+ "meat_on_bone", "spaghetti", "curry", "fried_shrimp", "bento", "sushi",
+ "fish_cake", "rice_ball", "rice_cracker", "rice", "ramen", "stew", "oden",
+ "dango", "egg", "bread", "doughnut", "custard", "icecream", "ice_cream",
+ "shaved_ice", "birthday", "cake", "cookie", "chocolate_bar", "candy",
+ "lollipop", "honey_pot", "apple", "green_apple", "tangerine", "lemon",
+ "cherries", "grapes", "watermelon", "strawberry", "peach", "melon",
+ "banana", "pear", "pineapple", "sweet_potato", "eggplant", "tomato", "corn",
+ "house", "house_with_garden", "school", "office", "post_office", "hospital",
+ "bank", "convenience_store", "love_hotel", "hotel", "wedding", "church",
+ "department_store", "european_post_office", "city_sunrise", "city_sunset",
+ "japanese_castle", "european_castle", "tent", "factory", "tokyo_tower",
+ "japan", "mount_fuji", "sunrise_over_mountains", "sunrise", "stars",
+ "statue_of_liberty", "bridge_at_night", "carousel_horse", "rainbow",
+ "ferris_wheel", "fountain", "roller_coaster", "ship", "speedboat", "boat",
+ "sailboat", "rowboat", "anchor", "rocket", "airplane", "helicopter",
+ "steam_locomotive", "tram", "mountain_railway", "bike", "aerial_tramway",
+ "suspension_railway", "mountain_cableway", "tractor", "blue_car",
+ "oncoming_automobile", "car", "red_car", "taxi", "oncoming_taxi",
+ "articulated_lorry", "bus", "oncoming_bus", "rotating_light", "police_car",
+ "oncoming_police_car", "fire_engine", "ambulance", "minibus", "truck",
+ "train", "station", "train2", "bullettrain_front", "bullettrain_side",
+ "light_rail", "monorail", "railway_car", "trolleybus", "ticket", "fuelpump",
+ "vertical_traffic_light", "traffic_light", "warning", "construction",
+ "beginner", "atm", "slot_machine", "busstop", "barber", "hotsprings",
+ "checkered_flag", "crossed_flags", "izakaya_lantern", "moyai",
+ "circus_tent", "performing_arts", "round_pushpin",
+ "triangular_flag_on_post", "jp", "kr", "cn", "us", "fr", "es", "it", "ru",
+ "gb", "uk", "de", "one", "two", "three", "four", "five", "six", "seven",
+ "eight", "nine", "keycap_ten", "1234", "zero", "hash", "symbols",
+ "arrow_backward", "arrow_down", "arrow_forward", "arrow_left",
+ "capital_abcd", "abcd", "abc", "arrow_lower_left", "arrow_lower_right",
+ "arrow_right", "arrow_up", "arrow_upper_left", "arrow_upper_right",
+ "arrow_double_down", "arrow_double_up", "arrow_down_small",
+ "arrow_heading_down", "arrow_heading_up", "leftwards_arrow_with_hook",
+ "arrow_right_hook", "left_right_arrow", "arrow_up_down", "arrow_up_small",
+ "arrows_clockwise", "arrows_counterclockwise", "rewind", "fast_forward",
+ "information_source", "ok", "twisted_rightwards_arrows", "repeat",
+ "repeat_one", "new", "top", "up", "cool", "free", "ng", "cinema", "koko",
+ "signal_strength", "u5272", "u5408", "u55b6", "u6307", "u6708", "u6709",
+ "u6e80", "u7121", "u7533", "u7a7a", "u7981", "sa", "restroom", "mens",
+ "womens", "baby_symbol", "no_smoking", "parking", "wheelchair", "metro",
+ "baggage_claim", "accept", "wc", "potable_water", "put_litter_in_its_place",
+ "secret", "congratulations", "m", "passport_control", "left_luggage",
+ "customs", "ideograph_advantage", "cl", "sos", "id", "no_entry_sign",
+ "underage", "no_mobile_phones", "do_not_litter", "non-potable_water",
+ "no_bicycles", "no_pedestrians", "children_crossing", "no_entry",
+ "eight_spoked_asterisk", "eight_pointed_black_star", "heart_decoration",
+ "vs", "vibration_mode", "mobile_phone_off", "chart", "currency_exchange",
+ "aries", "taurus", "gemini", "cancer", "leo", "virgo", "libra", "scorpius",
+ "sagittarius", "capricorn", "aquarius", "pisces", "ophiuchus",
+ "six_pointed_star", "negative_squared_cross_mark", "a", "b", "ab", "o2",
+ "diamond_shape_with_a_dot_inside", "recycle", "end", "on", "soon", "clock1",
+ "clock130", "clock10", "clock1030", "clock11", "clock1130", "clock12",
+ "clock1230", "clock2", "clock230", "clock3", "clock330", "clock4",
+ "clock430", "clock5", "clock530", "clock6", "clock630", "clock7",
+ "clock730", "clock8", "clock830", "clock9", "clock930", "heavy_dollar_sign",
+ "copyright", "registered", "tm", "x", "heavy_exclamation_mark", "bangbang",
+ "interrobang", "o", "heavy_multiplication_x", "heavy_plus_sign",
+ "heavy_minus_sign", "heavy_division_sign", "white_flower", "100",
+ "heavy_check_mark", "ballot_box_with_check", "radio_button", "link",
+ "curly_loop", "wavy_dash", "part_alternation_mark", "trident",
+ "black_square", "white_square", "white_check_mark", "black_square_button",
+ "white_square_button", "black_circle", "white_circle", "red_circle",
+ "large_blue_circle", "large_blue_diamond", "large_orange_diamond",
+ "small_blue_diamond", "small_orange_diamond", "small_red_triangle",
+ "small_red_triangle_down", "shipit"
+];
+
+export const AllEmojis = new RegExp(":(" + AllEmojiNames.join("|") + "):", "g");
\ No newline at end of file
diff --git a/src/angularjs/keys/allow-invite-type.ts b/src/angularjs/keys/allow-invite-type.ts
new file mode 100755
index 00000000..e2c368b4
--- /dev/null
+++ b/src/angularjs/keys/allow-invite-type.ts
@@ -0,0 +1,5 @@
+export enum UserAllowInvites {
+ Everyone = 'Everyone',
+ Friends = 'Friends',
+ Nobody = 'Nobody',
+}
diff --git a/src/angularjs/keys/defines.ts b/src/angularjs/keys/defines.ts
new file mode 100755
index 00000000..7bfcd375
--- /dev/null
+++ b/src/angularjs/keys/defines.ts
@@ -0,0 +1,32 @@
+export const DEBUG = false;
+export const FIREBASE_REF_DEBUG = false;
+
+export const DefaultAvatarProvider = "http://flathash.com";
+
+export const Minute = 60;
+export const Hour = Minute * 60;
+export const Day = Hour * 24;
+
+// Last visited
+// Show the click to chat box if the user has visited more than x hours
+export const LastVisitedTimeout = Hour;
+
+// TODO:
+export const DefaultUserPrefix = "ChatSDK";
+
+export const ProviderTypeCustom = 'custom';
+
+export const ProfileSettingsBox = 'profileSettingsBox';
+export const LoginBox = 'loginBox';
+export const MainBox = 'mainBox';
+export const CreateRoomBox = 'createRoomBox';
+export const ErrorBox = 'errorBox';
+
+export const ShowProfileSettingsBox = 'showProfileSettingsBox';
+export const ShowCreateChatBox = 'showCreateChatBox';
+
+// Notifications
+
+export const NotificationTypeWaiting = 'waiting';
+export const NotificationTypeAlert = 'alert';
+
diff --git a/src/angularjs/keys/dimensions.ts b/src/angularjs/keys/dimensions.ts
new file mode 100755
index 00000000..8afadeeb
--- /dev/null
+++ b/src/angularjs/keys/dimensions.ts
@@ -0,0 +1,15 @@
+export enum Dimensions {
+ ChatRoomWidth = 230,
+ ChatRoomHeight = 300,
+
+ ChatRoomTopMargin = 60,
+ ChatRoomSpacing = 15,
+
+ MainBoxWidth = 250,
+ MainBoxHeight = 300,
+
+ RoomListBoxWidth = 200,
+ RoomListBoxHeight = 300,
+
+ ProfileBoxWidth = 300,
+}
\ No newline at end of file
diff --git a/src/angularjs/keys/keys.ts b/src/angularjs/keys/keys.ts
new file mode 100755
index 00000000..aaf71708
--- /dev/null
+++ b/src/angularjs/keys/keys.ts
@@ -0,0 +1,25 @@
+
+// Deprecated
+export let SenderEntityID = "sender-entity-id";
+export let From = "from";
+
+// Deprecated
+export let CreatorEntityID = "creator-entity-id";
+export let Creator = "creator";
+
+export let userUID = "uid";
+export let ReadKey = 'read';
+export let DateKey = "date";
+export let MessageKey = "message";
+export let ThreadKey = "thread";
+export let MetaKey = "meta";
+export let DetailsKey = "details";
+export let ImageKey = "image";
+export let TimeKey = "time";
+export let UserCountKey = "user-count";
+export let ConfigKey = "config";
+export let OnlineKey = "online";
+export let TypeKey = "type";
+export let UsernameKey = 'username';
+export let PasswordKey = 'password';
+export let RoomIDKey = 'roomID';
diff --git a/src/angularjs/keys/login-mode-keys.ts b/src/angularjs/keys/login-mode-keys.ts
new file mode 100755
index 00000000..07381c4d
--- /dev/null
+++ b/src/angularjs/keys/login-mode-keys.ts
@@ -0,0 +1,5 @@
+export enum LoginMode {
+ Simple = "simple",
+ Authenticating = "authenticating",
+ ClickToChat = "clickToChat",
+}
\ No newline at end of file
diff --git a/src/angularjs/keys/message-keys.ts b/src/angularjs/keys/message-keys.ts
new file mode 100755
index 00000000..89ff2158
--- /dev/null
+++ b/src/angularjs/keys/message-keys.ts
@@ -0,0 +1,24 @@
+export enum MessageKeys {
+ UID = "user-firebase-id",
+ Type = "type",
+ Date = "date",
+ Meta = "meta",
+ JSONv2 = "json_v2",
+ UserName = "userName",
+ UserFirebaseID = "user-firebase-id",
+ From = "from",
+ To = "to",
+ Read = "read",
+ Status = "status",
+
+ // JSON Keys
+ Text = "text",
+ FileURL = "file-url",
+ ImageURL = "image-url",
+ MimeType = "mime-type",
+ ThumbnailURL = "thumbnail-url",
+
+ ImageWidth = "image-width",
+ ImageHeight = "image-height",
+
+}
\ No newline at end of file
diff --git a/src/angularjs/keys/message-type.ts b/src/angularjs/keys/message-type.ts
new file mode 100755
index 00000000..37078617
--- /dev/null
+++ b/src/angularjs/keys/message-type.ts
@@ -0,0 +1,6 @@
+export enum MessageType {
+ Text = 0,
+ Location = 1,
+ Image = 2,
+ File = 3,
+}
diff --git a/src/angularjs/keys/notification-keys.ts b/src/angularjs/keys/notification-keys.ts
new file mode 100755
index 00000000..0180b0b2
--- /dev/null
+++ b/src/angularjs/keys/notification-keys.ts
@@ -0,0 +1,50 @@
+export enum N {
+ VisibilityChanged = 'VisibilityChanged',
+
+ PublicRoomAdded = 'PublicRoomAdded',
+ PublicRoomRemoved = 'PublicRoomRemoved',
+
+ RoomAdded = 'RoomAdded',
+ RoomRemoved = 'RoomRemoved',
+
+ RoomOpened = 'RoomOpened',
+ RoomClosed = 'RoomClosed',
+
+ AnimateRoom = 'AnimateRoom',
+
+ RoomUpdated = 'RoomUpdated',
+ RoomPositionUpdated = 'RoomPositionUpdated',
+ RoomSizeUpdated = 'RoomSizeUpdated',
+ UpdateRoomActiveStatus = 'UpdateRoomActiveStatus',
+
+ LazyLoadedMessages = 'LazyLoadedMessages',
+
+ ChatUpdated = 'ChatUpdated',
+
+ UserOnlineStateChanged = 'UserOnlineStateChanged',
+ UserValueChanged = 'UserValueChanged',
+
+ ScreenSizeChanged = 'ScreenSizeChanged',
+
+ LoginComplete = 'LoginComplete',
+ Logout = 'Logout',
+
+ StartSocialLogin = 'StartSocialLogin',
+
+ RoomFlashHeader = 'RoomFlashHeader',
+ RoomBadgeChanged = 'RoomBadgeChanged',
+
+ OnlineUserAdded = 'OnlineUserAdded',
+ OnlineUserRemoved = 'OnlineUserRemoved',
+
+ UserBlocked = 'UserBlocked',
+ UserUnblocked = 'UserUnblocked',
+
+ FriendAdded = 'FriendAdded',
+ FriendRemoved = 'FriendRemoved',
+
+ DeleteMessage = 'DeleteMessage',
+ EditMessage = 'EditMessage',
+
+ ConfigUpdated = "ConfigUpdated",
+}
\ No newline at end of file
diff --git a/src/angularjs/keys/path-keys.ts b/src/angularjs/keys/path-keys.ts
new file mode 100755
index 00000000..21aa8032
--- /dev/null
+++ b/src/angularjs/keys/path-keys.ts
@@ -0,0 +1,13 @@
+export let UsersPath = "users";
+export let UsersMetaPath = "users";
+export let RoomsPath = "threads";
+export let PublicRoomsPath = "public-threads";
+export let MessagesPath = 'messages';
+export let FlaggedPath = 'flagged';
+export let TypingPath = 'typing';
+export let FriendsPath = 'contacts';
+export let BlockedPath = 'blocked';
+export let UpdatedPath = 'updated';
+export let OnlineUserCountKey = 'onlineUserCount';
+export let LastMessagePath = "lastMessage";
+export let FlaggedMessagesPath = "flagged";
\ No newline at end of file
diff --git a/src/angularjs/keys/room-keys.ts b/src/angularjs/keys/room-keys.ts
new file mode 100755
index 00000000..b07da830
--- /dev/null
+++ b/src/angularjs/keys/room-keys.ts
@@ -0,0 +1,16 @@
+export enum RoomKeys {
+ Created = "creation-date",
+ RID = "rid",
+ UserCreated = "userCreated",
+ Name = "name",
+ InvitesEnabled = "invitesEnabled",
+ Description = "description",
+ Weight = "weight",
+ Type = "type",
+ Type_v4 = "type_v4",// Deprecated
+ Image = "image-url",
+ CreatorEntityID = "creator-entity-id",
+ Creator = "creator",
+ InvitedBy = "invitedBy",
+ Deleted = "deleted",
+}
diff --git a/src/angularjs/keys/room-name-keys.ts b/src/angularjs/keys/room-name-keys.ts
new file mode 100755
index 00000000..aaca9ca3
--- /dev/null
+++ b/src/angularjs/keys/room-name-keys.ts
@@ -0,0 +1,4 @@
+export let RoomDefaultNameEmpty = "Empty Chat";
+export let RoomDefaultName1To1 = "Private Chat";
+export let RoomDefaultNameGroup = "Group Chat";
+export let RoomDefaultNamePublic = "Public Chat";
diff --git a/src/angularjs/keys/room-type.ts b/src/angularjs/keys/room-type.ts
new file mode 100755
index 00000000..67c25c2c
--- /dev/null
+++ b/src/angularjs/keys/room-type.ts
@@ -0,0 +1,6 @@
+export enum RoomType {
+ Invalid = 0x0,
+ Group = 0x1,
+ OneToOne = 0x2,
+ Public = 0x4,
+}
diff --git a/src/angularjs/keys/tab-keys.ts b/src/angularjs/keys/tab-keys.ts
new file mode 100755
index 00000000..8f2fac07
--- /dev/null
+++ b/src/angularjs/keys/tab-keys.ts
@@ -0,0 +1,5 @@
+export let UsersTab = 'users';
+export let RoomsTab = 'rooms';
+export let FriendsTab = 'friends';
+export let InboxTab = 'inbox';
+export let MessagesTab = 'messages';
diff --git a/src/angularjs/keys/user-keys.ts b/src/angularjs/keys/user-keys.ts
new file mode 100755
index 00000000..cde87532
--- /dev/null
+++ b/src/angularjs/keys/user-keys.ts
@@ -0,0 +1,13 @@
+export enum UserKeys {
+ Name = "name",
+ CountryCode = "country-code",
+ Location = "location",
+ ImageURL = "pictureURL",
+ Gender = "gender",
+ Status = "status",
+ ProfileLink = "profile-link",
+ HomepageLink = "homepage-link",
+ HomepageText = "homepage_text",
+ ProfileHTML = "profile-html",
+ AllowInvites = "allow-invites",
+}
\ No newline at end of file
diff --git a/src/angularjs/keys/user-status.ts b/src/angularjs/keys/user-status.ts
new file mode 100755
index 00000000..b08f87b9
--- /dev/null
+++ b/src/angularjs/keys/user-status.ts
@@ -0,0 +1,5 @@
+export enum UserStatus {
+ Owner = 'owner',
+ Member = 'member',
+ Closed = 'closed',
+}
\ No newline at end of file
diff --git a/src/angularjs/network/abstract-authentication-handler.ts b/src/angularjs/network/abstract-authentication-handler.ts
new file mode 100755
index 00000000..42b14059
--- /dev/null
+++ b/src/angularjs/network/abstract-authentication-handler.ts
@@ -0,0 +1,29 @@
+import * as angular from 'angular'
+
+export interface IAuthenticationHandler {
+ currentUserID(): string
+ setCurrentUserID(uid)
+}
+
+export class AbstractAuthenticationHandler implements IAuthenticationHandler {
+
+ static $inject = [];
+
+ constructor () {
+ }
+
+ private currentUserEntityID: string;
+
+ currentUserID(): string {
+ return this.currentUserEntityID;
+ }
+
+ setCurrentUserID(uid) {
+ if (uid !== this.currentUserEntityID) {
+ this.currentUserEntityID = uid;
+ }
+ }
+
+}
+
+angular.module('myApp.services').service('AbstractAuthenticationHandler', AbstractAuthenticationHandler);
\ No newline at end of file
diff --git a/src/angularjs/network/auth.ts b/src/angularjs/network/auth.ts
new file mode 100755
index 00000000..55d94533
--- /dev/null
+++ b/src/angularjs/network/auth.ts
@@ -0,0 +1,298 @@
+import * as firebase from 'firebase';
+
+import * as angular from 'angular'
+import * as Defines from "../keys/defines";
+import * as LoginModeKeys from "../keys/login-mode-keys";
+import {UserKeys} from "../keys/user-keys";
+import {Utils} from "../services/utils";
+import {IPresence} from "./presence";
+import {IUserStore} from "../persistence/user-store";
+import {IEnvironment} from "../services/environment";
+import {IRootScope} from "../controllers/app";
+import {IStateManager} from "../services/state-manager";
+import {IConfig, SetBy} from "../services/config";
+import {IPaths} from "./paths";
+import {ITime} from "../services/time";
+import {IAutoLogin} from "./auto-login";
+import {INetworkManager} from "./network-manager";
+
+export interface IAuth {
+
+}
+
+class Auth {
+
+ static $inject = ['$rootScope', 'Config', 'Paths', 'Environment', 'UserStore', 'Presence', 'StateManager', 'Time', 'AutoLogin', 'NetworkManager'];
+ constructor (
+ private $rootScope: IRootScope,
+ private Config: IConfig,
+ private Paths: IPaths,
+ private Environment: IEnvironment,
+ private UserStore: IUserStore,
+ private Presence: IPresence,
+ private StateManager: IStateManager,
+ private Time: ITime,
+ private AutoLogin: IAutoLogin,
+ private NetworkManager: INetworkManager
+ ) {}
+
+ mode = LoginModeKeys.LoginMode.Simple;
+ getToken = null;
+ authenticating = false;
+
+ isAuthenticating() {
+ return this.authenticating;
+ }
+
+ authenticate(credential): Promise {
+ return new Promise((resolve, reject) => {
+ if (this.authenticating) {
+ return reject("Already authenticating");
+ }
+ this.authenticating = true;
+
+ // Try to authenticate using auto login
+ let autoLoginCredential = this.AutoLogin.getCredentials();
+ if (!Utils.unORNull(autoLoginCredential)) {
+ credential = autoLoginCredential;
+ // this.logout();
+ }
+
+ if(this.isAuthenticated()) {
+ return resolve({
+ user: firebase.auth().currentUser
+ });
+ }
+ else if (Utils.unORNull(credential)) {
+ return Promise.reject();
+ }
+ else if(credential.getType() === credential.Email) {
+ return firebase.auth().signInWithEmailAndPassword(credential.getEmail(), credential.getPassword());
+ }
+ else if(credential.getType() === credential.Anonymous) {
+ return firebase.auth().signInAnonymously();
+ }
+ else if(credential.getType() === credential.CustomToken) {
+ return firebase.auth().signInWithCustomToken(credential.getToken());
+ }
+ else {
+
+ let scopes = null;
+ let provider = null;
+
+ if(credential.getType() === credential.Facebook) {
+ provider = new firebase.auth.FacebookAuthProvider();
+ scopes = "email,user_likes";
+ }
+ if(credential.getType() === credential.Github) {
+ provider = new firebase.auth.GithubAuthProvider();
+ scopes = "user,gist";
+ }
+ if(credential.getType() === credential.Google) {
+ provider = new firebase.auth.GoogleAuthProvider();
+ scopes = "email";
+ }
+ if(credential.getType() === credential.Twitter) {
+ provider = new firebase.auth.TwitterAuthProvider();
+ }
+
+ scopes = scopes.split(',');
+ for(let scope in scopes) {
+ if(scopes.hasOwnProperty(scope)) {
+ provider.addScope(scope);
+ }
+ }
+
+ return firebase.auth().signInWithPopup (provider);
+ }
+ }).then((authData: any) => {
+ this.authenticating = false;
+ this.Config.setConfig(SetBy.Include, this.Environment.config());
+ return this.bindUser(authData.user);
+ });
+ }
+
+ isAuthenticated() {
+ return firebase.auth().currentUser != null;
+ }
+
+ signUp(email, password) {
+ return firebase.auth().createUserWithEmailAndPassword(email, password);
+ }
+
+ resetPasswordByEmail(email) {
+ return firebase.auth().sendPasswordResetEmail(email);
+ }
+
+ logout() {
+ firebase.auth().signOut();
+ }
+
+ /**
+ * Create a new AngularFire simple login object
+ * this object will try to authenticate the user if
+ * a session exists
+ * @param authUser - the authentication user provided by Firebase
+ */
+ bindUser(authUser) {
+ return this.bindUserWithUID(authUser.uid).then(() => {
+
+ let user = this.UserStore.currentUser();
+
+ let oldMeta = angular.copy(user.meta);
+
+ let setUserProperty = (property, value, force?) => {
+ if((!user.meta[property] || user.meta[property].length === 0 || force) && value && value.length > 0) {
+ user.meta[property] = value;
+ return true;
+ }
+ return false;
+ };
+
+ // Get the third party data
+ let userData = {
+ id: null,
+ name: null,
+ gender: null,
+ profile_image_url: null,
+ description: null,
+ location: null,
+ avatar_url: null,
+ picture: null
+ };
+
+ let p = authUser.provider;
+ if(p === "facebook" || p === "twitter" || p === "google" || p === "github") {
+ if(authUser[p] && authUser[p].cachedUserProfile) {
+ userData = authUser[p].cachedUserProfile;
+ }
+ }
+ else if (p === "custom" && authUser.thirdPartyData) {
+ userData = authUser.thirdPartyData;
+ }
+
+ // Set the user's name
+ setUserProperty(UserKeys.Name, userData.name);
+ setUserProperty(UserKeys.Name, Defines.DefaultUserPrefix + Math.floor(Math.random() * 1000 + 1));
+
+ let imageURL = null;
+
+ /** SOCIAL INFORMATION **/
+ if(authUser.provider === "facebook") {
+
+ setUserProperty(UserKeys.Gender, userData.gender === "male" ? "M": "F");
+
+ // Make an API request to Facebook to get an appropriately sized
+ // photo
+ if(!user.hasImage()) {
+ const _ = user.updateImageURL('http://graph.facebook.com/'+userData.id+'/picture?width=300');
+ }
+ }
+ if(authUser.provider === "twitter") {
+
+ // We need to transform the twiter url to replace 'normal' with 'bigger'
+ // to get the 75px image instad of the 50px
+ if(userData.profile_image_url) {
+ imageURL = userData.profile_image_url.replace("normal", "bigger");
+ }
+
+ setUserProperty(UserKeys.Status, userData.description);
+ setUserProperty(UserKeys.Location, userData.location);
+
+ }
+ if(authUser.provider === "github") {
+ imageURL = userData.avatar_url;
+ setUserProperty(UserKeys.Name, authUser.login);
+ }
+ if(authUser.provider === "google") {
+ imageURL = userData.picture;
+ setUserProperty(UserKeys.Gender, userData.gender === "male" ? "M": "F");
+ }
+ if(authUser.provider === "anonymous") {
+
+ }
+ if(authUser.provider === "custom") {
+
+ setUserProperty(UserKeys.Status, userData[UserKeys.Status]);
+ setUserProperty(UserKeys.Location, userData[UserKeys.Location]);
+ setUserProperty(UserKeys.Gender, userData[UserKeys.Gender]);
+ setUserProperty(UserKeys.CountryCode, userData[UserKeys.CountryCode]);
+
+ // TODO: Deprecated
+ setUserProperty(UserKeys.HomepageLink, userData[UserKeys.HomepageLink], true);
+ setUserProperty(UserKeys.HomepageText, userData[UserKeys.HomepageText], true);
+
+ if(userData[UserKeys.ProfileHTML] && userData[UserKeys.ProfileHTML].length > 0) {
+ setUserProperty(UserKeys.ProfileHTML, userData[UserKeys.ProfileHTML], true);
+ }
+ else {
+ user.setProfileHTML("");
+ }
+
+ if(userData[UserKeys.ImageURL]) {
+ imageURL = userData[UserKeys.ImageURL];
+ }
+ }
+
+ if(!user.getName() || user.getName().length == 0) {
+ user.setName(this.Config.defaultUserName + Math.floor(Math.random() * 1000))
+ }
+
+ if(!imageURL) {
+ imageURL = Defines.DefaultAvatarProvider + "/" + user.getName() + ".png";
+ }
+
+ // If they don't have a profile picture load it from the social network
+ if(setUserProperty(UserKeys.ImageURL, imageURL)) {
+ user.setImageURL(imageURL);
+ user.setImage(imageURL);
+ }
+
+ let promise = Promise.resolve();
+ if(!angular.equals(user.meta, oldMeta)) {
+ promise = user.pushMeta()
+ }
+ promise.then(() => {
+ this.Presence.start(this.UserStore.currentUser());
+ }).catch((e) => {
+ console.log(e.message)
+ });
+
+ // Start listening to online user list and public rooms list
+ this.StateManager.on();
+
+ // Start listening to user
+ try {
+ this.StateManager.userOn(authUser.uid);
+ } catch (e) {
+ console.log(e.message)
+ }
+
+ // If the user has specified a room id in the URL via a get parameter
+ // then try to join that room
+ this.AutoLogin.tryToJoinRoom();
+
+ return authUser
+ });
+ }
+
+ bindUserWithUID(uid): Promise {
+ // Create the user
+ // TODO: if we do this we'll also be listening for meta updates...
+ this.NetworkManager.auth.setCurrentUserID(uid);
+ this.$rootScope.user = this.UserStore.currentUser();
+
+ let userPromise = this.UserStore.currentUser().on();
+ let timePromise = this.Time.start(uid);
+
+ return Promise.all([
+ userPromise,
+ timePromise
+ ]).catch((e) => {
+ console.log(e.message);
+ });
+ }
+
+}
+
+angular.module('myApp.services').service('Auth', Auth);
diff --git a/src/angularjs/network/auto-login.ts b/src/angularjs/network/auto-login.ts
new file mode 100755
index 00000000..3752ee0a
--- /dev/null
+++ b/src/angularjs/network/auto-login.ts
@@ -0,0 +1,96 @@
+import * as angular from 'angular'
+import {PasswordKey, RoomIDKey, UsernameKey} from "../keys/keys";
+import {RoomType} from "../keys/room-type";
+import {Utils} from "../services/utils";
+import {ICredential} from "./credential";
+import {IEnvironment} from "../services/environment";
+import {IRoomCreator} from "../entities/room";
+
+export interface IAutoLogin {
+ getCredentials(): ICredential
+ tryToJoinRoom(): void
+}
+
+class AutoLogin implements IAutoLogin {
+
+ username = "";
+ password = "";
+ roomID = "";
+ updated = false;
+
+ static $inject = ["$window", "Credential", "RoomCreator", "RoomStore", "Environment"];
+
+ constructor (
+ private $window: ng.IWindowService,
+ private Credential,
+ private RoomCreator: IRoomCreator,
+ private RoomStore,
+ private Environment: IEnvironment) {
+ }
+
+ updateParameters() {
+ if (this.updated) {
+ return;
+ }
+
+ let pairs = this.$window.location.search.replace("?", "").split("&");
+
+ for(let i = 0; i < pairs.length; i++) {
+ let values = pairs[i].split("=");
+ if(values.length === 2) {
+ let key = values[0];
+ let value = values[1];
+
+ if (key === UsernameKey) {
+ this.username = value;
+ }
+ if (key === PasswordKey) {
+ this.password = value;
+ }
+ if (key === RoomIDKey) {
+ this.roomID = value;
+ }
+ }
+ }
+
+ // If the parameters aren't set, check the config options
+ if (this.username === "" && !Utils.unORNull(this.Environment.config().username)) {
+ this.username = this.Environment.config().username;
+ }
+ if (this.password === "" && !Utils.unORNull(this.Environment.config().password)) {
+ this.password = this.Environment.config().password;
+ }
+ if (this.roomID === "" && !Utils.unORNull(this.Environment.config().roomID)) {
+ this.roomID = this.Environment.config().roomID;
+ }
+
+ this.updated = true;
+ }
+
+ autoLoginEnabled() {
+ this.updateParameters();
+ return this.username !== "" && this.password !== "";
+ }
+
+ getCredentials(): ICredential {
+ if (this.autoLoginEnabled()) {
+ return new this.Credential().emailAndPassword(this.username, this.password);
+ } else {
+ return null;
+ }
+ }
+
+ // username=1@d.co&password=123456&roomID=123
+
+ tryToJoinRoom(): void {
+ this.updateParameters();
+ if (this.roomID !== "") {
+ let room = this.RoomStore.getRoomWithID(this.roomID);
+ if (Utils.unORNull(room)) {
+ this.RoomCreator.createRoomWithRID(this.roomID, this.roomID, "", true, RoomType.Group, true, 0);
+ }
+ }
+ }
+}
+
+angular.module('myApp.services').service('AutoLogin', AutoLogin);
\ No newline at end of file
diff --git a/src/angularjs/network/credential.ts b/src/angularjs/network/credential.ts
new file mode 100755
index 00000000..3ece6e93
--- /dev/null
+++ b/src/angularjs/network/credential.ts
@@ -0,0 +1,79 @@
+import * as angular from 'angular'
+
+export interface ICredential {
+
+}
+
+angular.module('myApp.services').factory('Credential', [
+ function () {
+
+ function Credential () {}
+
+ Credential.prototype = {
+
+ Email: "email",
+ Facebook: "facebook",
+ Twitter: "twitter",
+ Google: "google",
+ Github: "github",
+ Anonymous: "anonymous",
+ CustomToken: "custom",
+
+ emailAndPassword: function(email, password) {
+ this.email = email;
+ this.password = password;
+ this.type = this.Email;
+ return this;
+ },
+
+ facebook: function() {
+ this.type = this.Facebook;
+ return this;
+ },
+
+ twitter: function() {
+ this.type = this.Twitter;
+ return this;
+ },
+
+ google: function() {
+ this.type = this.Google;
+ return this;
+ },
+
+ github: function () {
+ this.type = this.Github;
+ return this;
+ },
+
+ anonymous: function () {
+ this.type = this.Anonymous;
+ return this;
+ },
+
+ customToken: function (token) {
+ this.token = token;
+ this.type = this.CustomToken;
+ return this;
+ },
+
+ getEmail: function () {
+ return this.email;
+ },
+
+ getPassword: function () {
+ return this.password;
+ },
+
+ getToken: function () {
+ return this.token;
+ },
+
+ getType: function () {
+ return this.type;
+ }
+
+ };
+
+ return Credential;
+ }]);
\ No newline at end of file
diff --git a/src/angularjs/network/firebase-upload-handler.ts b/src/angularjs/network/firebase-upload-handler.ts
new file mode 100755
index 00000000..4617d4d1
--- /dev/null
+++ b/src/angularjs/network/firebase-upload-handler.ts
@@ -0,0 +1,45 @@
+import * as angular from 'angular'
+import * as firebase from 'firebase';
+import {IUploadHandler} from "./upload-handler";
+
+export interface IFirebaseUploadHandler extends IUploadHandler{
+ uuid(): string
+}
+
+class FirebaseUploadHandler implements IFirebaseUploadHandler {
+
+ $q: ng.IQService;
+
+ $inject = ['$q'];
+
+ constructor($q: ng.IQService) {
+ this.$q = $q;
+ }
+
+ uploadFile(file: File): Promise {
+ // Create a root reference
+ const storageRef = firebase.storage().ref();
+
+ // Create a reference to 'mountains.jpg'
+ const ref = storageRef.child('web/' + this.uuid() + '.jpg');
+
+ return ref.put(file).then(() => {
+ return ref.getDownloadURL().then((downloadURL) => {
+ console.log('File available at', downloadURL);
+ });
+ });
+ }
+
+ uuid(): string {
+ function s4() {
+ return Math.floor((1 + Math.random()) * 0x10000)
+ .toString(16)
+ .substring(1);
+ }
+
+ return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
+ s4() + '-' + s4() + s4() + s4();
+ }
+}
+
+angular.module('myApp.services').service('FirebaseUploadHandler', FirebaseUploadHandler);
\ No newline at end of file
diff --git a/src/angularjs/network/network-manager.ts b/src/angularjs/network/network-manager.ts
new file mode 100755
index 00000000..d7270d37
--- /dev/null
+++ b/src/angularjs/network/network-manager.ts
@@ -0,0 +1,26 @@
+import * as angular from 'angular'
+import {IUploadHandler} from "./upload-handler";
+import {IFirebaseUploadHandler} from "./firebase-upload-handler";
+import {AbstractAuthenticationHandler, IAuthenticationHandler} from "./abstract-authentication-handler";
+import {IAuth} from "./auth";
+
+export interface INetworkManager {
+ upload: IUploadHandler
+ auth: IAuthenticationHandler
+}
+
+class NetworkManager implements INetworkManager {
+
+ static $inject = ['FirebaseUploadHandler', 'AbstractAuthenticationHandler'];
+
+ upload: IUploadHandler;
+ auth: IAuthenticationHandler;
+
+ constructor (FirebaseUploadHandler: IFirebaseUploadHandler, AbstractAuthenticationHandler: IAuthenticationHandler) {
+ this.upload = FirebaseUploadHandler;
+ this.auth = AbstractAuthenticationHandler;
+ }
+
+}
+
+angular.module('myApp.services').service('NetworkManager', NetworkManager);
\ No newline at end of file
diff --git a/src/angularjs/network/paths.ts b/src/angularjs/network/paths.ts
new file mode 100755
index 00000000..dcbdb93b
--- /dev/null
+++ b/src/angularjs/network/paths.ts
@@ -0,0 +1,231 @@
+/**
+ * Created by benjaminsmiley-andrews on 24/07/17.
+ */
+
+import * as angular from 'angular'
+import * as firebase from 'firebase';
+
+import {FIREBASE_REF_DEBUG} from "../keys/defines";
+import {
+ BlockedPath, FlaggedPath,
+ FriendsPath,
+ LastMessagePath, MessagesPath, OnlineUserCountKey,
+ PublicRoomsPath,
+ RoomsPath, TypingPath,
+ UpdatedPath, UsersMetaPath,
+ UsersPath
+} from "../keys/path-keys";
+import {ConfigKey, DetailsKey, ImageKey, MetaKey, OnlineKey, TimeKey} from "../keys/keys";
+import {IEnvironment} from "../services/environment";
+
+export interface IFirebaseReference extends firebase.database.Reference {
+ setWithPriority(
+ newVal: any,
+ newPriority: string | number | any | null,
+ onComplete?: (a: Error | null) => any
+ ): Promise;
+}
+
+export interface IPaths {
+ firebase (): firebase.database.Reference
+ usersRef(): firebase.database.Reference
+ configRef(): firebase.database.Reference
+ timeRef (uid) : firebase.database.Reference
+ userRef(fid): firebase.database.Reference
+ userMetaRef(fid): firebase.database.Reference
+ userImageRef(fid): firebase.database.Reference
+ userStateRef(fid): firebase.database.Reference
+ userOnlineRef(fid): firebase.database.Reference
+ userFriendsRef(fid): firebase.database.Reference
+ userBlockedRef(fid): firebase.database.Reference
+ onlineUsersRef(): firebase.database.Reference
+ onlineUserRef(fid): firebase.database.Reference
+ roomsRef(): firebase.database.Reference
+ publicRoomsRef(): firebase.database.Reference
+ publicRoomRef(rid): firebase.database.Reference
+ roomRef(fid): firebase.database.Reference
+ roomMetaRef(fid): firebase.database.Reference
+ roomLastMessageRef(fid): firebase.database.Reference
+ roomStateRef(fid): firebase.database.Reference
+
+ // If we cast this as a reference, we can't set the priority as a timestamp
+ roomMessagesRef(fid): firebase.database.Reference
+ roomUsersRef(fid): firebase.database.Reference
+ roomTypingRef(fid): firebase.database.Reference
+ userRoomsRef(fid): firebase.database.Reference
+ messageUsersRef(rid, mid): firebase.database.Reference
+ messageRef(rid, mid): firebase.database.Reference
+ onlineUserCountRef(): firebase.database.Reference
+ publicRoomsRef(): firebase.database.Reference
+ flaggedMessageRef(mid): firebase.database.Reference
+ roomUsersRef(fid): firebase.database.Reference
+}
+
+class Paths implements IPaths {
+
+ static $inject = ['Environment'];
+
+ cid: string = null;
+ database: firebase.database.Database;
+
+ constructor (private Environment: IEnvironment) {}
+
+ setCID (cid: string) {
+ if(FIREBASE_REF_DEBUG) console.log("setCID: " + cid);
+ this.cid = cid;
+ }
+
+ firebase(): firebase.database.Reference {
+ if(firebase.apps.length == 0) {
+ firebase.initializeApp(this.Environment.firebaseConfig());
+ this.database = firebase.database();
+ }
+ if(FIREBASE_REF_DEBUG) console.log("firebase");
+ if(this.cid) {
+ return this.database.ref(this.cid);
+ }
+ else {
+ return this.database.ref();
+ }
+ }
+
+ usersRef(): firebase.database.Reference {
+ if(FIREBASE_REF_DEBUG) console.log("usersRef");
+ return this.firebase().child(UsersPath);
+ }
+
+ configRef(): firebase.database.Reference {
+ return this.firebase().child(ConfigKey);
+ }
+
+ timeRef (uid) : firebase.database.Reference {
+ if(FIREBASE_REF_DEBUG) console.log("timeRef");
+ return this.firebase().child(TimeKey).child(uid);
+ }
+
+ userRef(fid): firebase.database.Reference {
+ if(FIREBASE_REF_DEBUG) console.log("userRef");
+ return this.usersRef().child(fid);
+ }
+
+ userMetaRef(fid): firebase.database.Reference {
+ if(FIREBASE_REF_DEBUG) console.log("userMetaRef");
+ return this.userRef(fid).child(MetaKey);
+ }
+
+ userImageRef(fid): firebase.database.Reference {
+ if(FIREBASE_REF_DEBUG) console.log("userImageRef");
+ return this.userRef(fid).child(ImageKey);
+ }
+
+ userStateRef(fid): firebase.database.Reference {
+ if(FIREBASE_REF_DEBUG) console.log("userStateRef");
+ return this.userRef(fid).child(UpdatedPath);
+ }
+
+ userOnlineRef(fid): firebase.database.Reference {
+ return this.userRef(fid).child(OnlineKey);
+ }
+
+
+// userThumbnailRef (fid) {
+// if(DEBUG) console.log("");
+// return this.userRef(fid).child(bThumbnailKey);
+// }
+
+ userFriendsRef(fid): firebase.database.Reference {
+ if(FIREBASE_REF_DEBUG) console.log("userFriendsRef");
+ return this.userRef(fid).child(FriendsPath);
+ }
+
+ userBlockedRef(fid): firebase.database.Reference {
+ if(FIREBASE_REF_DEBUG) console.log("userBlockedRef");
+ return this.userRef(fid).child(BlockedPath);
+ }
+
+ onlineUsersRef(): firebase.database.Reference {
+ if(FIREBASE_REF_DEBUG) console.log("onlineUsersRef");
+ return this.firebase().child(OnlineKey);
+ }
+
+ onlineUserRef(fid): firebase.database.Reference {
+ if(FIREBASE_REF_DEBUG) console.log("onlineUserRef");
+ return this.onlineUsersRef().child(fid);
+ }
+
+ roomsRef(): firebase.database.Reference {
+ if(FIREBASE_REF_DEBUG) console.log("roomsRef");
+ return this.firebase().child(RoomsPath);
+ }
+
+ publicRoomsRef(): firebase.database.Reference {
+ if(FIREBASE_REF_DEBUG) console.log("publicRoomsRef");
+ return this.firebase().child(PublicRoomsPath);
+ }
+
+ publicRoomRef(rid): firebase.database.Reference {
+ if(FIREBASE_REF_DEBUG) console.log("publicRoomRef");
+ return this.publicRoomsRef().child(rid);
+ }
+
+ roomRef(fid): firebase.database.Reference {
+ if(FIREBASE_REF_DEBUG) console.log("roomRef");
+ return this.roomsRef().child(fid);
+ }
+
+ roomMetaRef(fid): firebase.database.Reference {
+ if(FIREBASE_REF_DEBUG) console.log("roomMetaRef");
+ return this.roomRef(fid).child(DetailsKey);
+ }
+
+ roomLastMessageRef(fid): firebase.database.Reference {
+ if(FIREBASE_REF_DEBUG) console.log("roomLastMessageRef");
+ return this.roomRef(fid).child(LastMessagePath);
+ }
+
+ roomStateRef(fid): firebase.database.Reference {
+ if(FIREBASE_REF_DEBUG) console.log("roomStateRef");
+ return this.roomRef(fid).child(UpdatedPath);
+ }
+
+ roomMessagesRef(fid): firebase.database.Reference {
+ if(FIREBASE_REF_DEBUG) console.log("roomMessagesRef");
+ return this.roomRef(fid).child(MessagesPath);
+ }
+
+ roomUsersRef(fid): firebase.database.Reference {
+ if(FIREBASE_REF_DEBUG) console.log("roomUsersRef");
+ return this.roomRef(fid).child(UsersMetaPath);
+ }
+
+ roomTypingRef(fid): firebase.database.Reference {
+ if(FIREBASE_REF_DEBUG) console.log("roomTypingRef");
+ return this.roomRef(fid).child(TypingPath);
+ }
+
+ userRoomsRef(fid): firebase.database.Reference {
+ if(FIREBASE_REF_DEBUG) console.log("userRoomsRef");
+ return this.userRef(fid).child(RoomsPath);
+ }
+
+ messageUsersRef(rid, mid): firebase.database.Reference {
+ if(FIREBASE_REF_DEBUG) console.log("messageUsersRef");
+ return this.messageRef(rid, mid).child(UsersPath);
+ }
+
+ messageRef(rid, mid): firebase.database.Reference {
+ if(FIREBASE_REF_DEBUG) console.log("messageRef");
+ return this.roomMessagesRef(rid).child(mid);
+ }
+
+ onlineUserCountRef(): firebase.database.Reference {
+ if(FIREBASE_REF_DEBUG) console.log("onlineUserCountRef");
+ return this.firebase().child(OnlineUserCountKey);
+ }
+
+ flaggedMessageRef(mid): firebase.database.Reference {
+ return this.firebase().child(FlaggedPath).child(MessagesPath).child(mid);
+ }
+}
+
+angular.module('myApp.services').service('Paths', Paths);
\ No newline at end of file
diff --git a/src/angularjs/network/presence.ts b/src/angularjs/network/presence.ts
new file mode 100755
index 00000000..663318a1
--- /dev/null
+++ b/src/angularjs/network/presence.ts
@@ -0,0 +1,116 @@
+import * as angular from 'angular'
+import * as firebase from 'firebase';
+
+
+import {UserStatus} from "../keys/user-status";
+import {N} from "../keys/notification-keys";
+/**
+ * The presence service handles the user's online / offline
+ * status
+ * We need to call visibility to make sure it's initilized
+ */
+
+export interface IPresence {
+ start(user): void
+}
+
+angular.module('myApp.services').factory('Presence', ['$rootScope', '$timeout', 'Visibility', 'Config', 'Cache', 'Paths', 'LocalStorage', 'BeforeUnload', '$q',
+ function ($rootScope, $timeout, Visibility, Config, Cache, Paths, LocalStorage, BeforeUnload, $q) {
+ let Presence = {
+
+ user: null,
+ inactiveTimerPromise: null,
+
+ init: function () {
+ return this;
+ },
+
+ // Initialize the visibility service
+ start: function (user): void {
+
+ this.user = user;
+
+ // Take the user online
+ this.goOnline();
+
+ $rootScope.$on(N.VisibilityChanged, (event, hidden) => {
+
+ if(this.inactiveTimerPromise) {
+ $timeout.cancel(this.inactiveTimerPromise);
+ }
+
+ if(!hidden) {
+
+ // If the user's clicked the screen then cancel the
+ // inactivity timer
+ this.goOnline();
+ }
+ else {
+ // If the user switches tabs and doesn't enter for
+ // 2 minutes take them offline
+ this.inactiveTimerPromise = $timeout(() => {
+ this.goOffline();
+ }, 1000 * 60 * Config.inactivityTimeout);
+ }
+ });
+
+ },
+
+ stop: function () {
+ this.user = null;
+ },
+
+ goOffline: function () {
+ firebase.database().goOffline();
+ },
+
+ goOnline: function (): Promise {
+ firebase.database().goOnline();
+ return this.update();
+ },
+
+ update: function (): Promise {
+
+ const promises = [];
+
+ if(this.user) {
+ const uid = this.user.uid();
+ if (uid) {
+
+ if(Config.onlineUsersEnabled) {
+ const ref = Paths.onlineUserRef(uid);
+
+ promises.push(ref.setWithPriority({
+ time: firebase.database.ServerValue.TIMESTAMP
+ }, this.user.getName()).then(() => {
+ const _ = ref.onDisconnect().remove();
+ }));
+ }
+
+ // Also store this information on the user object
+ const userOnlineRef = Paths.userOnlineRef(uid);
+ promises.push(userOnlineRef.set(true).then(() => {
+ const _ = userOnlineRef.onDisconnect().set(false);
+ }));
+
+ // Go online for the public rooms
+ const rooms = Cache.rooms;
+ let room;
+ for(let i = 0; i < rooms.length; i++) {
+ // TRAFFIC
+ // If this is a public room we would have removed it when we logged off
+ // We need to set ourself as a member again
+ room = rooms[i];
+ if(room.isPublic()) {
+ promises.push(room.join(UserStatus.Member));
+ }
+ }
+ }
+ }
+
+ return Promise.all(promises);
+ }
+ };
+
+ return Presence.init();
+ }]);
\ No newline at end of file
diff --git a/src/angularjs/network/single-sign-on.ts b/src/angularjs/network/single-sign-on.ts
new file mode 100755
index 00000000..55840597
--- /dev/null
+++ b/src/angularjs/network/single-sign-on.ts
@@ -0,0 +1,183 @@
+import * as angular from 'angular'
+import {Utils} from "../services/utils";
+
+export interface ISingleSignOn {
+
+}
+
+angular.module('myApp.services').factory('SingleSignOn', ['$rootScope', '$q', '$http', 'Config', 'LocalStorage',
+ function ($rootScope, $q, $http, Config, LocalStorage) {
+
+ // API Levels
+
+ // 0: Client makes request to SSO server every time chat loads
+ // each time it requests a new token
+
+ // 1: Introduced user token caching - client first makes request
+ // to get user's ID. It only requests a new token if the ID has
+ // changed
+
+ return {
+
+ defaultError: "Unable to reach server",
+ busy: false,
+
+ getAPILevel: function () {
+ let level = Config.singleSignOnAPILevel;
+
+ if(Utils.unORNull(level)) {
+ level = 0;
+ }
+
+ return level;
+ },
+
+ invalidate: function () {
+ LocalStorage.removeProperty(LocalStorage.tokenKey);
+ LocalStorage.removeProperty(LocalStorage.tokenExpiryKey);
+ LocalStorage.removeProperty(LocalStorage.UIDKey);
+ },
+
+ authenticate: function () {
+
+ let url = Config.singleSignOnURL;
+
+ this.busy = true;
+ switch (this.getAPILevel()) {
+ case 0:
+ return this.authenticateLevel0(url);
+ break;
+ case 1:
+ return this.authenticateLevel1(url);
+ break;
+ }
+ },
+
+ authenticateLevel0: function (url) {
+
+ let deferred = $q.defer();
+
+ this.executeRequest({
+ method: 'get',
+ params: {
+ action: 'cc_auth'
+ },
+ url: url
+ }).then((data) => {
+
+ // Update the config object with options that are set
+ // These will be overridden by options which are set on the
+ // config tab of the user's Firebase install
+ Config.setConfig(Config.setBySingleSignOn, data);
+
+
+ this.busy = false;
+ deferred.resolve(data);
+
+ }), (error) => {
+ this.busy = false;
+ deferred.reject(error);
+ };
+
+ return deferred.promise;
+ },
+
+ authenticateLevel1: function (url, force) {
+
+ //this.invalidate();
+
+ let deferred = $q.defer();
+
+ // Get the current user's information
+ this.getUserUID(url).then((response) => {
+
+ let currentUID = response.uid;
+
+ // Check to see if we have a token cached
+ let token = LocalStorage.getProperty(LocalStorage.tokenKey);
+ let expiry = LocalStorage.getProperty(LocalStorage.tokenExpiryKey);
+ let uid = LocalStorage.getProperty(LocalStorage.UIDKey);
+
+ // If any value isn't set or if the token is expired get a new token
+ if(!Utils.unORNull(token) && !Utils.unORNull(expiry) && !Utils.unORNull(uid) && !force) {
+ // Date since token was refreshed...
+ let timeSince = new Date().getTime() - expiry;
+ // Longer than 20 days
+ if(timeSince < 60 * 60 * 24 * 20 && uid == currentUID) {
+
+ Config.setConfig(Config.setBySingleSignOn, response);
+
+ this.busy = false;
+ response['token'] = token;
+ deferred.resolve(response);
+ return deferred.promise;
+ }
+ }
+
+ this.executeRequest({
+ method: 'get',
+ params: {
+ action: 'cc_get_token'
+ },
+ url: url
+ }).then((data) => {
+
+ // Cache the token and the user's current ID
+ LocalStorage.setProperty(LocalStorage.tokenKey, data.token);
+ LocalStorage.setProperty(LocalStorage.UIDKey, currentUID);
+ LocalStorage.setProperty(LocalStorage.tokenExpiryKey, new Date().getTime());
+
+ // Update the config object with options that are set
+ // These will be overridden by options which are set on the
+ // config tab of the user's Firebase install
+ Config.setConfig(Config.setBySingleSignOn, data);
+
+ this.busy = false;
+ deferred.resolve(data);
+
+ }, (error) => {
+ this.busy = false;
+ deferred.reject(error);
+ });
+
+ }, deferred.reject);
+
+ return deferred.promise;
+ },
+
+ getUserUID: function (url) {
+
+ return this.executeRequest({
+ method: 'get',
+ params: {
+ action: 'cc_get_uid'
+ },
+ url: url
+ });
+ },
+
+ executeRequest: function (params) {
+
+ let deferred = $q.defer();
+
+ $http(params).then((r) => {
+ if(r && r.data && r.status == 200) {
+ if(r.data.error) {
+ deferred.reject(r.data.error);
+ }
+ else {
+ deferred.resolve(r.data);
+ }
+ }
+ else {
+ deferred.reject(this.defaultError);
+ }
+ }, (error) => {
+ deferred.reject(error.message ? error.message : this.defaultError);
+ });
+
+ return deferred.promise;
+ }
+ };
+
+ }]);
\ No newline at end of file
diff --git a/src/angularjs/network/upload-handler.ts b/src/angularjs/network/upload-handler.ts
new file mode 100755
index 00000000..13dc0ce8
--- /dev/null
+++ b/src/angularjs/network/upload-handler.ts
@@ -0,0 +1,4 @@
+
+export interface IUploadHandler {
+ uploadFile(file: File): Promise<{}>
+}
diff --git a/src/angularjs/persistence/cache.ts b/src/angularjs/persistence/cache.ts
new file mode 100755
index 00000000..1b70f227
--- /dev/null
+++ b/src/angularjs/persistence/cache.ts
@@ -0,0 +1,135 @@
+import * as angular from 'angular'
+import {N} from "../keys/notification-keys";
+import {IRoom} from "../entities/room";
+import {ArrayUtils} from "../services/array-utils";
+import {Utils} from "../services/utils";
+
+/**
+ * Temporary cache i.e. current rooms etc...
+ */
+
+export interface ICache {
+ rooms: IRoom []
+ activeRooms(): IRoom []
+ isBlockedUser(uid): boolean
+ addBlockedUser(user): void
+ removeBlockedUserWithID(uid: string): void
+}
+
+class Cache implements ICache {
+
+ // The user's active rooms
+ //
+ rooms = [];
+
+ // These are user specific stores
+ //onlineUsers: {},
+ //friends: {},
+ blockedUsers = {};
+
+ static $inject = ['$rootScope', '$timeout'];
+
+ constructor (private $rootScope, private $timeout) {
+ }
+
+ /**
+ * Rooms
+ */
+
+ addRoom(room) {
+ if(!ArrayUtils.contains(this.rooms, room)) {
+ room.isOpen = true;
+ this.rooms.push(room);
+ }
+ }
+
+// roomExists(room) {
+// return ArrayUtils.contains(this.rooms, room);
+// }
+
+ removeRoom(room) {
+ room.isOpen = false;
+ ArrayUtils.remove(this.rooms, room);
+ }
+
+ activeRooms() {
+ let ar = [];
+ for(let i =0; i < this.rooms.length; i++) {
+ if(this.rooms[i].active) {
+ ar.push(this.rooms[i]);
+ }
+ }
+ return ar;
+ }
+
+ inactiveRooms() {
+ let ar = [];
+ for(let i =0; i < this.rooms.length; i++) {
+ if(!this.rooms[i].active) {
+ ar.push(this.rooms[i]);
+ }
+ }
+ return ar;
+ }
+
+ /**
+ * Blocked users
+ */
+
+ addBlockedUser(user): void {
+ if(user && user.meta && user.uid()) {
+ this.blockedUsers[user.uid()] = user;
+ user.blocked = true;
+ this.$rootScope.$broadcast(N.UserBlocked);
+ }
+ }
+
+ isBlockedUser(uid): boolean {
+ return !Utils.unORNull(this.blockedUsers[uid]);
+ }
+
+ removeBlockedUserWithID(uid: string): void {
+ if(uid) {
+ const user = this.blockedUsers[uid];
+ if(user) {
+ user.blocked = false;
+ delete this.blockedUsers[uid];
+ this.$rootScope.$broadcast(N.UserUnblocked);
+ }
+ }
+ }
+
+
+ /**
+ * Utility functions
+ */
+
+ clear() {
+
+ this.blockedUsers = {};
+ this.rooms = [];
+
+ this.$timeout(() => {
+ this.$rootScope.$digest();
+ });
+ }
+
+
+ getPrivateRoomsWithUsers(user1, user2) {
+ const rooms = ArrayUtils.getRoomsWithUsers(this.getPrivateRooms(), [user1, user2]);
+ return ArrayUtils.roomsSortedByMostRecent(rooms);
+ }
+
+ getPrivateRooms() {
+ const rooms = [];
+ for(let i = 0; i < this.rooms.length; i++) {
+ const room = this.rooms[i];
+ if(!room.isPublic()) {
+ rooms.push(room);
+ }
+ }
+ return rooms;
+ }
+}
+
+angular.module('myApp.services').service('Cache', Cache);
\ No newline at end of file
diff --git a/src/angularjs/persistence/local-storage.ts b/src/angularjs/persistence/local-storage.ts
new file mode 100755
index 00000000..0e704625
--- /dev/null
+++ b/src/angularjs/persistence/local-storage.ts
@@ -0,0 +1,197 @@
+import * as angular from 'angular'
+import {Utils} from "../services/utils";
+import {IUser} from "../entities/user";
+import {IRoom} from "../entities/room";
+import {IWebStorage} from "./web-storage";
+
+export interface ILocalStorage {
+ rooms: {}
+ storeUsers(users: {}): void
+ updateUserFromStore(user: IUser): boolean
+ updateRoomFromStore(room: IRoom): void
+ sync(): void
+ storeRooms(rooms: {}): void
+}
+
+class LocalStorage implements ILocalStorage {
+
+ static $inject = ['WebStorage'];
+
+ constructor (
+ private WebStorage: IWebStorage)
+ {
+ const rooms = this.getProperty(this.roomsKey);
+
+ if(rooms && rooms.length) {
+ let room = null;
+ for(let i = 0; i < rooms.length; i++) {
+ room = rooms[i];
+ if (room.meta) {
+ this.rooms[room.meta.rid] = room;
+ }
+ }
+ }
+
+ const sus = this.getProperty(this.usersKey);
+ if(sus && sus.length) {
+ let su = null;
+
+ for(let i = 0; i < sus.length; i++) {
+ su = sus[i];
+ if (su.meta) {
+ this.users[su.meta.uid] = su;
+ }
+ }
+ }
+ }
+
+ mainMinimizedKey = 'cc_main_minimized';
+ moreMinimizedKey = 'cc_more_minimized';
+
+ // Tokens
+ tokenKey = 'cc_token';
+ UIDKey = 'cc_uid';
+ tokenExpiryKey = 'cc_token_expiry';
+
+ // API Details
+ apiDetailsKey = 'cc_api_details';
+
+ roomsKey = 'cc_rooms';
+ usersKey = 'cc_users';
+
+ onlineCountKey = 'cc_online_count';
+ timestampKey = 'cc_timestamp';
+
+ lastVisited = 'cc_last_visited';
+
+ rooms = {};
+ users = {};
+
+ cacheCleared = false;
+
+ isOffline() {
+ return this.getProperty('cc_offline');
+ }
+
+ setOffline(offline) {
+ this.setProperty('cc_offline', offline);
+ }
+
+ isMuted() {
+ return this.getProperty('cc_muted');
+ }
+
+ setMuted(muted) {
+ this.setProperty('cc_muted', muted);
+ }
+
+ storeRooms(rooms: {}): void {
+ let room;
+ let sr = [];
+ for(let key in rooms) {
+ if(rooms.hasOwnProperty(key)) {
+ room = rooms[key];
+ sr.push(room.serialize());
+ }
+ }
+ this.setProperty(this.roomsKey, sr);
+ }
+
+ storeUsers(users: {}): void {
+ let user;
+ let su = [];
+ for(let key in users) {
+ if(users.hasOwnProperty(key)) {
+ user = users[key];
+ su.push(user.serialize());
+ }
+ }
+ this.setProperty(this.usersKey, su);
+ }
+
+ sync(): void {
+ this.WebStorage.sync();
+ }
+
+ updateRoomFromStore(room: IRoom): void {
+ let sr = this.rooms[room.rid()];
+ if(sr) {
+ room.deserialize(sr);
+ }
+ }
+
+ updateUserFromStore(user: IUser): boolean {
+ let su = this.users[user.uid()];
+ if(su) {
+ user.deserialize(su);
+ return true;
+ }
+ return false;
+ }
+
+ getLastVisited() {
+ return this.getProperty(this.lastVisited);
+ }
+
+ setLastVisited() {
+ this.setProperty(this.lastVisited, new Date().getTime());
+ }
+
+ setProperty(key, value) {
+ if(!this.cacheCleared) {
+ this.WebStorage.setProperty(key, value);
+ }
+ }
+
+ getProperty(key) {
+ let c = this.WebStorage.getProperty(key);
+
+ if(!Utils.unORNull(c)) {
+ let e;
+ try {
+ e = eval(c);
+ }
+ catch (error) {
+ e = c;
+ }
+ return e;
+ }
+ else {
+ return null;
+ }
+ }
+
+ removeProperty(key) {
+ this.setProperty(key, null);
+ }
+
+ clearCache() {
+ this.removeProperty(this.roomsKey);
+ this.removeProperty(this.usersKey);
+ this.removeProperty(this.lastVisited);
+ this.clearToken();
+ this.cacheCleared = true;
+ }
+
+ clearCacheWithTimestamp(timestamp) {
+ if(!timestamp) return;
+
+ let currentTimestamp = this.getProperty(this.timestampKey);
+ if(!currentTimestamp || timestamp > currentTimestamp) {
+ this.removeProperty(this.roomsKey);
+ this.removeProperty(this.usersKey);
+ this.clearToken();
+ this.setProperty(this.timestampKey, timestamp);
+ this.cacheCleared = true;
+ }
+ }
+
+ clearToken() {
+ this.removeProperty(this.tokenKey);
+ this.removeProperty(this.UIDKey);
+ this.removeProperty(this.tokenExpiryKey);
+ this.removeProperty(this.apiDetailsKey);
+ }
+}
+
+angular.module('myApp.services').service('LocalStorage', LocalStorage);
\ No newline at end of file
diff --git a/src/angularjs/persistence/room-store.ts b/src/angularjs/persistence/room-store.ts
new file mode 100755
index 00000000..ee5a724d
--- /dev/null
+++ b/src/angularjs/persistence/room-store.ts
@@ -0,0 +1,147 @@
+import * as angular from 'angular'
+import {IRoom} from "../entities/room";
+import {ILocalStorage} from "./local-storage";
+import {IUserStore} from "./user-store";
+import {ArrayUtils} from "../services/array-utils";
+
+export interface IRoomStore {
+ getOrCreateRoomWithID(rid): IRoom
+ getRoomWithID(rid: string): IRoom
+ removeRoom(room: IRoom): void
+}
+
+class RoomStore implements IRoomStore {
+
+ rooms = {};
+ roomsLoadedFromMemory = false;
+
+ static $inject = ['LocalStorage', 'Room', 'BeforeUnload', 'UserStore'];
+
+ constructor(
+ private LocalStorage: ILocalStorage,
+ private Room,
+ BeforeUnload,
+ private UserStore: IUserStore) {
+ BeforeUnload.addListener(this);
+ }
+
+ /**
+ * Load the private rooms so they're available
+ * to the inbox list
+ */
+ loadPrivateRoomsToMemory(): void {
+
+ if (this.roomsLoadedFromMemory || !this.UserStore.currentUser()) {
+ return;
+ }
+ this.roomsLoadedFromMemory = true;
+
+ // Load private rooms
+ let rooms = this.LocalStorage.rooms;
+ for (let key in rooms) {
+ if (rooms.hasOwnProperty(key)) {
+ this.getOrCreateRoomWithID(key);
+ }
+ }
+ }
+
+ beforeUnload(): void {
+ this.sync();
+ }
+
+ sync() {
+ this.LocalStorage.storeRooms(this.rooms);
+ this.LocalStorage.sync();
+ }
+
+ getOrCreateRoomWithID(rid: string): IRoom {
+
+ let room = this.getRoomWithID(rid);
+
+ if (!room) {
+ room = this.buildRoomWithID(rid);
+ this.addRoom(room);
+ }
+
+ return room;
+ }
+
+ buildRoomWithID(rid: string): IRoom {
+
+ let room = this.Room(rid);
+ room.associatedUserID = this.UserStore.currentUser().uid();
+
+// room.height = ChatRoomHeight;
+// room.width = ChatRoomWidth;
+
+ // Update the room from the saved state
+ this.LocalStorage.updateRoomFromStore(room);
+
+ return room;
+ }
+
+ addRoom(room) {
+ if (room && room.rid()) {
+ this.rooms[room.rid()] = room;
+ }
+ }
+
+ removeRoom(room: IRoom): void {
+ if (room && room.rid()) {
+ delete this.rooms[room.rid()];
+ }
+ }
+
+ getRoomWithID(rid: string): IRoom {
+ return this.rooms[rid];
+ }
+
+ clear() {
+ this.rooms = {};
+ }
+
+ getPrivateRooms() {
+
+ if (!this.UserStore.currentUser()) {
+ return [];
+ }
+
+ this.loadPrivateRoomsToMemory();
+
+ let rooms = [];
+ for (let rid in this.rooms) {
+ if (this.rooms.hasOwnProperty(rid)) {
+ let room = this.rooms[rid];
+ // Make sure that we only return private rooms for the current user
+ if (!room.isPublic() && !room.deleted && room.associatedUserID && room.associatedUserID == this.UserStore.currentUser().uid() && room.usersMeta != {}) {
+ rooms.push(this.rooms[rid]);
+ }
+ }
+ }
+ return rooms;
+ }
+
+ getPrivateRoomsWithUsers(user1, user2) {
+ let rooms = [];
+ for (let key in this.rooms) {
+ if (this.rooms.hasOwnProperty(key)) {
+ if (!this.rooms[key].isPublic()) {
+ rooms.push(this.rooms[key]);
+ }
+ }
+ }
+ rooms = ArrayUtils.getRoomsWithUsers(rooms, [user1, user2]);
+ return ArrayUtils.roomsSortedByMostRecent(rooms);
+ }
+
+ inboxBadgeCount() {
+ let count = 0;
+ let rooms = this.getPrivateRooms();
+ for (let i = 0; i < rooms.length; i++) {
+ count += rooms[i].badge;
+ }
+ return count;
+ }
+}
+
+angular.module('myApp.services').service('RoomStore', RoomStore);
\ No newline at end of file
diff --git a/src/angularjs/persistence/user-store.ts b/src/angularjs/persistence/user-store.ts
new file mode 100755
index 00000000..b2d276a9
--- /dev/null
+++ b/src/angularjs/persistence/user-store.ts
@@ -0,0 +1,83 @@
+import * as angular from 'angular'
+import {IUser} from "../entities/user";
+import {IRootScope} from "../controllers/app";
+import {ILocalStorage} from "./local-storage";
+import {IBeforeUnload, IBeforeUnloadListener} from "../services/before-unload";
+import {INetworkManager} from "../network/network-manager";
+
+export interface IUserStore {
+ getUserWithID (uid): IUser
+ getOrCreateUserWithID(uid: string, cancelOn?: boolean): IUser
+ currentUser(): IUser
+}
+
+class UserStore implements IUserStore, IBeforeUnloadListener {
+
+ users = {};
+
+ static $inject = ['$rootScope', 'LocalStorage', 'User', 'BeforeUnload', 'NetworkManager'];
+
+ constructor (
+ private $rootScope: IRootScope,
+ private LocalStorage: ILocalStorage,
+ private User,
+ private BeforeUnload: IBeforeUnload,
+ private NetworkManager: INetworkManager)
+ {
+ this.BeforeUnload.addListener(this);
+ }
+
+ beforeUnload(): void {
+ this.sync();
+ }
+
+ sync(): void {
+ this.LocalStorage.storeUsers(this.users);
+ this.LocalStorage.sync();
+ }
+
+ getOrCreateUserWithID(uid: string, cancelOn?: boolean): IUser {
+ let user = this.getUserWithID(uid);
+ if(!user) {
+ user = this.buildUserWithID(uid);
+ this.addUser(user);
+ }
+ if(!cancelOn) {
+ const _ = user.on();
+ }
+
+ return user;
+ }
+
+ buildUserWithID(uid: string): IUser {
+ const user = this.User(uid);
+ this.LocalStorage.updateUserFromStore(user);
+ return user;
+ }
+
+ getUserWithID(uid: string): IUser {
+ if (uid !== null) {
+ return this.users[uid];
+ } else {
+ return null;
+ }
+ }
+
+ // A cache of all users
+ addUser(user: IUser): void {
+ if(user && user.meta && user.uid()) {
+ this.users[user.uid()] = user;
+ }
+ }
+
+ clear(): void {
+ this.users = {};
+ }
+
+ currentUser(): IUser {
+ return this.getOrCreateUserWithID(this.NetworkManager.auth.currentUserID(), true);
+ }
+
+}
+
+angular.module('myApp.services').service('UserStore', UserStore);
\ No newline at end of file
diff --git a/src/angularjs/persistence/web-storage.ts b/src/angularjs/persistence/web-storage.ts
new file mode 100755
index 00000000..15d21328
--- /dev/null
+++ b/src/angularjs/persistence/web-storage.ts
@@ -0,0 +1,73 @@
+import * as angular from 'angular'
+
+export interface IWebStorage {
+ sync(): void
+ setProperty(key: string, value: any): void
+ getProperty(key: string): any
+}
+
+class WebStorage implements IWebStorage {
+
+ static $inject = ['$window'];
+
+ ls = null;
+ key = 'cc_web_storage_';
+ store = {};
+ cacheCleared = false;
+
+ constructor (private $window: ng.IWindowService) {
+ let localStorage = this.localStorage();
+ if(!localStorage) {
+ return;
+ }
+
+ // Load the items from the store
+ let key = null;
+ for (let i = 0, k; i < localStorage.length; i++) {
+ key = localStorage.key(i);
+ if(key.slice(0, this.key.length) === this.key) {
+ this.store[key.slice(this.key.length)] = angular.fromJson(localStorage.getItem(key));
+ }
+ }
+ }
+
+ sync(): void {
+ for(let key in this.store) {
+ if(this.store.hasOwnProperty(key)) {
+ const value = this.store[key];
+ if(angular.isDefined(value) && key.length && '$' !== key[0]) {
+ try {
+ this.localStorage().setItem(this.key + key, angular.toJson(value));
+ }
+ catch(e) {
+ // TODO: Handle this
+ console.log("Warning! Storage error " + e.description);
+ }
+ }
+ }
+ }
+ }
+
+ setProperty(key: string, value: any): void {
+ if(key && key.length && key[0] !== '$') {
+ this.store[key] = value;
+ }
+ }
+
+ getProperty(key: string): any {
+ return this.store[key];
+ }
+
+ localStorage() {
+ if(!this.ls) {
+ this.ls = this.$window['localStorage'];
+ }
+ return this.ls;
+ }
+
+ localStorageSupported() {
+ return this.localStorage() != null;
+ }
+}
+
+angular.module('myApp.services').service('WebStorage', WebStorage);
\ No newline at end of file
diff --git a/src/angularjs/services/array-utils.ts b/src/angularjs/services/array-utils.ts
new file mode 100755
index 00000000..fe27edb1
--- /dev/null
+++ b/src/angularjs/services/array-utils.ts
@@ -0,0 +1,110 @@
+import {IRoom} from "../entities/room";
+import {RoomType} from "../keys/room-type";
+import {MessageKeys} from "../keys/message-keys";
+import {Utils} from "./utils";
+
+export class ArrayUtils {
+
+ public static getRoomsWithUsers(rooms, users): Array {
+
+ let roomsWithUsers = [];
+ for(let i = 0; i < rooms.length; i++) {
+ let room = rooms[i];
+ if(room.containsOnlyUsers(users)) {
+ if((users.length == 2 && room.getType() == RoomType.OneToOne) || (users.length != 2 && room.getType() == RoomType.Group)) {
+ roomsWithUsers.push(room);
+ }
+ }
+ }
+ return roomsWithUsers;
+ }
+
+ public static roomsSortedByMostRecent(rooms: Array): Array {
+ rooms.sort((a: IRoom, b: IRoom) => {
+ const almt = a.lastMessageTime();
+ const blmt = b.lastMessageTime();
+ const at = almt ? almt : a.created();
+ const bt = blmt ? almt : b.created();
+ return bt - at;
+ });
+ return rooms;
+ }
+
+ public static indexOf(array, id, getID) {
+ for(let i = 0; i < array.length; i++) {
+ if(id == getID(array[i])) {
+ return i;
+ }
+ }
+ }
+
+ public static removeItem(array, id, getID): void {
+ if(array.length == 0) {
+ return;
+ }
+ let i = this.indexOf(array, id, getID);
+ array.splice(i, 1);
+ }
+
+ public static getItem(array, id, getID) {
+ if(array.length == 0) {
+ return null;
+ }
+ let i = this.indexOf(array, id, getID);
+ return array[i];
+ }
+
+ public static contains(array, obj): boolean {
+ for(let i = 0; i < array.length; i++) {
+ if(obj == array[i]) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static remove(array, obj): void {
+ for(let i = 0; i < array.length; i++) {
+ if(obj == array[i]) {
+ array.splice(i, 1);
+ break;
+ }
+ }
+ }
+
+ public static filterByKey(array, key, getKey) {
+ if(!key || key.length == 0 || key === "") {
+ return array;
+ }
+ else {
+ // Loop over all elements
+ let result = [];
+ let elm, t1, t2;
+ for(let i = 0; i < array.length; i++) {
+
+ elm = array[i];
+ // Switch to lower case and remove spaces
+ // to improve search results
+ let elmKey = getKey(elm);
+ if (!Utils.unORNull(key) && !Utils.unORNull(elmKey)) {
+ t1 = key.toLowerCase().replace(/ /g,'');
+ t2 = elmKey.toLowerCase().replace(/ /g,'');
+ if(t2.length >= t1.length && t2.substring(0, t1.length) == t1) {
+ result.push(elm);
+ }
+ }
+ }
+ return result;
+ }
+ }
+
+ public static objectToArray(object) {
+ let array = [];
+ for(let key in object) {
+ if(object.hasOwnProperty(key)) {
+ array.push(object[key]);
+ }
+ }
+ return array;
+ }
+}
\ No newline at end of file
diff --git a/src/angularjs/services/before-unload.ts b/src/angularjs/services/before-unload.ts
new file mode 100755
index 00000000..1ece4655
--- /dev/null
+++ b/src/angularjs/services/before-unload.ts
@@ -0,0 +1,53 @@
+import * as angular from 'angular'
+
+export interface IBeforeUnload {
+ addListener(listener: IBeforeUnloadListener): void
+ removeListener(listener: IBeforeUnloadListener): void
+}
+
+export interface IBeforeUnloadListener {
+ beforeUnload(): void
+}
+
+class BeforeUnload implements IBeforeUnload {
+
+ listeners = new Array();
+
+ static $inject = ['$window'];
+
+ constructor ($window: ng.IWindowService) {
+ let beforeUnloadHandler = (e) => {
+ let listener = null;
+ for(let i = 0; i < this.listeners.length; i++) {
+ listener = this.listeners[i];
+ try {
+ listener.beforeUnload();
+ }
+ catch (e) {
+
+ }
+ }
+ };
+
+ if ($window.addEventListener) {
+ $window.addEventListener('beforeunload', beforeUnloadHandler);
+ } else {
+ $window.onbeforeunload = beforeUnloadHandler;
+ }
+ }
+
+ addListener(listener: IBeforeUnloadListener): void {
+ if(this.listeners.indexOf(listener) == -1 && listener.beforeUnload) {
+ this.listeners.push(listener);
+ }
+ }
+
+ removeListener(listener: IBeforeUnloadListener): void {
+ let index = this.listeners.indexOf(listener);
+ if(index >= 0) {
+ this.listeners.splice(index, 1);
+ }
+ }
+}
+
+angular.module('myApp.services').service('BeforeUnload', BeforeUnload);
\ No newline at end of file
diff --git a/src/angularjs/services/cloud-image.ts b/src/angularjs/services/cloud-image.ts
new file mode 100755
index 00000000..61400d06
--- /dev/null
+++ b/src/angularjs/services/cloud-image.ts
@@ -0,0 +1,22 @@
+import * as angular from 'angular'
+
+export interface ICloudImage {
+ cloudImage(url: string, w: number, h: number): string
+}
+
+class CloudImage implements ICloudImage {
+
+ private cloudImageToken: string;
+
+ static $inject = ['Environment'];
+
+ constructor(Environment) {
+ this.cloudImageToken = Environment.cloudImageToken();
+ }
+
+ cloudImage(url: string, w: number, h: number): string {
+ return 'http://' + this.cloudImageToken + '.cloudimg.io/s/crop/'+w+'x'+h+'/' + url;
+ }
+}
+
+angular.module('myApp.controllers').service('CloudImage', CloudImage);
diff --git a/src/angularjs/services/config.ts b/src/angularjs/services/config.ts
new file mode 100755
index 00000000..d062be27
--- /dev/null
+++ b/src/angularjs/services/config.ts
@@ -0,0 +1,196 @@
+import * as angular from 'angular'
+import {N} from "../keys/notification-keys";
+import * as Defines from "../keys/defines"
+import {IPaths} from "../network/paths";
+import {Utils} from "./utils";
+
+export interface IConfig {
+ clockType: string
+ defaultUserName: string
+ publicRoomsEnabled: boolean
+ onlineUsersEnabled: boolean
+ friendsEnabled: boolean
+ setConfig (setBy: SetBy, config: Map)
+}
+
+export enum SetBy {
+ Default = 0,
+ ControlPanel = 10,
+ Include = 20,
+ SingleSignOn = 30,
+ Admin = 40,
+}
+
+class Config implements IConfig {
+
+ static $inject = ['$rootScope', '$timeout', 'Paths'];
+
+ singleSignOnURL = null;
+ singleSignOnURLSet = SetBy.Default;
+
+ loginURL = null;
+ loginURLSet = SetBy.Default;
+
+ registerURL = null;
+ registerURLSet = SetBy.Default;
+
+ // How many historic messages to set by default
+ maxHistoricMessages = 50;
+ maxHistoricMessagesSet = SetBy.Default;
+
+ // Stop the user from changing their name
+ disableUserNameChange = false;
+ disableUserNameChangeSet = SetBy.Default;
+
+ // Stop the profile box from being displayed
+ disableProfileBox = false;
+ disableProfileBoxSet = SetBy.Default;
+
+ // Clock type:
+ // - 12hour
+ // - 24hour
+ clockType = '12hour';
+ clockTypeSet = SetBy.Default;
+
+ // Are users allowed to create their own public rooms
+ usersCanCreatePublicRooms = false;
+ usersCanCreatePublicRoomsSet = SetBy.Default;
+
+ // The primary domain is used when the chat is needed
+ // across multiple subdomains
+ primaryDomain = '';
+ primaryDomainSet = SetBy.Default;
+
+ // Allow anonymous login?
+ anonymousLoginEnabled = false;
+ anonymousLoginEnabledSet = SetBy.Default;
+
+ // Can the user log in using social logins
+ socialLoginEnabled = true;
+ socialLoginEnabledSet = SetBy.Default;
+
+ // Header and tab color
+ headerColor = '#0d82b3';
+ headerColorSet = SetBy.Default;
+
+ // After how long should the user be marked as offline
+ inactivityTimeout = 5;
+ inactivityTimeoutSet = SetBy.Default;
+
+ // The Single sign on API to use
+ singleSignOnAPILevel = 1;
+ singleSignOnAPILevelSet = SetBy.Default;
+
+ singleSignOn = true;
+ singleSignOnSet = SetBy.Admin;
+
+ onlineUsersEnabled = true;
+ onlineUsersEnabledSet = SetBy.Default;
+
+ publicRoomsEnabled = true;
+ publicRoomsEnabledSet = SetBy.Default;
+
+ friendsEnabled = true;
+ friendsEnabledSet = SetBy.Default;
+
+ friends = [];
+ friendsSet = SetBy.Default;
+
+ fileMessagesEnabled = false;
+ fileMessagesEnabledSet = SetBy.Default;
+
+ imageMessagesEnabled = false;
+ imageMessagesEnabledSet = SetBy.Default;
+
+ marginRight = 0;
+ marginRightSet = SetBy.Default;
+
+ clearCacheTimestamp = null;
+ clearCacheTimestampSet = SetBy.Default;
+
+ disableUserInfoPopup = false;
+ disableUserInfoPopupSet = SetBy.Default;
+
+ clickToChatTimeout = Defines.LastVisitedTimeout;
+ clickToChatTimeoutSet = SetBy.Default;
+
+ userProfileLinkEnabled = false;
+ userProfileLinkEnabledSet = SetBy.Default;
+
+ defaultUserName = "ChatSDK";
+ defaultUserNameSet = SetBy.Default;
+
+ constructor(private $rootScope, private $timeout: ng.ITimeoutService, private Paths: IPaths) {
+ }
+
+
+ // We update the config using the data provided
+ // but we only update variables where the priority
+ // of this setBy entity is higher than the previous
+ // one
+ setConfig (setBy: SetBy, config: Map) {
+
+ this.setValue("inactivityTimeout", config, setBy);
+ this.inactivityTimeout = Math.max(this.inactivityTimeout, 2);
+ this.inactivityTimeout = Math.min(this.inactivityTimeout, 15);
+
+ this.setValue("maxHistoricMessages", config, setBy);
+ this.setValue("disableUserNameChange", config, setBy);
+ this.setValue("disableProfileBox", config, setBy);
+ this.setValue("clockType", config, setBy);
+ this.setValue("usersCanCreatePublicRooms", config, setBy);
+ this.setValue("primaryDomain", config, setBy);
+ this.setValue("anonymousLoginEnabled", config, setBy);
+
+ this.setValue("socialLoginEnabled", config, setBy);
+ this.setValue("headerColor", config, setBy);
+ this.setValue("singleSignOnAPILevel", config, setBy);
+ this.setValue("apiLevel", config, setBy);
+ this.setValue("singleSignOn", config, setBy);
+ this.setValue("singleSignOnURL", config, setBy);
+ this.setValue("registerURL", config, setBy);
+ this.setValue("loginURL", config, setBy);
+
+ this.setValue("onlineUsersEnabled", config, setBy);
+ this.setValue("publicRoomsEnabled", config, setBy);
+ this.setValue("friendsEnabled", config, setBy);
+ this.setValue("clearCacheTimestamp", config, setBy);
+ this.setValue("fileMessagesEnabled", config, setBy);
+ this.setValue("imageMessagesEnabled", config, setBy);
+ this.setValue("marginRight", config, setBy);
+
+ this.setValue("friends", config, setBy);
+
+ this.setValue("clickToChatTimeout", config, setBy);
+ this.setValue("userProfileLinkEnabled", config, setBy);
+ this.setValue("defaultUserName", config, setBy);
+
+ this.$rootScope.config = this;
+
+ this.$rootScope.$broadcast(N.ConfigUpdated);
+
+ this.$timeout(() => {
+ this.$rootScope.$digest()
+ });
+
+ }
+
+ setValue(name: string, data: any, setBy: SetBy) {
+ if(data && !Utils.unORNull(data[name]) && this[name+"Set"] <= setBy) {
+ this[name] = data[name];
+ this[name+"Set"] = setBy;
+ }
+ }
+
+ startConfigListener(): Promise {
+ return new Promise((resolve, reject) => {
+ const ref = this.Paths.configRef();
+ ref.on('value', (snapshot: firebase.database.DataSnapshot) => {
+ this.setConfig(SetBy.ControlPanel, snapshot.val());
+ resolve();
+ });
+ });
+ }
+}
+
+angular.module('myApp.services').service('Config', Config);
\ No newline at end of file
diff --git a/src/angularjs/services/emoji.ts b/src/angularjs/services/emoji.ts
new file mode 100755
index 00000000..42ada961
--- /dev/null
+++ b/src/angularjs/services/emoji.ts
@@ -0,0 +1,25 @@
+import * as angular from 'angular'
+import {AllEmojiNames} from "../keys/all-emojis";
+
+export interface IEmoji {
+ getEmojis() : string[]
+}
+
+export class Emoji implements IEmoji {
+
+ private store: string [] = [];
+
+ getEmojis() : string[] {
+ if (!this.store.length) {
+ for(let i = 0; i < 50; i++) {
+ this.store.push(":" + AllEmojiNames[i] + ":");
+ }
+ }
+ return this.store;
+ }
+
+}
+
+angular.module('myApp.services').service('Emoji', Emoji);
+
+
\ No newline at end of file
diff --git a/src/angularjs/services/environment.ts b/src/angularjs/services/environment.ts
new file mode 100755
index 00000000..5827093d
--- /dev/null
+++ b/src/angularjs/services/environment.ts
@@ -0,0 +1,83 @@
+import * as angular from 'angular'
+import {ChatSDKConfig} from "../app/config";
+import {Utils} from "./utils";
+
+export interface IEnvironment {
+ firebaseConfig(): any
+ config(): any
+}
+
+class Environment implements IEnvironment {
+
+ static $inject = ['$rootScope'];
+
+ constructor (private $rootScope) {
+ $rootScope.partialsURL = this.partialsURL();
+ }
+
+ firebaseConfig(): any {
+ return this.config().firebaseConfig;
+ }
+
+ config(): any {
+ return ChatSDKConfig;
+ }
+
+ showOnPaths() {
+ return this.config().showOnPaths;
+ }
+
+ rootURL() {
+ if(this.config().environment == 'test') {
+ return document.location.origin + '/';
+ }
+ else {
+ return 'https://' + this.firebaseConfig().authDomain + '/';
+ }
+ }
+
+ partialsURL() {
+ return this.resourceRootURL() + 'partials/';
+ }
+
+ imagesURL() {
+ return this.resourceRootURL() + 'img/';
+ }
+
+ audioURL() {
+ return this.resourceRootURL() + 'audio/';
+ }
+
+ defaultProfilePictureURL() {
+ return this.imagesURL() + 'cc-100-profile-pic.png';
+ }
+
+ defaultRoomPictureURL() {
+ return this.imagesURL() + 'cc-100-room-pic.png';
+ }
+
+ facebookAppID() {
+ return this.config().facebookAppID;
+ }
+
+ cloudImageToken() {
+ return this.config().cloudImageToken;
+ }
+
+ resourceRootURL() {
+ let url = this.config().resourceRootURL;
+ if(!Utils.unORNull(url)) {
+ if(!(url[url.length - 1] == '/')) {
+ url += '/';
+ }
+ return url;
+ }
+ return this.rootURL();
+ }
+
+ rootPath() {
+ return this.config().rootPath;
+ }
+}
+
+angular.module('myApp.services').service('Environment', Environment);
\ No newline at end of file
diff --git a/src/angularjs/services/log.ts b/src/angularjs/services/log.ts
new file mode 100755
index 00000000..c385169c
--- /dev/null
+++ b/src/angularjs/services/log.ts
@@ -0,0 +1,14 @@
+import * as Defines from "../keys/defines";
+
+export class Log {
+ public static notification (notification, context) {
+ if(Defines.DEBUG) {
+ if(!context) {
+ context = ""
+ } else {
+ context = ", context: " + context
+ }
+ console.log("Notification: " + notification + context)
+ }
+ }
+}
diff --git a/src/angularjs/services/marquee.ts b/src/angularjs/services/marquee.ts
new file mode 100755
index 00000000..c6914f93
--- /dev/null
+++ b/src/angularjs/services/marquee.ts
@@ -0,0 +1,45 @@
+import * as angular from 'angular'
+
+export interface IMarquee {
+
+}
+
+angular.module('myApp.services').factory('Marquee', ['$window', '$interval', function ($window, $interval) {
+ let Marquee = {
+
+ running: null,
+ title: "",
+
+ init: function () {
+ this.title = $window.document.title;
+ return this;
+ },
+
+ startWithMessage: function (message) {
+ if(this.running) {
+ this.stop();
+ }
+ let text = "Chatcat Message: " + message + "...";
+
+ this.running = $interval(() => {
+ // Change the page title
+ $window.document.title = text;
+ if(text.length > 0) {
+ text = text.slice(1);
+ }
+ else {
+ this.stop();
+ }
+ }, 80);
+ },
+
+ stop: function () {
+ $interval.cancel(this.running);
+ this.running = null;
+ // Change the page title
+ $window.document.title = this.title;
+ }
+
+ };
+ return Marquee.init();
+}]);
\ No newline at end of file
diff --git a/src/angularjs/services/partials.ts b/src/angularjs/services/partials.ts
new file mode 100755
index 00000000..e2de5dcd
--- /dev/null
+++ b/src/angularjs/services/partials.ts
@@ -0,0 +1,28 @@
+import * as angular from 'angular'
+
+export interface IPartials {
+
+}
+
+angular.module('myApp.services').factory('Partials', ['$http', '$templateCache', 'Environment', function ($http, $templateCache, Environment) {
+ return {
+ load: function () {
+ $http.get(Environment.partialsURL() + 'chat-room.html', {cache:$templateCache});
+ $http.get(Environment.partialsURL() + 'chat-room-embed.html', {cache:$templateCache});
+ $http.get(Environment.partialsURL() + 'chat-settings.html', {cache:$templateCache});
+ $http.get(Environment.partialsURL() + 'countries-select.html', {cache:$templateCache});
+ $http.get(Environment.partialsURL() + 'create-room-box.html', {cache:$templateCache});
+ $http.get(Environment.partialsURL() + 'emojis.html', {cache:$templateCache});
+ $http.get(Environment.partialsURL() + 'login-box.html', {cache:$templateCache});
+ $http.get(Environment.partialsURL() + 'main-box.html', {cache:$templateCache});
+ $http.get(Environment.partialsURL() + 'notification.html', {cache:$templateCache});
+ $http.get(Environment.partialsURL() + 'profile-box.html', {cache:$templateCache});
+ $http.get(Environment.partialsURL() + 'profile-settings-box.html', {cache:$templateCache});
+ $http.get(Environment.partialsURL() + 'room-description.html', {cache:$templateCache});
+ $http.get(Environment.partialsURL() + 'room-list.html', {cache:$templateCache});
+ $http.get(Environment.partialsURL() + 'room-list-box.html', {cache:$templateCache});
+ $http.get(Environment.partialsURL() + 'user-list.html', {cache:$templateCache});
+ $http.get(Environment.partialsURL() + 'year-of-birth-select.html', {cache:$templateCache});
+ }
+ };
+}]);
\ No newline at end of file
diff --git a/src/angularjs/services/path-analyser.ts b/src/angularjs/services/path-analyser.ts
new file mode 100755
index 00000000..37d3d340
--- /dev/null
+++ b/src/angularjs/services/path-analyser.ts
@@ -0,0 +1,66 @@
+import * as angular from 'angular'
+
+export interface IPathAnalyser {
+
+}
+
+class PathAnalyser implements IPathAnalyser {
+
+ toAscii(string): string {
+ let output = "";
+ for(let i = 0; i < string.length; i++) {
+ output += string.charCodeAt(i);
+ }
+ return output;
+ }
+
+ searchPath(query): boolean {
+
+ query = query.trim();
+
+ // First see if the query has any wildcards. The * is a wildcard
+ // and can be used at the start or end of the query
+ let wildPrefix = false;
+ if(query.length) {
+ wildPrefix = query[0] == '*';
+ }
+ let wildSuffix = false;
+ if(query.length) {
+ wildSuffix = query[query.length - 1] == '*';
+ }
+ if(wildPrefix) {
+ query = query.substring(1);
+ }
+ if(wildSuffix) {
+ query = query.substring(0, query.length - 1);
+ }
+
+ // Now convert to ascii. We do this because otherwise the special
+ // characters in the domain can mess up the regex search
+ query = (wildPrefix ? '' : '/^') + this.toAscii(query) + (wildSuffix ? '.*' : '$');
+
+ // Now get the path
+ let path = this.toAscii(document.location.href);
+ // First we check to see if the query has wild cards
+
+ return path.search(query)!= -1;
+ }
+
+ shouldShowChatOnPath(paths): boolean {
+ // Check to see if we should load the chat on this page?
+ let matches = false;
+
+ paths = paths.split(',');
+
+ for (let i = 0; i < paths.length; i++) {
+ let path = paths[i];
+ if (this.searchPath(path)) {
+ matches = true;
+ break;
+ }
+ }
+ return matches;
+ }
+}
+
+angular.module('myApp.services').service('PathAnalyser', PathAnalyser);
\ No newline at end of file
diff --git a/src/angularjs/services/room-open-queue.ts b/src/angularjs/services/room-open-queue.ts
new file mode 100755
index 00000000..4b107a2b
--- /dev/null
+++ b/src/angularjs/services/room-open-queue.ts
@@ -0,0 +1,36 @@
+import * as angular from 'angular'
+/**
+ * This service allows us to flag a room to be opened. This
+ * is useful because when we create a new room it's turned
+ * on in the impl_roomAdded function. We want to be able
+ * to flag it to be opened from anywhere and then let
+ * that function open it
+ */
+
+export interface IRoomOpenQueue {
+ roomExistsAndPop(rid: string): boolean
+ addRoomWithID(rid: string): void
+}
+
+class RoomOpenQueue implements IRoomOpenQueue {
+
+ rids = new Array();
+
+ addRoomWithID(rid: string): void {
+ if(this.rids.indexOf(rid) < 0) {
+ this.rids.push(rid);
+ }
+ }
+
+ roomExistsAndPop (rid: string): boolean {
+ let index = this.rids.indexOf(rid);
+ if(index >= 0) {
+ this.rids.splice(index, 1);
+ return true;
+ }
+ return false;
+ }
+
+}
+
+angular.module('myApp.services').service('RoomOpenQueue', RoomOpenQueue);
\ No newline at end of file
diff --git a/src/angularjs/services/room-position-manager.ts b/src/angularjs/services/room-position-manager.ts
new file mode 100755
index 00000000..ebbfd025
--- /dev/null
+++ b/src/angularjs/services/room-position-manager.ts
@@ -0,0 +1,333 @@
+/**
+ * This service keeps track of the slot positions
+ * while the rooms are moving around
+ */
+import * as angular from 'angular'
+import {N} from "../keys/notification-keys";
+
+import {IRoom} from "../entities/room";
+import {Dimensions} from "../keys/dimensions";
+import {ArrayUtils} from "./array-utils";
+
+export interface IRoomPositionManager {
+
+}
+
+angular.module('myApp.services').factory('RoomPositionManager', ['$rootScope', '$timeout', '$document', '$window', 'LocalStorage', 'Cache', 'Screen',
+ function ($rootScope, $timeout, $document, $window, LocalStorage, Cache, Screen) {
+
+ let rpm = {
+
+ rooms: [],
+ slotPositions: [],
+ dirty: true,
+
+ init: function () {
+
+ this.updateAllRoomActiveStatus();
+ $rootScope.$on(N.ScreenSizeChanged, () => {
+ this.updateAllRoomActiveStatus();
+ });
+
+ return this;
+ },
+
+ roomDragged: function (room) {
+
+ this.calculateSlotPositions();
+
+ // Right to left
+ let nextSlot = room.slot;
+ let nextRoom = null;
+
+ if(room.dragDirection > 0) {
+ nextSlot++;
+ if(this.rooms.length > nextSlot) {
+
+ nextRoom = this.rooms[nextSlot];
+
+ // If the room is covering over half of the next room
+ if(room.offset + room.width > this.slotPositions[nextSlot] + nextRoom.width/2) {
+ this.setDirty();
+ room.slot = nextSlot;
+ $rootScope.$broadcast(N.AnimateRoom, {
+ room: nextRoom,
+ slot: nextSlot - 1
+ });
+ }
+ }
+ }
+ // Left to right
+ else {
+ nextSlot--;
+ if(nextSlot >= 0) {
+
+ nextRoom = this.rooms[nextSlot];
+
+ // If the room is covering over half of the next room
+ if(room.offset < this.slotPositions[nextSlot] + nextRoom.width / 2) {
+ this.setDirty();
+ room.slot = nextSlot;
+ $rootScope.$broadcast(N.AnimateRoom, {
+ room: nextRoom,
+ slot: nextSlot + 1
+ });
+ }
+ }
+ }
+ },
+
+ insertRoom: function (room: IRoom, slot: number, duration) {
+
+ // If the room is already added then return
+ if(this.roomIsOpen(room)) {
+ room.isOpen = true;
+ return;
+ }
+
+ // Update the rooms from the cache
+ this.updateRoomsList();
+
+ // We have no slot so add it to the max position
+ if(slot == -1) {
+ slot = this.rooms.length;
+ }
+
+ let i;
+
+ // Move the rooms left
+ for(i = slot; i < this.rooms.length; i++) {
+ this.rooms[i].slot++;
+ }
+
+ // Add the room
+ Cache.addRoom(room);
+
+ room.slot = slot;
+
+ // Flag as dirty since we've added a room
+ this.setDirty();
+
+ // Recalculate
+ this.calculateSlotPositions();
+
+ $rootScope.$broadcast(N.RoomPositionUpdated, room);
+
+ for(i = slot; i < this.rooms.length; i++) {
+ $rootScope.$broadcast(N.AnimateRoom, {
+ room: this.rooms[i],
+ duration: duration
+ });
+ }
+
+ room.updateOffsetFromSlot();
+ $rootScope.$broadcast(N.RoomOpened, room);
+
+ this.updateAllRoomActiveStatus();
+
+ },
+
+ roomIsOpen: function (room) {
+ return ArrayUtils.contains(this.rooms, room);
+ },
+
+ closeRoom: function (room) {
+
+ if(!this.roomIsOpen(room)) {
+ room.isOpen = false;
+ return;
+ }
+
+ Cache.removeRoom(room);
+
+ // Set the room width to default
+ room.setSizeToDefault();
+
+ this.autoPosition(300);
+ this.updateAllRoomActiveStatus();
+
+ $rootScope.$broadcast(N.RoomClosed, room);
+
+ },
+
+ closeAllRooms: function () {
+ for(let i = 0; i < this.rooms.length; i++) {
+ this.closeRoom(this.rooms[i]);
+ }
+ },
+
+ autoSetSlots: function () {
+ for(let i = 0; i < this.rooms.length; i++) {
+ this.rooms[i].slot = i;
+ }
+ },
+
+ autoPosition: function (duration) {
+
+ this.calculateSlotPositions(true);
+ this.autoSetSlots();
+
+ // Are there any inactive rooms?
+ // We do this because we can't animate rooms that
+ // are inactive
+ if(Cache.inactiveRooms().length) {
+ duration = 0;
+ }
+
+ // Animate all rooms into position
+ for(let i = 0; i < this.rooms.length; i++) {
+ if(this.rooms[i].active && duration > 0) {
+ $rootScope.$broadcast(N.AnimateRoom, {
+ room: this.rooms[i],
+ duration: duration
+ });
+ }
+ // We need this because if a room isn't active then it's
+ // HTML and therefore controller won't exist
+ else {
+ this.rooms[i].updateOffsetFromSlot();
+ }
+ }
+ },
+
+ updateAllRoomActiveStatus: function () {
+
+ if(this.rooms.length === 0) {
+ return;
+ }
+
+ this.calculateSlotPositions(true);
+
+ let effectiveScreenWidth = this.effectiveScreenWidth();
+
+ // Get the index of the current room
+ // If any room has gone changed their active status then digest
+ let digest;
+
+ for(let i = 0; i < this.rooms.length; i++) {
+ if((this.slotPositions[i] + this.rooms[i].width) < effectiveScreenWidth) {
+ digest = digest || this.rooms[i].active == false;
+ this.rooms[i].setActive(true);
+ }
+ else {
+ digest = digest || this.rooms[i].active == true;
+ this.rooms[i].setActive(false);
+ }
+ }
+ if(digest) {
+ $rootScope.$broadcast(N.UpdateRoomActiveStatus);
+ }
+ },
+
+ updateRoomPositions: function (room, duration) {
+
+ this.calculateSlotPositions();
+
+ if(this.rooms.length) {
+ for(let i = Math.max(this.rooms.indexOf(room), 0); i < this.rooms.length; i++) {
+ if(this.rooms[i].active && duration > 0) {
+ $rootScope.$broadcast(N.AnimateRoom, {
+ room: this.rooms[i],
+ duration: duration
+ });
+ }
+ else {
+ this.rooms[i].updateOffsetFromSlot();
+ }
+ }
+ }
+ },
+
+ /**
+ * Returns the width of the screen -
+ * if the room list is showing then it subtracts it's width
+ * @returns {Usable screen width}
+ */
+ effectiveScreenWidth: function () {
+
+ this.calculateSlotPositions();
+
+ let width = Screen.screenWidth;
+
+ if(!this.rooms.length) {
+ return width;
+ }
+
+ // Check the last box to see if it's off the end of the
+ // screen
+ let lastRoom = this.rooms[this.rooms.length - 1];
+
+ // If we can fit the last room in then
+ // the rooms list will be hidden which will
+ // give us extra space
+ if(lastRoom.width + this.slotPositions[lastRoom.slot] > Screen.screenWidth) {
+ width -= Dimensions.RoomListBoxWidth;
+ }
+
+ return width;
+ },
+
+ getRooms: function () {
+ this.calculateSlotPositions();
+ return this.rooms;
+ },
+
+ updateRoomsList: function () {
+ this.rooms = Cache.rooms;
+
+ // Sort the rooms by slot
+ this.rooms.sort((a, b) => {
+ return a.slot - b.slot;
+ });
+ },
+
+ setDirty: function () {
+ this.dirty = true;
+ },
+
+ calculateSlotPositions: function (force) {
+ if(force) {
+ this.setDirty();
+ }
+ if(!this.dirty) {
+ return;
+ }
+
+ this.dirty = false;
+
+ this.updateRoomsList();
+
+ this.slotPositions = [];
+
+ // Work out the positions
+ let p = Dimensions.MainBoxWidth + Dimensions.ChatRoomSpacing;
+ for(let i = 0; i < this.rooms.length; i++) {
+
+ this.slotPositions.push(p);
+
+ p += this.rooms[i].minimized ? Dimensions.ChatRoomWidth : this.rooms[i].width;
+ p += Dimensions.ChatRoomSpacing;
+ }
+
+// for(let i in this.slotPositions) {
+// console.log("Slot: " + i + " - " + this.slotPositions[i]);
+// }
+// for(i = 0; i < this.rooms.length; i++) {
+// console.log("Room "+i+": " + this.rooms[i].slot);
+// if(i != this.rooms[i].slot) {
+// console.log("ERRR");
+// }
+// }
+
+ },
+
+ offsetForSlot: function (slot) {
+ this.calculateSlotPositions();
+ return this.slotPositions[slot];
+ }
+
+ }
+
+ return rpm.init();
+
+ }]);
\ No newline at end of file
diff --git a/src/angularjs/services/screen.ts b/src/angularjs/services/screen.ts
new file mode 100755
index 00000000..fa84c325
--- /dev/null
+++ b/src/angularjs/services/screen.ts
@@ -0,0 +1,39 @@
+import * as angular from 'angular'
+import {N} from "../keys/notification-keys";
+
+export interface IScreen {
+
+}
+
+angular.module('myApp.services').factory('Screen', ['$rootScope', '$timeout', '$document', '$window', 'LocalStorage', function ($rootScope, $timeout, $document, $window, LocalStorage) {
+
+ let screen = {
+
+ //rooms: [],
+ screenWidth: 0,
+ screenHeight: 0,
+
+ init: function () {
+
+ // Set the screen width and height
+ this.updateScreenSize();
+
+ // Monitor the window size
+ angular.element($window).bind('resize', () => {
+ this.updateScreenSize();
+ });
+
+ return this;
+ },
+
+ // TODO: Check this
+ updateScreenSize: function () {
+ this.screenWidth = $window.innerWidth;//$document.width();
+ this.screenHeight = $window.innerHeight;
+
+ $rootScope.$broadcast(N.ScreenSizeChanged);
+ }
+
+ };
+ return screen.init();
+}]);
\ No newline at end of file
diff --git a/src/angularjs/services/sound-effects.ts b/src/angularjs/services/sound-effects.ts
new file mode 100755
index 00000000..ff981d7a
--- /dev/null
+++ b/src/angularjs/services/sound-effects.ts
@@ -0,0 +1,36 @@
+import * as angular from 'angular'
+import * as Howl from 'howler'
+
+export interface ISoundEffects {
+
+}
+
+angular.module('myApp.services').factory('SoundEffects', ['LocalStorage', 'Environment', function (LocalStorage, Environment) {
+ return {
+
+ messageReceivedSoundNumber: 1,
+ muted: LocalStorage.isMuted(),
+
+ messageReceived: function () {
+ if(this.muted) {
+ return;
+ }
+ if(this.messageReceivedSoundNumber == 1) {
+ this.alert1();
+ }
+ },
+
+ alert1: function () {
+ let sound = new Howl({
+ src: [Environment.audioURL() + 'alert_1.mp3']
+ });
+ sound.play();
+ },
+
+ toggleMuted: function () {
+ this.muted = !this.muted;
+ LocalStorage.setMuted(this.muted);
+ return this.muted;
+ }
+ };
+}]);
\ No newline at end of file
diff --git a/src/angularjs/services/state-manager.ts b/src/angularjs/services/state-manager.ts
new file mode 100755
index 00000000..e9d068b1
--- /dev/null
+++ b/src/angularjs/services/state-manager.ts
@@ -0,0 +1,252 @@
+import * as angular from 'angular'
+import {RoomType} from "../keys/room-type";
+import {UserAllowInvites} from "../keys/allow-invite-type";
+import {RoomKeys} from "../keys/room-keys";
+import {N} from "../keys/notification-keys";
+import {IRootScope} from "../controllers/app";
+import {IFriendsConnector} from "../connectors/friend-connector";
+import {IConfig} from "./config";
+import {ICache} from "../persistence/cache";
+import {IRoomStore} from "../persistence/room-store";
+import {IUserStore} from "../persistence/user-store";
+import {IOnlineConnector} from "../connectors/online-connector";
+import {IPublicRoomsConnector} from "../connectors/public-rooms-connector";
+import {IPaths} from "../network/paths";
+import {IRoomOpenQueue} from "./room-open-queue";
+
+export interface IStateManager {
+ on(): void
+ userOn(uid): void
+}
+
+class StateManager implements IStateManager {
+
+ isOn = false;
+ onUserID = null;
+
+ static $inject = ['$rootScope', 'FriendsConnector', 'Config', 'Cache', 'RoomStore', 'UserStore', 'OnlineConnector', 'PublicRoomsConnector', 'Paths', 'RoomOpenQueue'];
+
+ constructor (
+ private $rootScope: IRootScope,
+ private FriendsConnector: IFriendsConnector,
+ private Config: IConfig,
+ private Cache: ICache,
+ private RoomStore: IRoomStore,
+ private UserStore: IUserStore,
+ private OnlineConnector: IOnlineConnector,
+ private PublicRoomsConnector: IPublicRoomsConnector,
+ private Paths: IPaths,
+ private RoomOpenQueue: IRoomOpenQueue) {}
+
+ /**
+ * Add universal listeners to Firebase
+ * these listeners are not specific to an individual user
+ */
+ on(): void {
+
+ if(this.isOn) {
+ return;
+ }
+ this.isOn = true;
+
+ /**
+ * Public rooms ref
+ */
+ if(this.Config.publicRoomsEnabled) {
+ this.PublicRoomsConnector.on();
+ }
+
+ /**
+ * Online users ref
+ */
+ if(this.Config.onlineUsersEnabled) {
+ this.OnlineConnector.on();
+ }
+
+ }
+
+ /**
+ * Stop listening to Firebase
+ */
+ off() {
+
+ this.isOn = false;
+
+ this.PublicRoomsConnector.off();
+
+ if(this.Config.onlineUsersEnabled) {
+ this.OnlineConnector.off();
+ }
+
+ }
+
+ /**
+ * Start listening to a specific user location
+ */
+ userOn(uid): void {
+
+ // Check to see that we've not already started to listen to this user
+ if(this.onUserID) {
+ if(this.onUserID == uid) {
+ console.log("You can't call on on a user twice");
+ return;
+ }
+ else {
+ this.userOff(this.onUserID);
+ }
+ }
+
+ this.onUserID = uid;
+
+ /**
+ * Rooms
+ */
+
+ let roomsRef = this.Paths.userRoomsRef(uid);
+
+ roomsRef.on('child_added', (snapshot) => {
+ if(snapshot.val()) {
+ this.impl_roomAdded(snapshot.key, snapshot.val()[RoomKeys.InvitedBy]);
+ }
+ });
+
+ roomsRef.on('child_removed', (snapshot) => {
+ let rid = snapshot.key;
+ if(rid) {
+ this.impl_roomRemoved(rid);
+ }
+ });
+
+ /**
+ * Friends
+ */
+
+ if(this.Config.friendsEnabled) {
+ this.FriendsConnector.on(uid);
+ }
+
+ /**
+ * Blocked
+ */
+
+ let blockedUsersRef = this.Paths.userBlockedRef(uid);
+ blockedUsersRef.on('child_added', (snapshot) => {
+
+ if(snapshot && snapshot.val()) {
+ this.impl_blockedAdded(snapshot);
+ }
+
+ });
+
+ blockedUsersRef.on('child_removed', (snapshot) => {
+
+ if(snapshot && snapshot.val()) {
+ this.impl_blockedRemoved(snapshot);
+ }
+
+ });
+
+ }
+
+ userOff(uid) {
+
+ this.onUserID = null;
+
+ let roomsRef = this.Paths.userRoomsRef(uid);
+
+ roomsRef.off('child_added');
+ roomsRef.off('child_removed');
+
+ this.FriendsConnector.off(uid);
+
+ let blockedUsersRef = this.Paths.userBlockedRef(uid);
+
+ blockedUsersRef.off('child_added');
+ blockedUsersRef.off('child_removed');
+
+ // Switch the rooms off
+ for(let i = 0; i < this.Cache.rooms.length; i++) {
+ let room = this.Cache.rooms[i];
+ room.off();
+ }
+
+ }
+
+ impl_blockedAdded(snapshot: firebase.database.DataSnapshot): void {
+
+ let uid = snapshot.key;
+ if(uid) {
+ let user = this.UserStore.getOrCreateUserWithID(uid);
+
+ user.unblock = () => {
+ snapshot.ref.remove();
+ };
+
+ this.Cache.addBlockedUser(user);
+ }
+
+ }
+
+ impl_blockedRemoved(snapshot: firebase.database.DataSnapshot): void {
+ this.Cache.removeBlockedUserWithID(snapshot.key);
+ }
+
+ /**
+ * This is called each time a room is added to the user's
+ * list of rooms
+ * @param rid
+ * @param invitedBy
+ */
+ impl_roomAdded(rid, invitedBy) {
+
+ if (rid && invitedBy) {
+ const invitedByUser = this.UserStore.getOrCreateUserWithID(invitedBy);
+
+ // First check if we want to accept the room
+ // This should never happen
+ if(this.Cache.isBlockedUser(invitedBy)) {
+ return;
+ }
+
+ if(!this.UserStore.currentUser().canBeInvitedByUser(invitedByUser)) {
+ return;
+ }
+ // If they only allow invites from friends
+ // the other user must be a friend
+ if(this.UserStore.currentUser().allowInvitesFrom(UserAllowInvites.Friends) && !this.FriendsConnector.isFriend(invitedByUser) && !invitedByUser.isMe()) {
+ return;
+ }
+
+ // Does the room already exist?
+ const room = this.RoomStore.getOrCreateRoomWithID(rid);
+
+ // If you clear the cache without this all the messages
+ // would show up as unread...
+ room.invitedBy = invitedByUser;
+ room.deleted = false;
+
+ room.on().then(() => {
+ if(room.isOpen) {
+ room.open(-1, 0);
+ }
+ // If the user just created the room...
+ if(this.RoomOpenQueue.roomExistsAndPop(room.rid())) {
+ room.open(0, 300);
+ }
+ });
+ }
+ }
+
+ impl_roomRemoved(rid) {
+
+ let room = this.RoomStore.getRoomWithID(rid);
+ room.close();
+
+ if(room.getType() === RoomType.OneToOne){
+ this.RoomStore.removeRoom(room);
+ this.$rootScope.$broadcast(N.RoomRemoved);
+ }
+ }
+}
+
+angular.module('myApp.services').service('StateManager', StateManager);
\ No newline at end of file
diff --git a/src/angularjs/services/time.ts b/src/angularjs/services/time.ts
new file mode 100755
index 00000000..06b47a2d
--- /dev/null
+++ b/src/angularjs/services/time.ts
@@ -0,0 +1,79 @@
+import * as angular from 'angular'
+import * as firebase from 'firebase';
+import * as moment from 'moment';
+
+export interface ITime {
+ formatTimestamp (timestamp: number, type: string): string
+ start(uid): Promise
+}
+
+angular.module('myApp.services').factory('Time', ['$q', 'Paths', function ($q, Paths) {
+ return {
+
+ localTime: null,
+ remoteTime: null,
+ uid: null,
+ working: false,
+
+ start: function (uid): Promise {
+
+ if(this.remoteTime && uid == this.uid) {
+ return Promise.resolve();
+ }
+
+ this.working = true;
+
+ let ref = Paths.timeRef(uid);
+ return ref.set(firebase.database.ServerValue.TIMESTAMP).then(() => {
+ return ref.once('value', (snapshot) => {
+ this.working = false;
+ if(snapshot.val()) {
+ this.impl_setTime(snapshot.val(), uid);
+ return this.remoteTime;
+ }
+ });
+ }, (error) => {
+ this.working = false;
+ });
+ },
+
+ impl_setTime: function (remoteTime, uid) {
+ this.remoteTime = remoteTime;
+ this.uid = uid;
+ this.working = false;
+ },
+
+ now: function () {
+ if(this.remoteTime) {
+ return new Date().getTime() - this.localTime + this.remoteTime;
+ }
+ return null;
+ },
+
+ secondsSince: function (time) {
+ return Math.abs(this.now() - time)/1000;
+ },
+
+ formatTimestamp: function (timestamp: number, type: string): string {
+ try {
+ if(type == '24hour') {
+ return moment(timestamp).format('HH:mm');
+ }
+ else {
+ return moment(timestamp).format('h:mm a');
+ }
+ }
+ // In some cases (maktab.pk) a javascript conflict seems to stop moment working
+ // TODO: Investigate this
+ catch (e) {
+ const date = new Date(timestamp);
+ // hours part from the timestamp
+ const hours = date.getHours();
+ // minutes part from the timestamp
+ const minutes = "0" + date.getMinutes();
+ // will display time in 10:30:23 format
+ return hours + ':' + minutes.substr(minutes.length-2);
+ }
+ }
+ }
+}]);
diff --git a/src/angularjs/services/utils.ts b/src/angularjs/services/utils.ts
new file mode 100755
index 00000000..66ced048
--- /dev/null
+++ b/src/angularjs/services/utils.ts
@@ -0,0 +1,34 @@
+export class Utils {
+
+ static unORNull(object): boolean {
+ return object === 'undefined' || object == null;
+ }
+
+ static empty(object): boolean {
+ return this.unORNull(object) || object.length === 0;
+ }
+
+ static stopDefault(e) {
+ if (e && e.preventDefault) {
+ e.preventDefault();
+ }
+ else {
+ window.event.returnValue = false;
+ }
+ return false;
+ }
+
+ static toObject (map: Map): {} {
+ const obj = {};
+ map.forEach((value: any, key: string) => {
+ obj[key] = map.get(key);
+ });
+ return obj;
+ }
+
+ static sameMinute (date1: Date, date2: Date): boolean {
+ return date1.getDay() == date2.getDay() &&
+ date1.getHours() == date2.getHours() &&
+ date1.getMinutes() == date2.getMinutes();
+ }
+}
diff --git a/src/angularjs/services/visibility.ts b/src/angularjs/services/visibility.ts
new file mode 100755
index 00000000..f9c19b35
--- /dev/null
+++ b/src/angularjs/services/visibility.ts
@@ -0,0 +1,37 @@
+import * as angular from 'angular'
+import {N} from "../keys/notification-keys";
+
+export interface IVisibility {
+
+}
+
+angular.module('myApp.services').factory('Visibility', ['$rootScope', '$document', function ($rootScope, $document) {
+
+ let Visibility = {
+
+ isHidden: false,
+ uid: "Test",
+
+ init: function () {
+ document.addEventListener("visibilitychange", () => this.changed);
+ document.addEventListener("webkitvisibilitychange", () => this.changed);
+ document.addEventListener("mozvisibilitychange", () => this.changed);
+ document.addEventListener("msvisibilitychange", () => this.changed);
+
+ this.uid = new Date().getTime();
+
+ return this;
+ },
+
+ changed: function (event) {
+ this.isHidden = $document.hidden || $document.webkitHidden || $document.mozHidden || $document.msHidden;
+ $rootScope.$broadcast(N.VisibilityChanged, this.isHidden);
+ },
+
+ getIsHidden: function () {
+ return this.isHidden;
+ }
+ };
+
+ return Visibility.init();
+}]);
diff --git a/src/angularjs/shim/shim.d.ts b/src/angularjs/shim/shim.d.ts
new file mode 100755
index 00000000..4065d1dd
--- /dev/null
+++ b/src/angularjs/shim/shim.d.ts
@@ -0,0 +1,13 @@
+// import * as _angular_ from 'angular';
+// declare global {
+// const angular: typeof _angular_;
+// }
+
+// declare global {
+// const ChatSDKOptions: {
+// firebaseConfig: {
+// authDomain: ""
+// },
+// hideMainBox: false
+// };
+// }
diff --git a/tsconfig.app.json b/tsconfig.app.json
index 565a11a2..45f325a1 100644
--- a/tsconfig.app.json
+++ b/tsconfig.app.json
@@ -2,7 +2,7 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
- "types": []
+ "types": ["node"]
},
"files": [
"src/main.ts",
diff --git a/tsconfig.json b/tsconfig.json
index 30956ae7..e971c9ba 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -11,6 +11,7 @@
"moduleResolution": "node",
"importHelpers": true,
"target": "es2015",
+ "types": ["node"],
"typeRoots": [
"node_modules/@types"
],