"use strict"; (function () { 'use strict'; /** * @ngdoc directive * @name myitsmApp:chatWindow * @restrict AE * @desription Chat windows are used for user chatting. It's a draggable window which contains chat message history and * chat controls bar. From chat window one can invite other user for a quick discussion of some issue. Also there is an option * to relate conversation with specific app instance and save chat log to instance timeline, at the end of conversation */ angular.module('myitsmApp') .directive('chatWindow', ['$filter', '$state', 'personModel', 'chatModel', 'searchModel', 'systemAlertService', '$window', 'userModel', '$timeout', function ($filter, $state, personModel, chatModel, searchModel, systemAlertService, $window, userModel, $timeout) { return { restrict: 'AE', templateUrl: 'components/chat/chat-window.html', replace: true, scope: { chatRoom: "=", isPopupWindow: "@" }, link: function (scope, $element) { var popupMode = !!scope.isPopupWindow; scope.parentWindow = window.opener; //Will be null if not popup window function parseStorageString(key, cacheString) { var storedValue = cacheString || localStorage.getItem(key); try { storedValue = JSON.parse(storedValue) || []; } catch (e) { storedValue = []; } return storedValue; } function onStorageEvent(storageEvent) { if (storageEvent.key != 'app.popup.setFocusRequest' || storageEvent.newValue != scope.chatRoom.id) return; localStorage.setItem('app.popup.setFocusRequest', ""); console.log("Already opened"); } if (popupMode) { var popupsList = parseStorageString('app.popup.rooms'); popupsList.push(scope.chatRoom.id); localStorage['app.popup.rooms'] = JSON.stringify(popupsList); $window.document.body.style.minWidth = 0; $window.document.body.style.overflow = "hidden"; $window.addEventListener('storage', onStorageEvent, false); $window.onbeforeunload = function () { var activeChatPopups = parseStorageString('app.popup.rooms'), updatedPopupsList = _.without(activeChatPopups, scope.chatRoom.id) || []; localStorage.setItem('app.popup.rooms', JSON.stringify(updatedPopupsList)); }; } else $element.draggable({ containment: "parent", scroll: false, handle: ".drag-handle" }); scope.state = { searchUserFormActive: false, searchTicketProfileActive: false, searchInProgress: false }; scope.userModel = userModel; scope.pendingSearchPromises = []; scope.chatActions = {}; scope.searchBar = { selectedItem: null }; scope.messageEditor = { messageBody: '' }; scope.chatRoom.roster = {}; scope.headerText = ""; var textAreaElement = $element.find(".chat__message-editor"); textAreaElement.focus(); /** * @ngdoc method * @name myitsmApp.chatWindow#getChatParticipantsData * @methodOf myitsmApp.chatWindow * * @description * Fills chat participants with their profiles information. There are two scenarios * of profile data retrieve - by loginId or by jid values, depending on which of two values is available * */ var getChatParticipantsData = function () { var chatProfiles = { getProfileForJids: [] }; _.each(scope.chatRoom.participants, function (participant) { var userJID = Strophe.getNodeFromJid(participant.jid); if (userJID != chatModel.Client.conn.authcid) { if (chatModel.cachedProfiles[userJID] && chatModel.cachedProfiles[userJID].firstName) { scope.chatRoom.roster[userJID] = chatModel.cachedProfiles[userJID]; } else { chatProfiles.getProfileForJids.push(userJID); } } }); if (chatProfiles.getProfileForJids.length) { chatModel.getUserProfileByJID(chatProfiles.getProfileForJids).then(function () { _.each(chatProfiles.getProfileForJids, function (jid) { scope.chatRoom.roster[jid] = chatModel.cachedProfiles[jid]; }); }); } }; /*Watching, when participants list changes*/ scope.$watch(function () { return scope.chatRoom.participants; }, function (newValue) { if (newValue) { getChatParticipantsData(); } }); /*Watching, when user adds text in search input*/ scope.$watch('searchBar.searchParam', function (newVal, oldVal) { if (newVal && (newVal != oldVal)) { scope.searchBar.handleUserQuery(); } if (oldVal && !newVal) { scope.pendingSearchPromises = []; scope.state.searchInProgress = false; scope.searchBar.resultsList = null; } }); /*Generates participants string, to show on chat window header*/ scope.generateChatHeaderCaption = function () { if (scope.chatRoom.generateRosterSummary) { return scope.chatRoom.generateRosterSummary(); } var profilesList = _.toArray(scope.chatRoom.roster), namesArray = _.map(scope.chatRoom.roster, function (profile) { if (profilesList.length < 3) { return profile.fullName || profile.displayName; } return profile.firstName; }); return namesArray.length > 0 ? namesArray.join(", ") : $filter('i18n')('chat.header.generic'); }; /*List of available chat window actions*/ scope.chatControls = [ { label: 'Leave Chat', iconClass: 'icon-exit', name: 'leaveChat' }, { label: 'Invite User', iconClass: 'icon-user_plus', name: 'showSearchForm', args: 'invite' }, { label: 'Popout Chat', iconClass: 'icon-pop_up', name: 'popoutChatWindow', disabled: scope.isPopupWindow }, { label: 'Popin Chat', iconClass: 'icon-pop_in', name: 'popinChatWindow', disabled: !scope.isPopupWindow || scope.parentWindow && scope.parentWindow.closed }, { label: 'Minimize Chat', iconClass: 'glyphicon glyphicon-minus', name: 'minimize', disabled: scope.isPopupWindow } ]; scope.redirectToAsignedItem = function () { var relatedObject = scope.chatRoom.parent, url; if (!relatedObject) { return false; } if (relatedObject.ticketType == EntityVO.TYPE_ASSET) { if (scope.isPopupWindow) { url = $state.href(EntityVO.TYPE_ASSET, { assetId: relatedObject.reconciliationId, assetClassId: relatedObject.classId }); window.open(url, "_blank"); return; } $state.go(EntityVO.TYPE_ASSET, { assetId: relatedObject.reconciliationId, assetClassId: relatedObject.classId }); } else { if (scope.isPopupWindow) { url = $state.href(relatedObject.type, { id: relatedObject.id }); window.open(url, "_blank"); return; } $state.go(relatedObject.type, { id: relatedObject.id }); } }; /*Sets proper class for chat controls icon*/ scope.getConditionalClasses = function (control) { if (control.name == 'showSearchForm') { return scope.state.searchUserFormActive ? 'active' : angular.noop(); } }; /*Chat controls click event transmitter. Uses controls item, to execute applicable click handler */ scope.handleChatControlsClick = function ($e, action) { if (scope.chatRoom.inactiveRoom) return false; return scope.chatActions[action.name](action.args); }; /*Removes chat window relation to app instance, if one was specified*/ scope.removeChatAssignment = function () { scope.chatRoom.loadingAssignments = true; chatModel.removeChatAssignment(scope.chatRoom.room).finally(function () { scope.chatRoom.loadingAssignments = false; }); }; scope.chatActions.popoutChatWindow = function () { var activeChatPopupsList = localStorage.getItem('app.popup.rooms') || "[]", popupURL = '#/chatPopupWindow'; if (activeChatPopupsList.indexOf(scope.chatRoom.id) >= 0) { localStorage.setItem('app.popup.setFocusRequest', scope.chatRoom.id); scope.chatActions.minimize(); return; } $window.popupDetails = { roomId: scope.chatRoom.id }; var minWidth = 400, minHeight = 500, width = Math.round((screen.width / 3) / 10) * 10, height = Math.round(screen.height * 70 / 100), popupHeight = _.max([minHeight, height]), popupWidth = _.max([minWidth, width]), popupName = "chatPopup_" + (Strophe.getNodeFromJid(scope.chatRoom.id) || "").split("-").join("_"), popupParams = "location=no, scrollbars=no, resizable=yes, width=" + popupWidth + ", height=" + popupHeight, popup = $window.open(popupURL, popupName, popupParams); popup.focus(); scope.chatActions.minimize(); }; scope.chatActions.popinChatWindow = function () { var parentScope, activeChatRoom; if (scope.parentWindow.closed) return; try { parentScope = scope.parentWindow.angular.element('body').scope(); activeChatRoom = _.findWhere(parentScope.chatModel.activeChatRooms, { id: scope.chatRoom.id }); } catch (e) { } if (parentScope && activeChatRoom) { parentScope.$apply(function () { activeChatRoom.isOpened = true; }); window.close(); } }; /*Minimizes chat window */ scope.chatActions.minimize = function () { scope.chatRoom.isOpened = false; }; /** * @ngdoc method * @name myitsmApp.chatWindow#leaveChat * @methodOf myitsmApp.chatWindow * * @description * Action can be triggered from chat controls, by clicking leave chat button. It prompts * confirmation modal with available actions based on user privileges within the chat * If chat creator leaves chat it will lead to destroy of the chat windows for all participants * Also owner can save chat log to a timeline of related app instance, if one was selected * */ scope.chatActions.leaveChat = function () { if (scope.chatRoom.isLoading) return; var nick = Strophe.escapeNode(scope.chatRoom.room.nick), affiliation = scope.chatRoom.participants[nick].affiliation, modalSettings; var modalText = $filter('i18n')('chat.participantLeave.modal.confirmAction.leave'), modalConfirm = { leave: true }; if (affiliation == 'owner') { if (scope.chatRoom.parent) { modalText = $filter('i18n')('chat.ownerLeave.modal.confirmAction.saveLog'); modalConfirm = { save: true }; } else { modalText = $filter('i18n')('chat.ownerLeave.modal.confirmAction.leave'); modalConfirm = { close: true }; } } modalSettings = { title: $filter('i18n')('chat.ownerLeave.modal.title'), text: modalText, buttons: [ { text: $filter('i18n')('common.labels.leave'), data: modalConfirm }, { text: $filter('i18n')('common.button.cancel') } ], isPopoutWindow: true }; var modalInstance = systemAlertService.modal(modalSettings); modalInstance.result.then(function (data) { if (!_.isEmpty(data)) { var userAction = data ? (data.save ? 'saveLog' : 'destroyRoom') : 'leave'; if (scope.isPopupWindow) { var parentScope, activeChatRoom; try { parentScope = scope.parentWindow.angular.element('body').scope(); activeChatRoom = _.findWhere(parentScope.chatModel.activeChatRooms, { id: scope.chatRoom.id }); if (parentScope && activeChatRoom) { parentScope.$apply(function () { activeChatRoom.isOpened = false; (userAction == 'saveLog') && parentScope.chatModel.saveChatLogToTicketWorknote(activeChatRoom, activeChatRoom.parent); (userAction != 'leave') && parentScope.chatModel.leaveRoomAndDestroyChatWindow(activeChatRoom); (userAction == 'leave') && parentScope.chatModel.leaveConversation(activeChatRoom); }); } } catch (e) { } window.close(); } else { scope.chatRoom.isOpened = false; (userAction == 'saveLog') && chatModel.saveChatLogToTicketWorknote(scope.chatRoom, scope.chatRoom.parent); (userAction != 'leave') && chatModel.leaveRoomAndDestroyChatWindow(scope.chatRoom); (userAction == 'leave') && chatModel.leaveConversation(scope.chatRoom); } } }); }; /** * @ngdoc method * @name myitsmApp.chatWindow#showSearchForm * @methodOf myitsmApp.chatWindow * * @description * Shows search bar to the user, when one clicks invite person button or relate chat link. Controls search context, * based clicked item. * */ scope.chatActions.showSearchForm = function (type) { if (scope.chatRoom.isLoading) return; if (type == 'connect') { scope.searchBar.clearSelection(); scope.state.searchTicketProfileActive = true; scope.state.searchUserFormActive = false; } else if (type == 'invite') { if (!scope.state.searchUserFormActive) { scope.searchBar.searchParam = null; scope.searchBar.clearSelection(); scope.state.searchUserFormActive = true; scope.state.searchTicketProfileActive = false; scope.filteredList = []; } else { scope.searchBar.clearSelection(); scope.state.searchUserFormActive = false; } } $timeout(function () { try { $element.find("[ng-model*='searchParam']")[0].focus(); } catch (ex) { } }, 500); }; scope.searchBar.suggestionMouseOver = function (index) { var hoveredItem = scope.filteredList[index]; if (scope.state.searchUserFormActive && hoveredItem.active != EntityVO.CHAT_STATUS_ONLINE) { return false; } scope.searchBar.alignWithTop = null; scope.searchBar.selectedSuggestionIndex = index; }; /*Keydown event handler for search input. Clears and hides search input when Esc is pressed*/ scope.searchBar.checkPressedKey = function ($event) { var keyCode = $event.keyCode || $event.which; if (keyCode == 27) { scope.searchBar.clearSelection(); scope.state.searchUserFormActive = false; scope.state.searchTicketProfileActive = false; } if (keyCode == 13) { $event.preventDefault(); var selectedItem = scope.filteredList[scope.searchBar.selectedSuggestionIndex]; if (!selectedItem || (selectedItem && scope.state.searchUserFormActive && selectedItem.available != EntityVO.CHAT_STATUS_ONLINE)) { return false; } else { scope.searchBar.selectSuggestion(selectedItem); } } if (keyCode == 38 || keyCode == 40) { if (scope.state.searchUserFormActive || scope.state.searchTicketProfileActive && scope.filteredList && scope.searchBar.resultsList.length > 0) { $event.preventDefault(); if (keyCode == 38) { if (scope.searchBar.selectedSuggestionIndex == 0) return false; else { scope.searchBar.alignWithTop = true; scope.searchBar.selectedSuggestionIndex--; } return false; } else { if (scope.searchBar.selectedSuggestionIndex == scope.filteredList.length - 1) return false; else { var nextIndex = scope.searchBar.selectedSuggestionIndex + 1, nextItem = scope.filteredList[nextIndex]; if (scope.state.searchUserFormActive && nextItem && nextItem.available != EntityVO.CHAT_STATUS_ONLINE) return false; else { scope.searchBar.alignWithTop = false; scope.searchBar.selectedSuggestionIndex++; } } return false; } } } return true; }; /** * Handles user search text input and executes server request to retrieve user profiles or instance list, if search text * pass length check * */ scope.searchBar.handleUserQuery = function () { var query = scope.searchBar.searchParam; if (scope.searchBar.selectedItem) { return; } if (!query) { scope.searchBar.selectedSuggestionIndex = 0; scope.searchBar.resultsList = []; scope.filteredList = []; return; } if (query.length >= 3) { scope.searchBar.selectedSuggestionIndex = 0; scope.state.searchInProgress = true; scope.getSearchResultsDebounce(query); } else { scope.searchBar.resultsList = []; scope.filteredList = []; } }; /*Controls placeholder value, based on context of search, selected by user*/ scope.searchBar.getSearchBarPlaceholder = function () { if (scope.state.searchUserFormActive) { return $filter('i18n')('chat.searchBar.userSearch.placeholder'); } else if (scope.state.searchTicketProfileActive) { return $filter('i18n')('chat.searchBar.ticketProfile.placeholder'); } }; /* * Handles item selection from suggestions list, returned from the server. * Fills search input with selected value. Next step should be confirmation or cancel * of selection * */ scope.searchBar.selectSuggestion = function (item) { if (item.hasOwnProperty('firstName') || item.hasOwnProperty('loginId')) { var available = item.available ? item.available.toLowerCase() : EntityVO.CHAT_STATUS_OFFLINE; if (available != EntityVO.CHAT_STATUS_ONLINE) { return; } } scope.searchBar.selectedItem = item; scope.searchBar.confirmSelection(); $timeout(function () { try { $element.find(".chat__search-bar_confirm-action")[0].focus(); } catch (ex) { } }, 500); }; /*Cancels suggested item selction*/ scope.searchBar.clearSelection = function () { scope.searchBar.selectedItem = null; scope.searchBar.searchParam = ''; scope.searchBar.resultsList = []; }; /** * Confirms selection and based on context * relate chat to app instance or invites user to chat */ scope.searchBar.confirmSelection = function () { if (scope.state.searchUserFormActive) { scope.chatRoom.roster[scope.searchBar.selectedItem.jid] = scope.searchBar.selectedItem; chatModel.inviteUserToChat(scope.searchBar.selectedItem, scope.chatRoom); scope.state.searchUserFormActive = false; } else { chatModel.assignChatRoom(scope.chatRoom.room, scope.searchBar.selectedItem); scope.chatRoom.parent = scope.searchBar.selectedItem; scope.state.searchTicketProfileActive = false; } scope.searchBar.clearSelection(); }; /*Used in ngRepeat as sorting function in suggestions list of invitees*/ scope.sortResults = function (item) { if (scope.state.searchUserFormActive) { !!item.available ? item.available = item.available.toLowerCase() : item.available = EntityVO.CHAT_STATUS_OFFLINE; var statusIndex = chatModel.Client.options.users_status_list.indexOf(item.available); return statusIndex + (item.fullName || item.firstName + " " + item.lastName); } }; /*Debounce function, which runs request to a server to get profiles or instances matching * user search criteria. * It will execute server request only when user stops typing or makes a pause in typing equal to 400 ms * Data returned from server will be used to build suggestions list. * */ scope.getSearchResultsDebounce = _.debounce(function (query) { var promise; if (scope.state.searchUserFormActive) { promise = personModel.getListOfPersonByName(query) .then(function (profiles) { chatModel.updateCachedProfiles(profiles); scope.filteredList = []; if (scope.searchBar.searchParam && !scope.searchBar.selectedItem && scope.state.searchUserFormActive) { var participants = scope.chatRoom.getParticipantJIDsArray(); scope.searchBar.resultsList = _.filter(profiles, function (profile) { var currentUserMatch = (chatModel.currentUser.loginId == profile.loginId); if (profile.jid) { var chatParticipant = (participants.indexOf(profile.jid) >= 0); } return !chatParticipant && !currentUserMatch; }); } }); } else { var filters = { types: [EntityVO.TYPE_INCIDENT, EntityVO.TYPE_WORKORDER, EntityVO.TYPE_SERVICEREQUEST, EntityVO.TYPE_TASK, EntityVO.TYPE_ASSET, EntityVO.TYPE_CHANGE, EntityVO.TYPE_KNOWNERROR, EntityVO.TYPE_PROBLEM, EntityVO.TYPE_RELEASE] }; promise = searchModel.getGlobalSearchResults(query, filters) .then(function () { if (scope.pendingSearchPromises.length == 0) { return; } if (scope.searchBar.searchParam && !scope.searchBar.selectedItem && scope.state.searchTicketProfileActive) { if (searchModel.globalSearchResults.totalCount > 0) { scope.searchBar.resultsList = []; var matchedItems = searchModel.globalSearchResults.items; _.each(matchedItems, function (item) { scope.searchBar.resultsList = scope.searchBar.resultsList.concat(item.results); }); } else { scope.searchBar.resultsList = []; } } }); } scope.pendingSearchPromises.push(promise); promise.finally(function () { var promiseIndex = scope.pendingSearchPromises.indexOf(promise); if (promiseIndex >= 0) { scope.pendingSearchPromises.splice(promiseIndex, 1); } if (scope.pendingSearchPromises.length == 0) { scope.state.searchInProgress = false; } }); }, 400); scope.tools = function () { chatModel.processChatMessage(EntityVO.CHATOPS_TOOLS, scope.chatRoom); }; /* * keypress event handler, which looks for enter button click and sends * new chat message, if shift button is not active * */ scope.messageEditor.handleTyping = function ($event) { var keyCode = $event.which || $event.keyCode; if (keyCode == 13 && !$event.shiftKey && scope.messageEditor.messageBody) { var messageText = scope.messageEditor.messageBody; if (scope.chatRoom.parent != null && scope.chatRoom.parent.type == 'incident') { var chatR = scope.chatRoom; if (messageText == EntityVO.CHATOPS_DETAILS || messageText == EntityVO.CHATOPS_KNOWLEDGE || messageText == EntityVO.CHATOPS_SIMILARINCIDENTS || messageText == EntityVO.CHATOPS_SWARM || messageText == EntityVO.CHATOPS_TOOLS) { // chatModel.sendChatMessage(scope.messageEditor.messageBody, scope.chatRoom.room); chatModel.processChatMessage(messageText, chatR); } else { chatModel.sendChatMessage(scope.messageEditor.messageBody, scope.chatRoom.room); } } else { chatModel.sendChatMessage(scope.messageEditor.messageBody, scope.chatRoom.room); } scope.messageEditor.messageBody = ''; $event.preventDefault(); } }; } }; }]); }());