// (C) Copyright 2015 Martin Dougiamas // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; import { IonicPage, Platform, NavController, NavParams, Content } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; import { AddonMessagesProvider } from '../../providers/messages'; import { AddonMessagesOfflineProvider } from '../../providers/messages-offline'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { AddonPushNotificationsDelegate } from '@addon/pushnotifications/providers/delegate'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreUserProvider } from '@core/user/providers/user'; /** * Page that displays the list of conversations, including group conversations. */ @IonicPage({ segment: 'addon-messages-group-conversations' }) @Component({ selector: 'page-addon-messages-group-conversations', templateUrl: 'group-conversations.html', }) export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { @ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent; @ViewChild(Content) content: Content; loaded = false; loadingMessage: string; selectedConversationId: number; selectedUserId: number; contactRequestsCount = 0; favourites: any = { type: null, favourites: true, count: 0, unread: 0 }; group: any = { type: AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_GROUP, favourites: false, count: 0, unread: 0 }; individual: any = { type: AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, favourites: false, count: 0, unread: 0 }; typeIndividual = AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_INDIVIDUAL; protected loadingString: string; protected siteId: string; protected currentUserId: number; protected conversationId: number; protected newMessagesObserver: any; protected pushObserver: any; protected appResumeSubscription: any; protected readChangedObserver: any; protected cronObserver: any; protected openConversationObserver: any; protected updateConversationListObserver: any; protected contactRequestsCountObserver: any; protected memberInfoObserver: any; constructor(eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, translate: TranslateService, private messagesProvider: AddonMessagesProvider, private domUtils: CoreDomUtilsProvider, navParams: NavParams, private navCtrl: NavController, platform: Platform, private utils: CoreUtilsProvider, pushNotificationsDelegate: AddonPushNotificationsDelegate, private messagesOffline: AddonMessagesOfflineProvider, private userProvider: CoreUserProvider) { this.loadingString = translate.instant('core.loading'); this.siteId = sitesProvider.getCurrentSiteId(); this.currentUserId = sitesProvider.getCurrentSiteUserId(); this.conversationId = navParams.get('conversationId') || false; // Update conversations when new message is received. this.newMessagesObserver = eventsProvider.on(AddonMessagesProvider.NEW_MESSAGE_EVENT, (data) => { // Search the conversation to update. const conversation = this.findConversation(data.conversationId, data.userId); if (typeof conversation == 'undefined') { // Probably a new conversation, refresh the list. this.loaded = false; this.refreshData().finally(() => { this.loaded = true; }); } else if (conversation.lastmessage != data.message || conversation.lastmessagedate != data.timecreated / 1000) { const isNewer = data.timecreated / 1000 > conversation.lastmessagedate; // An existing conversation has a new message, update the last message. conversation.lastmessage = data.message; conversation.lastmessagedate = data.timecreated / 1000; // Sort the affected list. const option = this.getConversationOption(conversation); option.conversations = this.messagesProvider.sortConversations(option.conversations); if (isNewer) { // The last message is newer than the previous one, scroll to top to keep viewing the conversation. this.domUtils.scrollToTop(this.content); } } }, this.siteId); // Update conversations when a message is read. this.readChangedObserver = eventsProvider.on(AddonMessagesProvider.READ_CHANGED_EVENT, (data) => { if (data.conversationId) { const conversation = this.findConversation(data.conversationId); if (typeof conversation != 'undefined') { // A conversation has been read reset counter. conversation.unreadcount = 0; // Conversations changed, invalidate them and refresh unread counts. this.messagesProvider.invalidateConversations(); this.messagesProvider.refreshUnreadConversationCounts(); } } }, this.siteId); // Load a discussion if we receive an event to do so. this.openConversationObserver = eventsProvider.on(AddonMessagesProvider.OPEN_CONVERSATION_EVENT, (data) => { if (data.conversationId || data.userId) { this.gotoConversation(data.conversationId, data.userId, undefined, true); } }, this.siteId); // Refresh the view when the app is resumed. this.appResumeSubscription = platform.resume.subscribe(() => { if (!this.loaded) { return; } this.loaded = false; this.refreshData().finally(() => { this.loaded = true; }); }); // Update conversations if we receive an event to do so. this.updateConversationListObserver = eventsProvider.on(AddonMessagesProvider.UPDATE_CONVERSATION_LIST_EVENT, () => { this.refreshData(); }, this.siteId); // If a message push notification is received, refresh the view. this.pushObserver = pushNotificationsDelegate.on('receive').subscribe((notification) => { // New message received. If it's from current site, refresh the data. if (utils.isFalseOrZero(notification.notif) && notification.site == this.siteId) { // Don't refresh unread counts, it's refreshed from the main menu handler in this case. this.refreshData(null, false); } }); // Update unread conversation counts. this.cronObserver = eventsProvider.on(AddonMessagesProvider.UNREAD_CONVERSATION_COUNTS_EVENT, (data) => { this.favourites.unread = data.favourites; this.individual.unread = data.individual; this.group.unread = data.group; }, this.siteId); // Update the contact requests badge. this.contactRequestsCountObserver = eventsProvider.on(AddonMessagesProvider.CONTACT_REQUESTS_COUNT_EVENT, (data) => { this.contactRequestsCount = data.count; }, this.siteId); // Update block status of a user. this.memberInfoObserver = eventsProvider.on(AddonMessagesProvider.MEMBER_INFO_CHANGED_EVENT, (data) => { if (!data.userBlocked && !data.userUnblocked) { // The block status has not changed, ignore. return; } const updateConversations = (conversations: any[]): void => { if (!conversations || conversations.length <= 0) { return; } const conversation = conversations.find((conv) => conv.userid == data.userId); if (conversation) { conversation.isblocked = data.userBlocked; } }; updateConversations(this.individual.conversations); updateConversations(this.favourites.conversations); }, this.siteId); } /** * Component loaded. */ ngOnInit(): void { if (this.conversationId) { // There is a discussion to load, open the discussion in a new state. this.gotoConversation(this.conversationId); } this.fetchData().then(() => { if (!this.conversationId && this.splitviewCtrl.isOn()) { // Load the first conversation. let conversation; if (this.favourites.expanded) { conversation = this.favourites.conversations[0]; } else if (this.group.expanded) { conversation = this.group.conversations[0]; } else if (this.individual.expanded) { conversation = this.individual.conversations[0]; } if (conversation) { this.gotoConversation(conversation.id); } } }); } /** * Fetch conversations. * * @return {Promise} Promise resolved when done. */ protected fetchData(): Promise { this.loadingMessage = this.loadingString; // Load the first conversations of each type. const promises = []; let offlineMessages; promises.push(this.fetchDataForOption(this.favourites, false)); promises.push(this.fetchDataForOption(this.group, false)); promises.push(this.fetchDataForOption(this.individual, false)); promises.push(this.fetchConversationCounts()); promises.push(this.messagesProvider.getUnreadConversationCounts()); // View updated by the event observer. promises.push(this.messagesProvider.getContactRequestsCount()); // View updated by the event observer. promises.push(this.messagesOffline.getAllMessages().then((messages) => { offlineMessages = messages; })); return Promise.all(promises).then(() => { return this.loadOfflineMessages(offlineMessages); }).then(() => { if (offlineMessages && offlineMessages.length) { // Sort the conversations, the offline messages could affect the order. this.favourites.conversations = this.messagesProvider.sortConversations(this.favourites.conversations); this.group.conversations = this.messagesProvider.sortConversations(this.group.conversations); this.individual.conversations = this.messagesProvider.sortConversations(this.individual.conversations); } if (typeof this.favourites.expanded == 'undefined') { // The expanded status hasn't been initialized. Do it now. if (this.conversationId) { // A certain conversation should be opened, expand its option. const conversation = this.findConversation(this.conversationId); if (conversation) { const option = this.getConversationOption(conversation); this.expandOption(option); return; } } // No conversation specified or not found, determine which one should be expanded. this.favourites.expanded = this.favourites.count != 0; this.group.expanded = this.favourites.count == 0 && this.group.count != 0; this.individual.expanded = this.favourites.count == 0 && this.group.count == 0; } }).catch((error) => { this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingdiscussions', true); }).finally(() => { this.loaded = true; }); } /** * Fetch data for a certain option. * * @param {any} option The option to fetch data for. * @param {boolean} [loadingMore} Whether we are loading more data or just the first ones. * @return {Promise} Promise resolved when done. */ fetchDataForOption(option: any, loadingMore?: boolean): Promise { option.loadMoreError = false; const limitFrom = loadingMore ? option.conversations.length : 0; return this.messagesProvider.getConversations(option.type, option.favourites, limitFrom).then((data) => { if (loadingMore) { option.conversations = option.conversations.concat(data.conversations); } else { option.conversations = data.conversations; } option.unread = 0; // @todo. option.canLoadMore = data.canLoadMore; }); } /** * Fetch conversation counts. * * @return {Promise} Promise resolved when done. */ protected fetchConversationCounts(): Promise { return this.messagesProvider.getConversationCounts().then((counts) => { this.favourites.count = counts.favourites; this.individual.count = counts.individual; this.group.count = counts.group; }); } /** * Find a conversation in the list of loaded conversations. * * @param {number} conversationId The conversation ID to search. * @param {number} userId User ID to search (if no conversationId). * @return {any} Conversation. */ protected findConversation(conversationId: number, userId?: number): any { if (conversationId) { const conversations = (this.favourites.conversations || []).concat(this.group.conversations || []) .concat(this.individual.conversations || []); return conversations.find((conv) => { return conv.id == conversationId; }); } const conversations = (this.favourites.conversations || []).concat(this.individual.conversations || []); return conversations.find((conv) => { return conv.userid == userId; }); } /** * Navigate to contacts view. */ gotoContacts(): void { this.splitviewCtrl.getMasterNav().push('AddonMessagesContactsPage'); } /** * Navigate to a particular conversation. * * @param {number} conversationId Conversation Id to load. * @param {number} userId User of the conversation. Only if there is no conversationId. * @param {number} [messageId] Message to scroll after loading the discussion. Used when searching. * @param {boolean} [scrollToConversation] Whether to scroll to the conversation. */ gotoConversation(conversationId: number, userId?: number, messageId?: number, scrollToConversation?: boolean): void { this.selectedConversationId = conversationId; this.selectedUserId = userId; const params = { conversationId: conversationId, userId: userId }; if (messageId) { params['message'] = messageId; } this.splitviewCtrl.push('AddonMessagesDiscussionPage', params); if (scrollToConversation) { // Search the conversation. const conversation = this.findConversation(conversationId, userId); if (conversation) { // First expand the option if it isn't expanded. const option = this.getConversationOption(conversation); this.expandOption(option); // Wait for the view to expand the option. setTimeout(() => { // Now scroll to the conversation. this.domUtils.scrollToElementBySelector(this.content, '#addon-message-conversation-' + (conversation.id ? conversation.id : 'user-' + conversation.userid)); }); } } } /** * Navigate to message settings. */ gotoSettings(): void { this.splitviewCtrl.push('AddonMessagesSettingsPage'); } /** * Function to load more conversations. * * @param {any} option The option to fetch data for. * @param {any} [infiniteComplete] Infinite scroll complete function. Only used from core-infinite-loading. * @return {Promise} Resolved when done. */ loadMoreConversations(option: any, infiniteComplete?: any): Promise { return this.fetchDataForOption(option, true).catch((error) => { this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingdiscussions', true); option.loadMoreError = true; }).finally(() => { infiniteComplete && infiniteComplete(); }); } /** * Load offline messages into the conversations. * * @param {any[]} messages Offline messages. * @return {Promise} Promise resolved when done. */ protected loadOfflineMessages(messages: any[]): Promise { const promises = []; messages.forEach((message) => { if (message.conversationid) { // It's an existing conversation. Search it. let conversation = this.findConversation(message.conversationid); if (conversation) { // Check if it's the last message. Offline messages are considered more recent than sent messages. if (typeof conversation.lastmessage === 'undefined' || conversation.lastmessage === null || !conversation.lastmessagepending || conversation.lastmessagedate <= message.timecreated / 1000) { this.addLastOfflineMessage(conversation, message); } } else { // Conversation not found, it's probably an old one. Add it. conversation = message.conversation || {}; conversation.id = message.conversationid; this.addLastOfflineMessage(conversation, message); this.addOfflineConversation(conversation); } } else { // Its a new conversation. Check if we already created it (there is more than one message for the same user). const conversation = this.findConversation(undefined, message.touserid); message.text = message.smallmessage; if (conversation) { // Check if it's the last message. Offline messages are considered more recent than sent messages. if (conversation.lastmessagedate <= message.timecreated / 1000) { this.addLastOfflineMessage(conversation, message); } } else { // Get the user data and create a new conversation. promises.push(this.userProvider.getProfile(message.touserid, undefined, true).catch(() => { // User not found. }).then((user) => { const conversation = { userid: message.touserid, name: user ? user.fullname : String(message.touserid), imageurl: user ? user.profileimageurl : '', type: AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_INDIVIDUAL }; this.addLastOfflineMessage(conversation, message); this.addOfflineConversation(conversation); })); } } }); return Promise.all(promises); } /** * Add an offline conversation into the right list of conversations. * * @param {any} conversation Offline conversation to add. */ protected addOfflineConversation(conversation: any): void { const option = this.getConversationOption(conversation); option.conversations.unshift(conversation); } /** * Add a last offline message into a conversation. * * @param {any} conversation Conversation where to put the last message. * @param {any} message Offline message to add. */ protected addLastOfflineMessage(conversation: any, message: any): void { conversation.lastmessage = message.text; conversation.lastmessagedate = message.timecreated / 1000; conversation.lastmessagepending = true; conversation.sentfromcurrentuser = true; } /** * Given a conversation, return its option (favourites, group, individual). * * @param {any} conversation Conversation to check. * @return {any} Option object. */ protected getConversationOption(conversation: any): any { if (conversation.isfavourite) { return this.favourites; } else if (conversation.type == AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_GROUP) { return this.group; } else { return this.individual; } } /** * Refresh the data. * * @param {any} [refresher] Refresher. * @param {booleam} [refreshUnreadCounts=true] Whether to refresh unread counts. * @return {Promise} Promise resolved when done. */ refreshData(refresher?: any, refreshUnreadCounts: boolean = true): Promise { const promises = [ this.messagesProvider.invalidateConversations(), this.messagesProvider.invalidateConversationCounts(), this.messagesProvider.invalidateContactRequestsCountCache() ]; if (refreshUnreadCounts) { promises.push(this.messagesProvider.invalidateUnreadConversationCounts()); } return this.utils.allPromises(promises).finally(() => { return this.fetchData().finally(() => { if (refresher) { refresher.complete(); } }); }); } /** * Toogle the visibility of an option (expand/collapse). * * @param {any} option The option to expand/collapse. */ toggle(option: any): void { if (option.expanded) { // Already expanded, close it. option.expanded = false; } else { this.expandOption(option); } } /** * Expand a certain option. * * @param {any} option The option to expand. */ protected expandOption(option: any): void { // Collapse all and expand the right one. this.favourites.expanded = false; this.group.expanded = false; this.individual.expanded = false; option.expanded = true; } /** * Navigate to the search page. */ gotoSearch(): void { this.navCtrl.push('AddonMessagesSearchPage'); } /** * Page destroyed. */ ngOnDestroy(): void { this.newMessagesObserver && this.newMessagesObserver.off(); this.appResumeSubscription && this.appResumeSubscription.unsubscribe(); this.pushObserver && this.pushObserver.unsubscribe(); this.readChangedObserver && this.readChangedObserver.off(); this.cronObserver && this.cronObserver.off(); this.openConversationObserver && this.openConversationObserver.off(); this.updateConversationListObserver && this.updateConversationListObserver.off(); this.contactRequestsCountObserver && this.contactRequestsCountObserver.off(); this.memberInfoObserver && this.memberInfoObserver.off(); } }