From a6f5543870f78ceea2457b245181c19f055e9ca2 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 19 Nov 2018 11:10:25 +0100 Subject: [PATCH 01/11] MOBILE-2632 message: Get conversation messages --- scripts/langindex.json | 3 + src/addon/messages/lang/en.json | 3 + .../messages/pages/discussion/discussion.html | 20 +- .../messages/pages/discussion/discussion.ts | 330 ++++++++++++----- .../group-conversations.html | 2 + .../group-conversations.ts | 13 +- src/addon/messages/providers/messages.ts | 348 ++++++++++++++++-- src/assets/lang/en.json | 3 + 8 files changed, 589 insertions(+), 133 deletions(-) diff --git a/scripts/langindex.json b/scripts/langindex.json index e9720fd30..e096f7155 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -165,7 +165,9 @@ "addon.messages.errorwhileretrievingcontacts": "local_moodlemobileapp", "addon.messages.errorwhileretrievingdiscussions": "local_moodlemobileapp", "addon.messages.errorwhileretrievingmessages": "local_moodlemobileapp", + "addon.messages.groupinfo": "message", "addon.messages.groupmessages": "message", + "addon.messages.info": "message", "addon.messages.message": "message", "addon.messages.messagenotsent": "local_moodlemobileapp", "addon.messages.messagepreferences": "message", @@ -178,6 +180,7 @@ "addon.messages.nousersfound": "local_moodlemobileapp", "addon.messages.removecontact": "message", "addon.messages.removecontactconfirm": "local_moodlemobileapp", + "addon.messages.showdeletemessages": "local_moodlemobileapp", "addon.messages.type_blocked": "local_moodlemobileapp", "addon.messages.type_offline": "local_moodlemobileapp", "addon.messages.type_online": "local_moodlemobileapp", diff --git a/src/addon/messages/lang/en.json b/src/addon/messages/lang/en.json index ed076fb7d..6de944be2 100644 --- a/src/addon/messages/lang/en.json +++ b/src/addon/messages/lang/en.json @@ -17,7 +17,9 @@ "errorwhileretrievingcontacts": "Error while retrieving contacts from the server.", "errorwhileretrievingdiscussions": "Error while retrieving discussions from the server.", "errorwhileretrievingmessages": "Error while retrieving messages from the server.", + "groupinfo": "Group info", "groupmessages": "Group messages", + "info": "Info", "messagenotsent": "The message was not sent. Please try again later.", "message": "Message", "messagepreferences": "Message preferences", @@ -30,6 +32,7 @@ "nousersfound": "No users found", "removecontact": "Remove contact", "removecontactconfirm": "Contact will be removed from your contacts list.", + "showdeletemessages": "Show delete messages", "type_blocked": "Blocked", "type_offline": "Offline", "type_online": "Online", diff --git a/src/addon/messages/pages/discussion/discussion.html b/src/addon/messages/pages/discussion/discussion.html index 89e20fe8b..74e8b3e88 100644 --- a/src/addon/messages/pages/discussion/discussion.html +++ b/src/addon/messages/pages/discussion/discussion.html @@ -1,15 +1,17 @@ - + + + + - - - - + + + + + @@ -22,12 +24,12 @@ {{ message.timecreated | coreFormatDate: "LL" }} - + {{ 'addon.messages.newmessages' | translate:{$a: title} }} - +

diff --git a/src/addon/messages/pages/discussion/discussion.ts b/src/addon/messages/pages/discussion/discussion.ts index 558421290..7df042ffa 100644 --- a/src/addon/messages/pages/discussion/discussion.ts +++ b/src/addon/messages/pages/discussion/discussion.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnDestroy, ViewChild } from '@angular/core'; +import { Component, OnDestroy, ViewChild, Optional } from '@angular/core'; import { IonicPage, NavParams, NavController, Content } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreEventsProvider } from '@providers/events'; @@ -25,6 +25,7 @@ import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreAppProvider } from '@providers/app'; import { coreSlideInOut } from '@classes/animations'; +import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { Md5 } from 'ts-md5/dist/md5'; import * as moment from 'moment'; @@ -53,12 +54,16 @@ export class AddonMessagesDiscussionPage implements OnDestroy { protected syncObserver: any; protected oldContentHeight = 0; protected keyboardObserver: any; + protected scrollBottom = true; + protected viewDestroyed = false; - userId: number; + conversationId: number; // Conversation ID. Undefined if it's a new individual conversation. + conversation: any; // The conversation object (if it exists). + userId: number; // User ID you're talking to (only if group messaging not enabled or it's a new individual conversation). currentUserId: number; title: string; - profileLink: string; - showProfileLink: boolean; + showInfo: boolean; + conversationImage: string; loaded = false; showKeyboard = false; canLoadMore = false; @@ -66,26 +71,30 @@ export class AddonMessagesDiscussionPage implements OnDestroy { messages = []; showDelete = false; canDelete = false; - scrollBottom = true; - viewDestroyed = false; + groupMessagingEnabled: boolean; + isGroup = false; constructor(private eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, navParams: NavParams, private userProvider: CoreUserProvider, private navCtrl: NavController, private messagesSync: AddonMessagesSyncProvider, private domUtils: CoreDomUtilsProvider, private messagesProvider: AddonMessagesProvider, logger: CoreLoggerProvider, - private utils: CoreUtilsProvider, private appProvider: CoreAppProvider, private translate: TranslateService) { + private utils: CoreUtilsProvider, private appProvider: CoreAppProvider, private translate: TranslateService, + @Optional() private svComponent: CoreSplitViewComponent) { this.siteId = sitesProvider.getCurrentSiteId(); this.currentUserId = sitesProvider.getCurrentSiteUserId(); + this.groupMessagingEnabled = this.messagesProvider.isGroupMessagingEnabled(); this.logger = logger.getInstance('AddonMessagesDiscussionPage'); + this.conversationId = navParams.get('conversationId'); this.userId = navParams.get('userId'); this.showKeyboard = navParams.get('showKeyboard'); // Refresh data if this discussion is synchronized automatically. this.syncObserver = eventsProvider.on(AddonMessagesSyncProvider.AUTO_SYNCED, (data) => { - if (data.userId == this.userId) { + if ((data.userId && data.userId == this.userId) || + (data.conversationId && data.conversationId == this.conversationId)) { // Fetch messages. - this.fetchData(); + this.fetchMessages(); // Show first warning if any. if (data.warnings && data.warnings[0]) { @@ -102,8 +111,8 @@ export class AddonMessagesDiscussionPage implements OnDestroy { * @param {boolean} [keep=true] If set the keep flag or not. */ protected addMessage(message: any, keep: boolean = true): void { - // Use smallmessage instead of message ID because ID changes when a message is read. - message.hash = Md5.hashAsciiStr(message.smallmessage) + '#' + message.timecreated + '#' + message.useridfrom; + // Use text instead of message ID because ID changes when a message is read. + message.hash = Md5.hashAsciiStr(message.text || '') + '#' + message.timecreated + '#' + message.useridfrom; if (typeof this.keepMessageMap[message.hash] === 'undefined') { // Message not added to the list. Add it now. this.messages.push(message); @@ -143,15 +152,17 @@ export class AddonMessagesDiscussionPage implements OnDestroy { ionViewDidLoad(): void { // Disable the profile button if we're already coming from a profile. const backViewPage = this.navCtrl.getPrevious() && this.navCtrl.getPrevious().component.name; - this.showProfileLink = !backViewPage || backViewPage !== 'CoreUserProfilePage'; + this.showInfo = !backViewPage || backViewPage !== 'CoreUserProfilePage'; - // Get the user profile to retrieve the user fullname and image. - this.userProvider.getProfile(this.userId, undefined, true).then((user) => { - if (!this.title) { - this.title = user.fullname; - } - this.profileLink = user.profileimageurl; - }); + if (!this.groupMessagingEnabled && this.userId) { + // Get the user profile to retrieve the user fullname and image. + this.userProvider.getProfile(this.userId, undefined, true).then((user) => { + if (!this.title) { + this.title = user.fullname; + } + this.conversationImage = user.profileimageurl; + }); + } // Synchronize messages if needed. this.messagesSync.syncDiscussion(this.userId).catch(() => { @@ -161,24 +172,34 @@ export class AddonMessagesDiscussionPage implements OnDestroy { this.domUtils.showErrorModal(warnings[0]); } - // Fetch the messages for the first time. - return this.fetchData().then(() => { - if (!this.title && this.messages.length) { - // Didn't receive the fullname via argument. Try to get it from messages. - // It's possible that name cannot be resolved when no messages were yet exchanged. - if (this.messages[0].useridto != this.currentUserId) { - this.title = this.messages[0].usertofullname || ''; - } else { - this.title = this.messages[0].userfromfullname || ''; + if (this.groupMessagingEnabled) { + // Get the conversation ID if it exists and we don't have it yet. + return this.getConversation().then((exists) => { + if (exists) { + // Fetch the messages for the first time. + return this.fetchMessages(); } - } - }).catch((error) => { - this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingmessages', true); - }).finally(() => { - this.checkCanDelete(); - this.resizeContent(); - this.loaded = true; - }); + }); + } else { + // Fetch the messages for the first time. + return this.fetchMessages().then(() => { + if (!this.title && this.messages.length) { + // Didn't receive the fullname via argument. Try to get it from messages. + // It's possible that name cannot be resolved when no messages were yet exchanged. + if (this.messages[0].useridto != this.currentUserId) { + this.title = this.messages[0].usertofullname || ''; + } else { + this.title = this.messages[0].userfromfullname || ''; + } + } + }); + } + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingmessages', true); + }).finally(() => { + this.checkCanDelete(); + this.resizeContent(); + this.loaded = true; }); // Recalculate footer position when keyboard is shown or hidden. @@ -204,12 +225,12 @@ export class AddonMessagesDiscussionPage implements OnDestroy { /** * Convenience function to fetch messages. + * * @return {Promise} Resolved when done. */ - protected fetchData(): Promise { + protected fetchMessages(): Promise { this.loadMoreError = false; - this.logger.debug(`Polling new messages for discussion with user '${this.userId}'`); if (this.messagesBeingSent > 0) { // We do not poll while a message is being sent or we could confuse the user. // Otherwise, his message would disappear from the list, and he'd have to wait for the interval to check for messages. @@ -217,6 +238,15 @@ export class AddonMessagesDiscussionPage implements OnDestroy { } else if (this.fetching) { // Already fetching. return Promise.reject(null); + } else if (this.groupMessagingEnabled && !this.conversationId) { + // Don't have enough data to fetch messages. + return Promise.reject(null); + } + + if (this.conversationId) { + this.logger.debug(`Polling new messages for conversation '${this.conversationId}'`); + } else { + this.logger.debug(`Polling new messages for discussion with user '${this.userId}'`); } this.fetching = true; @@ -224,11 +254,19 @@ export class AddonMessagesDiscussionPage implements OnDestroy { // Wait for synchronization process to finish. return this.messagesSync.waitForSync(this.userId).then(() => { // Fetch messages. Invalidate the cache before fetching. - return this.messagesProvider.invalidateDiscussionCache(this.userId).catch(() => { - // Ignore errors. - }); - }).then(() => { - return this.getDiscussion(this.pagesLoaded); + if (this.groupMessagingEnabled) { + return this.messagesProvider.invalidateConversationMessages(this.conversationId).catch(() => { + // Ignore errors. + }).then(() => { + return this.getConversationMessages(this.pagesLoaded); + }); + } else { + return this.messagesProvider.invalidateDiscussionCache(this.userId).catch(() => { + // Ignore errors. + }).then(() => { + return this.getDiscussionMessages(this.pagesLoaded); + }); + } }).then((messages) => { if (this.viewDestroyed) { return Promise.resolve(); @@ -261,11 +299,83 @@ export class AddonMessagesDiscussionPage implements OnDestroy { // Mark retrieved messages as read if they are not. this.markMessagesAsRead(); + }).finally(() => { this.fetching = false; }); } + /** + * Get the conversation. + * + * @return {Promise} Promise resolved with a boolean: whether the conversation exists or not. + */ + protected getConversation(): Promise { + let promise; + + if (this.conversationId) { + // Retrieve the conversation. Invalidate data first to get the right unreadcount. + promise = this.messagesProvider.invalidateConversation(this.conversationId).then(() => { + return this.messagesProvider.getConversation(this.conversationId); + }); + } else { + // We don't have the conversation ID, check if it exists. + promise = this.messagesProvider.getConversationBetweenUsers(this.userId).catch((error) => { + if (error.errorcode == 'conversationdoesntexist') { + // Conversation does not exist, return undefined. + return; + } + + return Promise.reject(error); + }); + } + + return promise.then((conversation) => { + this.conversation = conversation; + if (conversation) { + this.title = conversation.name; + this.conversationImage = conversation.imageurl; + this.isGroup = conversation.type == AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_GROUP; + if (!this.isGroup) { + this.userId = conversation.userid; + } + + return true; + } else { + return false; + } + }); + } + + /** + * Get the messages of the conversation. Used if group messaging is supported. + * + * @param {number} pagesToLoad Number of "pages" to load. + * @param {number} [offset=0] Offset for message list. + * @return {Promise} Promise resolved with the list of messages. + */ + protected getConversationMessages(pagesToLoad: number, offset: number = 0): Promise { + const excludePending = offset > 0; + + return this.messagesProvider.getConversationMessages(this.conversationId, excludePending, offset).then((result) => { + pagesToLoad--; + + if (pagesToLoad > 0 && result.canLoadMore) { + offset += AddonMessagesProvider.LIMIT_MESSAGES; + + // Get more messages. + return this.getConversationMessages(pagesToLoad, offset).then((nextMessages) => { + return result.messages.concat(nextMessages); + }); + } else { + // No more messages to load, return them. + this.canLoadMore = result.canLoadMore; + + return result.messages; + } + }); + } + /** * Get a discussion. Can load several "pages". * @@ -276,8 +386,8 @@ export class AddonMessagesDiscussionPage implements OnDestroy { * @param {number} [lfSentRead=0] Number of read sent messages already fetched, so fetch will be done from this. * @return {Promise} Resolved when done. */ - protected getDiscussion(pagesToLoad: number, lfReceivedUnread: number = 0, lfReceivedRead: number = 0, lfSentUnread: number = 0, - lfSentRead: number = 0): Promise { + protected getDiscussionMessages(pagesToLoad: number, lfReceivedUnread: number = 0, lfReceivedRead: number = 0, + lfSentUnread: number = 0, lfSentRead: number = 0): Promise { // Only get offline messages if we're loading the first "page". const excludePending = lfReceivedUnread > 0 || lfReceivedRead > 0 || lfSentUnread > 0 || lfSentRead > 0; @@ -308,7 +418,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { }); // Get next messages. - return this.getDiscussion(pagesToLoad, lfReceivedUnread, lfReceivedRead, lfSentUnread, lfSentRead) + return this.getDiscussionMessages(pagesToLoad, lfReceivedUnread, lfReceivedRead, lfSentUnread, lfSentRead) .then((nextMessages) => { return result.messages.concat(nextMessages); }); @@ -330,23 +440,39 @@ export class AddonMessagesDiscussionPage implements OnDestroy { if (this.messagesProvider.isMarkAllMessagesReadEnabled()) { let messageUnreadFound = false; - // Mark all messages at a time if one messages is unread. - for (const x in this.messages) { - const message = this.messages[x]; - // If an unread message is found, mark all messages as read. - if (message.useridfrom != this.currentUserId && message.read == 0) { - messageUnreadFound = true; - break; + + // Mark all messages at a time if there is any unread message. + if (this.groupMessagingEnabled) { + messageUnreadFound = this.conversation && this.conversation.unreadcount > 0 && this.conversationId > 0; + } else { + for (const x in this.messages) { + const message = this.messages[x]; + // If an unread message is found, mark all messages as read. + if (message.useridfrom != this.currentUserId && message.read == 0) { + messageUnreadFound = true; + break; + } } } + if (messageUnreadFound) { this.setUnreadLabelPosition(); - promises.push(this.messagesProvider.markAllMessagesRead(this.userId).then(() => { - readChanged = true; - // Mark all messages as read. - this.messages.forEach((message) => { - message.read = 1; + + let promise; + + if (this.groupMessagingEnabled) { + promise = this.messagesProvider.markAllConversationMessagesRead(this.conversationId); + } else { + promise = this.messagesProvider.markAllMessagesRead(this.userId).then(() => { + // Mark all messages as read. + this.messages.forEach((message) => { + message.read = 1; + }); }); + } + + promises.push(promise.then(() => { + readChanged = true; })); } } else { @@ -366,6 +492,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { Promise.all(promises).finally(() => { if (readChanged) { this.eventsProvider.trigger(AddonMessagesProvider.READ_CHANGED_EVENT, { + conversationId: this.conversationId, userId: this.userId }, this.siteId); } @@ -390,6 +517,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { if (trigger) { // Update discussions last message. this.eventsProvider.trigger(AddonMessagesProvider.NEW_MESSAGE_EVENT, { + conversationId: this.conversationId, userId: this.userId, message: this.lastMessage.text, timecreated: this.lastMessage.timecreated @@ -411,21 +539,39 @@ export class AddonMessagesDiscussionPage implements OnDestroy { return; } - let previousMessageRead = false; + if (this.groupMessagingEnabled) { + // Use the unreadcount from the conversation to calculate where should the label be placed. + if (this.conversation && this.conversation.unreadcount > 0 && this.messages) { + // Iterate over messages to find the right message using the unreadcount. Skip offline messages and own messages. + let found = 0; - for (const x in this.messages) { - const message = this.messages[x]; - if (message.useridfrom != this.currentUserId) { - // Place unread from message label only once. - message.unreadFrom = message.read == 0 && previousMessageRead; - - if (message.unreadFrom) { - // Save where the label is placed. - this.unreadMessageFrom = parseInt(message.id, 10); - break; + for (let i = this.messages.length - 1; i >= 0; i--) { + const message = this.messages[i]; + if (!message.pending && message.useridfrom != this.currentUserId) { + found++; + if (found == this.conversation.unreadcount) { + this.unreadMessageFrom = parseInt(message.id, 10); + break; + } + } } + } + } else { + let previousMessageRead = false; - previousMessageRead = message.read != 0; + for (const x in this.messages) { + const message = this.messages[x]; + if (message.useridfrom != this.currentUserId) { + const unreadFrom = message.read == 0 && previousMessageRead; + + if (unreadFrom) { + // Save where the label is placed. + this.unreadMessageFrom = parseInt(message.id, 10); + break; + } + + previousMessageRead = message.read != 0; + } } } @@ -450,15 +596,6 @@ export class AddonMessagesDiscussionPage implements OnDestroy { */ protected hideUnreadLabel(): void { if (this.unreadMessageFrom > 0) { - for (const x in this.messages) { - const message = this.messages[x]; - if (message.id == this.unreadMessageFrom) { - message.unreadFrom = false; - break; - } - } - - // Label hidden. this.unreadMessageFrom = -1; } } @@ -487,10 +624,15 @@ export class AddonMessagesDiscussionPage implements OnDestroy { * Set a polling to get new messages every certain time. */ protected setPolling(): void { + if (this.groupMessagingEnabled && !this.conversationId) { + // Don't have enough data to poll messages. + return; + } + if (!this.polling) { // Start polling. this.polling = setInterval(() => { - this.fetchData().catch(() => { + this.fetchMessages().catch(() => { // Ignore errors. }); }, AddonMessagesProvider.POLL_INTERVAL); @@ -509,12 +651,12 @@ export class AddonMessagesDiscussionPage implements OnDestroy { } /** - * Copy message to clipboard + * Copy message to clipboard. * - * @param {string} text Message text to be copied. + * @param {any} message Message to be copied. */ - copyMessage(text: string): void { - this.utils.copyToClipboard(text); + copyMessage(message: any): void { + this.utils.copyToClipboard(message.smallmessage || message.text || ''); } /** @@ -534,7 +676,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { this.removeMessage(message.hash); this.notifyNewMessage(); - this.fetchData(); // Re-fetch messages to update cached data. + this.fetchMessages(); // Re-fetch messages to update cached data. }).finally(() => { modal.dismiss(); }); @@ -554,9 +696,8 @@ export class AddonMessagesDiscussionPage implements OnDestroy { return this.waitForFetch().finally(() => { this.pagesLoaded++; - this.fetchData().catch((error) => { + this.fetchMessages().catch((error) => { this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading. - this.pagesLoaded--; this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingmessages', true); }).finally(() => { @@ -638,7 +779,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { if (data.sent) { // Message was sent, fetch messages right now. - promise = this.fetchData(); + promise = this.fetchMessages(); } else { promise = Promise.reject(null); } @@ -697,6 +838,19 @@ export class AddonMessagesDiscussionPage implements OnDestroy { this.showDelete = !this.showDelete; } + /** + * View info. If it's an individual conversation, go to the user profile. + * If it's a group conversation, view info about the group. + */ + viewInfo(): void { + if (this.isGroup) { + // @todo + } else { + const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl; + navCtrl.push('CoreUserProfilePage', { userId: this.userId }); + } + } + /** * Page destroyed. */ diff --git a/src/addon/messages/pages/group-conversations/group-conversations.html b/src/addon/messages/pages/group-conversations/group-conversations.html index c92ff7845..70bd51f67 100644 --- a/src/addon/messages/pages/group-conversations/group-conversations.html +++ b/src/addon/messages/pages/group-conversations/group-conversations.html @@ -8,6 +8,8 @@ + + diff --git a/src/addon/messages/pages/group-conversations/group-conversations.ts b/src/addon/messages/pages/group-conversations/group-conversations.ts index 091df7e7a..fd3ef4b74 100644 --- a/src/addon/messages/pages/group-conversations/group-conversations.ts +++ b/src/addon/messages/pages/group-conversations/group-conversations.ts @@ -94,7 +94,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { } else { // An existing conversation has a new message, update the last message. conversation.lastmessage = data.message; - conversation.lastmessagedate = data.timecreated; + conversation.lastmessagedate = data.timecreated / 1000; } } }, this.siteId); @@ -145,7 +145,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { ngOnInit(): void { if (this.conversationId) { // There is a discussion to load, open the discussion in a new state. - this.gotoConversation(this.conversationId, null); + this.gotoConversation(this.conversationId); } this.fetchData().then(() => { @@ -162,7 +162,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { } if (conversation) { - this.gotoConversation(conversation.id, conversation.userid); + this.gotoConversation(conversation.id); } } }); @@ -247,13 +247,14 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { * Navigate to a particular conversation. * * @param {number} conversationId Conversation Id to load. - * @param {number} userId User of the conversation. @todo This will probably be removed when group messaging is fully supported. + * @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. */ - gotoConversation(conversationId: number, userId: number, messageId?: number): void { + gotoConversation(conversationId: number, userId?: number, messageId?: number): void { this.selectedConversation = conversationId; const params = { + conversationId: conversationId, userId: userId }; if (messageId) { @@ -358,7 +359,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { * Page destroyed. */ ngOnDestroy(): void { - this.newMessagesObserver && this.newMessagesObserver.unsubscribe(); + this.newMessagesObserver && this.newMessagesObserver.off(); this.appResumeSubscription && this.appResumeSubscription.unsubscribe(); this.pushObserver && this.pushObserver.unsubscribe(); this.readChangedObserver && this.readChangedObserver.off(); diff --git a/src/addon/messages/providers/messages.ts b/src/addon/messages/providers/messages.ts index b8df8c02e..aaf5bba86 100644 --- a/src/addon/messages/providers/messages.ts +++ b/src/addon/messages/providers/messages.ts @@ -139,6 +139,41 @@ export class AddonMessagesProvider { }); } + /** + * Format a conversation. + * + * @param {any} conversation Conversation to format. + * @param {number} userId User ID viewing the conversation. + * @return {any} Formatted conversation. + */ + protected formatConversation(conversation: any, userId: number): any { + const numMessages = conversation.messages.length, + lastMessage = numMessages ? conversation.messages[numMessages - 1] : null; + + conversation.lastmessage = lastMessage ? lastMessage.text : null; + conversation.lastmessagedate = lastMessage ? lastMessage.timecreated : null; + conversation.sentfromcurrentuser = lastMessage ? lastMessage.useridfrom == userId : null; + + if (conversation.type == AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) { + const otherUser = conversation.members.reduce((carry, member) => { + if (!carry && member.id != userId) { + carry = member; + } + + return carry; + }, null); + + conversation.name = conversation.name ? conversation.name : otherUser.fullname; + conversation.imageurl = conversation.imageurl ? conversation.imageurl : otherUser.profileimageurl; + conversation.userid = otherUser.id; + conversation.showonlinestatus = otherUser.showonlinestatus; + conversation.isonline = otherUser.isonline; + conversation.isblocked = otherUser.isblocked; + } + + return conversation; + } + /** * Get the cache key for blocked contacts. * @@ -187,6 +222,39 @@ export class AddonMessagesProvider { return this.ROOT_CACHE_KEY + 'discussions'; } + /** + * Get cache key for get conversations. + * + * @param {number} userId User ID. + * @param {number} conversationId Conversation ID. + * @return {string} Cache key. + */ + protected getCacheKeyForConversation(userId: number, conversationId: number): string { + return this.ROOT_CACHE_KEY + 'conversation:' + userId + ':' + conversationId; + } + + /** + * Get cache key for get conversations between users. + * + * @param {number} userId User ID. + * @param {number} otherUserId Other user ID. + * @return {string} Cache key. + */ + protected getCacheKeyForConversationBetweenUsers(userId: number, otherUserId: number): string { + return this.ROOT_CACHE_KEY + 'conversationBetweenUsers:' + userId + ':' + otherUserId; + } + + /** + * Get cache key for get conversation messages. + * + * @param {number} userId User ID. + * @param {number} conversationId Conversation ID. + * @return {string} Cache key. + */ + protected getCacheKeyForConversationMessages(userId: number, conversationId: number): string { + return this.ROOT_CACHE_KEY + 'conversationMessages:' + userId + ':' + conversationId; + } + /** * Get cache key for get conversations. * @@ -297,6 +365,182 @@ export class AddonMessagesProvider { }); } + /** + * Get a conversation by the conversation ID. + * + * @param {number} conversationId Conversation ID to fetch. + * @param {boolean} [includeContactRequests] Include contact requests. + * @param {boolean} [includePrivacyInfo] Include privacy info. + * @param {number} [messageOffset=0] Offset for messages list. + * @param {number} [messageLimit=1] Limit of messages. Defaults to 1 (last message). + * We recommend getConversationMessages to get them. + * @param {number} [memberOffset=0] Offset for members list. + * @param {number} [memberLimit=2] Limit of members. Defaults to 2 (to be able to know the other user in individual ones). + * We recommend getConversationMembers to get them. + * @param {boolean} [newestFirst=true] Whether to order messages by newest first. + * @param {string} [siteId] Site ID. If not defined, use current site. + * @param {number} [userId] User ID. If not defined, current user in the site. + * @return {Promise} Promise resolved with the response. + * @since 3.6 + */ + getConversation(conversationId: number, includeContactRequests?: boolean, includePrivacyInfo?: boolean, + messageOffset: number = 0, messageLimit: number = 1, memberOffset: number = 0, memberLimit: number = 2, + newestFirst: boolean = true, siteId?: string, userId?: number): Promise { + + return this.sitesProvider.getSite(siteId).then((site) => { + userId = userId || site.getUserId(); + + const preSets = { + cacheKey: this.getCacheKeyForConversation(userId, conversationId) + }, + params: any = { + userid: userId, + conversationid: conversationId, + includecontactrequests: includeContactRequests ? 1 : 0, + includeprivacyinfo: includePrivacyInfo ? 1 : 0, + messageoffset: messageOffset, + messagelimit: messageLimit, + memberoffset: memberOffset, + memberlimit: memberLimit, + newestmessagesfirst: newestFirst ? 1 : 0 + }; + + return site.read('core_message_get_conversation', params, preSets).then((conversation) => { + return this.formatConversation(conversation, userId); + }); + }); + } + + /** + * Get a conversation between two users. + * + * @param {number} otherUserId The other user ID. + * @param {boolean} [includeContactRequests] Include contact requests. + * @param {boolean} [includePrivacyInfo] Include privacy info. + * @param {number} [messageOffset=0] Offset for messages list. + * @param {number} [messageLimit=1] Limit of messages. Defaults to 1 (last message). + * We recommend getConversationMessages to get them. + * @param {number} [memberOffset=0] Offset for members list. + * @param {number} [memberLimit=2] Limit of members. Defaults to 2 (to be able to know the other user in individual ones). + * We recommend getConversationMembers to get them. + * @param {boolean} [newestFirst=true] Whether to order messages by newest first. + * @param {string} [siteId] Site ID. If not defined, use current site. + * @param {number} [userId] User ID. If not defined, current user in the site. + * @return {Promise} Promise resolved with the response. + * @since 3.6 + */ + getConversationBetweenUsers(otherUserId: number, includeContactRequests?: boolean, includePrivacyInfo?: boolean, + messageOffset: number = 0, messageLimit: number = 1, memberOffset: number = 0, memberLimit: number = 2, + newestFirst: boolean = true, siteId?: string, userId?: number): Promise { + + return this.sitesProvider.getSite(siteId).then((site) => { + userId = userId || site.getUserId(); + + const preSets = { + cacheKey: this.getCacheKeyForConversationBetweenUsers(userId, otherUserId) + }, + params: any = { + userid: userId, + otheruserid: otherUserId, + includecontactrequests: includeContactRequests ? 1 : 0, + includeprivacyinfo: includePrivacyInfo ? 1 : 0, + messageoffset: messageOffset, + messagelimit: messageLimit, + memberoffset: memberOffset, + memberlimit: memberLimit, + newestmessagesfirst: newestFirst ? 1 : 0 + }; + + return site.read('core_message_get_conversation_between_users', params, preSets).then((conversation) => { + return this.formatConversation(conversation, userId); + }); + }); + } + + /** + * Get a conversation by the conversation ID. + * + * @param {number} conversationId Conversation ID to fetch. + * @param {boolean} excludePending True to exclude messages pending to be sent. + * @param {number} [limitFrom=0] Offset for messages list. + * @param {number} [limitTo] Limit of messages. + * @param {boolean} [newestFirst=true] Whether to order messages by newest first. + * @param {number} [timeFrom] The timestamp from which the messages were created. + * @param {string} [siteId] Site ID. If not defined, use current site. + * @param {number} [userId] User ID. If not defined, current user in the site. + * @return {Promise} Promise resolved with the response. + * @since 3.6 + */ + getConversationMessages(conversationId: number, excludePending: boolean, limitFrom: number = 0, limitTo?: number, + newestFirst: boolean = true, timeFrom: number = 0, siteId?: string, userId?: number): Promise { + + return this.sitesProvider.getSite(siteId).then((site) => { + userId = userId || site.getUserId(); + + if (typeof limitTo == 'undefined' || limitTo === null) { + limitTo = this.LIMIT_MESSAGES; + } + + const preSets = { + cacheKey: this.getCacheKeyForConversationMessages(userId, conversationId) + }, + params: any = { + currentuserid: userId, + convid: conversationId, + limitfrom: limitFrom, + limitnum: limitTo < 1 ? limitTo : limitTo + 1, // If there is a limit, get 1 more than requested. + newest: newestFirst ? 1 : 0, + timefrom: timeFrom + }; + + if (limitFrom > 0) { + // Do not use cache when retrieving older messages. + // This is to prevent storing too much data and to prevent inconsistencies between "pages" loaded. + preSets['getFromCache'] = false; + preSets['saveToCache'] = false; + preSets['emergencyCache'] = false; + } + + return site.read('core_message_get_conversation_messages', params, preSets).then((result) => { + if (limitTo < 1) { + result.canLoadMore = false; + result.messages = result.messages; + } else { + result.canLoadMore = result.messages.length > limitTo; + result.messages = result.messages.slice(0, limitTo); + } + + result.messages.forEach((message) => { + // Convert time to milliseconds. + message.timecreated = message.timecreated ? message.timecreated * 1000 : 0; + }); + + if (this.appProvider.isDesktop() && params.useridto == userId && limitFrom === 0) { + // Store the last received message (we cannot know if it's unread or not). Don't block the user for this. + this.storeLastReceivedMessageIfNeeded(conversationId, result.messages[0], site.getId()); + } + + if (excludePending) { + // No need to get offline messages, return the ones we have. + return result; + } + + // Get offline messages. + return this.messagesOffline.getMessages(userId).then((offlineMessages) => { + // Mark offline messages as pending. + offlineMessages.forEach((message) => { + message.pending = true; + message.text = message.smallmessage; + }); + + result.messages = result.messages.concat(offlineMessages); + + return result; + }); + }); + }); + } + /** * Get the discussions of a certain user. This function is used in Moodle sites higher than 3.6. * If the site is older than 3.6, please use getDiscussions. @@ -308,6 +552,7 @@ export class AddonMessagesProvider { * @param {string} [siteId] Site ID. If not defined, use current site. * @param {number} [userId] User ID. If not defined, current user in the site. * @return {Promise} Promise resolved with the conversations. + * @since 3.6 */ getConversations(type?: number, favourites?: boolean, limitFrom: number = 0, siteId?: string, userId?: number) : Promise<{conversations: any[], canLoadMore: boolean}> { @@ -333,32 +578,8 @@ export class AddonMessagesProvider { return site.read('core_message_get_conversations', params, preSets).then((response) => { // Format the conversations, adding some calculated fields. - const conversations = response.conversations.map((conversation) => { - const numMessages = conversation.messages.length, - lastMessage = numMessages ? conversation.messages[numMessages - 1] : null; - - conversation.lastmessage = lastMessage ? lastMessage.text : null; - conversation.lastmessagedate = lastMessage ? lastMessage.timecreated : null; - conversation.sentfromcurrentuser = lastMessage ? lastMessage.useridfrom == userId : null; - - if (conversation.type == AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) { - const otherUser = conversation.members.reduce((carry, member) => { - if (!carry && member.id != userId) { - carry = member; - } - - return carry; - }, null); - - conversation.name = conversation.name ? conversation.name : otherUser.fullname; - conversation.imageurl = conversation.imageurl ? conversation.imageurl : otherUser.profileimageurl; - conversation.userid = otherUser.id; - conversation.showonlinestatus = otherUser.showonlinestatus; - conversation.isonline = otherUser.isonline; - conversation.isblocked = otherUser.isblocked; - } - - return conversation; + const conversations = response.conversations.slice(0, this.LIMIT_MESSAGES).map((conversation) => { + return this.formatConversation(conversation, userId); }); return { @@ -813,7 +1034,55 @@ export class AddonMessagesProvider { } /** - * Invalidate contacts cache. + * Invalidate conversation. + * + * @param {number} conversationId Conversation ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @param {number} [userId] User ID. If not defined, current user in the site. + * @return {Promise} Resolved when done. + */ + invalidateConversation(conversationId: number, siteId?: string, userId?: number): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + userId = userId || site.getUserId(); + + return site.invalidateWsCacheForKey(this.getCacheKeyForConversation(conversationId, userId)); + }); + } + + /** + * Invalidate conversation between users. + * + * @param {number} otherUserId Other user ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @param {number} [userId] User ID. If not defined, current user in the site. + * @return {Promise} Resolved when done. + */ + invalidateConversationBetweenUsers(otherUserId: number, siteId?: string, userId?: number): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + userId = userId || site.getUserId(); + + return site.invalidateWsCacheForKey(this.getCacheKeyForConversationBetweenUsers(userId, otherUserId)); + }); + } + + /** + * Invalidate conversation messages cache. + * + * @param {number} conversationId Conversation ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @param {number} [userId] User ID. If not defined, current user in the site. + * @return {Promise} Resolved when done. + */ + invalidateConversationMessages(conversationId: number, siteId?: string, userId?: number): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + userId = userId || site.getUserId(); + + return site.invalidateWsCacheForKey(this.getCacheKeyForConversationMessages(userId, conversationId)); + }); + } + + /** + * Invalidate conversations cache. * * @param {string} [siteId] Site ID. If not defined, current site. * @param {number} [userId] User ID. If not defined, current user in the site. @@ -1003,6 +1272,25 @@ export class AddonMessagesProvider { return this.sitesProvider.getCurrentSite().write('core_message_mark_message_read', params); } + /** + * Mark all messages of a conversation as read. + * + * @param {number} conversationId Conversation ID. + * @returns {Promise} Promise resolved if success. + * @since 3.6 + */ + markAllConversationMessagesRead(conversationId?: number): Promise { + const params = { + userid: this.sitesProvider.getCurrentSiteUserId(), + conversationid: conversationId + }, + preSets = { + responseExpected: false + }; + + return this.sitesProvider.getCurrentSite().write('core_message_mark_all_conversation_messages_as_read', params, preSets); + } + /** * Mark all messages of a discussion as read. * @@ -1238,17 +1526,17 @@ export class AddonMessagesProvider { /** * Store the last received message if it's newer than the last stored. * - * @param {number} userIdFrom ID of the useridfrom retrieved, 0 for all users. + * @param {number} convIdOrUserIdFrom Conversation ID (3.6+) or ID of the useridfrom retrieved (3.5-), 0 for all users. * @param {any} message Last message received. * @param {string} [siteId] Site ID. If not defined, current site. * @return {Promise} Promise resolved when done. */ - protected storeLastReceivedMessageIfNeeded(userIdFrom: number, message: any, siteId?: string): Promise { + protected storeLastReceivedMessageIfNeeded(convIdOrUserIdFrom: number, message: any, siteId?: string): Promise { const component = AddonMessagesProvider.PUSH_SIMULATION_COMPONENT; // Get the last received message. return this.emulatorHelper.getLastReceivedNotification(component, siteId).then((lastMessage) => { - if (userIdFrom > 0 && (!message || !lastMessage)) { + if (convIdOrUserIdFrom > 0 && (!message || !lastMessage)) { // Seeing a single discussion. No received message or cannot know if it really is the last received message. Stop. return; } diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 591747ca6..29c852b9c 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -165,7 +165,9 @@ "addon.messages.errorwhileretrievingcontacts": "Error while retrieving contacts from the server.", "addon.messages.errorwhileretrievingdiscussions": "Error while retrieving discussions from the server.", "addon.messages.errorwhileretrievingmessages": "Error while retrieving messages from the server.", + "addon.messages.groupinfo": "Group info", "addon.messages.groupmessages": "Group messages", + "addon.messages.info": "Info", "addon.messages.message": "Message", "addon.messages.messagenotsent": "The message was not sent. Please try again later.", "addon.messages.messagepreferences": "Message preferences", @@ -178,6 +180,7 @@ "addon.messages.nousersfound": "No users found", "addon.messages.removecontact": "Remove contact", "addon.messages.removecontactconfirm": "Contact will be removed from your contacts list.", + "addon.messages.showdeletemessages": "Show delete messages", "addon.messages.type_blocked": "Blocked", "addon.messages.type_offline": "Offline", "addon.messages.type_online": "Online", From 471061090927cd0856d84921db4df4a9820546f4 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 19 Nov 2018 13:04:59 +0100 Subject: [PATCH 02/11] MOBILE-2632 message: Display user data from messages --- .../messages/pages/discussion/discussion.html | 8 +++++- .../messages/pages/discussion/discussion.ts | 27 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/addon/messages/pages/discussion/discussion.html b/src/addon/messages/pages/discussion/discussion.html index 74e8b3e88..36466b3c3 100644 --- a/src/addon/messages/pages/discussion/discussion.html +++ b/src/addon/messages/pages/discussion/discussion.html @@ -20,7 +20,7 @@ - + {{ message.timecreated | coreFormatDate: "LL" }} @@ -30,6 +30,12 @@ + + + + +

{{ members[message.useridfrom].fullname }}

+

diff --git a/src/addon/messages/pages/discussion/discussion.ts b/src/addon/messages/pages/discussion/discussion.ts index 7df042ffa..606e6081c 100644 --- a/src/addon/messages/pages/discussion/discussion.ts +++ b/src/addon/messages/pages/discussion/discussion.ts @@ -73,6 +73,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { canDelete = false; groupMessagingEnabled: boolean; isGroup = false; + members: any = {}; // Members that wrote a message, indexed by ID. constructor(private eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, navParams: NavParams, private userProvider: CoreUserProvider, private navCtrl: NavController, private messagesSync: AddonMessagesSyncProvider, @@ -294,6 +295,12 @@ export class AddonMessagesDiscussionPage implements OnDestroy { // Sort the messages. this.messagesProvider.sortMessages(this.messages); + // Calculate which messages need to display the date or user data. + this.messages.forEach((message, index): any => { + message.showDate = this.showDate(message, this.messages[index - 1]); + message.showUserData = this.showUserData(message, this.messages[index - 1]); + }); + // Notify that there can be a new message. this.notifyNewMessage(); @@ -360,6 +367,13 @@ export class AddonMessagesDiscussionPage implements OnDestroy { return this.messagesProvider.getConversationMessages(this.conversationId, excludePending, offset).then((result) => { pagesToLoad--; + // Treat members. Don't use CoreUtilsProvider.arrayToObject because we don't want to override the existing object. + if (result.members) { + result.members.forEach((member) => { + this.members[member.id] = member; + }); + } + if (pagesToLoad > 0 && result.canLoadMore) { offset += AddonMessagesProvider.LIMIT_MESSAGES; @@ -831,6 +845,19 @@ export class AddonMessagesDiscussionPage implements OnDestroy { return !moment(message.timecreated).isSame(prevMessage.timecreated, 'day'); } + /** + * Check if the user info should be displayed for the current message. + * User data is only displayed for group conversations if the previous message was from another user. + * + * @param {any} message Current message where to show the user info. + * @param {any} [prevMessage] Previous message. + * @return {boolean} Whether user data should be shown. + */ + showUserData(message: any, prevMessage?: any): boolean { + return this.isGroup && message.useridfrom != this.currentUserId && this.members[message.useridfrom] && + (!prevMessage || prevMessage.useridfrom != message.useridfrom || message.showDate); + } + /** * Toggles delete state. */ From 06f7a427c6537ccd6cefcd668845b9249f28cfaa Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 20 Nov 2018 11:31:52 +0100 Subject: [PATCH 03/11] MOBILE-2632 message: Send message online and offline --- src/addon/messages/lang/en.json | 1 + .../messages/pages/discussion/discussion.ts | 128 ++++++++------ .../messages/providers/messages-offline.ts | 150 +++++++++++++--- src/addon/messages/providers/messages.ts | 127 ++++++++++++- src/addon/messages/providers/sync.ts | 167 ++++++++++++++---- src/assets/lang/en.json | 1 + 6 files changed, 464 insertions(+), 110 deletions(-) diff --git a/src/addon/messages/lang/en.json b/src/addon/messages/lang/en.json index 6de944be2..1311eb52a 100644 --- a/src/addon/messages/lang/en.json +++ b/src/addon/messages/lang/en.json @@ -40,6 +40,7 @@ "type_strangers": "Others", "unblockuser": "Unblock user", "unblockuserconfirm": "Are you sure you want to unblock {{$a}}?", + "warningconversationmessagenotsent": "Couldn't send message(s) to conversation {{conversation}}. {{error}}", "warningmessagenotsent": "Couldn't send message(s) to user {{user}}. {{error}}", "you": "You:" } \ No newline at end of file diff --git a/src/addon/messages/pages/discussion/discussion.ts b/src/addon/messages/pages/discussion/discussion.ts index 606e6081c..fe0ee05cd 100644 --- a/src/addon/messages/pages/discussion/discussion.ts +++ b/src/addon/messages/pages/discussion/discussion.ts @@ -18,6 +18,7 @@ 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 { AddonMessagesSyncProvider } from '../../providers/sync'; import { CoreUserProvider } from '@core/user/providers/user'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; @@ -79,7 +80,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { private userProvider: CoreUserProvider, private navCtrl: NavController, private messagesSync: AddonMessagesSyncProvider, private domUtils: CoreDomUtilsProvider, private messagesProvider: AddonMessagesProvider, logger: CoreLoggerProvider, private utils: CoreUtilsProvider, private appProvider: CoreAppProvider, private translate: TranslateService, - @Optional() private svComponent: CoreSplitViewComponent) { + @Optional() private svComponent: CoreSplitViewComponent, private messagesOffline: AddonMessagesOfflineProvider) { this.siteId = sitesProvider.getCurrentSiteId(); this.currentUserId = sitesProvider.getCurrentSiteUserId(); this.groupMessagingEnabled = this.messagesProvider.isGroupMessagingEnabled(); @@ -166,7 +167,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { } // Synchronize messages if needed. - this.messagesSync.syncDiscussion(this.userId).catch(() => { + this.messagesSync.syncDiscussion(this.conversationId, this.userId).catch(() => { // Ignore errors. }).then((warnings) => { if (warnings && warnings[0]) { @@ -253,7 +254,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { this.fetching = true; // Wait for synchronization process to finish. - return this.messagesSync.waitForSync(this.userId).then(() => { + return this.messagesSync.waitForSyncConversation(this.conversationId, this.userId).then(() => { // Fetch messages. Invalidate the cache before fetching. if (this.groupMessagingEnabled) { return this.messagesProvider.invalidateConversationMessages(this.conversationId).catch(() => { @@ -269,49 +270,57 @@ export class AddonMessagesDiscussionPage implements OnDestroy { }); } }).then((messages) => { - if (this.viewDestroyed) { - return Promise.resolve(); - } - - // Check if we are at the bottom to scroll it after render. - this.scrollBottom = this.domUtils.getScrollHeight(this.content) - this.domUtils.getScrollTop(this.content) === - this.domUtils.getContentHeight(this.content); - - if (this.messagesBeingSent > 0) { - // Ignore polling due to a race condition. - return Promise.reject(null); - } - - // Add new messages to the list and mark the messages that should still be displayed. - messages.forEach((message) => { - this.addMessage(message); - }); - - // Remove messages that shouldn't be in the list anymore. - for (const hash in this.keepMessageMap) { - this.removeMessage(hash); - } - - // Sort the messages. - this.messagesProvider.sortMessages(this.messages); - - // Calculate which messages need to display the date or user data. - this.messages.forEach((message, index): any => { - message.showDate = this.showDate(message, this.messages[index - 1]); - message.showUserData = this.showUserData(message, this.messages[index - 1]); - }); - - // Notify that there can be a new message. - this.notifyNewMessage(); - - // Mark retrieved messages as read if they are not. - this.markMessagesAsRead(); - + this.loadMessages(messages); }).finally(() => { this.fetching = false; }); } + /** + * Format and load a list of messages into the view. + * + * @param {any[]} messages Messages to load. + */ + protected loadMessages(messages: any[]): void { + if (this.viewDestroyed) { + return; + } + + // Check if we are at the bottom to scroll it after render. + this.scrollBottom = this.domUtils.getScrollHeight(this.content) - this.domUtils.getScrollTop(this.content) === + this.domUtils.getContentHeight(this.content); + + if (this.messagesBeingSent > 0) { + // Ignore polling due to a race condition. + return; + } + + // Add new messages to the list and mark the messages that should still be displayed. + messages.forEach((message) => { + this.addMessage(message); + }); + + // Remove messages that shouldn't be in the list anymore. + for (const hash in this.keepMessageMap) { + this.removeMessage(hash); + } + + // Sort the messages. + this.messagesProvider.sortMessages(this.messages); + + // Calculate which messages need to display the date or user data. + this.messages.forEach((message, index): any => { + message.showDate = this.showDate(message, this.messages[index - 1]); + message.showUserData = this.showUserData(message, this.messages[index - 1]); + }); + + // Notify that there can be a new message. + this.notifyNewMessage(); + + // Mark retrieved messages as read if they are not. + this.markMessagesAsRead(); + } + /** * Get the conversation. * @@ -328,18 +337,29 @@ export class AddonMessagesDiscussionPage implements OnDestroy { } else { // We don't have the conversation ID, check if it exists. promise = this.messagesProvider.getConversationBetweenUsers(this.userId).catch((error) => { - if (error.errorcode == 'conversationdoesntexist') { - // Conversation does not exist, return undefined. - return; - } - return Promise.reject(error); + // Probably conversation does not exist or user is offline. Try to load offline messages. + return this.messagesOffline.getMessages(this.userId).then((messages) => { + if (messages && messages.length) { + // We have offline messages, this probably means that the conversation didn't exist. Don't display error. + messages.forEach((message) => { + message.pending = true; + message.text = message.smallmessage; + }); + + this.loadMessages(messages); + } else if (error.errorcode != 'conversationdoesntexist') { + // Display the error. + return Promise.reject(error); + } + }); }); } return promise.then((conversation) => { this.conversation = conversation; if (conversation) { + this.conversationId = conversation.id; this.title = conversation.name; this.conversationImage = conversation.imageurl; this.isGroup = conversation.type == AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_GROUP; @@ -677,7 +697,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { * Function to delete a message. * * @param {any} message Message object to delete. - * @param {number} index Index where the mesasge is to delete it from the view. + * @param {number} index Index where the message is to delete it from the view. */ deleteMessage(message: any, index: number): void { const langKey = message.pending ? 'core.areyousure' : 'addon.messages.deletemessageconfirmation'; @@ -779,6 +799,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { text: text, timecreated: new Date().getTime() }; + message.showDate = this.showDate(message, this.messages[this.messages.length - 1]); this.addMessage(message, false); this.messagesBeingSent++; @@ -786,7 +807,15 @@ export class AddonMessagesDiscussionPage implements OnDestroy { // If there is an ongoing fetch, wait for it to finish. // Otherwise, if a message is sent while fetching it could disappear until the next fetch. this.waitForFetch().finally(() => { - this.messagesProvider.sendMessage(this.userId, text).then((data) => { + let promise; + + if (this.conversationId) { + promise = this.messagesProvider.sendMessageToConversation(this.conversationId, text); + } else { + promise = this.messagesProvider.sendMessage(this.userId, text); + } + + promise.then((data) => { let promise; this.messagesBeingSent--; @@ -836,9 +865,6 @@ export class AddonMessagesDiscussionPage implements OnDestroy { if (!prevMessage) { // First message, show it. return true; - } else if (message.pending) { - // If pending, it has no date, not show. - return false; } // Check if day has changed. diff --git a/src/addon/messages/providers/messages-offline.ts b/src/addon/messages/providers/messages-offline.ts index 5bf12837c..022515c74 100644 --- a/src/addon/messages/providers/messages-offline.ts +++ b/src/addon/messages/providers/messages-offline.ts @@ -26,7 +26,8 @@ export class AddonMessagesOfflineProvider { protected logger; // Variables for database. - static MESSAGES_TABLE = 'addon_messages_offline_messages'; + static MESSAGES_TABLE = 'addon_messages_offline_messages'; // When group messaging isn't available or a new conversation starts. + static CONVERSATION_MESSAGES_TABLE = 'addon_messages_offline_conversation_messages'; // Conversation messages. protected tablesSchema = [ { name: AddonMessagesOfflineProvider.MESSAGES_TABLE, @@ -53,6 +54,28 @@ export class AddonMessagesOfflineProvider { } ], primaryKeys: ['touserid', 'smallmessage', 'timecreated'] + }, + { + name: AddonMessagesOfflineProvider.CONVERSATION_MESSAGES_TABLE, + columns: [ + { + name: 'conversationid', + type: 'INTEGER' + }, + { + name: 'text', + type: 'TEXT' + }, + { + name: 'timecreated', + type: 'INTEGER' + }, + { + name: 'deviceoffline', // If message was stored because device was offline. + type: 'INTEGER' + } + ], + primaryKeys: ['conversationid', 'text', 'timecreated'] } ]; @@ -61,6 +84,25 @@ export class AddonMessagesOfflineProvider { this.sitesProvider.createTablesFromSchema(this.tablesSchema); } + /** + * Delete a message. + * + * @param {number} conversationId Conversation ID. + * @param {string} message The message. + * @param {number} timeCreated The time the message was created. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if stored, rejected if failure. + */ + deleteConversationMessage(conversationId: number, message: string, timeCreated: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().deleteRecords(AddonMessagesOfflineProvider.CONVERSATION_MESSAGES_TABLE, { + conversationid: conversationId, + text: message, + timecreated: timeCreated + }); + }); + } + /** * Delete a message. * @@ -84,24 +126,18 @@ export class AddonMessagesOfflineProvider { * Get all messages where deviceoffline is set to 1. * * @param {string} [siteId] Site ID. If not defined, current site. - * @return {Promise} Promise resolved with messages. + * @return {Promise} Promise resolved with messages. */ - getAllDeviceOfflineMessages(siteId?: string): Promise { + getAllDeviceOfflineMessages(siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { - return site.getDb().getRecords(AddonMessagesOfflineProvider.MESSAGES_TABLE, {deviceoffline: 1}); - }); - } + const promises = []; - /** - * Get offline messages to send to a certain user. - * - * @param {number} toUserId User ID to get messages to. - * @param {string} [siteId] Site ID. If not defined, current site. - * @return {Promise} Promise resolved with messages. - */ - getMessages(toUserId: number, siteId?: string): Promise { - return this.sitesProvider.getSite(siteId).then((site) => { - return site.getDb().getRecords(AddonMessagesOfflineProvider.MESSAGES_TABLE, {touserid: toUserId}); + promises.push(site.getDb().getRecords(AddonMessagesOfflineProvider.MESSAGES_TABLE, {deviceoffline: 1})); + promises.push(site.getDb().getRecords(AddonMessagesOfflineProvider.CONVERSATION_MESSAGES_TABLE, {deviceoffline: 1})); + + return Promise.all(promises).then((results) => { + return results[0].concat(results[1]); + }); }); } @@ -113,7 +149,54 @@ export class AddonMessagesOfflineProvider { */ getAllMessages(siteId?: string): Promise { return this.sitesProvider.getSite(siteId).then((site) => { - return site.getDb().getAllRecords(AddonMessagesOfflineProvider.MESSAGES_TABLE); + const promises = []; + + promises.push(site.getDb().getAllRecords(AddonMessagesOfflineProvider.MESSAGES_TABLE)); + promises.push(site.getDb().getAllRecords(AddonMessagesOfflineProvider.CONVERSATION_MESSAGES_TABLE)); + + return Promise.all(promises).then((results) => { + return results[0].concat(results[1]); + }); + }); + } + + /** + * Get offline messages to send to a certain user. + * + * @param {number} conversationId Conversation ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with messages. + */ + getConversationMessages(conversationId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecords(AddonMessagesOfflineProvider.CONVERSATION_MESSAGES_TABLE, + {conversationid: conversationId}); + }); + } + + /** + * Get offline messages to send to a certain user. + * + * @param {number} toUserId User ID to get messages to. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with messages. + */ + getMessages(toUserId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.getDb().getRecords(AddonMessagesOfflineProvider.MESSAGES_TABLE, {touserid: toUserId}); + }); + } + + /** + * Check if there are offline messages to send to a conversation. + * + * @param {number} conversationId Conversation ID. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with boolean: true if has offline messages, false otherwise. + */ + hasConversationMessages(conversationId: number, siteId?: string): Promise { + return this.getConversationMessages(conversationId, siteId).then((messages) => { + return !!messages.length; }); } @@ -122,14 +205,37 @@ export class AddonMessagesOfflineProvider { * * @param {number} toUserId User ID to check. * @param {string} [siteId] Site ID. If not defined, current site. - * @return {Promise} Promise resolved with boolean: true if has offline messages, false otherwise. + * @return {Promise} Promise resolved with boolean: true if has offline messages, false otherwise. */ - hasMessages(toUserId: number, siteId?: string): Promise { + hasMessages(toUserId: number, siteId?: string): Promise { return this.getMessages(toUserId, siteId).then((messages) => { return !!messages.length; }); } + /** + * Save a conversation message to be sent later. + * + * @param {number} conversationId Conversation ID. + * @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 { + return this.sitesProvider.getSite(siteId).then((site) => { + const entry = { + conversationid: conversationId, + text: message, + timecreated: Date.now(), + deviceoffline: this.appProvider.isOnline() ? 0 : 1 + }; + + return site.getDb().insertRecord(AddonMessagesOfflineProvider.CONVERSATION_MESSAGES_TABLE, entry).then(() => { + return entry; + }); + }); + } + /** * Save a message to be sent later. * @@ -169,7 +275,11 @@ export class AddonMessagesOfflineProvider { data = { deviceoffline: value ? 1 : 0 }; messages.forEach((message) => { - promises.push(db.insertRecord(AddonMessagesOfflineProvider.MESSAGES_TABLE, data)); + if (message.conversationid) { + promises.push(db.insertRecord(AddonMessagesOfflineProvider.CONVERSATION_MESSAGES_TABLE, data)); + } else { + promises.push(db.insertRecord(AddonMessagesOfflineProvider.MESSAGES_TABLE, data)); + } }); return Promise.all(promises); diff --git a/src/addon/messages/providers/messages.ts b/src/addon/messages/providers/messages.ts index aaf5bba86..d96bba2e9 100644 --- a/src/addon/messages/providers/messages.ts +++ b/src/addon/messages/providers/messages.ts @@ -115,7 +115,11 @@ export class AddonMessagesProvider { } // It's an offline message. - return this.messagesOffline.deleteMessage(message.touserid, message.smallmessage, message.timecreated); + if (message.conversationid) { + return this.messagesOffline.deleteConversationMessage(message.conversationid, message.text, message.timecreated); + } else { + return this.messagesOffline.deleteMessage(message.touserid, message.smallmessage, message.timecreated); + } } /** @@ -127,13 +131,15 @@ export class AddonMessagesProvider { * @return {Promise} Promise resolved when the message has been deleted. */ deleteMessageOnline(id: number, read: number, userId?: number): Promise { - userId = userId || this.sitesProvider.getCurrentSiteUserId(); - const params = { + const params: any = { messageid: id, - userid: userId, - read: read + userid: userId || this.sitesProvider.getCurrentSiteUserId() }; + if (typeof read != 'undefined') { + params.read = read; + } + return this.sitesProvider.getCurrentSite().write('core_message_delete_message', params).then(() => { return this.invalidateDiscussionCache(userId); }); @@ -526,11 +532,11 @@ export class AddonMessagesProvider { } // Get offline messages. - return this.messagesOffline.getMessages(userId).then((offlineMessages) => { + return this.messagesOffline.getConversationMessages(conversationId).then((offlineMessages) => { // Mark offline messages as pending. offlineMessages.forEach((message) => { message.pending = true; - message.text = message.smallmessage; + message.useridfrom = userId; }); result.messages = result.messages.concat(offlineMessages); @@ -1497,6 +1503,113 @@ export class AddonMessagesProvider { }); } + /** + * Send a message to a conversation. + * + * @param {number} conversationId Conversation ID. + * @param {string} message The message to send. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved with: + * - sent (boolean) True if message was sent to server, false if stored in device. + * - message (any) If sent=false, contains the stored message. + * @since 3.6 + */ + sendMessageToConversation(conversationId: number, 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 { + sent: false, + message: entry + }; + }); + }; + + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + if (!this.appProvider.isOnline()) { + // App is offline, store the message. + return storeOffline(); + } + + // 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(() => { + // Error, it's safer to assume it has messages. + return true; + }).then((hasStoredMessages) => { + if (hasStoredMessages) { + return storeOffline(); + } + + // Online and no messages stored. Send it to server. + return this.sendMessageToConversationOnline(conversationId, message).then(() => { + return { sent: true }; + }).catch((error) => { + if (this.utils.isWebServiceError(error)) { + // It's a WebService error, the user cannot send the message so don't store it. + return Promise.reject(error); + } + + // Error sending message, store it to retry later. + return storeOffline(); + }); + }); + } + + /** + * Send a message to a conversation. It will fail if offline or cannot connect. + * + * @param {number} conversationId Conversation ID. + * @param {string} message The message to send + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if success, rejected if failure. + * @since 3.6 + */ + sendMessageToConversationOnline(conversationId: number, message: string, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + const messages = [ + { + text: message, + textformat: 1 + } + ]; + + return this.sendMessagesToConversationOnline(conversationId, messages, siteId).then((response) => { + return this.invalidateConversationMessages(conversationId, siteId).catch(() => { + // Ignore errors. + }).then(() => { + return response[0]; + }); + }); + } + + /** + * Send some messages to a conversation. It will fail if offline or cannot connect. + * + * @param {number} conversationId Conversation ID. + * @param {any} messages Messages to send. Each message must contain text and, optionally, textformat. + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved if success, rejected if failure. + * @since 3.6 + */ + sendMessagesToConversationOnline(conversationId: number, messages: any, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + conversationid: conversationId, + messages: messages.map((message) => { + return { + text: message.text, + textformat: typeof message.textformat != 'undefined' ? message.textformat : 1 + }; + }) + }; + + return site.write('core_message_send_messages_to_conversation', params); + }); + } + /** * Helper method to sort messages by time. * diff --git a/src/addon/messages/providers/sync.ts b/src/addon/messages/providers/sync.ts index 0a9b77ac7..0b2330bf0 100644 --- a/src/addon/messages/providers/sync.ts +++ b/src/addon/messages/providers/sync.ts @@ -42,6 +42,21 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider { super('AddonMessagesSync', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate); } + /** + * Get the ID of a discussion sync. + * + * @param {number} conversationId Conversation ID. + * @param {number} userId User ID talking to (if no conversation ID). + * @return {string} Sync ID. + */ + protected getSyncId(conversationId: number, userId: number): string { + if (conversationId) { + return 'conversationid:' + conversationId; + } else { + return 'userid:' + userId; + } + } + /** * Try to synchronize all the discussions in a certain site or in all sites. * @@ -70,22 +85,39 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider { return promise.then((messages) => { const userIds = [], + conversationIds = [], promises = []; - // Get all the discussions to be synced. + // Get all the conversations to be synced. messages.forEach((message) => { - if (userIds.indexOf(message.touserid) == -1) { + if (message.conversationid) { + if (conversationIds.indexOf(message.conversationid) == -1) { + conversationIds.push(message.conversationid); + } + } else if (userIds.indexOf(message.touserid) == -1) { userIds.push(message.touserid); } }); - // Sync all discussions. - userIds.forEach((userId) => { - promises.push(this.syncDiscussion(userId, siteId).then((warnings) => { + // Sync all conversations. + conversationIds.forEach((conversationId) => { + promises.push(this.syncDiscussion(conversationId, undefined, siteId).then((warnings) => { if (typeof warnings != 'undefined') { // Sync successful, send event. this.eventsProvider.trigger(AddonMessagesSyncProvider.AUTO_SYNCED, { - userid: userId, + conversationId: conversationId, + warnings: warnings + }, siteId); + } + })); + }); + + userIds.forEach((userId) => { + promises.push(this.syncDiscussion(undefined, userId, siteId).then((warnings) => { + if (typeof warnings != 'undefined') { + // Sync successful, send event. + this.eventsProvider.trigger(AddonMessagesSyncProvider.AUTO_SYNCED, { + userId: userId, warnings: warnings }, siteId); } @@ -99,24 +131,39 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider { /** * Synchronize a discussion. * - * @param {number} userId User ID of the discussion. - * @param {string} [siteId] Site ID. If not defined, current site. - * @return {Promise} Promise resolved if sync is successful, rejected otherwise. + * @param {number} conversationId Conversation ID. + * @param {number} userId User ID talking to (if no conversation ID). + * @return {Promise} Promise resolved if sync is successful, rejected otherwise. */ - syncDiscussion(userId: number, siteId?: string): Promise { + syncDiscussion(conversationId: number, userId: number, siteId?: string): Promise { siteId = siteId || this.sitesProvider.getCurrentSiteId(); - if (this.isSyncing(userId, siteId)) { - // There's already a sync ongoing for this SCORM, return the promise. - return this.getOngoingSync(userId, siteId); + const syncId = this.getSyncId(conversationId, userId), + groupMessagingEnabled = this.messagesProvider.isGroupMessagingEnabled(); + + if (this.isSyncing(syncId, siteId)) { + // There's already a sync ongoing for this conversation, return the promise. + return this.getOngoingSync(syncId, siteId); } const warnings = []; - this.logger.debug(`Try to sync discussion with user '${userId}'`); + if (conversationId) { + this.logger.debug(`Try to sync conversation '${conversationId}'`); + } else { + this.logger.debug(`Try to sync discussion with user '${userId}'`); + } // Get offline messages to be sent. - const syncPromise = this.messagesOffline.getMessages(userId, siteId).then((messages) => { + let syncPromise; + + if (conversationId) { + syncPromise = this.messagesOffline.getConversationMessages(conversationId, siteId); + } else { + syncPromise = this.messagesOffline.getMessages(userId, siteId); + } + + syncPromise = syncPromise.then((messages) => { if (!messages.length) { // Nothing to sync. return []; @@ -134,12 +181,19 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider { messages = this.messagesProvider.sortMessages(messages); // Send the messages. - // We don't use AddonMessagesProvider#sendMessagesOnline because there's a problem with display order. - // @todo Use AddonMessagesProvider#sendMessagesOnline once the display order is fixed. + // Send them 1 by 1 to simulate web's behaviour and to make sure we know which message has failed. messages.forEach((message, index) => { // Chain message sending. If 1 message fails to be sent we'll stop sending. promise = promise.then(() => { - return this.messagesProvider.sendMessageOnline(userId, message.smallmessage, siteId).catch((error) => { + let subPromise; + + if (conversationId) { + subPromise = this.messagesProvider.sendMessageToConversationOnline(conversationId, message.text, siteId); + } else { + subPromise = this.messagesProvider.sendMessageOnline(userId, message.smallmessage, siteId); + } + + return subPromise.catch((error) => { if (this.utils.isWebServiceError(error)) { // Error returned by WS. Store the error to show a warning but keep sending messages. if (errors.indexOf(error) == -1) { @@ -158,22 +212,61 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider { return Promise.reject(error); }).then(() => { // Message was sent, delete it from local DB. - return this.messagesOffline.deleteMessage(userId, message.smallmessage, message.timecreated, siteId); + if (conversationId) { + return this.messagesOffline.deleteConversationMessage(conversationId, message.text, + message.timecreated, siteId); + } else { + return this.messagesOffline.deleteMessage(userId, message.smallmessage, message.timecreated, siteId); + } }).then(() => { - // All done. Wait 1 second to ensure timecreated of messages is different. - if (index < messages.length - 1) { + // In some Moodle versions, wait 1 second to make sure timecreated is different. + // This is because there was a bug where messages with the same timecreated had a wrong order. + if (!groupMessagingEnabled && index < messages.length - 1) { return setTimeout(() => {return; }, 1000); } }); }); }); - return promise.then(() => { - return errors; - }); + return promise; }).then((errors) => { - if (errors && errors.length) { - // At least an error occurred, get user full name and add errors to warnings array. + return this.handleSyncErrors(conversationId, userId, errors, warnings); + }).then(() => { + // All done, return the warnings. + return warnings; + }); + + return this.addOngoingSync(syncId, syncPromise, siteId); + } + + /** + * Handle sync errors. + * + * @param {number} conversationId Conversation ID. + * @param {number} userId User ID talking to (if no conversation ID). + * @param {any[]} errors List of errors. + * @param {any[]} warnings Array where to place the warnings. + * @return {Promise} Promise resolved when done. + */ + protected handleSyncErrors(conversationId: number, userId: number, errors: any[], warnings: any[]): Promise { + if (errors && errors.length) { + if (conversationId) { + + // Get conversation name and add errors to warnings array. + return this.messagesProvider.getConversation(conversationId, false, false).catch(() => { + // Ignore errors. + return {}; + }).then((conversation) => { + errors.forEach((error) => { + warnings.push(this.translate.instant('addon.messages.warningconversationmessagenotsent', { + conversation: conversation.name ? conversation.name : conversationId, + error: this.textUtils.getErrorMessageFromError(error) + })); + }); + }); + } else { + + // Get user full name and add errors to warnings array. return this.userProvider.getProfile(userId, undefined, true).catch(() => { // Ignore errors. return {}; @@ -181,16 +274,26 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider { errors.forEach((error) => { warnings.push(this.translate.instant('addon.messages.warningmessagenotsent', { user: user.fullname ? user.fullname : userId, - error: error + error: this.textUtils.getErrorMessageFromError(error) })); }); }); } - }).then(() => { - // All done, return the warnings. - return warnings; - }); + } + } - return this.addOngoingSync(userId, syncPromise, siteId); + /** + * If there's an ongoing sync for a certain conversation, wait for it to end. + * If there's no sync ongoing the promise will be resolved right away. + * + * @param {number} conversationId Conversation ID. + * @param {number} userId User ID talking to (if no conversation ID). + * @param {string} [siteId] Site ID. If not defined, current site. + * @return {Promise} Promise resolved when there's no sync going on for the identifier. + */ + waitForSyncConversation(conversationId: number, userId: number, siteId?: string): Promise { + const syncId = this.getSyncId(conversationId, userId); + + return this.waitForSync(syncId, siteId); } } diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 29c852b9c..e6386fb88 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -188,6 +188,7 @@ "addon.messages.type_strangers": "Others", "addon.messages.unblockuser": "Unblock user", "addon.messages.unblockuserconfirm": "Are you sure you want to unblock {{$a}}?", + "addon.messages.warningconversationmessagenotsent": "Couldn't send message(s) to conversation {{conversation}}. {{error}}", "addon.messages.warningmessagenotsent": "Couldn't send message(s) to user {{user}}. {{error}}", "addon.messages.you": "You:", "addon.mod_assign.acceptsubmissionstatement": "Please accept the submission statement.", From ffc98f2c71ad81fd5b2fad77a82a46e8648167f0 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 20 Nov 2018 13:16:31 +0100 Subject: [PATCH 04/11] 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) { From 7588808b75751011f14a5a524a4ce96e1aa2666e Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 20 Nov 2018 15:08:24 +0100 Subject: [PATCH 05/11] MOBILE-2632 message: Get conversation after sending first message --- .../messages/pages/discussion/discussion.ts | 35 +++++++++++++------ .../group-conversations.html | 2 +- .../group-conversations.ts | 6 ++-- src/addon/messages/providers/messages.ts | 16 ++++++--- 4 files changed, 42 insertions(+), 17 deletions(-) diff --git a/src/addon/messages/pages/discussion/discussion.ts b/src/addon/messages/pages/discussion/discussion.ts index 953a57f21..e2f2100b2 100644 --- a/src/addon/messages/pages/discussion/discussion.ts +++ b/src/addon/messages/pages/discussion/discussion.ts @@ -141,7 +141,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { const position = this.messages.findIndex((message) => { return message.hash == hash; }); - if (position > 0) { + if (position >= 0) { this.messages.splice(position, 1); } } @@ -176,7 +176,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { if (this.groupMessagingEnabled) { // Get the conversation ID if it exists and we don't have it yet. - return this.getConversation().then((exists) => { + return this.getConversation(this.conversationId, this.userId).then((exists) => { if (exists) { // Fetch the messages for the first time. return this.fetchMessages(); @@ -202,6 +202,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { this.checkCanDelete(); this.resizeContent(); this.loaded = true; + this.setPolling(); // Make sure we're polling messages. }); // Recalculate footer position when keyboard is shown or hidden. @@ -324,22 +325,24 @@ export class AddonMessagesDiscussionPage implements OnDestroy { /** * Get the conversation. * + * @param {number} conversationId Conversation ID. + * @param {number} userId User ID. * @return {Promise} Promise resolved with a boolean: whether the conversation exists or not. */ - protected getConversation(): Promise { + protected getConversation(conversationId: number, userId: number): Promise { let promise; - if (this.conversationId) { + if (conversationId) { // Retrieve the conversation. Invalidate data first to get the right unreadcount. - promise = this.messagesProvider.invalidateConversation(this.conversationId).then(() => { - return this.messagesProvider.getConversation(this.conversationId); + promise = this.messagesProvider.invalidateConversation(conversationId).then(() => { + return this.messagesProvider.getConversation(conversationId); }); } else { // We don't have the conversation ID, check if it exists. - promise = this.messagesProvider.getConversationBetweenUsers(this.userId).catch((error) => { + promise = this.messagesProvider.getConversationBetweenUsers(userId).catch((error) => { // Probably conversation does not exist or user is offline. Try to load offline messages. - return this.messagesOffline.getMessages(this.userId).then((messages) => { + return this.messagesOffline.getMessages(userId).then((messages) => { if (messages && messages.length) { // We have offline messages, this probably means that the conversation didn't exist. Don't display error. messages.forEach((message) => { @@ -358,6 +361,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { return promise.then((conversation) => { this.conversation = conversation; + if (conversation) { this.conversationId = conversation.id; this.title = conversation.name; @@ -822,8 +826,19 @@ export class AddonMessagesDiscussionPage implements OnDestroy { this.messagesBeingSent--; if (data.sent) { - // Message was sent, fetch messages right now. - promise = this.fetchMessages(); + if (!this.conversationId && data.message && data.message.conversationid) { + // Message sent to a new conversation, try to load the conversation. + promise = this.getConversation(data.message.conversationid, this.userId).then(() => { + // Now fetch messages. + return this.fetchMessages(); + }).finally(() => { + // Start polling messages now that the conversation exists. + this.setPolling(); + }); + } else { + // Message was sent, fetch messages right now. + promise = this.fetchMessages(); + } } else { promise = Promise.reject(null); } diff --git a/src/addon/messages/pages/group-conversations/group-conversations.html b/src/addon/messages/pages/group-conversations/group-conversations.html index 70bd51f67..92c80a6b4 100644 --- a/src/addon/messages/pages/group-conversations/group-conversations.html +++ b/src/addon/messages/pages/group-conversations/group-conversations.html @@ -97,7 +97,7 @@ - + - + + -

-

+

+

+ + +

+ + {{result.lastmessagedate | coreDateDayOrTime}} + +

+

From ea78f65656af480edcf1f71a4ab5b6d66c31faa8 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 22 Nov 2018 11:07:53 +0100 Subject: [PATCH 08/11] MOBILE-2632 message: View group info --- scripts/langindex.json | 2 + src/addon/messages/lang/en.json | 1 + .../conversation-info/conversation-info.html | 42 ++++++ .../conversation-info.module.ts | 33 +++++ .../conversation-info/conversation-info.scss | 17 +++ .../conversation-info/conversation-info.ts | 130 ++++++++++++++++++ .../messages/pages/discussion/discussion.html | 1 + .../messages/pages/discussion/discussion.ts | 28 +++- .../group-conversations.html | 2 +- .../group-conversations.ts | 127 ++++++++++++----- src/addon/messages/providers/messages.ts | 76 ++++++++++ src/assets/lang/en.json | 1 + 12 files changed, 417 insertions(+), 43 deletions(-) create mode 100644 src/addon/messages/pages/conversation-info/conversation-info.html create mode 100644 src/addon/messages/pages/conversation-info/conversation-info.module.ts create mode 100644 src/addon/messages/pages/conversation-info/conversation-info.scss create mode 100644 src/addon/messages/pages/conversation-info/conversation-info.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index e096f7155..9f1566add 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -178,6 +178,7 @@ "addon.messages.nogroupmessages": "message", "addon.messages.nomessages": "message", "addon.messages.nousersfound": "local_moodlemobileapp", + "addon.messages.numparticipants": "message", "addon.messages.removecontact": "message", "addon.messages.removecontactconfirm": "local_moodlemobileapp", "addon.messages.showdeletemessages": "local_moodlemobileapp", @@ -188,6 +189,7 @@ "addon.messages.type_strangers": "local_moodlemobileapp", "addon.messages.unblockuser": "message", "addon.messages.unblockuserconfirm": "message", + "addon.messages.warningconversationmessagenotsent": "local_moodlemobileapp", "addon.messages.warningmessagenotsent": "local_moodlemobileapp", "addon.messages.you": "message", "addon.mod_assign.acceptsubmissionstatement": "local_moodlemobileapp", diff --git a/src/addon/messages/lang/en.json b/src/addon/messages/lang/en.json index 1311eb52a..a95948ee2 100644 --- a/src/addon/messages/lang/en.json +++ b/src/addon/messages/lang/en.json @@ -30,6 +30,7 @@ "nogroupmessages": "No group messages", "nomessages": "No messages", "nousersfound": "No users found", + "numparticipants": "{{$a}} participants", "removecontact": "Remove contact", "removecontactconfirm": "Contact will be removed from your contacts list.", "showdeletemessages": "Show delete messages", diff --git a/src/addon/messages/pages/conversation-info/conversation-info.html b/src/addon/messages/pages/conversation-info/conversation-info.html new file mode 100644 index 000000000..0aaee5df7 --- /dev/null +++ b/src/addon/messages/pages/conversation-info/conversation-info.html @@ -0,0 +1,42 @@ + + + {{ 'addon.messages.groupinfo' | translate }} + + + + + + + + + + + + +
+ +
+

+

+

{{ 'addon.messages.numparticipants' | translate:{$a: conversation.membercount} }}

+
+ + + + + + +

+

+ + +

+

+
+ + +
+
diff --git a/src/addon/messages/pages/conversation-info/conversation-info.module.ts b/src/addon/messages/pages/conversation-info/conversation-info.module.ts new file mode 100644 index 000000000..f2b0cb96c --- /dev/null +++ b/src/addon/messages/pages/conversation-info/conversation-info.module.ts @@ -0,0 +1,33 @@ +// (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 { NgModule } from '@angular/core'; +import { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { AddonMessagesConversationInfoPage } from './conversation-info'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +@NgModule({ + declarations: [ + AddonMessagesConversationInfoPage, + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + IonicPageModule.forChild(AddonMessagesConversationInfoPage), + TranslateModule.forChild() + ], +}) +export class AddonMessagesConversationInfoPageModule {} diff --git a/src/addon/messages/pages/conversation-info/conversation-info.scss b/src/addon/messages/pages/conversation-info/conversation-info.scss new file mode 100644 index 000000000..26a039907 --- /dev/null +++ b/src/addon/messages/pages/conversation-info/conversation-info.scss @@ -0,0 +1,17 @@ +ion-app.app-root page-addon-messages-group-conversations { + h2 { + display: flex; + justify-content: space-between; + + .note { + margin: 0; + align-self: flex-end; + display: inline-flex; + font-size: initial; + } + } + + core-format-text.addon-message-last-message { + display: inline; + } +} diff --git a/src/addon/messages/pages/conversation-info/conversation-info.ts b/src/addon/messages/pages/conversation-info/conversation-info.ts new file mode 100644 index 000000000..c35856e4a --- /dev/null +++ b/src/addon/messages/pages/conversation-info/conversation-info.ts @@ -0,0 +1,130 @@ +// (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 } from '@angular/core'; +import { IonicPage, NavParams, ViewController } from 'ionic-angular'; +import { AddonMessagesProvider } from '../../providers/messages'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; + +/** + * Page that displays the list of conversations, including group conversations. + */ +@IonicPage({ segment: 'addon-messages-conversation-info' }) +@Component({ + selector: 'page-addon-messages-conversation-info', + templateUrl: 'conversation-info.html', +}) +export class AddonMessagesConversationInfoPage implements OnInit { + + loaded = false; + conversation: any; + members = []; + canLoadMore = false; + + protected conversationId: number; + + constructor(private messagesProvider: AddonMessagesProvider, private domUtils: CoreDomUtilsProvider, navParams: NavParams, + protected viewCtrl: ViewController) { + this.conversationId = navParams.get('conversationId'); + } + + /** + * Component loaded. + */ + ngOnInit(): void { + this.fetchData().finally(() => { + this.loaded = true; + }); + } + + /** + * Fetch the required data. + * + * @return {Promise} Promise resolved when done. + */ + protected fetchData(): Promise { + // Get the conversation data first. + return this.messagesProvider.getConversation(this.conversationId, false, false, 0, 0).then((conversation) => { + this.conversation = conversation; + + // Now get the members. + return this.fetchMembers(); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'Error getting members.'); + }); + } + + /** + * Get conversation members. + * + * @param {boolean} [loadingMore} Whether we are loading more data or just the first ones. + * @return {Promise} Promise resolved when done. + */ + protected fetchMembers(loadingMore?: boolean): Promise { + const limitFrom = loadingMore ? this.members.length : 0; + + return this.messagesProvider.getConversationMembers(this.conversationId, limitFrom).then((data) => { + if (loadingMore) { + this.members = this.members.concat(data.members); + } else { + this.members = data.members; + } + + this.canLoadMore = data.canLoadMore; + }); + } + + /** + * Function to load more members. + * + * @param {any} [infiniteComplete] Infinite scroll complete function. Only used from core-infinite-loading. + * @return {Promise} Resolved when done. + */ + loadMoreMembers(infiniteComplete?: any): Promise { + return this.fetchMembers(true).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'Error getting members.'); + this.canLoadMore = false; + }).finally(() => { + infiniteComplete && infiniteComplete(); + }); + } + + /** + * Refresh the data. + * + * @param {any} [refresher] Refresher. + * @return {Promise} Promise resolved when done. + */ + refreshData(refresher?: any): Promise { + const promises = []; + + promises.push(this.messagesProvider.invalidateConversation(this.conversationId)); + promises.push(this.messagesProvider.invalidateConversationMembers(this.conversationId)); + + return Promise.all(promises).then(() => { + return this.fetchData().finally(() => { + refresher && refresher.complete(); + }); + }); + } + + /** + * Close modal. + * + * @param {number} [userId] User conversation to load. + */ + closeModal(userId?: number): void { + this.viewCtrl.dismiss(userId); + } +} diff --git a/src/addon/messages/pages/discussion/discussion.html b/src/addon/messages/pages/discussion/discussion.html index c9a42162c..0629928fc 100644 --- a/src/addon/messages/pages/discussion/discussion.html +++ b/src/addon/messages/pages/discussion/discussion.html @@ -56,6 +56,7 @@
+ diff --git a/src/addon/messages/pages/discussion/discussion.ts b/src/addon/messages/pages/discussion/discussion.ts index e2f2100b2..7cbe7df9f 100644 --- a/src/addon/messages/pages/discussion/discussion.ts +++ b/src/addon/messages/pages/discussion/discussion.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Component, OnDestroy, ViewChild, Optional } from '@angular/core'; -import { IonicPage, NavParams, NavController, Content } from 'ionic-angular'; +import { IonicPage, NavParams, NavController, Content, ModalController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; @@ -80,7 +80,9 @@ export class AddonMessagesDiscussionPage implements OnDestroy { private userProvider: CoreUserProvider, private navCtrl: NavController, private messagesSync: AddonMessagesSyncProvider, private domUtils: CoreDomUtilsProvider, private messagesProvider: AddonMessagesProvider, logger: CoreLoggerProvider, private utils: CoreUtilsProvider, private appProvider: CoreAppProvider, private translate: TranslateService, - @Optional() private svComponent: CoreSplitViewComponent, private messagesOffline: AddonMessagesOfflineProvider) { + @Optional() private svComponent: CoreSplitViewComponent, private messagesOffline: AddonMessagesOfflineProvider, + private modalCtrl: ModalController) { + this.siteId = sitesProvider.getCurrentSiteId(); this.currentUserId = sitesProvider.getCurrentSiteUserId(); this.groupMessagingEnabled = this.messagesProvider.isGroupMessagingEnabled(); @@ -351,7 +353,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { }); this.loadMessages(messages); - } else if (error.errorcode != 'conversationdoesntexist') { + } else if (error.errorcode != 'errorconversationdoesnotexist') { // Display the error. return Promise.reject(error); } @@ -913,8 +915,26 @@ export class AddonMessagesDiscussionPage implements OnDestroy { */ viewInfo(): void { if (this.isGroup) { - // @todo + // Display the group information. + const modal = this.modalCtrl.create('AddonMessagesConversationInfoPage', { + conversationId: this.conversationId + }); + + modal.present(); + modal.onDidDismiss((userId) => { + if (typeof userId != 'undefined') { + // Open user conversation. + if (this.svComponent) { + // Notify the left pane to load it, this way the right conversation will be highlighted. + this.eventsProvider.trigger(AddonMessagesProvider.OPEN_CONVERSATION_EVENT, {userId: userId}, this.siteId); + } else { + // Open the discussion in a new view. + this.navCtrl.push('AddonMessagesDiscussionPage', {userId: userId}); + } + } + }); } else { + // Open the user profile. const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl; navCtrl.push('CoreUserProfilePage', { userId: this.userId }); } diff --git a/src/addon/messages/pages/group-conversations/group-conversations.html b/src/addon/messages/pages/group-conversations/group-conversations.html index fa9281a0b..130477f88 100644 --- a/src/addon/messages/pages/group-conversations/group-conversations.html +++ b/src/addon/messages/pages/group-conversations/group-conversations.html @@ -105,7 +105,7 @@ - + - +

{{ 'addon.messages.nofavourites' | translate }}

@@ -75,7 +75,7 @@
- +

{{ 'addon.messages.nogroupmessages' | translate }}

@@ -90,7 +90,7 @@
- +

{{ 'addon.messages.nomessages' | translate }}

diff --git a/src/addon/messages/pages/group-conversations/group-conversations.ts b/src/addon/messages/pages/group-conversations/group-conversations.ts index 84a9c6e73..cffa4d295 100644 --- a/src/addon/messages/pages/group-conversations/group-conversations.ts +++ b/src/addon/messages/pages/group-conversations/group-conversations.ts @@ -253,6 +253,8 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { * @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) => { @@ -353,7 +355,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { loadMoreConversations(option: any, infiniteComplete?: any): Promise { return this.fetchDataForOption(option, true).catch((error) => { this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingdiscussions', true); - option.canLoadMore = false; + option.loadMoreError = true; }).finally(() => { infiniteComplete && infiniteComplete(); }); From f09906bc95f0487ca2a058df9c6bf36291ac0199 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 23 Nov 2018 16:02:01 +0100 Subject: [PATCH 10/11] MOBILE-2632 message: Apply online status to new views --- .../conversation-info/conversation-info.html | 6 +----- .../group-conversations.html | 19 ++++++++++--------- .../group-conversations.ts | 1 + src/addon/messages/providers/messages.ts | 1 + src/components/user-avatar/user-avatar.ts | 18 ++++++++++++++---- 5 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/addon/messages/pages/conversation-info/conversation-info.html b/src/addon/messages/pages/conversation-info/conversation-info.html index 69b3dddb3..cfebb4a96 100644 --- a/src/addon/messages/pages/conversation-info/conversation-info.html +++ b/src/addon/messages/pages/conversation-info/conversation-info.html @@ -24,11 +24,7 @@
- - - - +

diff --git a/src/addon/messages/pages/group-conversations/group-conversations.html b/src/addon/messages/pages/group-conversations/group-conversations.html index 274ec53d5..a4cfd033d 100644 --- a/src/addon/messages/pages/group-conversations/group-conversations.html +++ b/src/addon/messages/pages/group-conversations/group-conversations.html @@ -31,9 +31,7 @@ - - - +

@@ -73,7 +71,7 @@

- + @@ -104,13 +102,16 @@ - + - - - + + + + + + +

diff --git a/src/addon/messages/pages/group-conversations/group-conversations.ts b/src/addon/messages/pages/group-conversations/group-conversations.ts index cffa4d295..2c8cf2adc 100644 --- a/src/addon/messages/pages/group-conversations/group-conversations.ts +++ b/src/addon/messages/pages/group-conversations/group-conversations.ts @@ -61,6 +61,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { type: AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, favourites: false }; + typeIndividual = AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_INDIVIDUAL; protected loadingString: string; protected siteId: string; diff --git a/src/addon/messages/providers/messages.ts b/src/addon/messages/providers/messages.ts index 0d855e119..45fbd4cba 100644 --- a/src/addon/messages/providers/messages.ts +++ b/src/addon/messages/providers/messages.ts @@ -176,6 +176,7 @@ export class AddonMessagesProvider { conversation.showonlinestatus = otherUser.showonlinestatus; conversation.isonline = otherUser.isonline; conversation.isblocked = otherUser.isblocked; + conversation.otherUser = otherUser; } return conversation; diff --git a/src/components/user-avatar/user-avatar.ts b/src/components/user-avatar/user-avatar.ts index 20aab67fd..bbbb09c90 100644 --- a/src/components/user-avatar/user-avatar.ts +++ b/src/components/user-avatar/user-avatar.ts @@ -15,6 +15,7 @@ import { Component, Input, OnInit, OnChanges, SimpleChange } from '@angular/core'; import { NavController } from 'ionic-angular'; import { CoreSitesProvider } from '@providers/sites'; +import { CoreUtilsProvider } from '@providers/utils/utils'; /** * Component to display a "user avatar". @@ -41,7 +42,7 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges { protected myUser = false; protected currentUserId: number; - constructor(private navCtrl: NavController, private sitesProvider: CoreSitesProvider) { + constructor(private navCtrl: NavController, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider) { this.currentUserId = this.sitesProvider.getCurrentSiteUserId(); } @@ -75,7 +76,7 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges { this.fullname = this.fullname || (this.user && (this.user.fullname || this.user.userfullname)); - this.userId = this.userId || (this.user && this.user.userid); + this.userId = this.userId || (this.user && (this.user.userid || this.user.id)); this.courseId = this.courseId || (this.user && this.user.courseid); // If not available we cannot ensure the avatar is from the current user. @@ -89,9 +90,18 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges { * @return boolean */ isOnline(): boolean { - const time = new Date().getTime() - this.timetoshowusers; + if (this.myUser || this.utils.isFalseOrZero(this.user.isonline)) { + return false; + } - return !this.myUser && ((this.user.lastaccess && this.user.lastaccess * 1000 >= time) || this.user.isonline); + if (this.user.lastaccess) { + // If the time has passed, don't show the online status. + const time = new Date().getTime() - this.timetoshowusers; + + return this.user.lastaccess * 1000 >= time; + } else { + return this.user.isonline; + } } /** From b125dcdbc92d5423f69b1b5a3aefcea0edc6c7e2 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 27 Nov 2018 03:48:50 +0100 Subject: [PATCH 11/11] MOBILE-2632 message: Fix delay when syncing messages --- src/addon/messages/providers/sync.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/addon/messages/providers/sync.ts b/src/addon/messages/providers/sync.ts index 0b2330bf0..0e87d42f5 100644 --- a/src/addon/messages/providers/sync.ts +++ b/src/addon/messages/providers/sync.ts @@ -222,7 +222,9 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider { // In some Moodle versions, wait 1 second to make sure timecreated is different. // This is because there was a bug where messages with the same timecreated had a wrong order. if (!groupMessagingEnabled && index < messages.length - 1) { - return setTimeout(() => {return; }, 1000); + return new Promise((resolve, reject): any => { + setTimeout(resolve, 1000); + }); } }); });