Merge pull request #1633 from dpalou/MOBILE-2632

Mobile 2632
main
Juan Leyva 2018-11-28 11:34:43 +01:00 committed by GitHub
commit e44c28ccdd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 1749 additions and 289 deletions

View File

@ -165,7 +165,9 @@
"addon.messages.errorwhileretrievingcontacts": "local_moodlemobileapp", "addon.messages.errorwhileretrievingcontacts": "local_moodlemobileapp",
"addon.messages.errorwhileretrievingdiscussions": "local_moodlemobileapp", "addon.messages.errorwhileretrievingdiscussions": "local_moodlemobileapp",
"addon.messages.errorwhileretrievingmessages": "local_moodlemobileapp", "addon.messages.errorwhileretrievingmessages": "local_moodlemobileapp",
"addon.messages.groupinfo": "message",
"addon.messages.groupmessages": "message", "addon.messages.groupmessages": "message",
"addon.messages.info": "message",
"addon.messages.message": "message", "addon.messages.message": "message",
"addon.messages.messagenotsent": "local_moodlemobileapp", "addon.messages.messagenotsent": "local_moodlemobileapp",
"addon.messages.messagepreferences": "message", "addon.messages.messagepreferences": "message",
@ -176,8 +178,10 @@
"addon.messages.nogroupmessages": "message", "addon.messages.nogroupmessages": "message",
"addon.messages.nomessages": "message", "addon.messages.nomessages": "message",
"addon.messages.nousersfound": "local_moodlemobileapp", "addon.messages.nousersfound": "local_moodlemobileapp",
"addon.messages.numparticipants": "message",
"addon.messages.removecontact": "message", "addon.messages.removecontact": "message",
"addon.messages.removecontactconfirm": "local_moodlemobileapp", "addon.messages.removecontactconfirm": "local_moodlemobileapp",
"addon.messages.showdeletemessages": "local_moodlemobileapp",
"addon.messages.type_blocked": "local_moodlemobileapp", "addon.messages.type_blocked": "local_moodlemobileapp",
"addon.messages.type_offline": "local_moodlemobileapp", "addon.messages.type_offline": "local_moodlemobileapp",
"addon.messages.type_online": "local_moodlemobileapp", "addon.messages.type_online": "local_moodlemobileapp",
@ -185,6 +189,7 @@
"addon.messages.type_strangers": "local_moodlemobileapp", "addon.messages.type_strangers": "local_moodlemobileapp",
"addon.messages.unblockuser": "message", "addon.messages.unblockuser": "message",
"addon.messages.unblockuserconfirm": "message", "addon.messages.unblockuserconfirm": "message",
"addon.messages.warningconversationmessagenotsent": "local_moodlemobileapp",
"addon.messages.warningmessagenotsent": "local_moodlemobileapp", "addon.messages.warningmessagenotsent": "local_moodlemobileapp",
"addon.messages.you": "message", "addon.messages.you": "message",
"addon.mod_assign.acceptsubmissionstatement": "local_moodlemobileapp", "addon.mod_assign.acceptsubmissionstatement": "local_moodlemobileapp",

View File

@ -17,7 +17,9 @@
"errorwhileretrievingcontacts": "Error while retrieving contacts from the server.", "errorwhileretrievingcontacts": "Error while retrieving contacts from the server.",
"errorwhileretrievingdiscussions": "Error while retrieving discussions from the server.", "errorwhileretrievingdiscussions": "Error while retrieving discussions from the server.",
"errorwhileretrievingmessages": "Error while retrieving messages from the server.", "errorwhileretrievingmessages": "Error while retrieving messages from the server.",
"groupinfo": "Group info",
"groupmessages": "Group messages", "groupmessages": "Group messages",
"info": "Info",
"messagenotsent": "The message was not sent. Please try again later.", "messagenotsent": "The message was not sent. Please try again later.",
"message": "Message", "message": "Message",
"messagepreferences": "Message preferences", "messagepreferences": "Message preferences",
@ -28,8 +30,10 @@
"nogroupmessages": "No group messages", "nogroupmessages": "No group messages",
"nomessages": "No messages", "nomessages": "No messages",
"nousersfound": "No users found", "nousersfound": "No users found",
"numparticipants": "{{$a}} participants",
"removecontact": "Remove contact", "removecontact": "Remove contact",
"removecontactconfirm": "Contact will be removed from your contacts list.", "removecontactconfirm": "Contact will be removed from your contacts list.",
"showdeletemessages": "Show delete messages",
"type_blocked": "Blocked", "type_blocked": "Blocked",
"type_offline": "Offline", "type_offline": "Offline",
"type_online": "Online", "type_online": "Online",
@ -37,6 +41,7 @@
"type_strangers": "Others", "type_strangers": "Others",
"unblockuser": "Unblock user", "unblockuser": "Unblock user",
"unblockuserconfirm": "Are you sure you want to unblock {{$a}}?", "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}}", "warningmessagenotsent": "Couldn't send message(s) to user {{user}}. {{error}}",
"you": "You:" "you": "You:"
} }

View File

@ -0,0 +1,38 @@
<ion-header>
<ion-navbar core-back-button>
<ion-title>{{ 'addon.messages.groupinfo' | translate }}</ion-title>
<ion-buttons end>
<button ion-button icon-only (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<ion-icon name="close"></ion-icon>
</button>
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content>
<ion-refresher [enabled]="loaded" (ionRefresh)="refreshData($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded">
<ion-item text-center *ngIf="conversation">
<div class="item-avatar-center" *ngIf="conversation.imageurl">
<img class="avatar" [src]="conversation.imageurl" core-external-content [alt]="conversation.name" role="presentation">
</div>
<h2><core-format-text [text]="conversation.name"></core-format-text></h2>
<p><core-format-text *ngIf="conversation.subname" [text]="conversation.subname"></core-format-text></p>
<p>{{ 'addon.messages.numparticipants' | translate:{$a: conversation.membercount} }}</p>
</ion-item>
<a ion-item text-wrap *ngFor="let member of members" (click)="closeModal(member.id)">
<ion-avatar core-user-avatar [user]="member" [linkProfile]="false" [checkOnline]="member.showonlinestatus" item-start></ion-avatar>
<h2>
<p>
<core-format-text [text]="member.fullname"></core-format-text>
<core-icon name="fa-ban" *ngIf="member.isblocked" [attr.aria-label]="'addon.messages.contactblocked' | translate"></core-icon>
</p>
</h2>
</a>
<core-infinite-loading [enabled]="canLoadMore" (action)="loadMoreMembers($event)" [error]="loadMoreError"></core-infinite-loading>
</core-loading>
</ion-content>

View File

@ -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 {}

View File

@ -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;
}
}

View File

@ -0,0 +1,133 @@
// (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;
loadMoreError = 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<any>} Promise resolved when done.
*/
protected fetchData(): Promise<any> {
// 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<any>} Promise resolved when done.
*/
protected fetchMembers(loadingMore?: boolean): Promise<any> {
this.loadMoreError = false;
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<any>} Resolved when done.
*/
loadMoreMembers(infiniteComplete?: any): Promise<any> {
return this.fetchMembers(true).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'Error getting members.');
this.loadMoreError = true;
}).finally(() => {
infiniteComplete && infiniteComplete();
});
}
/**
* Refresh the data.
*
* @param {any} [refresher] Refresher.
* @return {Promise<any>} Promise resolved when done.
*/
refreshData(refresher?: any): Promise<any> {
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);
}
}

View File

@ -1,15 +1,17 @@
<ion-header> <ion-header>
<ion-navbar core-back-button> <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-buttons end></ion-buttons>
</ion-navbar> </ion-navbar>
<core-navbar-buttons end> <core-navbar-buttons end>
<button ion-button icon-only clear="true" (click)="toggleDelete()" [hidden]="!canDelete"> <core-context-menu>
<ion-icon name="trash"></ion-icon> <core-context-menu-item [hidden]="!showInfo || isGroup" [priority]="1000" [content]="'addon.messages.info' | translate" (action)="viewInfo()" [iconAction]="'information-circle'"></core-context-menu-item>
</button> <core-context-menu-item [hidden]="!showInfo || !isGroup" [priority]="999" [content]="'addon.messages.groupinfo' | translate" (action)="viewInfo()" [iconAction]="'information-circle'"></core-context-menu-item>
<a [hidden]="!showProfileLink" core-user-link [userId]="userId" [attr.aria-label]=" 'core.user.viewprofile' | translate"> <core-context-menu-item [hidden]="!canDelete" [priority]="800" [content]="'addon.messages.showdeletemessages' | translate" (action)="toggleDelete()" [iconAction]="'trash'"></core-context-menu-item>
<img class="button core-bar-button-image" [src]="profileLink" core-external-content onError="this.src='assets/img/user-avatar.png'"> </core-context-menu>
</a>
</core-navbar-buttons> </core-navbar-buttons>
</ion-header> </ion-header>
<ion-content class="has-footer"> <ion-content class="has-footer">
@ -18,16 +20,22 @@
<core-infinite-loading [enabled]="canLoadMore" (action)="loadPrevious($event)" position="top" [error]="loadMoreError"></core-infinite-loading> <core-infinite-loading [enabled]="canLoadMore" (action)="loadPrevious($event)" position="top" [error]="loadMoreError"></core-infinite-loading>
<ion-list class="addon-messages-discussion-container safe-area-page" [attr.aria-live]="polite"> <ion-list class="addon-messages-discussion-container safe-area-page" [attr.aria-live]="polite">
<ng-container *ngFor="let message of messages; index as index; last as last"> <ng-container *ngFor="let message of messages; index as index; last as last">
<ion-chip *ngIf="showDate(message, messages[index - 1])" class="addon-messages-date" color="light"> <ion-chip *ngIf="message.showDate" class="addon-messages-date" color="light">
<ion-label>{{ message.timecreated | coreFormatDate: "LL" }}</ion-label> <ion-label>{{ message.timecreated | coreFormatDate: "LL" }}</ion-label>
</ion-chip> </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-label>{{ 'addon.messages.newmessages' | translate:{$a: title} }}</ion-label>
<ion-icon name="arrow-round-down"></ion-icon> <ion-icon name="arrow-round-down"></ion-icon>
</ion-chip> </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'">
<!-- User data. -->
<ion-avatar item-start *ngIf="message.showUserData">
<img [src]="members[message.useridfrom].profileimageurl" [alt]="'core.pictureof' | translate:{$a: members[message.useridfrom].fullname}" core-external-content onError="this.src='assets/img/user-avatar.png'">
</ion-avatar>
<h2 *ngIf="message.showUserData">{{ members[message.useridfrom].fullname }}</h2>
<!-- Some messages have <p> and some others don't. Add a <p> so they all have same styles. --> <!-- Some messages have <p> and some others don't. Add a <p> so they all have same styles. -->
<p class="addon-message-text"> <p class="addon-message-text">
<core-format-text (afterRender)="last && scrollToBottom()" [text]="message.text"></core-format-text> <core-format-text (afterRender)="last && scrollToBottom()" [text]="message.text"></core-format-text>
@ -46,8 +54,9 @@
<core-empty-box *ngIf="!messages || messages.length <= 0" icon="chatbubbles" [message]="'addon.messages.nomessages' | translate"></core-empty-box> <core-empty-box *ngIf="!messages || messages.length <= 0" icon="chatbubbles" [message]="'addon.messages.nomessages' | translate"></core-empty-box>
</core-loading> </core-loading>
</ion-content> </ion-content>
<ion-footer color="light" class="footer-adjustable"> <ion-footer color="light" class="footer-adjustable" *ngIf="!conversationId || conversation">
<ion-toolbar color="light" position="bottom"> <ion-toolbar color="light" position="bottom">
<!-- @todo: Check if the user can send messages. -->
<core-send-message-form (onSubmit)="sendMessage($event)" [showKeyboard]="showKeyboard" [placeholder]="'addon.messages.newmessage' | translate" (onResize)="resizeContent()"></core-send-message-form> <core-send-message-form (onSubmit)="sendMessage($event)" [showKeyboard]="showKeyboard" [placeholder]="'addon.messages.newmessage' | translate" (onResize)="resizeContent()"></core-send-message-form>
</ion-toolbar> </ion-toolbar>
</ion-footer> </ion-footer>

View File

@ -12,12 +12,13 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // 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 { IonicPage, NavParams, NavController, Content, ModalController } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { CoreEventsProvider } from '@providers/events'; import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites'; import { CoreSitesProvider } from '@providers/sites';
import { AddonMessagesProvider } from '../../providers/messages'; import { AddonMessagesProvider } from '../../providers/messages';
import { AddonMessagesOfflineProvider } from '../../providers/messages-offline';
import { AddonMessagesSyncProvider } from '../../providers/sync'; import { AddonMessagesSyncProvider } from '../../providers/sync';
import { CoreUserProvider } from '@core/user/providers/user'; import { CoreUserProvider } from '@core/user/providers/user';
import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreDomUtilsProvider } from '@providers/utils/dom';
@ -25,6 +26,7 @@ import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreLoggerProvider } from '@providers/logger'; import { CoreLoggerProvider } from '@providers/logger';
import { CoreAppProvider } from '@providers/app'; import { CoreAppProvider } from '@providers/app';
import { coreSlideInOut } from '@classes/animations'; import { coreSlideInOut } from '@classes/animations';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { Md5 } from 'ts-md5/dist/md5'; import { Md5 } from 'ts-md5/dist/md5';
import * as moment from 'moment'; import * as moment from 'moment';
@ -53,12 +55,16 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
protected syncObserver: any; protected syncObserver: any;
protected oldContentHeight = 0; protected oldContentHeight = 0;
protected keyboardObserver: any; 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; currentUserId: number;
title: string; title: string;
profileLink: string; showInfo: boolean;
showProfileLink: boolean; conversationImage: string;
loaded = false; loaded = false;
showKeyboard = false; showKeyboard = false;
canLoadMore = false; canLoadMore = false;
@ -66,26 +72,33 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
messages = []; messages = [];
showDelete = false; showDelete = false;
canDelete = false; canDelete = false;
scrollBottom = true; groupMessagingEnabled: boolean;
viewDestroyed = false; isGroup = false;
members: any = {}; // Members that wrote a message, indexed by ID.
constructor(private eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, navParams: NavParams, constructor(private eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, navParams: NavParams,
private userProvider: CoreUserProvider, private navCtrl: NavController, private messagesSync: AddonMessagesSyncProvider, private userProvider: CoreUserProvider, private navCtrl: NavController, private messagesSync: AddonMessagesSyncProvider,
private domUtils: CoreDomUtilsProvider, private messagesProvider: AddonMessagesProvider, logger: CoreLoggerProvider, 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, private messagesOffline: AddonMessagesOfflineProvider,
private modalCtrl: ModalController) {
this.siteId = sitesProvider.getCurrentSiteId(); this.siteId = sitesProvider.getCurrentSiteId();
this.currentUserId = sitesProvider.getCurrentSiteUserId(); this.currentUserId = sitesProvider.getCurrentSiteUserId();
this.groupMessagingEnabled = this.messagesProvider.isGroupMessagingEnabled();
this.logger = logger.getInstance('AddonMessagesDiscussionPage'); this.logger = logger.getInstance('AddonMessagesDiscussionPage');
this.conversationId = navParams.get('conversationId');
this.userId = navParams.get('userId'); this.userId = navParams.get('userId');
this.showKeyboard = navParams.get('showKeyboard'); this.showKeyboard = navParams.get('showKeyboard');
// Refresh data if this discussion is synchronized automatically. // Refresh data if this discussion is synchronized automatically.
this.syncObserver = eventsProvider.on(AddonMessagesSyncProvider.AUTO_SYNCED, (data) => { 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. // Fetch messages.
this.fetchData(); this.fetchMessages();
// Show first warning if any. // Show first warning if any.
if (data.warnings && data.warnings[0]) { if (data.warnings && data.warnings[0]) {
@ -102,8 +115,8 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
* @param {boolean} [keep=true] If set the keep flag or not. * @param {boolean} [keep=true] If set the keep flag or not.
*/ */
protected addMessage(message: any, keep: boolean = true): void { protected addMessage(message: any, keep: boolean = true): void {
// Use smallmessage instead of message ID because ID changes when a message is read. // Use text instead of message ID because ID changes when a message is read.
message.hash = Md5.hashAsciiStr(message.smallmessage) + '#' + message.timecreated + '#' + message.useridfrom; message.hash = Md5.hashAsciiStr(message.text || '') + '#' + message.timecreated + '#' + message.useridfrom;
if (typeof this.keepMessageMap[message.hash] === 'undefined') { if (typeof this.keepMessageMap[message.hash] === 'undefined') {
// Message not added to the list. Add it now. // Message not added to the list. Add it now.
this.messages.push(message); this.messages.push(message);
@ -130,7 +143,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
const position = this.messages.findIndex((message) => { const position = this.messages.findIndex((message) => {
return message.hash == hash; return message.hash == hash;
}); });
if (position > 0) { if (position >= 0) {
this.messages.splice(position, 1); this.messages.splice(position, 1);
} }
} }
@ -143,42 +156,55 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
ionViewDidLoad(): void { ionViewDidLoad(): void {
// Disable the profile button if we're already coming from a profile. // Disable the profile button if we're already coming from a profile.
const backViewPage = this.navCtrl.getPrevious() && this.navCtrl.getPrevious().component.name; 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. if (!this.groupMessagingEnabled && this.userId) {
this.userProvider.getProfile(this.userId, undefined, true).then((user) => { // Get the user profile to retrieve the user fullname and image.
if (!this.title) { this.userProvider.getProfile(this.userId, undefined, true).then((user) => {
this.title = user.fullname; if (!this.title) {
} this.title = user.fullname;
this.profileLink = user.profileimageurl; }
}); this.conversationImage = user.profileimageurl;
});
}
// Synchronize messages if needed. // Synchronize messages if needed.
this.messagesSync.syncDiscussion(this.userId).catch(() => { this.messagesSync.syncDiscussion(this.conversationId, this.userId).catch(() => {
// Ignore errors. // Ignore errors.
}).then((warnings) => { }).then((warnings) => {
if (warnings && warnings[0]) { if (warnings && warnings[0]) {
this.domUtils.showErrorModal(warnings[0]); this.domUtils.showErrorModal(warnings[0]);
} }
// Fetch the messages for the first time. if (this.groupMessagingEnabled) {
return this.fetchData().then(() => { // Get the conversation ID if it exists and we don't have it yet.
if (!this.title && this.messages.length) { return this.getConversation(this.conversationId, this.userId).then((exists) => {
// Didn't receive the fullname via argument. Try to get it from messages. if (exists) {
// It's possible that name cannot be resolved when no messages were yet exchanged. // Fetch the messages for the first time.
if (this.messages[0].useridto != this.currentUserId) { return this.fetchMessages();
this.title = this.messages[0].usertofullname || '';
} else {
this.title = this.messages[0].userfromfullname || '';
} }
} });
}).catch((error) => { } else {
this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingmessages', true); // Fetch the messages for the first time.
}).finally(() => { return this.fetchMessages().then(() => {
this.checkCanDelete(); if (!this.title && this.messages.length) {
this.resizeContent(); // Didn't receive the fullname via argument. Try to get it from messages.
this.loaded = true; // 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;
this.setPolling(); // Make sure we're polling messages.
}); });
// Recalculate footer position when keyboard is shown or hidden. // Recalculate footer position when keyboard is shown or hidden.
@ -204,12 +230,12 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
/** /**
* Convenience function to fetch messages. * Convenience function to fetch messages.
*
* @return {Promise<any>} Resolved when done. * @return {Promise<any>} Resolved when done.
*/ */
protected fetchData(): Promise<any> { protected fetchMessages(): Promise<any> {
this.loadMoreError = false; this.loadMoreError = false;
this.logger.debug(`Polling new messages for discussion with user '${this.userId}'`);
if (this.messagesBeingSent > 0) { if (this.messagesBeingSent > 0) {
// We do not poll while a message is being sent or we could confuse the user. // 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. // Otherwise, his message would disappear from the list, and he'd have to wait for the interval to check for messages.
@ -217,55 +243,179 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
} else if (this.fetching) { } else if (this.fetching) {
// Already fetching. // Already fetching.
return Promise.reject(null); 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; this.fetching = true;
// Wait for synchronization process to finish. // 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. // Fetch messages. Invalidate the cache before fetching.
return this.messagesProvider.invalidateDiscussionCache(this.userId).catch(() => { if (this.groupMessagingEnabled) {
// Ignore errors. return this.messagesProvider.invalidateConversationMessages(this.conversationId).catch(() => {
}); // Ignore errors.
}).then(() => { }).then(() => {
return this.getDiscussion(this.pagesLoaded); return this.getConversationMessages(this.pagesLoaded);
});
} else {
return this.messagesProvider.invalidateDiscussionCache(this.userId).catch(() => {
// Ignore errors.
}).then(() => {
return this.getDiscussionMessages(this.pagesLoaded);
});
}
}).then((messages) => { }).then((messages) => {
if (this.viewDestroyed) { this.loadMessages(messages);
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);
// Notify that there can be a new message.
this.notifyNewMessage();
// Mark retrieved messages as read if they are not.
this.markMessagesAsRead();
}).finally(() => { }).finally(() => {
this.fetching = false; 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.
*
* @param {number} conversationId Conversation ID.
* @param {number} userId User ID.
* @return {Promise<boolean>} Promise resolved with a boolean: whether the conversation exists or not.
*/
protected getConversation(conversationId: number, userId: number): Promise<boolean> {
let promise;
if (conversationId) {
// Retrieve the conversation. Invalidate data first to get the right unreadcount.
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(userId).catch((error) => {
// Probably conversation does not exist or user is offline. Try to load offline 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) => {
message.pending = true;
message.text = message.smallmessage;
});
this.loadMessages(messages);
} else if (error.errorcode != 'errorconversationdoesnotexist') {
// 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;
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--;
// 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;
// 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". * Get a discussion. Can load several "pages".
* *
@ -276,8 +426,8 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
* @param {number} [lfSentRead=0] Number of read sent messages already fetched, so fetch will be done from this. * @param {number} [lfSentRead=0] Number of read sent messages already fetched, so fetch will be done from this.
* @return {Promise<any>} Resolved when done. * @return {Promise<any>} Resolved when done.
*/ */
protected getDiscussion(pagesToLoad: number, lfReceivedUnread: number = 0, lfReceivedRead: number = 0, lfSentUnread: number = 0, protected getDiscussionMessages(pagesToLoad: number, lfReceivedUnread: number = 0, lfReceivedRead: number = 0,
lfSentRead: number = 0): Promise<any> { lfSentUnread: number = 0, lfSentRead: number = 0): Promise<any> {
// Only get offline messages if we're loading the first "page". // Only get offline messages if we're loading the first "page".
const excludePending = lfReceivedUnread > 0 || lfReceivedRead > 0 || lfSentUnread > 0 || lfSentRead > 0; const excludePending = lfReceivedUnread > 0 || lfReceivedRead > 0 || lfSentUnread > 0 || lfSentRead > 0;
@ -308,7 +458,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
}); });
// Get next messages. // Get next messages.
return this.getDiscussion(pagesToLoad, lfReceivedUnread, lfReceivedRead, lfSentUnread, lfSentRead) return this.getDiscussionMessages(pagesToLoad, lfReceivedUnread, lfReceivedRead, lfSentUnread, lfSentRead)
.then((nextMessages) => { .then((nextMessages) => {
return result.messages.concat(nextMessages); return result.messages.concat(nextMessages);
}); });
@ -330,23 +480,39 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
if (this.messagesProvider.isMarkAllMessagesReadEnabled()) { if (this.messagesProvider.isMarkAllMessagesReadEnabled()) {
let messageUnreadFound = false; let messageUnreadFound = false;
// Mark all messages at a time if one messages is unread.
for (const x in this.messages) { // Mark all messages at a time if there is any unread message.
const message = this.messages[x]; if (this.groupMessagingEnabled) {
// If an unread message is found, mark all messages as read. messageUnreadFound = this.conversation && this.conversation.unreadcount > 0 && this.conversationId > 0;
if (message.useridfrom != this.currentUserId && message.read == 0) { } else {
messageUnreadFound = true; for (const x in this.messages) {
break; 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) { if (messageUnreadFound) {
this.setUnreadLabelPosition(); this.setUnreadLabelPosition();
promises.push(this.messagesProvider.markAllMessagesRead(this.userId).then(() => {
readChanged = true; let promise;
// Mark all messages as read.
this.messages.forEach((message) => { if (this.groupMessagingEnabled) {
message.read = 1; 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 { } else {
@ -366,6 +532,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
Promise.all(promises).finally(() => { Promise.all(promises).finally(() => {
if (readChanged) { if (readChanged) {
this.eventsProvider.trigger(AddonMessagesProvider.READ_CHANGED_EVENT, { this.eventsProvider.trigger(AddonMessagesProvider.READ_CHANGED_EVENT, {
conversationId: this.conversationId,
userId: this.userId userId: this.userId
}, this.siteId); }, this.siteId);
} }
@ -390,6 +557,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
if (trigger) { if (trigger) {
// Update discussions last message. // Update discussions last message.
this.eventsProvider.trigger(AddonMessagesProvider.NEW_MESSAGE_EVENT, { this.eventsProvider.trigger(AddonMessagesProvider.NEW_MESSAGE_EVENT, {
conversationId: this.conversationId,
userId: this.userId, userId: this.userId,
message: this.lastMessage.text, message: this.lastMessage.text,
timecreated: this.lastMessage.timecreated timecreated: this.lastMessage.timecreated
@ -411,21 +579,39 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
return; 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) { for (let i = this.messages.length - 1; i >= 0; i--) {
const message = this.messages[x]; const message = this.messages[i];
if (message.useridfrom != this.currentUserId) { if (!message.pending && message.useridfrom != this.currentUserId) {
// Place unread from message label only once. found++;
message.unreadFrom = message.read == 0 && previousMessageRead; if (found == this.conversation.unreadcount) {
this.unreadMessageFrom = parseInt(message.id, 10);
if (message.unreadFrom) { break;
// Save where the label is placed. }
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 +636,6 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
*/ */
protected hideUnreadLabel(): void { protected hideUnreadLabel(): void {
if (this.unreadMessageFrom > 0) { 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; this.unreadMessageFrom = -1;
} }
} }
@ -487,10 +664,15 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
* Set a polling to get new messages every certain time. * Set a polling to get new messages every certain time.
*/ */
protected setPolling(): void { protected setPolling(): void {
if (this.groupMessagingEnabled && !this.conversationId) {
// Don't have enough data to poll messages.
return;
}
if (!this.polling) { if (!this.polling) {
// Start polling. // Start polling.
this.polling = setInterval(() => { this.polling = setInterval(() => {
this.fetchData().catch(() => { this.fetchMessages().catch(() => {
// Ignore errors. // Ignore errors.
}); });
}, AddonMessagesProvider.POLL_INTERVAL); }, AddonMessagesProvider.POLL_INTERVAL);
@ -509,19 +691,19 @@ 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 { copyMessage(message: any): void {
this.utils.copyToClipboard(text); this.utils.copyToClipboard(message.smallmessage || message.text || '');
} }
/** /**
* Function to delete a message. * Function to delete a message.
* *
* @param {any} message Message object to delete. * @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 { deleteMessage(message: any, index: number): void {
const langKey = message.pending ? 'core.areyousure' : 'addon.messages.deletemessageconfirmation'; const langKey = message.pending ? 'core.areyousure' : 'addon.messages.deletemessageconfirmation';
@ -534,7 +716,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
this.removeMessage(message.hash); this.removeMessage(message.hash);
this.notifyNewMessage(); this.notifyNewMessage();
this.fetchData(); // Re-fetch messages to update cached data. this.fetchMessages(); // Re-fetch messages to update cached data.
}).finally(() => { }).finally(() => {
modal.dismiss(); modal.dismiss();
}); });
@ -554,9 +736,8 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
return this.waitForFetch().finally(() => { return this.waitForFetch().finally(() => {
this.pagesLoaded++; this.pagesLoaded++;
this.fetchData().catch((error) => { this.fetchMessages().catch((error) => {
this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading. this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading.
this.pagesLoaded--; this.pagesLoaded--;
this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingmessages', true); this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingmessages', true);
}).finally(() => { }).finally(() => {
@ -606,6 +787,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
/** /**
* Sends a message to the server. * Sends a message to the server.
*
* @param {string} text Message text. * @param {string} text Message text.
*/ */
sendMessage(text: string): void { sendMessage(text: string): void {
@ -624,6 +806,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
text: text, text: text,
timecreated: new Date().getTime() timecreated: new Date().getTime()
}; };
message.showDate = this.showDate(message, this.messages[this.messages.length - 1]);
this.addMessage(message, false); this.addMessage(message, false);
this.messagesBeingSent++; this.messagesBeingSent++;
@ -631,14 +814,33 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
// If there is an ongoing fetch, wait for it to finish. // 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. // Otherwise, if a message is sent while fetching it could disappear until the next fetch.
this.waitForFetch().finally(() => { this.waitForFetch().finally(() => {
this.messagesProvider.sendMessage(this.userId, text).then((data) => { let promise;
if (this.conversationId) {
promise = this.messagesProvider.sendMessageToConversation(this.conversation, text);
} else {
promise = this.messagesProvider.sendMessage(this.userId, text);
}
promise.then((data) => {
let promise; let promise;
this.messagesBeingSent--; this.messagesBeingSent--;
if (data.sent) { if (data.sent) {
// Message was sent, fetch messages right now. if (!this.conversationId && data.message && data.message.conversationid) {
promise = this.fetchData(); // 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 { } else {
promise = Promise.reject(null); promise = Promise.reject(null);
} }
@ -681,15 +883,25 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
if (!prevMessage) { if (!prevMessage) {
// First message, show it. // First message, show it.
return true; return true;
} else if (message.pending) {
// If pending, it has no date, not show.
return false;
} }
// Check if day has changed. // Check if day has changed.
return !moment(message.timecreated).isSame(prevMessage.timecreated, 'day'); 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. * Toggles delete state.
*/ */
@ -697,6 +909,37 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
this.showDelete = !this.showDelete; 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) {
// 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 });
}
}
/** /**
* Page destroyed. * Page destroyed.
*/ */

View File

@ -8,6 +8,8 @@
<button ion-button icon-only (click)="gotoSettings($event)" [attr.aria-label]="'addon.messages.messagepreferences' | translate"> <button ion-button icon-only (click)="gotoSettings($event)" [attr.aria-label]="'addon.messages.messagepreferences' | translate">
<ion-icon name="cog"></ion-icon> <ion-icon name="cog"></ion-icon>
</button> </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-buttons>
</ion-navbar> </ion-navbar>
</ion-header> </ion-header>
@ -27,13 +29,19 @@
<h2>{{ 'core.searchresults' | translate }}</h2> <h2>{{ 'core.searchresults' | translate }}</h2>
<ion-note item-end>{{ search.results.length }}</ion-note> <ion-note item-end>{{ search.results.length }}</ion-note>
</ion-item-divider> </ion-item-divider>
<!-- @todo: Search conversations does not return the conversationid. Check how it is solved in web. -->
<a ion-item text-wrap *ngFor="let result of search.results" [title]="result.fullname" (click)="gotoConversation(result.id, result.userid, result.messageid)" [class.core-split-item-selected]="result.id == selectedConversation" detail-none> <a ion-item text-wrap *ngFor="let result of search.results" [title]="result.fullname" (click)="gotoConversation(result.conversationid, result.userid, result.messageid)" [class.core-split-item-selected]="(result.conversationid && result.conversationid == selectedConversationId) || (result.userid && result.userid == selectedUserId)" detail-none>
<ion-avatar item-start> <ion-avatar core-user-avatar [user]="result" [linkProfile]="false" item-start></ion-avatar>
<img src="{{result.profileimageurl}}" [alt]="'core.pictureof' | translate:{$a: result.fullname}" core-external-content onError="this.src='assets/img/user-avatar.png'"> <h2>
</ion-avatar> <p>
<h2><core-format-text [text]="result.fullname"></core-format-text></h2> <core-format-text [text]="result.fullname"></core-format-text>
<p><core-format-text clean="true" singleLine="true" [text]="result.lastmessage"></core-format-text></p> <core-icon name="fa-ban" *ngIf="result.isblocked" [attr.aria-label]="'addon.messages.contactblocked' | translate"></core-icon>
</p>
<ion-note *ngIf="result.lastmessagedate > 0">
{{result.lastmessagedate | coreDateDayOrTime}}
</ion-note>
</h2>
<p><core-format-text clean="true" singleLine="true" [text]="result.lastmessage" class="addon-message-last-message"></core-format-text></p>
</a> </a>
</ion-list> </ion-list>
@ -49,7 +57,7 @@
<div *ngIf="favourites.conversations && favourites.expanded"> <div *ngIf="favourites.conversations && favourites.expanded">
<ng-container *ngTemplateOutlet="conversationsTemplate; context: {conversations: favourites.conversations}"></ng-container> <ng-container *ngTemplateOutlet="conversationsTemplate; context: {conversations: favourites.conversations}"></ng-container>
<!-- The infinite loading cannot be inside the ng-template, it fails because it doesn't find ion-content. --> <!-- The infinite loading cannot be inside the ng-template, it fails because it doesn't find ion-content. -->
<core-infinite-loading [enabled]="favourites.canLoadMore" (action)="loadMoreConversations(favourites, $event)"></core-infinite-loading> <core-infinite-loading [enabled]="favourites.canLoadMore" (action)="loadMoreConversations(favourites, $event)" [error]="favourites.loadMoreError"></core-infinite-loading>
<ion-item text-wrap *ngIf="favourites.conversations.length == 0"> <ion-item text-wrap *ngIf="favourites.conversations.length == 0">
<p>{{ 'addon.messages.nofavourites' | translate }}</p> <p>{{ 'addon.messages.nofavourites' | translate }}</p>
</ion-item> </ion-item>
@ -63,9 +71,9 @@
<!-- @todo: Unread total of group conversations (MDL-63913). --> <!-- @todo: Unread total of group conversations (MDL-63913). -->
</ion-item-divider> </ion-item-divider>
<div *ngIf="group.conversations && group.expanded"> <div *ngIf="group.conversations && group.expanded">
<ng-container *ngTemplateOutlet="conversationsTemplate; context: {conversations: group.conversations, avatarOptional: true}"></ng-container> <ng-container *ngTemplateOutlet="conversationsTemplate; context: {conversations: group.conversations}"></ng-container>
<!-- The infinite loading cannot be inside the ng-template, it fails because it doesn't find ion-content. --> <!-- The infinite loading cannot be inside the ng-template, it fails because it doesn't find ion-content. -->
<core-infinite-loading [enabled]="group.canLoadMore" (action)="loadMoreConversations(group, $event)"></core-infinite-loading> <core-infinite-loading [enabled]="group.canLoadMore" (action)="loadMoreConversations(group, $event)" [error]="group.loadMoreError"></core-infinite-loading>
<ion-item text-wrap *ngIf="group.conversations.length == 0"> <ion-item text-wrap *ngIf="group.conversations.length == 0">
<p>{{ 'addon.messages.nogroupmessages' | translate }}</p> <p>{{ 'addon.messages.nogroupmessages' | translate }}</p>
</ion-item> </ion-item>
@ -80,7 +88,7 @@
<div *ngIf="individual.conversations && individual.expanded"> <div *ngIf="individual.conversations && individual.expanded">
<ng-container *ngTemplateOutlet="conversationsTemplate; context: {conversations: individual.conversations}"></ng-container> <ng-container *ngTemplateOutlet="conversationsTemplate; context: {conversations: individual.conversations}"></ng-container>
<!-- The infinite loading cannot be inside the ng-template, it fails because it doesn't find ion-content. --> <!-- The infinite loading cannot be inside the ng-template, it fails because it doesn't find ion-content. -->
<core-infinite-loading [enabled]="individual.canLoadMore" (action)="loadMoreConversations(individual, $event)"></core-infinite-loading> <core-infinite-loading [enabled]="individual.canLoadMore" (action)="loadMoreConversations(individual, $event)" [error]="individual.loadMoreError"></core-infinite-loading>
<ion-item text-wrap *ngIf="individual.conversations.length == 0"> <ion-item text-wrap *ngIf="individual.conversations.length == 0">
<p>{{ 'addon.messages.nomessages' | translate }}</p> <p>{{ 'addon.messages.nomessages' | translate }}</p>
</ion-item> </ion-item>
@ -94,13 +102,16 @@
</core-split-view> </core-split-view>
<!-- Template to render a list of conversations. --> <!-- Template to render a list of conversations. -->
<ng-template #conversationsTemplate let-conversations="conversations" let-avatarOptional="avatarOptional"> <ng-template #conversationsTemplate let-conversations="conversations">
<a ion-item text-wrap *ngFor="let conversation of conversations" [title]="conversation.name" detail-none (click)="gotoConversation(conversation.id, conversation.userid)" [class.core-split-item-selected]="conversation.id == selectedConversation"> <a ion-item text-wrap *ngFor="let conversation of conversations" [title]="conversation.name" detail-none (click)="gotoConversation(conversation.id, conversation.userid)" [class.core-split-item-selected]="(conversation.id && conversation.id == selectedConversationId) || (conversation.userid && conversation.userid == selectedUserId)" id="addon-message-conversation-{{ conversation.id ? conversation.id : 'user-' + conversation.userid }}">
<ion-avatar item-start *ngIf="conversation.imageurl || !avatarOptional"> <!-- Group conversation image. -->
<img src="{{conversation.imageurl}}" [alt]="conversation.name" core-external-content onError="this.src='assets/img/user-avatar.png'"> <ion-avatar item-start *ngIf="conversation.type != typeIndividual && conversation.imageurl">
<!-- @todo: Display connection status. <img [src]="conversation.imageurl" [alt]="conversation.name" core-external-content>
<span *ngIf="conversation.showonlinestatus" class="core-primary-circle" [ngClass]='{"addon-message-contact-online": conversation.isonline}'></span> -->
</ion-avatar> </ion-avatar>
<!-- Avatar for individual conversations. -->
<ion-avatar *ngIf="conversation.type == typeIndividual" core-user-avatar [user]="conversation.otherUser" [linkProfile]="false" [checkOnline]="conversation.showonlinestatus" item-start></ion-avatar>
<h2> <h2>
<p> <p>
<core-format-text [text]="conversation.name"></core-format-text> <core-format-text [text]="conversation.name"></core-format-text>

View File

@ -13,16 +13,18 @@
// limitations under the License. // limitations under the License.
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { IonicPage, Platform, NavParams } from 'ionic-angular'; import { IonicPage, Platform, NavParams, Content } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { CoreEventsProvider } from '@providers/events'; import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites'; import { CoreSitesProvider } from '@providers/sites';
import { AddonMessagesProvider } from '../../providers/messages'; import { AddonMessagesProvider } from '../../providers/messages';
import { AddonMessagesOfflineProvider } from '../../providers/messages-offline';
import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreAppProvider } from '@providers/app'; import { CoreAppProvider } from '@providers/app';
import { AddonPushNotificationsDelegate } from '@addon/pushnotifications/providers/delegate'; import { AddonPushNotificationsDelegate } from '@addon/pushnotifications/providers/delegate';
import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreUserProvider } from '@core/user/providers/user';
/** /**
* Page that displays the list of conversations, including group conversations. * Page that displays the list of conversations, including group conversations.
@ -34,10 +36,12 @@ import { CoreSplitViewComponent } from '@components/split-view/split-view';
}) })
export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
@ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent; @ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent;
@ViewChild(Content) content: Content;
loaded = false; loaded = false;
loadingMessage: string; loadingMessage: string;
selectedConversation: number; selectedConversationId: number;
selectedUserId: number;
search = { search = {
enabled: false, enabled: false,
showResults: false, showResults: false,
@ -57,6 +61,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
type: AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, type: AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
favourites: false favourites: false
}; };
typeIndividual = AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_INDIVIDUAL;
protected loadingString: string; protected loadingString: string;
protected siteId: string; protected siteId: string;
@ -67,11 +72,13 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
protected appResumeSubscription: any; protected appResumeSubscription: any;
protected readChangedObserver: any; protected readChangedObserver: any;
protected cronObserver: any; protected cronObserver: any;
protected openConversationObserver: any;
constructor(private eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, translate: TranslateService, constructor(private eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, translate: TranslateService,
private messagesProvider: AddonMessagesProvider, private domUtils: CoreDomUtilsProvider, navParams: NavParams, private messagesProvider: AddonMessagesProvider, private domUtils: CoreDomUtilsProvider, navParams: NavParams,
private appProvider: CoreAppProvider, platform: Platform, utils: CoreUtilsProvider, private appProvider: CoreAppProvider, platform: Platform, utils: CoreUtilsProvider,
pushNotificationsDelegate: AddonPushNotificationsDelegate) { pushNotificationsDelegate: AddonPushNotificationsDelegate, private messagesOffline: AddonMessagesOfflineProvider,
private userProvider: CoreUserProvider) {
this.search.loading = translate.instant('core.searching'); this.search.loading = translate.instant('core.searching');
this.loadingString = translate.instant('core.loading'); this.loadingString = translate.instant('core.loading');
@ -81,20 +88,29 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
// Update conversations when new message is received. // Update conversations when new message is received.
this.newMessagesObserver = eventsProvider.on(AddonMessagesProvider.NEW_MESSAGE_EVENT, (data) => { this.newMessagesObserver = eventsProvider.on(AddonMessagesProvider.NEW_MESSAGE_EVENT, (data) => {
if (data.conversationId) { // Search the conversation to update.
// Search the conversation to update. const conversation = this.findConversation(data.conversationId, data.userId);
const conversation = this.findConversation(data.conversationId);
if (typeof conversation == 'undefined') { if (typeof conversation == 'undefined') {
// Probably a new conversation, refresh the list. // Probably a new conversation, refresh the list.
this.loaded = false; this.loaded = false;
this.refreshData().finally(() => { this.refreshData().finally(() => {
this.loaded = true; this.loaded = true;
}); });
} else { } else if (conversation.lastmessage != data.message || conversation.lastmessagedate != data.timecreated / 1000) {
// An existing conversation has a new message, update the last message. const isNewer = data.timecreated / 1000 > conversation.lastmessagedate;
conversation.lastmessage = data.message;
conversation.lastmessagedate = data.timecreated; // An existing conversation has a new message, update the last message.
conversation.lastmessage = data.message;
conversation.lastmessagedate = data.timecreated / 1000;
// Sort the affected list.
const option = this.getConversationOption(conversation);
option.conversations = this.messagesProvider.sortConversations(option.conversations);
if (isNewer) {
// The last message is newer than the previous one, scroll to top to keep viewing the conversation.
this.domUtils.scrollToTop(this.content);
} }
} }
}, this.siteId); }, this.siteId);
@ -119,6 +135,13 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
this.refreshData(); this.refreshData();
}, this.siteId); }, this.siteId);
// Load a discussion if we receive an event to do so.
this.openConversationObserver = eventsProvider.on(AddonMessagesProvider.OPEN_CONVERSATION_EVENT, (data) => {
if (data.conversationId || data.userId) {
this.gotoConversation(data.conversationId, data.userId, undefined, true);
}
}, this.siteId);
// Refresh the view when the app is resumed. // Refresh the view when the app is resumed.
this.appResumeSubscription = platform.resume.subscribe(() => { this.appResumeSubscription = platform.resume.subscribe(() => {
if (!this.loaded) { if (!this.loaded) {
@ -145,7 +168,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
ngOnInit(): void { ngOnInit(): void {
if (this.conversationId) { if (this.conversationId) {
// There is a discussion to load, open the discussion in a new state. // 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(() => { this.fetchData().then(() => {
@ -162,7 +185,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
} }
if (conversation) { if (conversation) {
this.gotoConversation(conversation.id, conversation.userid); this.gotoConversation(conversation.id);
} }
} }
}); });
@ -179,14 +202,39 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
// Load the first conversations of each type. // Load the first conversations of each type.
const promises = []; const promises = [];
let offlineMessages;
promises.push(this.fetchDataForOption(this.favourites, false)); promises.push(this.fetchDataForOption(this.favourites, false));
promises.push(this.fetchDataForOption(this.group, false)); promises.push(this.fetchDataForOption(this.group, false));
promises.push(this.fetchDataForOption(this.individual, false)); promises.push(this.fetchDataForOption(this.individual, false));
promises.push(this.messagesOffline.getAllMessages().then((messages) => {
offlineMessages = messages;
}));
return Promise.all(promises).then(() => { return Promise.all(promises).then(() => {
return this.loadOfflineMessages(offlineMessages);
}).then(() => {
if (offlineMessages && offlineMessages.length) {
// Sort the conversations, the offline messages could affect the order.
this.favourites.conversations = this.messagesProvider.sortConversations(this.favourites.conversations);
this.group.conversations = this.messagesProvider.sortConversations(this.group.conversations);
this.individual.conversations = this.messagesProvider.sortConversations(this.individual.conversations);
}
if (typeof this.favourites.expanded == 'undefined') { if (typeof this.favourites.expanded == 'undefined') {
// The expanded status hasn't been initialized. Do it now. // The expanded status hasn't been initialized. Do it now.
if (this.conversationId) {
// A certain conversation should be opened, expand its option.
const conversation = this.findConversation(this.conversationId);
if (conversation) {
const option = this.getConversationOption(conversation);
option.expanded = true;
return;
}
}
// No conversation specified or not found, determine which one should be expanded.
this.favourites.expanded = this.favourites.count != 0; this.favourites.expanded = this.favourites.count != 0;
this.group.expanded = this.favourites.count == 0 && this.group.count != 0; this.group.expanded = this.favourites.count == 0 && this.group.count != 0;
this.individual.expanded = this.favourites.count == 0 && this.group.count == 0; this.individual.expanded = this.favourites.count == 0 && this.group.count == 0;
@ -206,6 +254,8 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
* @return {Promise<any>} Promise resolved when done. * @return {Promise<any>} Promise resolved when done.
*/ */
fetchDataForOption(option: any, loadingMore?: boolean): Promise<void> { fetchDataForOption(option: any, loadingMore?: boolean): Promise<void> {
option.loadMoreError = false;
const limitFrom = loadingMore ? option.conversations.length : 0; const limitFrom = loadingMore ? option.conversations.length : 0;
return this.messagesProvider.getConversations(option.type, option.favourites, limitFrom).then((data) => { return this.messagesProvider.getConversations(option.type, option.favourites, limitFrom).then((data) => {
@ -225,15 +275,22 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
* Find a conversation in the list of loaded conversations. * Find a conversation in the list of loaded conversations.
* *
* @param {number} conversationId The conversation ID to search. * @param {number} conversationId The conversation ID to search.
* @param {number} userId User ID to search (if no conversationId).
* @return {any} Conversation. * @return {any} Conversation.
*/ */
protected findConversation(conversationId: number): any { protected findConversation(conversationId: number, userId?: number): any {
const conversations = (this.favourites.conversations || []).concat(this.group.conversations || []) if (conversationId) {
.concat(this.individual.conversations || []); const conversations = (this.favourites.conversations || []).concat(this.group.conversations || [])
.concat(this.individual.conversations || []);
return conversations.find((conv) => { return conversations.find((conv) => {
return conv.id == conversationId; return conv.id == conversationId;
}); });
} else if (this.individual.conversations) {
return this.individual.conversations.find((conv) => {
return conv.userid == userId;
});
}
} }
/** /**
@ -247,19 +304,39 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
* Navigate to a particular conversation. * Navigate to a particular conversation.
* *
* @param {number} conversationId Conversation Id to load. * @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. * @param {number} [messageId] Message to scroll after loading the discussion. Used when searching.
* @param {boolean} [scrollToConversation] Whether to scroll to the conversation.
*/ */
gotoConversation(conversationId: number, userId: number, messageId?: number): void { gotoConversation(conversationId: number, userId?: number, messageId?: number, scrollToConversation?: boolean): void {
this.selectedConversation = conversationId; this.selectedConversationId = conversationId;
this.selectedUserId = userId;
const params = { const params = {
conversationId: conversationId,
userId: userId userId: userId
}; };
if (messageId) { if (messageId) {
params['message'] = messageId; params['message'] = messageId;
} }
this.splitviewCtrl.push('AddonMessagesDiscussionPage', params); this.splitviewCtrl.push('AddonMessagesDiscussionPage', params);
if (scrollToConversation) {
// Search the conversation.
const conversation = this.findConversation(conversationId, userId);
if (conversation) {
// First expand the option if it isn't expanded.
const option = this.getConversationOption(conversation);
this.expandOption(option);
// Wait for the view to expand the option.
setTimeout(() => {
// Now scroll to the conversation.
this.domUtils.scrollToElementBySelector(this.content, '#addon-message-conversation-' +
(conversation.id ? conversation.id : 'user-' + conversation.userid));
});
}
}
} }
/** /**
@ -279,12 +356,113 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
loadMoreConversations(option: any, infiniteComplete?: any): Promise<any> { loadMoreConversations(option: any, infiniteComplete?: any): Promise<any> {
return this.fetchDataForOption(option, true).catch((error) => { return this.fetchDataForOption(option, true).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingdiscussions', true); this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingdiscussions', true);
option.canLoadMore = false; option.loadMoreError = true;
}).finally(() => { }).finally(() => {
infiniteComplete && infiniteComplete(); infiniteComplete && infiniteComplete();
}); });
} }
/**
* Load offline messages into the conversations.
*
* @param {any[]} messages Offline messages.
* @return {Promise<any>} Promise resolved when done.
*/
protected loadOfflineMessages(messages: any[]): Promise<any> {
const promises = [];
messages.forEach((message) => {
if (message.conversationid) {
// It's an existing conversation. Search it.
let conversation = this.findConversation(message.conversationid);
if (conversation) {
// Check if it's the last message. Offline messages are considered more recent than sent messages.
if (typeof conversation.lastmessage === 'undefined' || conversation.lastmessage === null ||
!conversation.lastmessagepending || conversation.lastmessagedate <= message.timecreated / 1000) {
this.addLastOfflineMessage(conversation, message);
}
} else {
// Conversation not found, it's probably an old one. Add it.
conversation = message.conversation || {};
conversation.id = message.conversationid;
this.addLastOfflineMessage(conversation, message);
this.addOfflineConversation(conversation);
}
} else {
// Its a new conversation. Check if we already created it (there is more than one message for the same user).
const conversation = this.findConversation(undefined, 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 {
const option = this.getConversationOption(conversation);
option.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;
}
/**
* Given a conversation, return its option (favourites, group, individual).
*
* @param {any} conversation Conversation to check.
* @return {any} Option object.
*/
protected getConversationOption(conversation: any): any {
if (conversation.isfavourite) {
return this.favourites;
} else if (conversation.type == AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_GROUP) {
return this.group;
} else {
return this.individual;
}
}
/** /**
* Refresh the data. * Refresh the data.
* *
@ -313,14 +491,23 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
// Already expanded, close it. // Already expanded, close it.
option.expanded = false; option.expanded = false;
} else { } else {
// Collapse all and expand the clicked one. this.expandOption(option);
this.favourites.expanded = false;
this.group.expanded = false;
this.individual.expanded = false;
option.expanded = true;
} }
} }
/**
* Expand a certain option.
*
* @param {any} option The option to expand.
*/
protected expandOption(option: any): void {
// Collapse all and expand the right one.
this.favourites.expanded = false;
this.group.expanded = false;
this.individual.expanded = false;
option.expanded = true;
}
/** /**
* Clear search and show conversations again. * Clear search and show conversations again.
*/ */
@ -358,10 +545,11 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
* Page destroyed. * Page destroyed.
*/ */
ngOnDestroy(): void { ngOnDestroy(): void {
this.newMessagesObserver && this.newMessagesObserver.unsubscribe(); this.newMessagesObserver && this.newMessagesObserver.off();
this.appResumeSubscription && this.appResumeSubscription.unsubscribe(); this.appResumeSubscription && this.appResumeSubscription.unsubscribe();
this.pushObserver && this.pushObserver.unsubscribe(); this.pushObserver && this.pushObserver.unsubscribe();
this.readChangedObserver && this.readChangedObserver.off(); this.readChangedObserver && this.readChangedObserver.off();
this.cronObserver && this.cronObserver.off(); this.cronObserver && this.cronObserver.off();
this.openConversationObserver && this.openConversationObserver.off();
} }
} }

View File

@ -16,6 +16,7 @@ import { Injectable } from '@angular/core';
import { CoreLoggerProvider } from '@providers/logger'; import { CoreLoggerProvider } from '@providers/logger';
import { CoreSitesProvider } from '@providers/sites'; import { CoreSitesProvider } from '@providers/sites';
import { CoreAppProvider } from '@providers/app'; import { CoreAppProvider } from '@providers/app';
import { CoreTextUtilsProvider } from '@providers/utils/text';
/** /**
* Service to handle Offline messages. * Service to handle Offline messages.
@ -26,7 +27,8 @@ export class AddonMessagesOfflineProvider {
protected logger; protected logger;
// Variables for database. // 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 = [ protected tablesSchema = [
{ {
name: AddonMessagesOfflineProvider.MESSAGES_TABLE, name: AddonMessagesOfflineProvider.MESSAGES_TABLE,
@ -53,14 +55,60 @@ export class AddonMessagesOfflineProvider {
} }
], ],
primaryKeys: ['touserid', 'smallmessage', 'timecreated'] 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'
},
{
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.logger = logger.getInstance('AddonMessagesOfflineProvider');
this.sitesProvider.createTablesFromSchema(this.tablesSchema); 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<any>} Promise resolved if stored, rejected if failure.
*/
deleteConversationMessage(conversationId: number, message: string, timeCreated: number, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().deleteRecords(AddonMessagesOfflineProvider.CONVERSATION_MESSAGES_TABLE, {
conversationid: conversationId,
text: message,
timecreated: timeCreated
});
});
}
/** /**
* Delete a message. * Delete a message.
* *
@ -84,24 +132,20 @@ export class AddonMessagesOfflineProvider {
* Get all messages where deviceoffline is set to 1. * Get all messages where deviceoffline is set to 1.
* *
* @param {string} [siteId] Site ID. If not defined, current site. * @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved with messages. * @return {Promise<any[]>} Promise resolved with messages.
*/ */
getAllDeviceOfflineMessages(siteId?: string): Promise<any> { getAllDeviceOfflineMessages(siteId?: string): Promise<any[]> {
return this.sitesProvider.getSite(siteId).then((site) => { return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().getRecords(AddonMessagesOfflineProvider.MESSAGES_TABLE, {deviceoffline: 1}); const promises = [];
});
}
/** promises.push(site.getDb().getRecords(AddonMessagesOfflineProvider.MESSAGES_TABLE, {deviceoffline: 1}));
* Get offline messages to send to a certain user. promises.push(site.getDb().getRecords(AddonMessagesOfflineProvider.CONVERSATION_MESSAGES_TABLE, {deviceoffline: 1}));
*
* @param {number} toUserId User ID to get messages to. return Promise.all(promises).then((results) => {
* @param {string} [siteId] Site ID. If not defined, current site. results[1] = this.parseConversationMessages(results[1]);
* @return {Promise<any>} Promise resolved with messages.
*/ return results[0].concat(results[1]);
getMessages(toUserId: number, siteId?: string): Promise<any> { });
return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().getRecords(AddonMessagesOfflineProvider.MESSAGES_TABLE, {touserid: toUserId});
}); });
} }
@ -113,7 +157,59 @@ export class AddonMessagesOfflineProvider {
*/ */
getAllMessages(siteId?: string): Promise<any> { getAllMessages(siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => { 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) => {
results[1] = this.parseConversationMessages(results[1]);
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<any[]>} Promise resolved with messages.
*/
getConversationMessages(conversationId: number, siteId?: string): Promise<any[]> {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().getRecords(AddonMessagesOfflineProvider.CONVERSATION_MESSAGES_TABLE,
{conversationid: conversationId}).then((messages) => {
return this.parseConversationMessages(messages);
});
});
}
/**
* 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<any[]>} Promise resolved with messages.
*/
getMessages(toUserId: number, siteId?: string): Promise<any[]> {
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<boolean>} Promise resolved with boolean: true if has offline messages, false otherwise.
*/
hasConversationMessages(conversationId: number, siteId?: string): Promise<boolean> {
return this.getConversationMessages(conversationId, siteId).then((messages) => {
return !!messages.length;
}); });
} }
@ -122,14 +218,64 @@ export class AddonMessagesOfflineProvider {
* *
* @param {number} toUserId User ID to check. * @param {number} toUserId User ID to check.
* @param {string} [siteId] Site ID. If not defined, current site. * @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved with boolean: true if has offline messages, false otherwise. * @return {Promise<boolean>} Promise resolved with boolean: true if has offline messages, false otherwise.
*/ */
hasMessages(toUserId: number, siteId?: string): Promise<any> { hasMessages(toUserId: number, siteId?: string): Promise<boolean> {
return this.getMessages(toUserId, siteId).then((messages) => { return this.getMessages(toUserId, siteId).then((messages) => {
return !!messages.length; return !!messages.length;
}); });
} }
/**
* 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 {any} conversation Conversation.
* @param {string} message The message to send.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved if stored, rejected if failure.
*/
saveConversationMessage(conversation: any, message: string, siteId?: string): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
const entry = {
conversationid: conversation.id,
text: message,
timecreated: Date.now(),
deviceoffline: this.appProvider.isOnline() ? 0 : 1,
conversation: JSON.stringify({
name: conversation.name || '',
subname: conversation.subname || '',
imageurl: conversation.imageurl || '',
isfavourite: conversation.isfavourite ? 1 : 0,
type: conversation.type
})
};
return site.getDb().insertRecord(AddonMessagesOfflineProvider.CONVERSATION_MESSAGES_TABLE, entry).then(() => {
return entry;
});
});
}
/** /**
* Save a message to be sent later. * Save a message to be sent later.
* *
@ -169,7 +315,13 @@ export class AddonMessagesOfflineProvider {
data = { deviceoffline: value ? 1 : 0 }; data = { deviceoffline: value ? 1 : 0 };
messages.forEach((message) => { messages.forEach((message) => {
promises.push(db.insertRecord(AddonMessagesOfflineProvider.MESSAGES_TABLE, data)); if (message.conversationid) {
promises.push(db.updateRecords(AddonMessagesOfflineProvider.CONVERSATION_MESSAGES_TABLE, data,
{conversationid: message.conversationid, text: message.text, timecreated: message.timecreated}));
} else {
promises.push(db.updateRecords(AddonMessagesOfflineProvider.MESSAGES_TABLE, data,
{touserid: message.touserid, smallmessage: message.smallmessage, timecreated: message.timecreated}));
}
}); });
return Promise.all(promises); return Promise.all(promises);

View File

@ -33,6 +33,7 @@ export class AddonMessagesProvider {
static NEW_MESSAGE_EVENT = 'addon_messages_new_message_event'; static NEW_MESSAGE_EVENT = 'addon_messages_new_message_event';
static READ_CHANGED_EVENT = 'addon_messages_read_changed_event'; static READ_CHANGED_EVENT = 'addon_messages_read_changed_event';
static READ_CRON_EVENT = 'addon_messages_read_cron_event'; static READ_CRON_EVENT = 'addon_messages_read_cron_event';
static OPEN_CONVERSATION_EVENT = 'addon_messages_open_conversation_event'; // Notify that a conversation should be opened.
static SPLIT_VIEW_LOAD_EVENT = 'addon_messages_split_view_load_event'; static SPLIT_VIEW_LOAD_EVENT = 'addon_messages_split_view_load_event';
static POLL_INTERVAL = 10000; static POLL_INTERVAL = 10000;
static PUSH_SIMULATION_COMPONENT = 'AddonMessagesPushSimulation'; static PUSH_SIMULATION_COMPONENT = 'AddonMessagesPushSimulation';
@ -115,7 +116,11 @@ export class AddonMessagesProvider {
} }
// It's an offline message. // 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,18 +132,56 @@ export class AddonMessagesProvider {
* @return {Promise<any>} Promise resolved when the message has been deleted. * @return {Promise<any>} Promise resolved when the message has been deleted.
*/ */
deleteMessageOnline(id: number, read: number, userId?: number): Promise<any> { deleteMessageOnline(id: number, read: number, userId?: number): Promise<any> {
userId = userId || this.sitesProvider.getCurrentSiteUserId(); const params: any = {
const params = {
messageid: id, messageid: id,
userid: userId, userid: userId || this.sitesProvider.getCurrentSiteUserId()
read: read
}; };
if (typeof read != 'undefined') {
params.read = read;
}
return this.sitesProvider.getCurrentSite().write('core_message_delete_message', params).then(() => { return this.sitesProvider.getCurrentSite().write('core_message_delete_message', params).then(() => {
return this.invalidateDiscussionCache(userId); return this.invalidateDiscussionCache(userId);
}); });
} }
/**
* 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;
conversation.otherUser = otherUser;
}
return conversation;
}
/** /**
* Get the cache key for blocked contacts. * Get the cache key for blocked contacts.
* *
@ -187,6 +230,50 @@ export class AddonMessagesProvider {
return this.ROOT_CACHE_KEY + 'discussions'; 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 members.
*
* @param {number} userId User ID.
* @param {number} conversationId Conversation ID.
* @return {string} Cache key.
*/
protected getCacheKeyForConversationMembers(userId: number, conversationId: number): string {
return this.ROOT_CACHE_KEY + 'conversationMembers:' + userId + ':' + conversationId;
}
/**
* 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. * Get cache key for get conversations.
* *
@ -297,6 +384,230 @@ 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 members.
*
* @param {number} conversationId Conversation ID to fetch.
* @param {number} [limitFrom=0] Offset for members list.
* @param {number} [limitTo] Limit of members.
* @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
*/
getConversationMembers(conversationId: number, limitFrom: number = 0, limitTo?: number, includeContactRequests?: boolean,
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.getCacheKeyForConversationMembers(userId, conversationId)
},
params: any = {
userid: userId,
conversationid: conversationId,
limitfrom: limitFrom,
limitnum: limitTo < 1 ? limitTo : limitTo + 1, // If there is a limit, get 1 more than requested.
includecontactrequests: includeContactRequests ? 1 : 0
};
return site.read('core_message_get_conversation_members', params, preSets).then((members) => {
const result: any = {};
if (limitTo < 1) {
result.canLoadMore = false;
result.members = members;
} else {
result.canLoadMore = members.length > limitTo;
result.members = members.slice(0, limitTo);
}
return result;
});
});
}
/**
* 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.getConversationMessages(conversationId).then((offlineMessages) => {
// Mark offline messages as pending.
offlineMessages.forEach((message) => {
message.pending = true;
message.useridfrom = userId;
});
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. * 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. * If the site is older than 3.6, please use getDiscussions.
@ -308,6 +619,7 @@ export class AddonMessagesProvider {
* @param {string} [siteId] Site ID. If not defined, use current site. * @param {string} [siteId] Site ID. If not defined, use current site.
* @param {number} [userId] User ID. If not defined, current user in the site. * @param {number} [userId] User ID. If not defined, current user in the site.
* @return {Promise<any>} Promise resolved with the conversations. * @return {Promise<any>} Promise resolved with the conversations.
* @since 3.6
*/ */
getConversations(type?: number, favourites?: boolean, limitFrom: number = 0, siteId?: string, userId?: number) getConversations(type?: number, favourites?: boolean, limitFrom: number = 0, siteId?: string, userId?: number)
: Promise<{conversations: any[], canLoadMore: boolean}> { : Promise<{conversations: any[], canLoadMore: boolean}> {
@ -333,32 +645,8 @@ export class AddonMessagesProvider {
return site.read('core_message_get_conversations', params, preSets).then((response) => { return site.read('core_message_get_conversations', params, preSets).then((response) => {
// Format the conversations, adding some calculated fields. // Format the conversations, adding some calculated fields.
const conversations = response.conversations.map((conversation) => { const conversations = response.conversations.slice(0, this.LIMIT_MESSAGES).map((conversation) => {
const numMessages = conversation.messages.length, return this.formatConversation(conversation, userId);
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;
}); });
return { return {
@ -813,7 +1101,71 @@ 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 members 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.
*/
invalidateConversationMembers(conversationId: number, siteId?: string, userId?: number): Promise<any> {
return this.sitesProvider.getSite(siteId).then((site) => {
userId = userId || site.getUserId();
return site.invalidateWsCacheForKey(this.getCacheKeyForConversationMembers(userId, conversationId));
});
}
/**
* 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 {string} [siteId] Site ID. If not defined, current site.
* @param {number} [userId] User ID. If not defined, current user in the site. * @param {number} [userId] User ID. If not defined, current user in the site.
@ -1003,6 +1355,25 @@ export class AddonMessagesProvider {
return this.sitesProvider.getCurrentSite().write('core_message_mark_message_read', params); 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. * Mark all messages of a discussion as read.
* *
@ -1144,8 +1515,11 @@ export class AddonMessagesProvider {
} }
// Online and no messages stored. Send it to server. // Online and no messages stored. Send it to server.
return this.sendMessageOnline(toUserId, message).then(() => { return this.sendMessageOnline(toUserId, message).then((result) => {
return { sent: true }; return {
sent: true,
message: result
};
}).catch((error) => { }).catch((error) => {
if (this.utils.isWebServiceError(error)) { if (this.utils.isWebServiceError(error)) {
// It's a WebService error, the user cannot send the message so don't store it. // It's a WebService error, the user cannot send the message so don't store it.
@ -1185,6 +1559,8 @@ export class AddonMessagesProvider {
return this.invalidateDiscussionCache(toUserId, siteId).catch(() => { return this.invalidateDiscussionCache(toUserId, siteId).catch(() => {
// Ignore errors. // Ignore errors.
}).then(() => {
return response[0];
}); });
}); });
} }
@ -1209,13 +1585,143 @@ export class AddonMessagesProvider {
}); });
} }
/**
* Send a message to a conversation.
*
* @param {any} conversation Conversation.
* @param {string} message The message to send.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} 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(conversation: any, message: string, siteId?: string): Promise<any> {
// Convenience function to store a message to be synchronized later.
const storeOffline = (): Promise<any> => {
return this.messagesOffline.saveConversationMessage(conversation, 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(conversation.id, 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(conversation.id, message).then((result) => {
return {
sent: true,
message: result
};
}).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<any>} Promise resolved if success, rejected if failure.
* @since 3.6
*/
sendMessageToConversationOnline(conversationId: number, message: string, siteId?: string): Promise<any> {
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<any>} Promise resolved if success, rejected if failure.
* @since 3.6
*/
sendMessagesToConversationOnline(conversationId: number, messages: any, siteId?: string): Promise<any> {
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 conversations by last message time.
*
* @param {any[]} conversations Array of conversations.
* @return {any[]} Conversations sorted with most recent last.
*/
sortConversations(conversations: any[]): any[] {
return conversations.sort((a, b) => {
const timeA = parseInt(a.lastmessagedate, 10),
timeB = parseInt(b.lastmessagedate, 10);
if (timeA == timeB && a.id) {
// Same time, sort by ID.
return a.id <= b.id ? 1 : -1;
}
return timeA <= timeB ? 1 : -1;
});
}
/** /**
* Helper method to sort messages by time. * Helper method to sort messages by time.
* *
* @param {any} messages Array of messages containing the key 'timecreated'. * @param {any[]} messages Array of messages containing the key 'timecreated'.
* @return {any} Messages sorted with most recent last. * @return {any[]} Messages sorted with most recent last.
*/ */
sortMessages(messages: any): any { sortMessages(messages: any[]): any[] {
return messages.sort((a, b) => { return messages.sort((a, b) => {
// Pending messages last. // Pending messages last.
if (a.pending && !b.pending) { if (a.pending && !b.pending) {
@ -1238,17 +1744,17 @@ export class AddonMessagesProvider {
/** /**
* Store the last received message if it's newer than the last stored. * 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 {any} message Last message received.
* @param {string} [siteId] Site ID. If not defined, current site. * @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise<any>} Promise resolved when done. * @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; const component = AddonMessagesProvider.PUSH_SIMULATION_COMPONENT;
// Get the last received message. // Get the last received message.
return this.emulatorHelper.getLastReceivedNotification(component, siteId).then((lastMessage) => { 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. // Seeing a single discussion. No received message or cannot know if it really is the last received message. Stop.
return; return;
} }

View File

@ -42,6 +42,21 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider {
super('AddonMessagesSync', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate); 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. * 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) => { return promise.then((messages) => {
const userIds = [], const userIds = [],
conversationIds = [],
promises = []; promises = [];
// Get all the discussions to be synced. // Get all the conversations to be synced.
messages.forEach((message) => { 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); userIds.push(message.touserid);
} }
}); });
// Sync all discussions. // Sync all conversations.
userIds.forEach((userId) => { conversationIds.forEach((conversationId) => {
promises.push(this.syncDiscussion(userId, siteId).then((warnings) => { promises.push(this.syncDiscussion(conversationId, undefined, siteId).then((warnings) => {
if (typeof warnings != 'undefined') { if (typeof warnings != 'undefined') {
// Sync successful, send event. // Sync successful, send event.
this.eventsProvider.trigger(AddonMessagesSyncProvider.AUTO_SYNCED, { 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 warnings: warnings
}, siteId); }, siteId);
} }
@ -99,24 +131,39 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider {
/** /**
* Synchronize a discussion. * Synchronize a discussion.
* *
* @param {number} userId User ID of the discussion. * @param {number} conversationId Conversation ID.
* @param {string} [siteId] Site ID. If not defined, current site. * @param {number} userId User ID talking to (if no conversation ID).
* @return {Promise<any>} Promise resolved if sync is successful, rejected otherwise. * @return {Promise<any>} Promise resolved if sync is successful, rejected otherwise.
*/ */
syncDiscussion(userId: number, siteId?: string): Promise<any> { syncDiscussion(conversationId: number, userId: number, siteId?: string): Promise<any> {
siteId = siteId || this.sitesProvider.getCurrentSiteId(); siteId = siteId || this.sitesProvider.getCurrentSiteId();
if (this.isSyncing(userId, siteId)) { const syncId = this.getSyncId(conversationId, userId),
// There's already a sync ongoing for this SCORM, return the promise. groupMessagingEnabled = this.messagesProvider.isGroupMessagingEnabled();
return this.getOngoingSync(userId, siteId);
if (this.isSyncing(syncId, siteId)) {
// There's already a sync ongoing for this conversation, return the promise.
return this.getOngoingSync(syncId, siteId);
} }
const warnings = []; 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. // 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) { if (!messages.length) {
// Nothing to sync. // Nothing to sync.
return []; return [];
@ -134,12 +181,19 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider {
messages = this.messagesProvider.sortMessages(messages); messages = this.messagesProvider.sortMessages(messages);
// Send the messages. // Send the messages.
// We don't use AddonMessagesProvider#sendMessagesOnline because there's a problem with display order. // Send them 1 by 1 to simulate web's behaviour and to make sure we know which message has failed.
// @todo Use AddonMessagesProvider#sendMessagesOnline once the display order is fixed.
messages.forEach((message, index) => { messages.forEach((message, index) => {
// Chain message sending. If 1 message fails to be sent we'll stop sending. // Chain message sending. If 1 message fails to be sent we'll stop sending.
promise = promise.then(() => { 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)) { if (this.utils.isWebServiceError(error)) {
// Error returned by WS. Store the error to show a warning but keep sending messages. // Error returned by WS. Store the error to show a warning but keep sending messages.
if (errors.indexOf(error) == -1) { if (errors.indexOf(error) == -1) {
@ -158,22 +212,63 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider {
return Promise.reject(error); return Promise.reject(error);
}).then(() => { }).then(() => {
// Message was sent, delete it from local DB. // 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(() => { }).then(() => {
// All done. Wait 1 second to ensure timecreated of messages is different. // In some Moodle versions, wait 1 second to make sure timecreated is different.
if (index < messages.length - 1) { // This is because there was a bug where messages with the same timecreated had a wrong order.
return setTimeout(() => {return; }, 1000); if (!groupMessagingEnabled && index < messages.length - 1) {
return new Promise((resolve, reject): any => {
setTimeout(resolve, 1000);
});
} }
}); });
}); });
}); });
return promise.then(() => { return promise;
return errors;
});
}).then((errors) => { }).then((errors) => {
if (errors && errors.length) { return this.handleSyncErrors(conversationId, userId, errors, warnings);
// At least an error occurred, get user full name and add errors to warnings array. }).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<any>} Promise resolved when done.
*/
protected handleSyncErrors(conversationId: number, userId: number, errors: any[], warnings: any[]): Promise<any> {
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(() => { return this.userProvider.getProfile(userId, undefined, true).catch(() => {
// Ignore errors. // Ignore errors.
return {}; return {};
@ -181,16 +276,26 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider {
errors.forEach((error) => { errors.forEach((error) => {
warnings.push(this.translate.instant('addon.messages.warningmessagenotsent', { warnings.push(this.translate.instant('addon.messages.warningmessagenotsent', {
user: user.fullname ? user.fullname : userId, 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<any>} Promise resolved when there's no sync going on for the identifier.
*/
waitForSyncConversation(conversationId: number, userId: number, siteId?: string): Promise<any> {
const syncId = this.getSyncId(conversationId, userId);
return this.waitForSync(syncId, siteId);
} }
} }

View File

@ -165,7 +165,9 @@
"addon.messages.errorwhileretrievingcontacts": "Error while retrieving contacts from the server.", "addon.messages.errorwhileretrievingcontacts": "Error while retrieving contacts from the server.",
"addon.messages.errorwhileretrievingdiscussions": "Error while retrieving discussions 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.errorwhileretrievingmessages": "Error while retrieving messages from the server.",
"addon.messages.groupinfo": "Group info",
"addon.messages.groupmessages": "Group messages", "addon.messages.groupmessages": "Group messages",
"addon.messages.info": "Info",
"addon.messages.message": "Message", "addon.messages.message": "Message",
"addon.messages.messagenotsent": "The message was not sent. Please try again later.", "addon.messages.messagenotsent": "The message was not sent. Please try again later.",
"addon.messages.messagepreferences": "Message preferences", "addon.messages.messagepreferences": "Message preferences",
@ -176,8 +178,10 @@
"addon.messages.nogroupmessages": "No group messages", "addon.messages.nogroupmessages": "No group messages",
"addon.messages.nomessages": "No messages", "addon.messages.nomessages": "No messages",
"addon.messages.nousersfound": "No users found", "addon.messages.nousersfound": "No users found",
"addon.messages.numparticipants": "{{$a}} participants",
"addon.messages.removecontact": "Remove contact", "addon.messages.removecontact": "Remove contact",
"addon.messages.removecontactconfirm": "Contact will be removed from your contacts list.", "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_blocked": "Blocked",
"addon.messages.type_offline": "Offline", "addon.messages.type_offline": "Offline",
"addon.messages.type_online": "Online", "addon.messages.type_online": "Online",
@ -185,6 +189,7 @@
"addon.messages.type_strangers": "Others", "addon.messages.type_strangers": "Others",
"addon.messages.unblockuser": "Unblock user", "addon.messages.unblockuser": "Unblock user",
"addon.messages.unblockuserconfirm": "Are you sure you want to unblock {{$a}}?", "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.warningmessagenotsent": "Couldn't send message(s) to user {{user}}. {{error}}",
"addon.messages.you": "You:", "addon.messages.you": "You:",
"addon.mod_assign.acceptsubmissionstatement": "Please accept the submission statement.", "addon.mod_assign.acceptsubmissionstatement": "Please accept the submission statement.",

View File

@ -15,6 +15,7 @@
import { Component, Input, OnInit, OnChanges, SimpleChange } from '@angular/core'; import { Component, Input, OnInit, OnChanges, SimpleChange } from '@angular/core';
import { NavController } from 'ionic-angular'; import { NavController } from 'ionic-angular';
import { CoreSitesProvider } from '@providers/sites'; import { CoreSitesProvider } from '@providers/sites';
import { CoreUtilsProvider } from '@providers/utils/utils';
/** /**
* Component to display a "user avatar". * Component to display a "user avatar".
@ -41,7 +42,7 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges {
protected myUser = false; protected myUser = false;
protected currentUserId: number; 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(); 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.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); this.courseId = this.courseId || (this.user && this.user.courseid);
// If not available we cannot ensure the avatar is from the current user. // 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 * @return boolean
*/ */
isOnline(): 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;
}
} }
/** /**