gaf-mobile
Version:
GAF mobile Web site
472 lines (419 loc) • 14.9 kB
JavaScript
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);
});
});
;
angular.