MOBILE-2632 message: Get conversation messages

main
Dani Palou 2018-11-19 11:10:25 +01:00
parent 601508aefc
commit a6f5543870
8 changed files with 589 additions and 133 deletions

View File

@ -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",

View File

@ -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",

View File

@ -1,15 +1,17 @@
<ion-header>
<ion-navbar core-back-button>
<ion-title><core-format-text [text]="title"></core-format-text></ion-title>
<ion-title>
<img *ngIf="conversationImage" class="core-bar-button-image" [src]="conversationImage">
<core-format-text [text]="title"></core-format-text>
</ion-title>
<ion-buttons end></ion-buttons>
</ion-navbar>
<core-navbar-buttons end>
<button ion-button icon-only clear="true" (click)="toggleDelete()" [hidden]="!canDelete">
<ion-icon name="trash"></ion-icon>
</button>
<a [hidden]="!showProfileLink" core-user-link [userId]="userId" [attr.aria-label]=" 'core.user.viewprofile' | translate">
<img class="button core-bar-button-image" [src]="profileLink" core-external-content onError="this.src='assets/img/user-avatar.png'">
</a>
<core-context-menu>
<core-context-menu-item [hidden]="!showInfo || isGroup" [priority]="1000" [content]="'addon.messages.info' | translate" (action)="viewInfo()" [iconAction]="'information-circle'"></core-context-menu-item>
<core-context-menu-item [hidden]="!showInfo || !isGroup" [priority]="999" [content]="'addon.messages.groupinfo' | translate" (action)="viewInfo()" [iconAction]="'information-circle'"></core-context-menu-item>
<core-context-menu-item [hidden]="!canDelete" [priority]="800" [content]="'addon.messages.showdeletemessages' | translate" (action)="toggleDelete()" [iconAction]="'trash'"></core-context-menu-item>
</core-context-menu>
</core-navbar-buttons>
</ion-header>
<ion-content class="has-footer">
@ -22,12 +24,12 @@
<ion-label>{{ message.timecreated | coreFormatDate: "LL" }}</ion-label>
</ion-chip>
<ion-chip class="addon-messages-unreadfrom" *ngIf="message.unreadFrom" color="light">
<ion-chip class="addon-messages-unreadfrom" *ngIf="unreadMessageFrom && message.id == unreadMessageFrom" color="light">
<ion-label>{{ 'addon.messages.newmessages' | translate:{$a: title} }}</ion-label>
<ion-icon name="arrow-round-down"></ion-icon>
</ion-chip>
<ion-item text-wrap (longPress)="copyMessage(message.smallmessage)" class="addon-message" [class.addon-message-mine]="message.useridfrom == currentUserId" [@coreSlideInOut]="message.useridfrom == currentUserId ? '' : 'fromLeft'">
<ion-item text-wrap (longPress)="copyMessage(message)" class="addon-message" [class.addon-message-mine]="message.useridfrom == currentUserId" [@coreSlideInOut]="message.useridfrom == currentUserId ? '' : 'fromLeft'">
<!-- Some messages have <p> and some others don't. Add a <p> so they all have same styles. -->
<p class="addon-message-text">
<core-format-text (afterRender)="last && scrollToBottom()" [text]="message.text"></core-format-text>

View File

@ -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';
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.profileLink = user.profileimageurl;
this.conversationImage = user.profileimageurl;
});
}
// Synchronize messages if needed.
this.messagesSync.syncDiscussion(this.userId).catch(() => {
@ -161,8 +172,17 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
this.domUtils.showErrorModal(warnings[0]);
}
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.fetchData().then(() => {
return this.fetchMessages();
}
});
} 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.
@ -172,6 +192,8 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
this.title = this.messages[0].userfromfullname || '';
}
}
});
}
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingmessages', true);
}).finally(() => {
@ -179,7 +201,6 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
this.resizeContent();
this.loaded = true;
});
});
// Recalculate footer position when keyboard is shown or hidden.
this.keyboardObserver = this.eventsProvider.on(CoreEventsProvider.KEYBOARD_CHANGE, (kbHeight) => {
@ -204,12 +225,12 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
/**
* Convenience function to fetch messages.
*
* @return {Promise<any>} Resolved when done.
*/
protected fetchData(): Promise<any> {
protected fetchMessages(): Promise<any> {
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.
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.getDiscussion(this.pagesLoaded);
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<boolean>} Promise resolved with a boolean: whether the conversation exists or not.
*/
protected getConversation(): Promise<boolean> {
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<any[]>} Promise resolved with the list of messages.
*/
protected getConversationMessages(pagesToLoad: number, offset: number = 0): Promise<any[]> {
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<any>} Resolved when done.
*/
protected getDiscussion(pagesToLoad: number, lfReceivedUnread: number = 0, lfReceivedRead: number = 0, lfSentUnread: number = 0,
lfSentRead: number = 0): Promise<any> {
protected getDiscussionMessages(pagesToLoad: number, lfReceivedUnread: number = 0, lfReceivedRead: number = 0,
lfSentUnread: number = 0, lfSentRead: number = 0): Promise<any> {
// 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,7 +440,11 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
if (this.messagesProvider.isMarkAllMessagesReadEnabled()) {
let messageUnreadFound = false;
// Mark all messages at a time if one messages is unread.
// 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.
@ -339,14 +453,26 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
break;
}
}
}
if (messageUnreadFound) {
this.setUnreadLabelPosition();
promises.push(this.messagesProvider.markAllMessagesRead(this.userId).then(() => {
readChanged = true;
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,15 +539,32 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
return;
}
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 (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;
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;
const unreadFrom = message.read == 0 && previousMessageRead;
if (message.unreadFrom) {
if (unreadFrom) {
// Save where the label is placed.
this.unreadMessageFrom = parseInt(message.id, 10);
break;
@ -428,6 +573,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
previousMessageRead = message.read != 0;
}
}
}
// Do not update the message unread from label on next refresh.
if (this.unreadMessageFrom == 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.
*/

View File

@ -8,6 +8,8 @@
<button ion-button icon-only (click)="gotoSettings($event)" [attr.aria-label]="'addon.messages.messagepreferences' | translate">
<ion-icon name="cog"></ion-icon>
</button>
<!-- Add an empty context menu so discussion page can add items in split view, otherwise the menu disappears in some cases. -->
<core-context-menu></core-context-menu>
</ion-buttons>
</ion-navbar>
</ion-header>

View File

@ -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();

View File

@ -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<any>} 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<any> {
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<any>} 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<any> {
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<any>} 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<any> {
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<any>} 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<any>} Resolved when done.
*/
invalidateConversation(conversationId: number, siteId?: string, userId?: number): Promise<any> {
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<any>} Resolved when done.
*/
invalidateConversationBetweenUsers(otherUserId: number, siteId?: string, userId?: number): Promise<any> {
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<any>} Resolved when done.
*/
invalidateConversationMessages(conversationId: number, siteId?: string, userId?: number): Promise<any> {
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<any>} Promise resolved if success.
* @since 3.6
*/
markAllConversationMessagesRead(conversationId?: number): Promise<any> {
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<any>} Promise resolved when done.
*/
protected storeLastReceivedMessageIfNeeded(userIdFrom: number, message: any, siteId?: string): Promise<any> {
protected storeLastReceivedMessageIfNeeded(convIdOrUserIdFrom: number, message: any, siteId?: string): Promise<any> {
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;
}

View File

@ -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",