MOBILE-2632 message: Display offline messages in conversations list

main
Dani Palou 2018-11-20 13:16:31 +01:00
parent 06f7a427c6
commit ffc98f2c71
5 changed files with 190 additions and 19 deletions

View File

@ -54,7 +54,7 @@
<core-empty-box *ngIf="!messages || messages.length <= 0" icon="chatbubbles" [message]="'addon.messages.nomessages' | translate"></core-empty-box> <core-empty-box *ngIf="!messages || messages.length <= 0" icon="chatbubbles" [message]="'addon.messages.nomessages' | translate"></core-empty-box>
</core-loading> </core-loading>
</ion-content> </ion-content>
<ion-footer color="light" class="footer-adjustable"> <ion-footer color="light" class="footer-adjustable" *ngIf="!conversationId || conversation">
<ion-toolbar color="light" position="bottom"> <ion-toolbar color="light" position="bottom">
<core-send-message-form (onSubmit)="sendMessage($event)" [showKeyboard]="showKeyboard" [placeholder]="'addon.messages.newmessage' | translate" (onResize)="resizeContent()"></core-send-message-form> <core-send-message-form (onSubmit)="sendMessage($event)" [showKeyboard]="showKeyboard" [placeholder]="'addon.messages.newmessage' | translate" (onResize)="resizeContent()"></core-send-message-form>
</ion-toolbar> </ion-toolbar>

View File

@ -781,6 +781,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
/** /**
* Sends a message to the server. * Sends a message to the server.
*
* @param {string} text Message text. * @param {string} text Message text.
*/ */
sendMessage(text: string): void { sendMessage(text: string): void {
@ -810,7 +811,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
let promise; let promise;
if (this.conversationId) { if (this.conversationId) {
promise = this.messagesProvider.sendMessageToConversation(this.conversationId, text); promise = this.messagesProvider.sendMessageToConversation(this.conversation, text);
} else { } else {
promise = this.messagesProvider.sendMessage(this.userId, text); promise = this.messagesProvider.sendMessage(this.userId, text);
} }

View File

@ -18,11 +18,13 @@ import { TranslateService } from '@ngx-translate/core';
import { CoreEventsProvider } from '@providers/events'; import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites'; import { CoreSitesProvider } from '@providers/sites';
import { AddonMessagesProvider } from '../../providers/messages'; import { AddonMessagesProvider } from '../../providers/messages';
import { AddonMessagesOfflineProvider } from '../../providers/messages-offline';
import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreAppProvider } from '@providers/app'; import { CoreAppProvider } from '@providers/app';
import { AddonPushNotificationsDelegate } from '@addon/pushnotifications/providers/delegate'; import { AddonPushNotificationsDelegate } from '@addon/pushnotifications/providers/delegate';
import { CoreSplitViewComponent } from '@components/split-view/split-view'; 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. * 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, constructor(private eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, translate: TranslateService,
private messagesProvider: AddonMessagesProvider, private domUtils: CoreDomUtilsProvider, navParams: NavParams, private messagesProvider: AddonMessagesProvider, private domUtils: CoreDomUtilsProvider, navParams: NavParams,
private appProvider: CoreAppProvider, platform: Platform, utils: CoreUtilsProvider, 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.search.loading = translate.instant('core.searching');
this.loadingString = translate.instant('core.loading'); this.loadingString = translate.instant('core.loading');
@ -179,12 +182,25 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
// Load the first conversations of each type. // Load the first conversations of each type.
const promises = []; const promises = [];
let offlineMessages;
promises.push(this.fetchDataForOption(this.favourites, false)); promises.push(this.fetchDataForOption(this.favourites, false));
promises.push(this.fetchDataForOption(this.group, false)); promises.push(this.fetchDataForOption(this.group, false));
promises.push(this.fetchDataForOption(this.individual, false)); promises.push(this.fetchDataForOption(this.individual, false));
promises.push(this.messagesOffline.getAllMessages().then((messages) => {
offlineMessages = messages;
}));
return Promise.all(promises).then(() => { 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') { if (typeof this.favourites.expanded == 'undefined') {
// The expanded status hasn't been initialized. Do it now. // The expanded status hasn't been initialized. Do it now.
this.favourites.expanded = this.favourites.count != 0; 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<any>} Promise resolved when done.
*/
protected loadOfflineMessages(messages: any[]): Promise<any> {
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. * Refresh the data.
* *

View File

@ -16,6 +16,7 @@ import { Injectable } from '@angular/core';
import { CoreLoggerProvider } from '@providers/logger'; import { CoreLoggerProvider } from '@providers/logger';
import { CoreSitesProvider } from '@providers/sites'; import { CoreSitesProvider } from '@providers/sites';
import { CoreAppProvider } from '@providers/app'; import { CoreAppProvider } from '@providers/app';
import { CoreTextUtilsProvider } from '@providers/utils/text';
/** /**
* Service to handle Offline messages. * Service to handle Offline messages.
@ -73,13 +74,18 @@ export class AddonMessagesOfflineProvider {
{ {
name: 'deviceoffline', // If message was stored because device was offline. name: 'deviceoffline', // If message was stored because device was offline.
type: 'INTEGER' type: 'INTEGER'
},
{
name: 'conversation', // Data about the conversation.
type: 'TEXT'
} }
], ],
primaryKeys: ['conversationid', 'text', 'timecreated'] 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.logger = logger.getInstance('AddonMessagesOfflineProvider');
this.sitesProvider.createTablesFromSchema(this.tablesSchema); this.sitesProvider.createTablesFromSchema(this.tablesSchema);
} }
@ -136,6 +142,8 @@ export class AddonMessagesOfflineProvider {
promises.push(site.getDb().getRecords(AddonMessagesOfflineProvider.CONVERSATION_MESSAGES_TABLE, {deviceoffline: 1})); promises.push(site.getDb().getRecords(AddonMessagesOfflineProvider.CONVERSATION_MESSAGES_TABLE, {deviceoffline: 1}));
return Promise.all(promises).then((results) => { return Promise.all(promises).then((results) => {
results[1] = this.parseConversationMessages(results[1]);
return results[0].concat(results[1]); return results[0].concat(results[1]);
}); });
}); });
@ -155,6 +163,8 @@ export class AddonMessagesOfflineProvider {
promises.push(site.getDb().getAllRecords(AddonMessagesOfflineProvider.CONVERSATION_MESSAGES_TABLE)); promises.push(site.getDb().getAllRecords(AddonMessagesOfflineProvider.CONVERSATION_MESSAGES_TABLE));
return Promise.all(promises).then((results) => { return Promise.all(promises).then((results) => {
results[1] = this.parseConversationMessages(results[1]);
return results[0].concat(results[1]); return results[0].concat(results[1]);
}); });
}); });
@ -170,7 +180,10 @@ export class AddonMessagesOfflineProvider {
getConversationMessages(conversationId: number, siteId?: string): Promise<any[]> { getConversationMessages(conversationId: number, siteId?: string): Promise<any[]> {
return this.sitesProvider.getSite(siteId).then((site) => { return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().getRecords(AddonMessagesOfflineProvider.CONVERSATION_MESSAGES_TABLE, 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. * 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} message The message to send.
* @param {string} [siteId] Site ID. If not defined, current site. * @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved if stored, rejected if failure. * @return {Promise<any>} Promise resolved if stored, rejected if failure.
*/ */
saveConversationMessage(conversationId: number, message: string, siteId?: string): Promise<any> { saveConversationMessage(conversation: any, message: string, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => { return this.sitesProvider.getSite(siteId).then((site) => {
const entry = { const entry = {
conversationid: conversationId, conversationid: conversation.id,
text: message, text: message,
timecreated: Date.now(), 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(() => { return site.getDb().insertRecord(AddonMessagesOfflineProvider.CONVERSATION_MESSAGES_TABLE, entry).then(() => {
@ -276,9 +316,11 @@ export class AddonMessagesOfflineProvider {
messages.forEach((message) => { messages.forEach((message) => {
if (message.conversationid) { 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 { } 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}));
} }
}); });

View File

@ -1506,7 +1506,7 @@ export class AddonMessagesProvider {
/** /**
* Send a message to a conversation. * Send a message to a conversation.
* *
* @param {number} conversationId Conversation ID. * @param {any} conversation Conversation.
* @param {string} message The message to send. * @param {string} message The message to send.
* @param {string} [siteId] Site ID. If not defined, current site. * @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved with: * @return {Promise<any>} Promise resolved with:
@ -1514,10 +1514,10 @@ export class AddonMessagesProvider {
* - message (any) If sent=false, contains the stored message. * - message (any) If sent=false, contains the stored message.
* @since 3.6 * @since 3.6
*/ */
sendMessageToConversation(conversationId: number, message: string, siteId?: string): Promise<any> { sendMessageToConversation(conversation: any, message: string, siteId?: string): Promise<any> {
// Convenience function to store a message to be synchronized later. // Convenience function to store a message to be synchronized later.
const storeOffline = (): Promise<any> => { const storeOffline = (): Promise<any> => {
return this.messagesOffline.saveConversationMessage(conversationId, message, siteId).then((entry) => { return this.messagesOffline.saveConversationMessage(conversation, message, siteId).then((entry) => {
return { return {
sent: false, sent: false,
message: entry message: entry
@ -1534,7 +1534,7 @@ export class AddonMessagesProvider {
// Check if this conversation already has offline messages. // Check if this conversation already has offline messages.
// If so, store this message since they need to be sent in order. // 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. // Error, it's safer to assume it has messages.
return true; return true;
}).then((hasStoredMessages) => { }).then((hasStoredMessages) => {
@ -1543,7 +1543,7 @@ export class AddonMessagesProvider {
} }
// Online and no messages stored. Send it to server. // 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 }; return { sent: true };
}).catch((error) => { }).catch((error) => {
if (this.utils.isWebServiceError(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. * Helper method to sort messages by time.
* *
* @param {any} messages Array of messages containing the key 'timecreated'. * @param {any[]} messages Array of messages containing the key 'timecreated'.
* @return {any} Messages sorted with most recent last. * @return {any[]} Messages sorted with most recent last.
*/ */
sortMessages(messages: any): any { sortMessages(messages: any[]): any[] {
return messages.sort((a, b) => { return messages.sort((a, b) => {
// Pending messages last. // Pending messages last.
if (a.pending && !b.pending) { if (a.pending && !b.pending) {