UNPKG

gaf-mobile

Version:

GAF mobile Web site

472 lines (419 loc) 14.9 kB
'use strict'; angular.module('gafMobileApp') /** * @ngdoc directive * @name gafMobileApp.thread * @element E * @function * * @description * Component for showing user's thread based on thread id or project id and * for user id from URL params * * @example * <pre> * <thread></thread> * </pre> */ .directive('thread', function() { return { restrict: 'E', templateUrl: 'components/thread/thread.html', scope: {}, controller: 'ThreadCtrl', controllerAs: 'ctrl', bindTo: true }; }); /** * @ngdoc controller * @name gafMobileApp.ThreadCtrl * @description * Controller for Thread */ angular.module('gafMobileApp') .controller('ThreadCtrl', function($q, $route, $location, $scope, $timeout, $window, $anchorScroll, $document, Threads, Messages, Auth, Projects, Bids, Analytics, Experiments, MSG_SOURCE_TYPE) { var _this = this; /** * @ngdoc property * @name gafMobileApp.ThreadCtrl#user * @propertyOf gafMobileApp.ThreadCtrl * @description * Stores the user id of the logged in user */ _this.user = Auth.getUserId(); /** * @ngdoc property * @name gafMobileApp.ThreadCtrl#messages * @propertyOf gafMobileApp.ThreadCtrl * @description * List of user messages displayed on view */ _this.messages = []; /** * @ngdoc property * @name gafMobileApp.ThreadCtrl#state * @propertyOf gafMobileApp.ThreadCtrl * @description * Stores the overall view state of the page, can be: 'loading', 'empty', * 'error', 'active'. Upon init, state is `loading`. */ _this.state = 'loading'; /** * @ngdoc property * @name gafMobileApp.ThreadCtrl#error * @propertyOf gafMobileApp.ThreadCtrl * @description * Stores the existing errors in the module */ _this.error = {}; /** * @ngdoc property * @name gafMobileApp.ThreadCtrl#messageSentHandlers * @propertyOf gafMobileApp.ThreadCtrl * @description * Stores the existing handlers in the module that are called * after the user sends a message */ _this.messageSentHandlers = []; /** * @ngdoc property * @name gafMobileApp.ThreadCtrl#link * @propertyOf gafMobileApp.ThreadCtrl * @description * Link used to redirect users when username is clicked */ _this.link = null; _this.hasMoreMessages = true; var isLoadingMessages = false; var getOtherUser = function(thread) { var thrInfo = thread.get(); var users = thread.getUsers(); // Gets the profile of the other user in the thread return thrInfo.thread.members[0] !== Auth.getUserId() ? users[thrInfo.thread.members[0]] : users[thrInfo.thread.members[1]]; }; /** * @ngdoc method * @name gafMobileApp.ThreadCtrl#postNewMessage * @methodOf gafMobileApp.ThreadCtrl * @param {String} message The message that needs to be sent * @description * Sends a new message into an existing thread. If a `newThread = true`, * this creates a new thread with the projectId and employerId obtained * from URL params. */ _this.sendMessage = function(thread, toUser, message) { if (_this.messages.length > 0) { var currentId = _this.messagesObj.addFakeMessage(message, null, thread.get().id); _this.messages = _this.messagesObj.get(); _this.newMessage = ''; Threads.sendMessage(thread.get().id, message, MSG_SOURCE_TYPE, Date.now()) .catch(function(error) { var currentMessage = _this.messagesObj.getFakeMessages(currentId)[0]; currentMessage.errors.push(error.data); }); // trigger handlers for message sent event _this.messageSentHandlers.forEach(function(handler) { handler(); }); } else { var contextType = thread.get().thread.context.type; var otherMember = thread.get().thread.members[0]; var createThread; if (contextType === 'Project' || contextType === 'project') { createThread = Threads.createThread('private_chat', 'project', thread.get().thread.context.id, otherMember, message); } else if (contextType === 'none') { createThread = Threads.createThread('primary', null, null, otherMember, message); } if (createThread) { createThread.then(function() { // TODO: Fix this once Threads.createThread can return user_details // for the created thread $route.reload(); }).catch(function(error) { _this.state = 'displayError'; if (error.code === 'PERMISSION_DENIED') { _this.error.permissionDenied = true; } }); } } }; /** * @ngdoc method * @name gafMobileApp.ThreadCtrl#goToPreviousPage * @methodOf gafMobileApp.ThreadCtrl * @description * Goes back the previous page */ _this.goToPreviousPage = function() { $window.history.back(); }; /** * @ngdoc method * @name gafMobileApp.ThreadCtrl#onMessageSent * @methodOf gafMobileApp.ThreadCtrl * @param {Function} handler The handler that will be called after sending * a message * @description * Adds the handler to the list and returns a function that removes it from * the list when called */ _this.onMessageSent = function(handler) { _this.messageSentHandlers.push(handler); }; /** * @ngdoc method * @name gafMobileApp.ThreadCtrl#isTyping * @methodOf gafMobileApp.ThreadCtrl * @description * Returns true if other user's typing status is active, false if not */ _this.isTyping = function() { if (_this.threadObj) { return _this.threadObj.get().typing; } }; /** * @ngdoc method * @name gafMobileApp.ThreadCtrl#textEntered * @methodOf gafMobileApp.ThreadCtrl * @description * Dispatches `chatEntertext` event for sending out logged in user's * typing status */ _this.textEntered = function() { // If this is not a new thread and thread object exists, emit event if (_this.threadObj) { $scope.$emit('chatEnterText'); _this.threadObj.type(); } }; /** * @ngdoc method * @name gafMobileApp.ThreadCtrl#loadMoreMessages * @methodOf gafMobileApp.ThreadCtrl * @description * Lazy loads messages into view when user reaches top of thread div */ _this.loadMoreMessages = function(reset) { var currentMessages = reset ? [] : _this.messages; if (!isLoadingMessages && (_this.hasMoreMessages || reset)) { isLoadingMessages = true; return Messages.getList({ 'threads[]': _this.threadObj.get().id, 'limit': 50, 'offset': currentMessages.length, 'tag': 'websocketReloadMobileWeb' }).then(function(response) { _this.messages = currentMessages; // set scroll position to where user initiated loading more messages var currentMessage = _this.messages[0]; if (currentMessage && currentMessage.id) { $timeout(function() { $anchorScroll(currentMessage.id.toString()); }, 0); } // add fetched messages to start of current list _this.messagesObj = _this.messagesObj || response; var messages = response.get(); messages.forEach(function(message) { _this.messages.unshift(message); }); _this.hasMoreMessages = (messages.length > 0); }) .catch(function() { _this.state = 'displayError'; _this.error.unknownError = true; }) .finally(function() { isLoadingMessages = false; _this.state = _this.messages.length > 0 ? 'active' : 'empty'; // TO DO: Clean up once AB Test is done // Case where we have a thread with an empty message if (_this.state === 'empty') { _this.newChatBgAbTest = Experiments.activateTest('NewChatBackground', true); _this.isOwner = _this.threadObj.getProject().get() ? _this.threadObj.getProject().isOwner() : false; } }); } else { return $q.when(); } }; /** * @ngdoc method * @name gafMobileApp.ThreadCtrl#canAwardBid * @methodOf gafMobileApp.ThreadCtrl * @description * Returns true if bid can be awarded */ _this.canAwardBid = function() { return _this.selectedBid ? _this.selectedBid.canBeAwarded(): null; }; /** * @ngdoc method * @name gafMobileApp.ThreadCtrl#canAcceptProject * @methodOf gafMobileApp.ThreadCtrl * @description * Returns true if project can be accepted */ _this.canAcceptProject = function() { return _this.selectedBid ? _this.selectedBid.canBeAccepted() : null; }; /** * @ngdoc method * @name gafMobileApp.ThreadCtrl#canCreateMilestone * @methodOf gafMobileApp.ThreadCtrl * @description * Returns true if milestone can be created for project */ _this.canCreateMilestone = function() { return _this.selectedBid ? _this.selectedBid.canCreateMilestone() : null; }; /** * @ngdoc method * @name gafMobileApp.ThreadCtrl#canRequestMilestone * @methodOf gafMobileApp.ThreadCtrl * @description * Returns true if milestone can be requested for project */ _this.canRequestMilestone = function() { return _this.selectedBid ? _this.selectedBid.canRequestMilestone() : null; }; var threadPromise = $q.reject(); var params = { 'context_details': true, 'user_details': true }; // URL parameters for getting thread var threadId = parseInt($route.current.params.threadId); var projectId = parseInt($location.search().pid); var forUserId = parseInt($location.search().uid); var bidId = parseInt($location.search().bid); if (threadId) { threadPromise = Threads.getById(threadId, params); } else if (projectId && forUserId) { threadPromise = Threads.getList(angular.extend({ 'contexts[]': projectId, 'members[]': forUserId, 'context_type': 'Project' }, params)).then(function(threads) { var thread = threads.getList().filter(function(element) { return element.get().thread.thread_type === 'private_chat'; }); return thread[0]; }); } else if (forUserId) { threadPromise = Threads.getList(angular.extend({ 'members[]': forUserId, 'thread_type': 'primary' }, params)).then(function(threads) { return threads.getList()[0]; }); } else { $location.url('/messages'); } threadPromise.then(function(thread) { if (!thread) { // Handles new threads for `context_type = Project` if (projectId && forUserId && bidId) { _this.threadObj = Threads.createEmptyThread('private_chat', 'Project', projectId, [forUserId]); // TO DO: Clean up once AB Test is done _this.newChatBgAbTest = Experiments.activateTest('NewChatBackground', true); if (!_this.newChatBgAbTest) { _this.state = 'empty'; } loadBidAndProjectDetails(bidId, projectId); } else if (forUserId) { _this.threadObj = Threads.createEmptyThread('primary', 'none', null, [Auth.getUserId(), forUserId]); // Gets the profile of the other user in the thread _this.threadObj.fetchUserDetails().then(function() { _this.otherUser = getOtherUser(_this.threadObj); _this.link = '/u/' + _this.otherUser.username; }); _this.state = 'empty'; } else { // user is not involved in the chat thread $location.url('/messages'); } } else { _this.threadObj = thread; _this.threadType = _this.threadObj.get().thread.thread_type; // Gets the profile of the other user in the thread _this.otherUser = getOtherUser(_this.threadObj); _this.link = '/u/' + _this.otherUser.username; // TO DO: Clean up once OnlineOffline AB Test is done _this.showOnlineOfflineStatus = Experiments.activateTest('OnlineOfflineStatus', true); // Gets the profile of the project if project exists if (_this.threadObj.getProject()) { _this.project = _this.threadObj.getProject().get(); if (_this.project) { _this.link = '/projects/' + _this.project.seo_url; } } if (_this.threadObj.getContextType() !== 'none') { _this.threadObj.fetchBidDetails().then(function(response) { if (response) { _this.selectedBid = response.getBidsList()[0]; } }); } return _this.loadMoreMessages(); } }).catch(function() { _this.error.unknownError = true; _this.state = 'displayError'; }); function loadBidAndProjectDetails(bidId, projectId) { var p = []; p.push(Bids.get({ bidId: bidId, user_details: true })); p.push(Projects.getById(projectId)); $q.all(p).then(function(response) { var bidData = response[0]; var projData = response[1]; _this.selectedBid = bidData; _this.otherUser = bidData.get().owner; _this.project = projData.get(); // TO DO: Clean up once OnlineOffline AB Test is done _this.showOnlineOfflineStatus = Experiments.activateTest('OnlineOfflineStatus', true); // TO DO: Clean up once newChatBg AB Test is done _this.state = 'empty'; _this.isOwner = projData.isOwner(); _this.link = '/projects/' + _this.project.seo_url; }); } // Subscribes to notifications for a new message has arrived $scope.$on('messages:private', function() { if (_this.threadObj) { var newMessages = _this.threadObj.getMessages().get(); // TODO: Known constraint here is that when user sends message from // other device it will not push here. To do that we must ensure that // Notification service can work in most mobile devices that we // support if (newMessages.length > 0 && newMessages[0].from_user !== Auth.getUserId()) { $scope.$apply(function() { _this.messages.push(newMessages[0]); }); } } }); $document.on('libnotify.connected', function() { // _this.loadMoreMessages(true); Analytics.trackEvent('Thread', 'ReloadingMessages', 'UserId', _this.user); }); });