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" ],