From ffc98f2c71ad81fd5b2fad77a82a46e8648167f0 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 20 Nov 2018 13:16:31 +0100 Subject: [PATCH] MOBILE-2632 message: Display offline messages in conversations list --- .../messages/pages/discussion/discussion.html | 2 +- .../messages/pages/discussion/discussion.ts | 3 +- .../group-conversations.ts | 110 +++++++++++++++++- .../messages/providers/messages-offline.ts | 58 +++++++-- src/addon/messages/providers/messages.ts | 36 ++++-- 5 files changed, 190 insertions(+), 19 deletions(-) diff --git a/src/addon/messages/pages/discussion/discussion.html b/src/addon/messages/pages/discussion/discussion.html index 36466b3c3..c9a42162c 100644 --- a/src/addon/messages/pages/discussion/discussion.html +++ b/src/addon/messages/pages/discussion/discussion.html @@ -54,7 +54,7 @@ - + diff --git a/src/addon/messages/pages/discussion/discussion.ts b/src/addon/messages/pages/discussion/discussion.ts index fe0ee05cd..953a57f21 100644 --- a/src/addon/messages/pages/discussion/discussion.ts +++ b/src/addon/messages/pages/discussion/discussion.ts @@ -781,6 +781,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { /** * Sends a message to the server. + * * @param {string} text Message text. */ sendMessage(text: string): void { @@ -810,7 +811,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { let promise; if (this.conversationId) { - promise = this.messagesProvider.sendMessageToConversation(this.conversationId, text); + promise = this.messagesProvider.sendMessageToConversation(this.conversation, text); } else { promise = this.messagesProvider.sendMessage(this.userId, text); } diff --git a/src/addon/messages/pages/group-conversations/group-conversations.ts b/src/addon/messages/pages/group-conversations/group-conversations.ts index fd3ef4b74..893e438f7 100644 --- a/src/addon/messages/pages/group-conversations/group-conversations.ts +++ b/src/addon/messages/pages/group-conversations/group-conversations.ts @@ -18,11 +18,13 @@ 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 { CoreAppProvider } from '@providers/app'; 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. @@ -71,7 +73,8 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { constructor(private eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, translate: TranslateService, private messagesProvider: AddonMessagesProvider, private domUtils: CoreDomUtilsProvider, navParams: NavParams, private appProvider: CoreAppProvider, platform: Platform, utils: CoreUtilsProvider, - pushNotificationsDelegate: AddonPushNotificationsDelegate) { + pushNotificationsDelegate: AddonPushNotificationsDelegate, private messagesOffline: AddonMessagesOfflineProvider, + private userProvider: CoreUserProvider) { this.search.loading = translate.instant('core.searching'); this.loadingString = translate.instant('core.loading'); @@ -179,12 +182,25 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { // 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.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. this.favourites.expanded = this.favourites.count != 0; @@ -286,6 +302,98 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { }); } + /** + * 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.individual.conversations.find((conv) => { + return conv.userid == 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 { + if (conversation.isfavourite) { + this.favourites.conversations.unshift(conversation); + } else if (conversation.type == AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_GROUP) { + this.group.conversations.unshift(conversation); + } else { + this.individual.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; + } + /** * Refresh the data. * diff --git a/src/addon/messages/providers/messages-offline.ts b/src/addon/messages/providers/messages-offline.ts index 022515c74..f746c9147 100644 --- a/src/addon/messages/providers/messages-offline.ts +++ b/src/addon/messages/providers/messages-offline.ts @@ -16,6 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider } from '@providers/sites'; import { CoreAppProvider } from '@providers/app'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; /** * Service to handle Offline messages. @@ -73,13 +74,18 @@ export class AddonMessagesOfflineProvider { { name: 'deviceoffline', // If message was stored because device was offline. type: 'INTEGER' + }, + { + name: 'conversation', // Data about the conversation. + type: 'TEXT' } ], primaryKeys: ['conversationid', 'text', 'timecreated'] } ]; - constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private appProvider: CoreAppProvider) { + constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private appProvider: CoreAppProvider, + private textUtils: CoreTextUtilsProvider) { this.logger = logger.getInstance('AddonMessagesOfflineProvider'); this.sitesProvider.createTablesFromSchema(this.tablesSchema); } @@ -136,6 +142,8 @@ export class AddonMessagesOfflineProvider { promises.push(site.getDb().getRecords(AddonMessagesOfflineProvider.CONVERSATION_MESSAGES_TABLE, {deviceoffline: 1})); return Promise.all(promises).then((results) => { + results[1] = this.parseConversationMessages(results[1]); + return results[0].concat(results[1]); }); }); @@ -155,6 +163,8 @@ export class AddonMessagesOfflineProvider { promises.push(site.getDb().getAllRecords(AddonMessagesOfflineProvider.CONVERSATION_MESSAGES_TABLE)); return Promise.all(promises).then((results) => { + results[1] = this.parseConversationMessages(results[1]); + return results[0].concat(results[1]); }); }); @@ -170,7 +180,10 @@ export class AddonMessagesOfflineProvider { getConversationMessages(conversationId: number, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { return site.getDb().getRecords(AddonMessagesOfflineProvider.CONVERSATION_MESSAGES_TABLE, - {conversationid: conversationId}); + {conversationid: conversationId}).then((messages) => { + + return this.parseConversationMessages(messages); + }); }); } @@ -213,21 +226,48 @@ export class AddonMessagesOfflineProvider { }); } + /** + * Parse some fields of each offline conversation messages. + * + * @param {any[]} messages List of messages to parse. + * @return {any[]} Parsed messages. + */ + protected parseConversationMessages(messages: any[]): any[] { + if (!messages) { + return []; + } + + messages.forEach((message) => { + if (message.conversation) { + message.conversation = this.textUtils.parseJSON(message.conversation, {}); + } + }); + + return messages; + } + /** * Save a conversation message to be sent later. * - * @param {number} conversationId Conversation ID. + * @param {any} conversation Conversation. * @param {string} message The message to send. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved if stored, rejected if failure. */ - saveConversationMessage(conversationId: number, message: string, siteId?: string): Promise { + saveConversationMessage(conversation: any, message: string, siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { const entry = { - conversationid: conversationId, + conversationid: conversation.id, text: message, timecreated: Date.now(), - deviceoffline: this.appProvider.isOnline() ? 0 : 1 + deviceoffline: this.appProvider.isOnline() ? 0 : 1, + conversation: JSON.stringify({ + name: conversation.name || '', + subname: conversation.subname || '', + imageurl: conversation.imageurl || '', + isfavourite: conversation.isfavourite ? 1 : 0, + type: conversation.type + }) }; return site.getDb().insertRecord(AddonMessagesOfflineProvider.CONVERSATION_MESSAGES_TABLE, entry).then(() => { @@ -276,9 +316,11 @@ export class AddonMessagesOfflineProvider { messages.forEach((message) => { if (message.conversationid) { - promises.push(db.insertRecord(AddonMessagesOfflineProvider.CONVERSATION_MESSAGES_TABLE, data)); + promises.push(db.updateRecords(AddonMessagesOfflineProvider.CONVERSATION_MESSAGES_TABLE, data, + {conversationid: message.conversationid, text: message.text, timecreated: message.timecreated})); } else { - promises.push(db.insertRecord(AddonMessagesOfflineProvider.MESSAGES_TABLE, data)); + promises.push(db.updateRecords(AddonMessagesOfflineProvider.MESSAGES_TABLE, data, + {touserid: message.touserid, smallmessage: message.smallmessage, timecreated: message.timecreated})); } }); diff --git a/src/addon/messages/providers/messages.ts b/src/addon/messages/providers/messages.ts index d96bba2e9..50b05641c 100644 --- a/src/addon/messages/providers/messages.ts +++ b/src/addon/messages/providers/messages.ts @@ -1506,7 +1506,7 @@ export class AddonMessagesProvider { /** * Send a message to a conversation. * - * @param {number} conversationId Conversation ID. + * @param {any} conversation Conversation. * @param {string} message The message to send. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved with: @@ -1514,10 +1514,10 @@ export class AddonMessagesProvider { * - message (any) If sent=false, contains the stored message. * @since 3.6 */ - sendMessageToConversation(conversationId: number, message: string, siteId?: string): Promise { + sendMessageToConversation(conversation: any, message: string, siteId?: string): Promise { // Convenience function to store a message to be synchronized later. const storeOffline = (): Promise => { - return this.messagesOffline.saveConversationMessage(conversationId, message, siteId).then((entry) => { + return this.messagesOffline.saveConversationMessage(conversation, message, siteId).then((entry) => { return { sent: false, message: entry @@ -1534,7 +1534,7 @@ export class AddonMessagesProvider { // Check if this conversation already has offline messages. // If so, store this message since they need to be sent in order. - return this.messagesOffline.hasConversationMessages(conversationId, siteId).catch(() => { + return this.messagesOffline.hasConversationMessages(conversation.id, siteId).catch(() => { // Error, it's safer to assume it has messages. return true; }).then((hasStoredMessages) => { @@ -1543,7 +1543,7 @@ export class AddonMessagesProvider { } // Online and no messages stored. Send it to server. - return this.sendMessageToConversationOnline(conversationId, message).then(() => { + return this.sendMessageToConversationOnline(conversation.id, message).then(() => { return { sent: true }; }).catch((error) => { if (this.utils.isWebServiceError(error)) { @@ -1610,13 +1610,33 @@ export class AddonMessagesProvider { }); } + /** + * Helper method to sort conversations by last message time. + * + * @param {any[]} conversations Array of conversations. + * @return {any[]} Conversations sorted with most recent last. + */ + sortConversations(conversations: any[]): any[] { + return conversations.sort((a, b) => { + const timeA = parseInt(a.lastmessagedate, 10), + timeB = parseInt(b.lastmessagedate, 10); + + if (timeA == timeB && a.id) { + // Same time, sort by ID. + return a.id <= b.id ? 1 : -1; + } + + return timeA <= timeB ? 1 : -1; + }); + } + /** * Helper method to sort messages by time. * - * @param {any} messages Array of messages containing the key 'timecreated'. - * @return {any} Messages sorted with most recent last. + * @param {any[]} messages Array of messages containing the key 'timecreated'. + * @return {any[]} Messages sorted with most recent last. */ - sortMessages(messages: any): any { + sortMessages(messages: any[]): any[] { return messages.sort((a, b) => { // Pending messages last. if (a.pending && !b.pending) {