// (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 { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '../../../providers/logger'; import { CoreSitesProvider } from '../../../providers/sites'; import { CoreAppProvider } from '../../../providers/app'; import { CoreUserProvider } from '../../../core/user/providers/user'; import { AddonMessagesOfflineProvider } from './messages-offline'; /** * Service to handle messages. */ @Injectable() export class AddonMessagesProvider { protected ROOT_CACHE_KEY = 'mmaMessages:'; protected LIMIT_MESSAGES = 50; static NEW_MESSAGE_EVENT = 'new_message_event'; static READ_CHANGED_EVENT = 'read_changed_event'; static READ_CRON_EVENT = 'read_cron_event'; static SPLIT_VIEW_LOAD_EVENT = 'split_view_load_event'; protected logger; constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private appProvider: CoreAppProvider, private userProvider: CoreUserProvider, private messagesOffline: AddonMessagesOfflineProvider) { this.logger = logger.getInstance('AddonMessagesProvider'); } /** * Get the cache key for contacts. * * @return {string} Cache key. */ protected getCacheKeyForContacts(): string { return this.ROOT_CACHE_KEY + 'contacts'; } /** * Get the cache key for the list of discussions. * * @return {string} Cache key. */ protected getCacheKeyForDiscussions(): string { return this.ROOT_CACHE_KEY + 'discussions'; } /** * Get the discussions of the current user. * * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Resolved with an object where the keys are the user ID of the other user. */ getDiscussions(siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const discussions = {}, currentUserId = site.getUserId(), params = { useridto: currentUserId, useridfrom: 0, limitnum: this.LIMIT_MESSAGES }, preSets = { cacheKey: this.getCacheKeyForDiscussions() }; /** * Convenience function to treat a recent message, adding it to discussions list if needed. */ const treatRecentMessage = (message: any, userId: number, userFullname: string): void => { if (typeof discussions[userId] === 'undefined') { discussions[userId] = { fullname: userFullname, profileimageurl: '' }; if (!message.timeread && !message.pending && message.useridfrom != currentUserId) { discussions[userId].unread = true; } } // Extract the most recent message. Pending messages are considered more recent than messages already sent. const discMessage = discussions[userId].message; if (typeof discMessage === 'undefined' || (!discMessage.pending && message.pending) || (discMessage.pending == message.pending && (discMessage.timecreated < message.timecreated || (discMessage.timecreated == message.timecreated && discMessage.id < message.id)))) { discussions[userId].message = { id: message.id, user: userId, message: message.text, timecreated: message.timecreated, pending: message.pending }; } }; // Get recent messages sent to current user. return this.getRecentMessages(params, preSets, undefined, undefined, undefined, site.getId()).then((messages) => { // Extract the discussions by filtering same senders. messages.forEach((message) => { treatRecentMessage(message, message.useridfrom, message.userfromfullname); }); // Now get the last messages sent by the current user. params.useridfrom = params.useridto; params.useridto = 0; return this.getRecentMessages(params, preSets); }).then((messages) => { // Extract the discussions by filtering same senders. messages.forEach((message) => { treatRecentMessage(message, message.useridto, message.usertofullname); }); // Now get unsent messages. return this.messagesOffline.getAllMessages(site.getId()); }).then((offlineMessages) => { offlineMessages.forEach((message) => { message.pending = true; message.text = message.smallmessage; treatRecentMessage(message, message.touserid, ''); }); return this.getDiscussionsUserImg(discussions, site.getId()).then((discussions) => { this.storeUsersFromDiscussions(discussions); return discussions; }); }); }); } /** * Get user images for all the discussions that don't have one already. * * @param {any} discussions List of discussions. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise always resolved. Resolve param is the formatted discussions. */ protected getDiscussionsUserImg(discussions: any, siteId?: string): Promise { const promises = []; for (const userId in discussions) { if (!discussions[userId].profileimageurl) { // We don't have the user image. Try to retrieve it. promises.push(this.userProvider.getProfile(discussions[userId].message.user, 0, true, siteId).then((user) => { discussions[userId].profileimageurl = user.profileimageurl; }).catch(() => { // Error getting profile, resolve promise without adding any extra data. })); } } return Promise.all(promises).then(() => { return discussions; }); } /** * Get messages according to the params. * * @param {any} params Parameters to pass to the WS. * @param {any} preSets Set of presets for the WS. * @param {boolean} [toDisplay=true] True if messages will be displayed to the user, either in view or in a notification. * @param {string} [siteId] Site ID. If not defined, use current site. * @return {Promise} */ protected getMessages(params: any, preSets: any, toDisplay: boolean = true, siteId?: string): Promise { params['type'] = 'conversations'; params['newestfirst'] = 1; return this.sitesProvider.getSite(siteId).then((site) => { const userId = site.getUserId(); return site.read('core_message_get_messages', params, preSets).then((response) => { response.messages.forEach((message) => { message.read = params.read == 0 ? 0 : 1; // Convert times to milliseconds. message.timecreated = message.timecreated ? message.timecreated * 1000 : 0; message.timeread = message.timeread ? message.timeread * 1000 : 0; }); if (toDisplay && this.appProvider.isDesktop() && !params.read && params.useridto == userId && params.limitfrom === 0) { // Store the last unread received messages. Don't block the user for this. // @todo // this.storeLastReceivedMessageIfNeeded(params.useridfrom, response.messages[0], site.getId()); } return response; }); }); } /** * Get the most recent messages. * * @param {any} params Parameters to pass to the WS. * @param {any} preSets Set of presets for the WS. * @param {number} [limitFromUnread=0] Number of read messages already fetched, so fetch will be done from this number. * @param {number} [limitFromRead=0] Number of unread messages already fetched, so fetch will be done from this number. * @param {boolean} [toDisplay=true] True if messages will be displayed to the user, either in view or in a notification. * @param {string} [siteId] Site ID. If not defined, use current site. * @return {Promise} */ protected getRecentMessages(params: any, preSets: any, limitFromUnread: number = 0, limitFromRead: number = 0, toDisplay: boolean = true, siteId?: string): Promise { limitFromUnread = limitFromUnread || 0; limitFromRead = limitFromRead || 0; params['read'] = 0; params['limitfrom'] = limitFromUnread; return this.getMessages(params, preSets, toDisplay, siteId).then((response) => { let messages = response.messages; if (messages) { if (messages.length >= params.limitnum) { return messages; } // We need to fetch more messages. params.limitnum = params.limitnum - messages.length; params.read = 1; params.limitfrom = limitFromRead; return this.getMessages(params, preSets, toDisplay, siteId).then((response) => { if (response.messages) { messages = messages.concat(response.messages); } return messages; }).catch(() => { return messages; }); } else { return Promise.reject(null); } }); } /** * Invalidate contacts cache. * * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Resolved when done. */ invalidateContactsCache(siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { return site.invalidateWsCacheForKey(this.getCacheKeyForContacts()); }); } /** * Invalidate discussions cache. * * Note that {@link this.getDiscussions} uses the contacts, so we need to invalidate contacts too. * * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Resolved when done. */ invalidateDiscussionsCache(siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { return site.invalidateWsCacheForKey(this.getCacheKeyForDiscussions()).then(() => { return this.invalidateContactsCache(site.getId()); }); }); } /** * Returns whether or not the plugin is enabled in a certain site. * * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with true if enabled, rejected or resolved with false otherwise. */ isPluginEnabled(siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { return site.canUseAdvancedFeature('messaging'); }); } /** * Returns whether or not we can search messages. * * @return {boolean} * @since 3.2 */ isSearchMessagesEnabled(): boolean { return this.sitesProvider.getCurrentSite().wsAvailable('core_message_data_for_messagearea_search_messages'); } /** * Search for all the messges with a specific text. * * @param {string} query The query string * @param {number} [userId] The user ID. If not defined, current user. * @param {number} [from=0] Position of the first result to get. Defaults to 0. * @param {number} [limit] Number of results to get. Defaults to LIMIT_MESSAGES. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with the results. */ searchMessages(query: string, userId?: number, from: number = 0, limit: number = this.LIMIT_MESSAGES, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const param = { userid: userId || site.getUserId(), search: query, limitfrom: from, limitnum: limit }, preSets = { getFromCache: false // Always try to get updated data. If it fails, it will get it from cache. }; return site.read('core_message_data_for_messagearea_search_messages', param, preSets).then((searchResults) => { return searchResults.contacts; }); }); } /** * Store user data from discussions in local DB. * * @param {any} discussions List of discussions. * @param {string} [siteId] Site ID. If not defined, current site. */ protected storeUsersFromDiscussions(discussions: any, siteId?: string): void { const users = []; for (const userId in discussions) { if (typeof userId != 'undefined' && !isNaN(parseInt(userId))) { users.push({ id: userId, fullname: discussions[userId].fullname, profileimageurl: discussions[userId].profileimageurl }); } } this.userProvider.storeUsers(users, siteId); } }