commit
						e44c28ccdd
					
				| @ -165,7 +165,9 @@ | ||||
|   "addon.messages.errorwhileretrievingcontacts": "local_moodlemobileapp", | ||||
|   "addon.messages.errorwhileretrievingdiscussions": "local_moodlemobileapp", | ||||
|   "addon.messages.errorwhileretrievingmessages": "local_moodlemobileapp", | ||||
|   "addon.messages.groupinfo": "message", | ||||
|   "addon.messages.groupmessages": "message", | ||||
|   "addon.messages.info": "message", | ||||
|   "addon.messages.message": "message", | ||||
|   "addon.messages.messagenotsent": "local_moodlemobileapp", | ||||
|   "addon.messages.messagepreferences": "message", | ||||
| @ -176,8 +178,10 @@ | ||||
|   "addon.messages.nogroupmessages": "message", | ||||
|   "addon.messages.nomessages": "message", | ||||
|   "addon.messages.nousersfound": "local_moodlemobileapp", | ||||
|   "addon.messages.numparticipants": "message", | ||||
|   "addon.messages.removecontact": "message", | ||||
|   "addon.messages.removecontactconfirm": "local_moodlemobileapp", | ||||
|   "addon.messages.showdeletemessages": "local_moodlemobileapp", | ||||
|   "addon.messages.type_blocked": "local_moodlemobileapp", | ||||
|   "addon.messages.type_offline": "local_moodlemobileapp", | ||||
|   "addon.messages.type_online": "local_moodlemobileapp", | ||||
| @ -185,6 +189,7 @@ | ||||
|   "addon.messages.type_strangers": "local_moodlemobileapp", | ||||
|   "addon.messages.unblockuser": "message", | ||||
|   "addon.messages.unblockuserconfirm": "message", | ||||
|   "addon.messages.warningconversationmessagenotsent": "local_moodlemobileapp", | ||||
|   "addon.messages.warningmessagenotsent": "local_moodlemobileapp", | ||||
|   "addon.messages.you": "message", | ||||
|   "addon.mod_assign.acceptsubmissionstatement": "local_moodlemobileapp", | ||||
|  | ||||
| @ -17,7 +17,9 @@ | ||||
|     "errorwhileretrievingcontacts": "Error while retrieving contacts from the server.", | ||||
|     "errorwhileretrievingdiscussions": "Error while retrieving discussions from the server.", | ||||
|     "errorwhileretrievingmessages": "Error while retrieving messages from the server.", | ||||
|     "groupinfo": "Group info", | ||||
|     "groupmessages": "Group messages", | ||||
|     "info": "Info", | ||||
|     "messagenotsent": "The message was not sent. Please try again later.", | ||||
|     "message": "Message", | ||||
|     "messagepreferences": "Message preferences", | ||||
| @ -28,8 +30,10 @@ | ||||
|     "nogroupmessages": "No group messages", | ||||
|     "nomessages": "No messages", | ||||
|     "nousersfound": "No users found", | ||||
|     "numparticipants": "{{$a}} participants", | ||||
|     "removecontact": "Remove contact", | ||||
|     "removecontactconfirm": "Contact will be removed from your contacts list.", | ||||
|     "showdeletemessages": "Show delete messages", | ||||
|     "type_blocked": "Blocked", | ||||
|     "type_offline": "Offline", | ||||
|     "type_online": "Online", | ||||
| @ -37,6 +41,7 @@ | ||||
|     "type_strangers": "Others", | ||||
|     "unblockuser": "Unblock user", | ||||
|     "unblockuserconfirm": "Are you sure you want to unblock {{$a}}?", | ||||
|     "warningconversationmessagenotsent": "Couldn't send message(s) to conversation {{conversation}}. {{error}}", | ||||
|     "warningmessagenotsent": "Couldn't send message(s) to user {{user}}. {{error}}", | ||||
|     "you": "You:" | ||||
| } | ||||
| @ -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> | ||||
| @ -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 {} | ||||
| @ -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; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										133
									
								
								src/addon/messages/pages/conversation-info/conversation-info.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								src/addon/messages/pages/conversation-info/conversation-info.ts
									
									
									
									
									
										Normal 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); | ||||
|     } | ||||
| } | ||||
| @ -1,15 +1,17 @@ | ||||
| <ion-header> | ||||
|     <ion-navbar core-back-button> | ||||
|         <ion-title><core-format-text [text]="title"></core-format-text></ion-title> | ||||
|         <ion-title> | ||||
|             <img *ngIf="conversationImage" class="core-bar-button-image" [src]="conversationImage"> | ||||
|             <core-format-text [text]="title"></core-format-text> | ||||
|         </ion-title> | ||||
|         <ion-buttons end></ion-buttons> | ||||
|     </ion-navbar> | ||||
|     <core-navbar-buttons end> | ||||
|         <button ion-button icon-only clear="true" (click)="toggleDelete()" [hidden]="!canDelete"> | ||||
|             <ion-icon name="trash"></ion-icon> | ||||
|         </button> | ||||
|         <a [hidden]="!showProfileLink" core-user-link [userId]="userId" [attr.aria-label]=" 'core.user.viewprofile' | translate"> | ||||
|             <img class="button core-bar-button-image" [src]="profileLink" core-external-content onError="this.src='assets/img/user-avatar.png'"> | ||||
|         </a> | ||||
|         <core-context-menu> | ||||
|             <core-context-menu-item [hidden]="!showInfo || isGroup" [priority]="1000" [content]="'addon.messages.info' | translate" (action)="viewInfo()" [iconAction]="'information-circle'"></core-context-menu-item> | ||||
|             <core-context-menu-item [hidden]="!showInfo || !isGroup" [priority]="999" [content]="'addon.messages.groupinfo' | translate" (action)="viewInfo()" [iconAction]="'information-circle'"></core-context-menu-item> | ||||
|             <core-context-menu-item [hidden]="!canDelete" [priority]="800" [content]="'addon.messages.showdeletemessages' | translate" (action)="toggleDelete()" [iconAction]="'trash'"></core-context-menu-item> | ||||
|         </core-context-menu> | ||||
|     </core-navbar-buttons> | ||||
| </ion-header> | ||||
| <ion-content class="has-footer"> | ||||
| @ -18,16 +20,22 @@ | ||||
|         <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"> | ||||
|             <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-chip> | ||||
| 
 | ||||
|                 <ion-chip class="addon-messages-unreadfrom" *ngIf="message.unreadFrom" color="light"> | ||||
|                 <ion-chip class="addon-messages-unreadfrom" *ngIf="unreadMessageFrom && message.id == unreadMessageFrom" color="light"> | ||||
|                     <ion-label>{{ 'addon.messages.newmessages' | translate:{$a: title} }}</ion-label> | ||||
|                     <ion-icon name="arrow-round-down"></ion-icon> | ||||
|                 </ion-chip> | ||||
| 
 | ||||
|                 <ion-item text-wrap (longPress)="copyMessage(message.smallmessage)" class="addon-message" [class.addon-message-mine]="message.useridfrom == currentUserId" [@coreSlideInOut]="message.useridfrom == currentUserId ? '' : 'fromLeft'"> | ||||
|                 <ion-item text-wrap (longPress)="copyMessage(message)" class="addon-message" [class.addon-message-mine]="message.useridfrom == currentUserId" [@coreSlideInOut]="message.useridfrom == currentUserId ? '' : 'fromLeft'"> | ||||
|                     <!-- 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. --> | ||||
|                     <p class="addon-message-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-loading> | ||||
| </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"> | ||||
|         <!-- @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> | ||||
|     </ion-toolbar> | ||||
| </ion-footer> | ||||
|  | ||||
| @ -12,12 +12,13 @@ | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component, OnDestroy, ViewChild } from '@angular/core'; | ||||
| import { IonicPage, NavParams, NavController, Content } from 'ionic-angular'; | ||||
| import { Component, OnDestroy, ViewChild, Optional } from '@angular/core'; | ||||
| import { IonicPage, NavParams, NavController, Content, ModalController } from 'ionic-angular'; | ||||
| import { TranslateService } from '@ngx-translate/core'; | ||||
| import { CoreEventsProvider } from '@providers/events'; | ||||
| import { CoreSitesProvider } from '@providers/sites'; | ||||
| import { AddonMessagesProvider } from '../../providers/messages'; | ||||
| import { AddonMessagesOfflineProvider } from '../../providers/messages-offline'; | ||||
| import { AddonMessagesSyncProvider } from '../../providers/sync'; | ||||
| import { CoreUserProvider } from '@core/user/providers/user'; | ||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||
| @ -25,6 +26,7 @@ import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||
| import { CoreLoggerProvider } from '@providers/logger'; | ||||
| import { CoreAppProvider } from '@providers/app'; | ||||
| import { coreSlideInOut } from '@classes/animations'; | ||||
| import { CoreSplitViewComponent } from '@components/split-view/split-view'; | ||||
| import { Md5 } from 'ts-md5/dist/md5'; | ||||
| import * as moment from 'moment'; | ||||
| 
 | ||||
| @ -53,12 +55,16 @@ export class AddonMessagesDiscussionPage implements OnDestroy { | ||||
|     protected syncObserver: any; | ||||
|     protected oldContentHeight = 0; | ||||
|     protected keyboardObserver: any; | ||||
|     protected scrollBottom = true; | ||||
|     protected viewDestroyed = false; | ||||
| 
 | ||||
|     userId: number; | ||||
|     conversationId: number; // Conversation ID. Undefined if it's a new individual conversation.
 | ||||
|     conversation: any; // The conversation object (if it exists).
 | ||||
|     userId: number; // User ID you're talking to (only if group messaging not enabled or it's a new individual conversation).
 | ||||
|     currentUserId: number; | ||||
|     title: string; | ||||
|     profileLink: string; | ||||
|     showProfileLink: boolean; | ||||
|     showInfo: boolean; | ||||
|     conversationImage: string; | ||||
|     loaded = false; | ||||
|     showKeyboard = false; | ||||
|     canLoadMore = false; | ||||
| @ -66,26 +72,33 @@ export class AddonMessagesDiscussionPage implements OnDestroy { | ||||
|     messages = []; | ||||
|     showDelete = false; | ||||
|     canDelete = false; | ||||
|     scrollBottom = true; | ||||
|     viewDestroyed = false; | ||||
|     groupMessagingEnabled: boolean; | ||||
|     isGroup = false; | ||||
|     members: any = {}; // Members that wrote a message, indexed by ID.
 | ||||
| 
 | ||||
|     constructor(private eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, navParams: NavParams, | ||||
|             private userProvider: CoreUserProvider, private navCtrl: NavController, private messagesSync: AddonMessagesSyncProvider, | ||||
|             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.currentUserId = sitesProvider.getCurrentSiteUserId(); | ||||
|         this.groupMessagingEnabled = this.messagesProvider.isGroupMessagingEnabled(); | ||||
| 
 | ||||
|         this.logger = logger.getInstance('AddonMessagesDiscussionPage'); | ||||
| 
 | ||||
|         this.conversationId = navParams.get('conversationId'); | ||||
|         this.userId = navParams.get('userId'); | ||||
|         this.showKeyboard = navParams.get('showKeyboard'); | ||||
| 
 | ||||
|         // Refresh data if this discussion is synchronized automatically.
 | ||||
|         this.syncObserver = eventsProvider.on(AddonMessagesSyncProvider.AUTO_SYNCED, (data) => { | ||||
|             if (data.userId == this.userId) { | ||||
|             if ((data.userId && data.userId == this.userId) || | ||||
|                     (data.conversationId && data.conversationId == this.conversationId)) { | ||||
|                 // Fetch messages.
 | ||||
|                 this.fetchData(); | ||||
|                 this.fetchMessages(); | ||||
| 
 | ||||
|                 // Show first warning if any.
 | ||||
|                 if (data.warnings && data.warnings[0]) { | ||||
| @ -102,8 +115,8 @@ export class AddonMessagesDiscussionPage implements OnDestroy { | ||||
|      * @param {boolean} [keep=true] If set the keep flag or not. | ||||
|      */ | ||||
|     protected addMessage(message: any, keep: boolean = true): void { | ||||
|         // Use smallmessage instead of message ID because ID changes when a message is read.
 | ||||
|         message.hash = Md5.hashAsciiStr(message.smallmessage) + '#' + message.timecreated + '#' + message.useridfrom; | ||||
|         // Use text instead of message ID because ID changes when a message is read.
 | ||||
|         message.hash = Md5.hashAsciiStr(message.text || '') + '#' + message.timecreated + '#' + message.useridfrom; | ||||
|         if (typeof this.keepMessageMap[message.hash] === 'undefined') { | ||||
|             // Message not added to the list. Add it now.
 | ||||
|             this.messages.push(message); | ||||
| @ -130,7 +143,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { | ||||
|         const position = this.messages.findIndex((message) => { | ||||
|             return message.hash == hash; | ||||
|         }); | ||||
|         if (position > 0) { | ||||
|         if (position >= 0) { | ||||
|             this.messages.splice(position, 1); | ||||
|         } | ||||
|     } | ||||
| @ -143,42 +156,55 @@ export class AddonMessagesDiscussionPage implements OnDestroy { | ||||
|     ionViewDidLoad(): void { | ||||
|         // Disable the profile button if we're already coming from a profile.
 | ||||
|         const backViewPage = this.navCtrl.getPrevious() && this.navCtrl.getPrevious().component.name; | ||||
|         this.showProfileLink = !backViewPage || backViewPage !== 'CoreUserProfilePage'; | ||||
|         this.showInfo = !backViewPage || backViewPage !== 'CoreUserProfilePage'; | ||||
| 
 | ||||
|         // Get the user profile to retrieve the user fullname and image.
 | ||||
|         this.userProvider.getProfile(this.userId, undefined, true).then((user) => { | ||||
|             if (!this.title) { | ||||
|                 this.title = user.fullname; | ||||
|             } | ||||
|             this.profileLink = user.profileimageurl; | ||||
|         }); | ||||
|         if (!this.groupMessagingEnabled && this.userId) { | ||||
|             // Get the user profile to retrieve the user fullname and image.
 | ||||
|             this.userProvider.getProfile(this.userId, undefined, true).then((user) => { | ||||
|                 if (!this.title) { | ||||
|                     this.title = user.fullname; | ||||
|                 } | ||||
|                 this.conversationImage = user.profileimageurl; | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         // Synchronize messages if needed.
 | ||||
|         this.messagesSync.syncDiscussion(this.userId).catch(() => { | ||||
|         this.messagesSync.syncDiscussion(this.conversationId, this.userId).catch(() => { | ||||
|             // Ignore errors.
 | ||||
|         }).then((warnings) => { | ||||
|             if (warnings && warnings[0]) { | ||||
|                 this.domUtils.showErrorModal(warnings[0]); | ||||
|             } | ||||
| 
 | ||||
|             // Fetch the messages for the first time.
 | ||||
|             return this.fetchData().then(() => { | ||||
|                 if (!this.title && this.messages.length) { | ||||
|                     // Didn't receive the fullname via argument. Try to get it from messages.
 | ||||
|                     // It's possible that name cannot be resolved when no messages were yet exchanged.
 | ||||
|                     if (this.messages[0].useridto != this.currentUserId) { | ||||
|                         this.title = this.messages[0].usertofullname || ''; | ||||
|                     } else { | ||||
|                         this.title = this.messages[0].userfromfullname || ''; | ||||
|             if (this.groupMessagingEnabled) { | ||||
|                 // Get the conversation ID if it exists and we don't have it yet.
 | ||||
|                 return this.getConversation(this.conversationId, this.userId).then((exists) => { | ||||
|                     if (exists) { | ||||
|                         // Fetch the messages for the first time.
 | ||||
|                         return this.fetchMessages(); | ||||
|                     } | ||||
|                 } | ||||
|             }).catch((error) => { | ||||
|                 this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingmessages', true); | ||||
|             }).finally(() => { | ||||
|                 this.checkCanDelete(); | ||||
|                 this.resizeContent(); | ||||
|                 this.loaded = true; | ||||
|             }); | ||||
|                 }); | ||||
|             } else { | ||||
|                 // Fetch the messages for the first time.
 | ||||
|                 return this.fetchMessages().then(() => { | ||||
|                     if (!this.title && this.messages.length) { | ||||
|                         // Didn't receive the fullname via argument. Try to get it from messages.
 | ||||
|                         // It's possible that name cannot be resolved when no messages were yet exchanged.
 | ||||
|                         if (this.messages[0].useridto != this.currentUserId) { | ||||
|                             this.title = this.messages[0].usertofullname || ''; | ||||
|                         } else { | ||||
|                             this.title = this.messages[0].userfromfullname || ''; | ||||
|                         } | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|         }).catch((error) => { | ||||
|             this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingmessages', true); | ||||
|         }).finally(() => { | ||||
|             this.checkCanDelete(); | ||||
|             this.resizeContent(); | ||||
|             this.loaded = true; | ||||
|             this.setPolling(); // Make sure we're polling messages.
 | ||||
|         }); | ||||
| 
 | ||||
|         // Recalculate footer position when keyboard is shown or hidden.
 | ||||
| @ -204,12 +230,12 @@ export class AddonMessagesDiscussionPage implements OnDestroy { | ||||
| 
 | ||||
|     /** | ||||
|      * Convenience function to fetch messages. | ||||
|      * | ||||
|      * @return {Promise<any>} Resolved when done. | ||||
|      */ | ||||
|     protected fetchData(): Promise<any> { | ||||
|     protected fetchMessages(): Promise<any> { | ||||
|         this.loadMoreError = false; | ||||
| 
 | ||||
|         this.logger.debug(`Polling new messages for discussion with user '${this.userId}'`); | ||||
|         if (this.messagesBeingSent > 0) { | ||||
|             // We do not poll while a message is being sent or we could confuse the user.
 | ||||
|             // Otherwise, his message would disappear from the list, and he'd have to wait for the interval to check for messages.
 | ||||
| @ -217,55 +243,179 @@ export class AddonMessagesDiscussionPage implements OnDestroy { | ||||
|         } else if (this.fetching) { | ||||
|             // Already fetching.
 | ||||
|             return Promise.reject(null); | ||||
|         } else if (this.groupMessagingEnabled && !this.conversationId) { | ||||
|             // Don't have enough data to fetch messages.
 | ||||
|             return Promise.reject(null); | ||||
|         } | ||||
| 
 | ||||
|         if (this.conversationId) { | ||||
|             this.logger.debug(`Polling new messages for conversation '${this.conversationId}'`); | ||||
|         } else { | ||||
|             this.logger.debug(`Polling new messages for discussion with user '${this.userId}'`); | ||||
|         } | ||||
| 
 | ||||
|         this.fetching = true; | ||||
| 
 | ||||
|         // 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.
 | ||||
|             return this.messagesProvider.invalidateDiscussionCache(this.userId).catch(() => { | ||||
|                 // Ignore errors.
 | ||||
|             }); | ||||
|         }).then(() => { | ||||
|             return this.getDiscussion(this.pagesLoaded); | ||||
|             if (this.groupMessagingEnabled) { | ||||
|                 return this.messagesProvider.invalidateConversationMessages(this.conversationId).catch(() => { | ||||
|                     // Ignore errors.
 | ||||
|                 }).then(() => { | ||||
|                     return this.getConversationMessages(this.pagesLoaded); | ||||
|                 }); | ||||
|             } else { | ||||
|                 return this.messagesProvider.invalidateDiscussionCache(this.userId).catch(() => { | ||||
|                     // Ignore errors.
 | ||||
|                 }).then(() => { | ||||
|                     return this.getDiscussionMessages(this.pagesLoaded); | ||||
|                 }); | ||||
|             } | ||||
|         }).then((messages) => { | ||||
|             if (this.viewDestroyed) { | ||||
|                 return Promise.resolve(); | ||||
|             } | ||||
| 
 | ||||
|             // 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(); | ||||
|             this.loadMessages(messages); | ||||
|         }).finally(() => { | ||||
|             this.fetching = false; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Format and load a list of messages into the view. | ||||
|      * | ||||
|      * @param {any[]} messages Messages to load. | ||||
|      */ | ||||
|     protected loadMessages(messages: any[]): void { | ||||
|         if (this.viewDestroyed) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // Check if we are at the bottom to scroll it after render.
 | ||||
|         this.scrollBottom = this.domUtils.getScrollHeight(this.content) - this.domUtils.getScrollTop(this.content) === | ||||
|             this.domUtils.getContentHeight(this.content); | ||||
| 
 | ||||
|         if (this.messagesBeingSent > 0) { | ||||
|             // Ignore polling due to a race condition.
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // Add new messages to the list and mark the messages that should still be displayed.
 | ||||
|         messages.forEach((message) => { | ||||
|             this.addMessage(message); | ||||
|         }); | ||||
| 
 | ||||
|         // Remove messages that shouldn't be in the list anymore.
 | ||||
|         for (const hash in this.keepMessageMap) { | ||||
|             this.removeMessage(hash); | ||||
|         } | ||||
| 
 | ||||
|         // Sort the messages.
 | ||||
|         this.messagesProvider.sortMessages(this.messages); | ||||
| 
 | ||||
|         // Calculate which messages need to display the date or user data.
 | ||||
|         this.messages.forEach((message, index): any => { | ||||
|             message.showDate = this.showDate(message, this.messages[index - 1]); | ||||
|             message.showUserData = this.showUserData(message, this.messages[index - 1]); | ||||
|         }); | ||||
| 
 | ||||
|         // Notify that there can be a new message.
 | ||||
|         this.notifyNewMessage(); | ||||
| 
 | ||||
|         // Mark retrieved messages as read if they are not.
 | ||||
|         this.markMessagesAsRead(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the conversation. | ||||
|      * | ||||
|      * @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". | ||||
|      * | ||||
| @ -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. | ||||
|      * @return {Promise<any>}  Resolved when done. | ||||
|      */ | ||||
|     protected getDiscussion(pagesToLoad: number, lfReceivedUnread: number = 0, lfReceivedRead: number = 0, lfSentUnread: number = 0, | ||||
|             lfSentRead: number = 0): Promise<any> { | ||||
|     protected getDiscussionMessages(pagesToLoad: number, lfReceivedUnread: number = 0, lfReceivedRead: number = 0, | ||||
|             lfSentUnread: number = 0, lfSentRead: number = 0): Promise<any> { | ||||
| 
 | ||||
|         // Only get offline messages if we're loading the first "page".
 | ||||
|         const excludePending = lfReceivedUnread > 0 || lfReceivedRead > 0 || lfSentUnread > 0 || lfSentRead > 0; | ||||
| @ -308,7 +458,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { | ||||
|                 }); | ||||
| 
 | ||||
|                 // Get next messages.
 | ||||
|                 return this.getDiscussion(pagesToLoad, lfReceivedUnread, lfReceivedRead, lfSentUnread, lfSentRead) | ||||
|                 return this.getDiscussionMessages(pagesToLoad, lfReceivedUnread, lfReceivedRead, lfSentUnread, lfSentRead) | ||||
|                         .then((nextMessages) => { | ||||
|                     return result.messages.concat(nextMessages); | ||||
|                 }); | ||||
| @ -330,23 +480,39 @@ export class AddonMessagesDiscussionPage implements OnDestroy { | ||||
| 
 | ||||
|         if (this.messagesProvider.isMarkAllMessagesReadEnabled()) { | ||||
|             let messageUnreadFound = false; | ||||
|             // Mark all messages at a time if one messages is unread.
 | ||||
|             for (const x in this.messages) { | ||||
|                 const message = this.messages[x]; | ||||
|                 // If an unread message is found, mark all messages as read.
 | ||||
|                 if (message.useridfrom != this.currentUserId && message.read == 0) { | ||||
|                     messageUnreadFound = true; | ||||
|                     break; | ||||
| 
 | ||||
|             // Mark all messages at a time if there is any unread message.
 | ||||
|             if (this.groupMessagingEnabled) { | ||||
|                 messageUnreadFound = this.conversation && this.conversation.unreadcount > 0 && this.conversationId > 0; | ||||
|             } else { | ||||
|                 for (const x in this.messages) { | ||||
|                     const message = this.messages[x]; | ||||
|                     // If an unread message is found, mark all messages as read.
 | ||||
|                     if (message.useridfrom != this.currentUserId && message.read == 0) { | ||||
|                         messageUnreadFound = true; | ||||
|                         break; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (messageUnreadFound) { | ||||
|                 this.setUnreadLabelPosition(); | ||||
|                 promises.push(this.messagesProvider.markAllMessagesRead(this.userId).then(() => { | ||||
|                     readChanged = true; | ||||
|                     // Mark all messages as read.
 | ||||
|                     this.messages.forEach((message) => { | ||||
|                         message.read = 1; | ||||
| 
 | ||||
|                 let promise; | ||||
| 
 | ||||
|                 if (this.groupMessagingEnabled) { | ||||
|                     promise = this.messagesProvider.markAllConversationMessagesRead(this.conversationId); | ||||
|                 } else { | ||||
|                     promise = this.messagesProvider.markAllMessagesRead(this.userId).then(() => { | ||||
|                         // Mark all messages as read.
 | ||||
|                         this.messages.forEach((message) => { | ||||
|                             message.read = 1; | ||||
|                         }); | ||||
|                     }); | ||||
|                 } | ||||
| 
 | ||||
|                 promises.push(promise.then(() => { | ||||
|                     readChanged = true; | ||||
|                 })); | ||||
|             } | ||||
|         } else { | ||||
| @ -366,6 +532,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { | ||||
|         Promise.all(promises).finally(() => { | ||||
|             if (readChanged) { | ||||
|                 this.eventsProvider.trigger(AddonMessagesProvider.READ_CHANGED_EVENT, { | ||||
|                     conversationId: this.conversationId, | ||||
|                     userId: this.userId | ||||
|                 }, this.siteId); | ||||
|             } | ||||
| @ -390,6 +557,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { | ||||
|         if (trigger) { | ||||
|             // Update discussions last message.
 | ||||
|             this.eventsProvider.trigger(AddonMessagesProvider.NEW_MESSAGE_EVENT, { | ||||
|                 conversationId: this.conversationId, | ||||
|                 userId: this.userId, | ||||
|                 message: this.lastMessage.text, | ||||
|                 timecreated: this.lastMessage.timecreated | ||||
| @ -411,21 +579,39 @@ export class AddonMessagesDiscussionPage implements OnDestroy { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         let previousMessageRead = false; | ||||
|         if (this.groupMessagingEnabled) { | ||||
|             // Use the unreadcount from the conversation to calculate where should the label be placed.
 | ||||
|             if (this.conversation && this.conversation.unreadcount > 0 && this.messages) { | ||||
|                 // Iterate over messages to find the right message using the unreadcount. Skip offline messages and own messages.
 | ||||
|                 let found = 0; | ||||
| 
 | ||||
|         for (const x in this.messages) { | ||||
|             const message = this.messages[x]; | ||||
|             if (message.useridfrom != this.currentUserId) { | ||||
|                 // Place unread from message label only once.
 | ||||
|                 message.unreadFrom = message.read == 0 && previousMessageRead; | ||||
| 
 | ||||
|                 if (message.unreadFrom) { | ||||
|                     // Save where the label is placed.
 | ||||
|                     this.unreadMessageFrom = parseInt(message.id, 10); | ||||
|                     break; | ||||
|                 for (let i = this.messages.length - 1; i >= 0; i--) { | ||||
|                     const message = this.messages[i]; | ||||
|                     if (!message.pending && message.useridfrom != this.currentUserId) { | ||||
|                         found++; | ||||
|                         if (found == this.conversation.unreadcount) { | ||||
|                             this.unreadMessageFrom = parseInt(message.id, 10); | ||||
|                             break; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } else { | ||||
|             let previousMessageRead = false; | ||||
| 
 | ||||
|                 previousMessageRead = message.read != 0; | ||||
|             for (const x in this.messages) { | ||||
|                 const message = this.messages[x]; | ||||
|                 if (message.useridfrom != this.currentUserId) { | ||||
|                     const unreadFrom = message.read == 0 && previousMessageRead; | ||||
| 
 | ||||
|                     if (unreadFrom) { | ||||
|                         // Save where the label is placed.
 | ||||
|                         this.unreadMessageFrom = parseInt(message.id, 10); | ||||
|                         break; | ||||
|                     } | ||||
| 
 | ||||
|                     previousMessageRead = message.read != 0; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
| @ -450,15 +636,6 @@ export class AddonMessagesDiscussionPage implements OnDestroy { | ||||
|      */ | ||||
|     protected hideUnreadLabel(): void { | ||||
|         if (this.unreadMessageFrom > 0) { | ||||
|             for (const x in this.messages) { | ||||
|                 const message = this.messages[x]; | ||||
|                 if (message.id == this.unreadMessageFrom) { | ||||
|                     message.unreadFrom = false; | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             // Label hidden.
 | ||||
|             this.unreadMessageFrom = -1; | ||||
|         } | ||||
|     } | ||||
| @ -487,10 +664,15 @@ export class AddonMessagesDiscussionPage implements OnDestroy { | ||||
|      * Set a polling to get new messages every certain time. | ||||
|      */ | ||||
|     protected setPolling(): void { | ||||
|         if (this.groupMessagingEnabled && !this.conversationId) { | ||||
|             // Don't have enough data to poll messages.
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         if (!this.polling) { | ||||
|             // Start polling.
 | ||||
|             this.polling = setInterval(() => { | ||||
|                 this.fetchData().catch(() => { | ||||
|                 this.fetchMessages().catch(() => { | ||||
|                     // Ignore errors.
 | ||||
|                 }); | ||||
|             }, AddonMessagesProvider.POLL_INTERVAL); | ||||
| @ -509,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 { | ||||
|         this.utils.copyToClipboard(text); | ||||
|     copyMessage(message: any): void { | ||||
|         this.utils.copyToClipboard(message.smallmessage || message.text || ''); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Function to delete a message. | ||||
|      * | ||||
|      * @param {any} message  Message object to delete. | ||||
|      * @param {number} index Index where the mesasge is to delete it from the view. | ||||
|      * @param {number} index Index where the message is to delete it from the view. | ||||
|      */ | ||||
|     deleteMessage(message: any, index: number): void { | ||||
|         const langKey = message.pending ? 'core.areyousure' : 'addon.messages.deletemessageconfirmation'; | ||||
| @ -534,7 +716,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { | ||||
|                 this.removeMessage(message.hash); | ||||
|                 this.notifyNewMessage(); | ||||
| 
 | ||||
|                 this.fetchData(); // Re-fetch messages to update cached data.
 | ||||
|                 this.fetchMessages(); // Re-fetch messages to update cached data.
 | ||||
|             }).finally(() => { | ||||
|                 modal.dismiss(); | ||||
|             }); | ||||
| @ -554,9 +736,8 @@ export class AddonMessagesDiscussionPage implements OnDestroy { | ||||
|         return this.waitForFetch().finally(() => { | ||||
|             this.pagesLoaded++; | ||||
| 
 | ||||
|             this.fetchData().catch((error) => { | ||||
|             this.fetchMessages().catch((error) => { | ||||
|                 this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading.
 | ||||
| 
 | ||||
|                 this.pagesLoaded--; | ||||
|                 this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingmessages', true); | ||||
|             }).finally(() => { | ||||
| @ -606,6 +787,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { | ||||
| 
 | ||||
|     /** | ||||
|      * Sends a message to the server. | ||||
|      * | ||||
|      * @param {string} text Message text. | ||||
|      */ | ||||
|     sendMessage(text: string): void { | ||||
| @ -624,6 +806,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { | ||||
|             text: text, | ||||
|             timecreated: new Date().getTime() | ||||
|         }; | ||||
|         message.showDate = this.showDate(message, this.messages[this.messages.length - 1]); | ||||
|         this.addMessage(message, false); | ||||
| 
 | ||||
|         this.messagesBeingSent++; | ||||
| @ -631,14 +814,33 @@ export class AddonMessagesDiscussionPage implements OnDestroy { | ||||
|         // If there is an ongoing fetch, wait for it to finish.
 | ||||
|         // Otherwise, if a message is sent while fetching it could disappear until the next fetch.
 | ||||
|         this.waitForFetch().finally(() => { | ||||
|             this.messagesProvider.sendMessage(this.userId, text).then((data) => { | ||||
|             let promise; | ||||
| 
 | ||||
|             if (this.conversationId) { | ||||
|                 promise = this.messagesProvider.sendMessageToConversation(this.conversation, text); | ||||
|             } else { | ||||
|                 promise = this.messagesProvider.sendMessage(this.userId, text); | ||||
|             } | ||||
| 
 | ||||
|             promise.then((data) => { | ||||
|                 let promise; | ||||
| 
 | ||||
|                 this.messagesBeingSent--; | ||||
| 
 | ||||
|                 if (data.sent) { | ||||
|                     // Message was sent, fetch messages right now.
 | ||||
|                     promise = this.fetchData(); | ||||
|                     if (!this.conversationId && data.message && data.message.conversationid) { | ||||
|                         // Message sent to a new conversation, try to load the conversation.
 | ||||
|                         promise = this.getConversation(data.message.conversationid, this.userId).then(() => { | ||||
|                             // Now fetch messages.
 | ||||
|                             return this.fetchMessages(); | ||||
|                         }).finally(() => { | ||||
|                             // Start polling messages now that the conversation exists.
 | ||||
|                             this.setPolling(); | ||||
|                         }); | ||||
|                     } else { | ||||
|                         // Message was sent, fetch messages right now.
 | ||||
|                         promise = this.fetchMessages(); | ||||
|                     } | ||||
|                 } else { | ||||
|                     promise = Promise.reject(null); | ||||
|                 } | ||||
| @ -681,15 +883,25 @@ export class AddonMessagesDiscussionPage implements OnDestroy { | ||||
|         if (!prevMessage) { | ||||
|             // First message, show it.
 | ||||
|             return true; | ||||
|         } else if (message.pending) { | ||||
|             // If pending, it has no date, not show.
 | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         // Check if day has changed.
 | ||||
|         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. | ||||
|      */ | ||||
| @ -697,6 +909,37 @@ export class AddonMessagesDiscussionPage implements OnDestroy { | ||||
|         this.showDelete = !this.showDelete; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * View info. If it's an individual conversation, go to the user profile. | ||||
|      * If it's a group conversation, view info about the group. | ||||
|      */ | ||||
|     viewInfo(): void { | ||||
|         if (this.isGroup) { | ||||
|             // 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. | ||||
|      */ | ||||
|  | ||||
| @ -8,6 +8,8 @@ | ||||
|             <button ion-button icon-only (click)="gotoSettings($event)" [attr.aria-label]="'addon.messages.messagepreferences' | translate"> | ||||
|                 <ion-icon name="cog"></ion-icon> | ||||
|             </button> | ||||
|             <!-- Add an empty context menu so discussion page can add items in split view, otherwise the menu disappears in some cases. --> | ||||
|             <core-context-menu></core-context-menu> | ||||
|         </ion-buttons> | ||||
|     </ion-navbar> | ||||
| </ion-header> | ||||
| @ -27,13 +29,19 @@ | ||||
|                     <h2>{{ 'core.searchresults' | translate }}</h2> | ||||
|                     <ion-note item-end>{{ search.results.length }}</ion-note> | ||||
|                 </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> | ||||
|                     <ion-avatar item-start> | ||||
|                         <img src="{{result.profileimageurl}}" [alt]="'core.pictureof' | translate:{$a: result.fullname}" core-external-content onError="this.src='assets/img/user-avatar.png'"> | ||||
|                     </ion-avatar> | ||||
|                     <h2><core-format-text [text]="result.fullname"></core-format-text></h2> | ||||
|                     <p><core-format-text clean="true" singleLine="true" [text]="result.lastmessage"></core-format-text></p> | ||||
| 
 | ||||
|                 <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 core-user-avatar [user]="result" [linkProfile]="false" item-start></ion-avatar> | ||||
|                     <h2> | ||||
|                         <p> | ||||
|                             <core-format-text [text]="result.fullname"></core-format-text> | ||||
|                             <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> | ||||
|             </ion-list> | ||||
| 
 | ||||
| @ -49,7 +57,7 @@ | ||||
|                 <div *ngIf="favourites.conversations && favourites.expanded"> | ||||
|                     <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. --> | ||||
|                     <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"> | ||||
|                         <p>{{ 'addon.messages.nofavourites' | translate }}</p> | ||||
|                     </ion-item> | ||||
| @ -63,9 +71,9 @@ | ||||
|                     <!-- @todo: Unread total of group conversations (MDL-63913). --> | ||||
|                 </ion-item-divider> | ||||
|                 <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. --> | ||||
|                     <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"> | ||||
|                         <p>{{ 'addon.messages.nogroupmessages' | translate }}</p> | ||||
|                     </ion-item> | ||||
| @ -80,7 +88,7 @@ | ||||
|                 <div *ngIf="individual.conversations && individual.expanded"> | ||||
|                     <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. --> | ||||
|                     <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"> | ||||
|                         <p>{{ 'addon.messages.nomessages' | translate }}</p> | ||||
|                     </ion-item> | ||||
| @ -94,13 +102,16 @@ | ||||
| </core-split-view> | ||||
| 
 | ||||
| <!-- Template to render a list of conversations. --> | ||||
| <ng-template #conversationsTemplate let-conversations="conversations" let-avatarOptional="avatarOptional"> | ||||
|     <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"> | ||||
|         <ion-avatar item-start *ngIf="conversation.imageurl || !avatarOptional"> | ||||
|             <img src="{{conversation.imageurl}}" [alt]="conversation.name" core-external-content onError="this.src='assets/img/user-avatar.png'"> | ||||
|             <!-- @todo: Display connection status. | ||||
|                 <span *ngIf="conversation.showonlinestatus" class="core-primary-circle" [ngClass]='{"addon-message-contact-online": conversation.isonline}'></span> --> | ||||
| <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 && conversation.id == selectedConversationId) || (conversation.userid && conversation.userid == selectedUserId)" id="addon-message-conversation-{{ conversation.id ? conversation.id : 'user-' + conversation.userid }}"> | ||||
|         <!-- Group conversation image. --> | ||||
|         <ion-avatar item-start *ngIf="conversation.type != typeIndividual && conversation.imageurl"> | ||||
|             <img [src]="conversation.imageurl" [alt]="conversation.name" core-external-content> | ||||
|         </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> | ||||
|             <p> | ||||
|                 <core-format-text [text]="conversation.name"></core-format-text> | ||||
|  | ||||
| @ -13,16 +13,18 @@ | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| 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 { CoreEventsProvider } from '@providers/events'; | ||||
| import { CoreSitesProvider } from '@providers/sites'; | ||||
| import { AddonMessagesProvider } from '../../providers/messages'; | ||||
| import { AddonMessagesOfflineProvider } from '../../providers/messages-offline'; | ||||
| import { CoreDomUtilsProvider } from '@providers/utils/dom'; | ||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||
| import { CoreAppProvider } from '@providers/app'; | ||||
| import { AddonPushNotificationsDelegate } from '@addon/pushnotifications/providers/delegate'; | ||||
| import { CoreSplitViewComponent } from '@components/split-view/split-view'; | ||||
| import { CoreUserProvider } from '@core/user/providers/user'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that displays the list of conversations, including group conversations. | ||||
| @ -34,10 +36,12 @@ import { CoreSplitViewComponent } from '@components/split-view/split-view'; | ||||
| }) | ||||
| export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { | ||||
|     @ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent; | ||||
|     @ViewChild(Content) content: Content; | ||||
| 
 | ||||
|     loaded = false; | ||||
|     loadingMessage: string; | ||||
|     selectedConversation: number; | ||||
|     selectedConversationId: number; | ||||
|     selectedUserId: number; | ||||
|     search = { | ||||
|         enabled: false, | ||||
|         showResults: false, | ||||
| @ -57,6 +61,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { | ||||
|         type: AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, | ||||
|         favourites: false | ||||
|     }; | ||||
|     typeIndividual = AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_INDIVIDUAL; | ||||
| 
 | ||||
|     protected loadingString: string; | ||||
|     protected siteId: string; | ||||
| @ -67,11 +72,13 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { | ||||
|     protected appResumeSubscription: any; | ||||
|     protected readChangedObserver: any; | ||||
|     protected cronObserver: any; | ||||
|     protected openConversationObserver: any; | ||||
| 
 | ||||
|     constructor(private eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, translate: TranslateService, | ||||
|             private messagesProvider: AddonMessagesProvider, private domUtils: CoreDomUtilsProvider, navParams: NavParams, | ||||
|             private appProvider: CoreAppProvider, platform: Platform, utils: CoreUtilsProvider, | ||||
|             pushNotificationsDelegate: AddonPushNotificationsDelegate) { | ||||
|             pushNotificationsDelegate: AddonPushNotificationsDelegate, private messagesOffline: AddonMessagesOfflineProvider, | ||||
|             private userProvider: CoreUserProvider) { | ||||
| 
 | ||||
|         this.search.loading =  translate.instant('core.searching'); | ||||
|         this.loadingString = translate.instant('core.loading'); | ||||
| @ -81,20 +88,29 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { | ||||
| 
 | ||||
|         // Update conversations when new message is received.
 | ||||
|         this.newMessagesObserver = eventsProvider.on(AddonMessagesProvider.NEW_MESSAGE_EVENT, (data) => { | ||||
|             if (data.conversationId) { | ||||
|                 // Search the conversation to update.
 | ||||
|                 const conversation = this.findConversation(data.conversationId); | ||||
|             // Search the conversation to update.
 | ||||
|             const conversation = this.findConversation(data.conversationId, data.userId); | ||||
| 
 | ||||
|                 if (typeof conversation == 'undefined') { | ||||
|                     // Probably a new conversation, refresh the list.
 | ||||
|                     this.loaded = false; | ||||
|                     this.refreshData().finally(() => { | ||||
|                         this.loaded = true; | ||||
|                     }); | ||||
|                 } else { | ||||
|                     // An existing conversation has a new message, update the last message.
 | ||||
|                     conversation.lastmessage = data.message; | ||||
|                     conversation.lastmessagedate = data.timecreated; | ||||
|             if (typeof conversation == 'undefined') { | ||||
|                 // Probably a new conversation, refresh the list.
 | ||||
|                 this.loaded = false; | ||||
|                 this.refreshData().finally(() => { | ||||
|                     this.loaded = true; | ||||
|                 }); | ||||
|             } else if (conversation.lastmessage != data.message || conversation.lastmessagedate != data.timecreated / 1000) { | ||||
|                 const isNewer = data.timecreated / 1000 > conversation.lastmessagedate; | ||||
| 
 | ||||
|                 // 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); | ||||
| @ -119,6 +135,13 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { | ||||
|             this.refreshData(); | ||||
|         }, 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.
 | ||||
|         this.appResumeSubscription = platform.resume.subscribe(() => { | ||||
|             if (!this.loaded) { | ||||
| @ -145,7 +168,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { | ||||
|     ngOnInit(): void { | ||||
|         if (this.conversationId) { | ||||
|             // There is a discussion to load, open the discussion in a new state.
 | ||||
|             this.gotoConversation(this.conversationId, null); | ||||
|             this.gotoConversation(this.conversationId); | ||||
|         } | ||||
| 
 | ||||
|         this.fetchData().then(() => { | ||||
| @ -162,7 +185,7 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { | ||||
|                 } | ||||
| 
 | ||||
|                 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.
 | ||||
|         const promises = []; | ||||
|         let offlineMessages; | ||||
| 
 | ||||
|         promises.push(this.fetchDataForOption(this.favourites, false)); | ||||
|         promises.push(this.fetchDataForOption(this.group, false)); | ||||
|         promises.push(this.fetchDataForOption(this.individual, false)); | ||||
|         promises.push(this.messagesOffline.getAllMessages().then((messages) => { | ||||
|             offlineMessages = messages; | ||||
|         })); | ||||
| 
 | ||||
|         return Promise.all(promises).then(() => { | ||||
|             return this.loadOfflineMessages(offlineMessages); | ||||
|         }).then(() => { | ||||
|             if (offlineMessages && offlineMessages.length) { | ||||
|                 // Sort the conversations, the offline messages could affect the order.
 | ||||
|                 this.favourites.conversations = this.messagesProvider.sortConversations(this.favourites.conversations); | ||||
|                 this.group.conversations = this.messagesProvider.sortConversations(this.group.conversations); | ||||
|                 this.individual.conversations = this.messagesProvider.sortConversations(this.individual.conversations); | ||||
|             } | ||||
| 
 | ||||
|             if (typeof this.favourites.expanded == 'undefined') { | ||||
|                 // The expanded status hasn't been initialized. Do it now.
 | ||||
|                 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.group.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. | ||||
|      */ | ||||
|     fetchDataForOption(option: any, loadingMore?: boolean): Promise<void> { | ||||
|         option.loadMoreError = false; | ||||
| 
 | ||||
|         const limitFrom = loadingMore ? option.conversations.length : 0; | ||||
| 
 | ||||
|         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. | ||||
|      * | ||||
|      * @param {number} conversationId The conversation ID to search. | ||||
|      * @param {number} userId User ID to search (if no conversationId). | ||||
|      * @return {any} Conversation. | ||||
|      */ | ||||
|     protected findConversation(conversationId: number): any { | ||||
|         const conversations = (this.favourites.conversations || []).concat(this.group.conversations || []) | ||||
|                 .concat(this.individual.conversations || []); | ||||
|     protected findConversation(conversationId: number, userId?: number): any { | ||||
|         if (conversationId) { | ||||
|             const conversations = (this.favourites.conversations || []).concat(this.group.conversations || []) | ||||
|                     .concat(this.individual.conversations || []); | ||||
| 
 | ||||
|         return conversations.find((conv) => { | ||||
|             return conv.id == conversationId; | ||||
|         }); | ||||
|             return conversations.find((conv) => { | ||||
|                 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. | ||||
|      * | ||||
|      * @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 {boolean} [scrollToConversation] Whether to scroll to the conversation. | ||||
|      */ | ||||
|     gotoConversation(conversationId: number, userId: number, messageId?: number): void { | ||||
|         this.selectedConversation = conversationId; | ||||
|     gotoConversation(conversationId: number, userId?: number, messageId?: number, scrollToConversation?: boolean): void { | ||||
|         this.selectedConversationId = conversationId; | ||||
|         this.selectedUserId = userId; | ||||
| 
 | ||||
|         const params = { | ||||
|             conversationId: conversationId, | ||||
|             userId: userId | ||||
|         }; | ||||
|         if (messageId) { | ||||
|             params['message'] = messageId; | ||||
|         } | ||||
|         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> { | ||||
|         return this.fetchDataForOption(option, true).catch((error) => { | ||||
|             this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingdiscussions', true); | ||||
|             option.canLoadMore = false; | ||||
|             option.loadMoreError = true; | ||||
|         }).finally(() => { | ||||
|             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. | ||||
|      * | ||||
| @ -313,14 +491,23 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { | ||||
|             // Already expanded, close it.
 | ||||
|             option.expanded = false; | ||||
|         } else { | ||||
|             // Collapse all and expand the clicked one.
 | ||||
|             this.favourites.expanded = false; | ||||
|             this.group.expanded = false; | ||||
|             this.individual.expanded = false; | ||||
|             option.expanded = true; | ||||
|             this.expandOption(option); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 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. | ||||
|      */ | ||||
| @ -358,10 +545,11 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { | ||||
|      * Page destroyed. | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         this.newMessagesObserver && this.newMessagesObserver.unsubscribe(); | ||||
|         this.newMessagesObserver && this.newMessagesObserver.off(); | ||||
|         this.appResumeSubscription && this.appResumeSubscription.unsubscribe(); | ||||
|         this.pushObserver && this.pushObserver.unsubscribe(); | ||||
|         this.readChangedObserver && this.readChangedObserver.off(); | ||||
|         this.cronObserver && this.cronObserver.off(); | ||||
|         this.openConversationObserver && this.openConversationObserver.off(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -16,6 +16,7 @@ import { Injectable } from '@angular/core'; | ||||
| import { CoreLoggerProvider } from '@providers/logger'; | ||||
| import { CoreSitesProvider } from '@providers/sites'; | ||||
| import { CoreAppProvider } from '@providers/app'; | ||||
| import { CoreTextUtilsProvider } from '@providers/utils/text'; | ||||
| 
 | ||||
| /** | ||||
|  * Service to handle Offline messages. | ||||
| @ -26,7 +27,8 @@ export class AddonMessagesOfflineProvider { | ||||
|     protected logger; | ||||
| 
 | ||||
|     // Variables for database.
 | ||||
|     static MESSAGES_TABLE = 'addon_messages_offline_messages'; | ||||
|     static MESSAGES_TABLE = 'addon_messages_offline_messages'; // When group messaging isn't available or a new conversation starts.
 | ||||
|     static CONVERSATION_MESSAGES_TABLE = 'addon_messages_offline_conversation_messages'; // Conversation messages.
 | ||||
|     protected tablesSchema = [ | ||||
|         { | ||||
|             name: AddonMessagesOfflineProvider.MESSAGES_TABLE, | ||||
| @ -53,14 +55,60 @@ export class AddonMessagesOfflineProvider { | ||||
|                 } | ||||
|             ], | ||||
|             primaryKeys: ['touserid', 'smallmessage', 'timecreated'] | ||||
|         }, | ||||
|         { | ||||
|             name: AddonMessagesOfflineProvider.CONVERSATION_MESSAGES_TABLE, | ||||
|             columns: [ | ||||
|                 { | ||||
|                     name: 'conversationid', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'text', | ||||
|                     type: 'TEXT' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'timecreated', | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'deviceoffline', // If message was stored because device was offline.
 | ||||
|                     type: 'INTEGER' | ||||
|                 }, | ||||
|                 { | ||||
|                     name: 'conversation', // Data about the conversation.
 | ||||
|                     type: 'TEXT' | ||||
|                 } | ||||
|             ], | ||||
|             primaryKeys: ['conversationid', 'text', 'timecreated'] | ||||
|         } | ||||
|     ]; | ||||
| 
 | ||||
|     constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private appProvider: CoreAppProvider) { | ||||
|     constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private appProvider: CoreAppProvider, | ||||
|             private textUtils: CoreTextUtilsProvider) { | ||||
|         this.logger = logger.getInstance('AddonMessagesOfflineProvider'); | ||||
|         this.sitesProvider.createTablesFromSchema(this.tablesSchema); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 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. | ||||
|      * | ||||
| @ -84,24 +132,20 @@ export class AddonMessagesOfflineProvider { | ||||
|      * Get all messages where deviceoffline is set to 1. | ||||
|      * | ||||
|      * @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 site.getDb().getRecords(AddonMessagesOfflineProvider.MESSAGES_TABLE, {deviceoffline: 1}); | ||||
|         }); | ||||
|     } | ||||
|             const promises = []; | ||||
| 
 | ||||
|     /** | ||||
|      * Get offline messages to send to a certain user. | ||||
|      * | ||||
|      * @param  {number} toUserId       User ID to get messages to. | ||||
|      * @param  {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<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}); | ||||
|             promises.push(site.getDb().getRecords(AddonMessagesOfflineProvider.MESSAGES_TABLE, {deviceoffline: 1})); | ||||
|             promises.push(site.getDb().getRecords(AddonMessagesOfflineProvider.CONVERSATION_MESSAGES_TABLE, {deviceoffline: 1})); | ||||
| 
 | ||||
|             return Promise.all(promises).then((results) => { | ||||
|                 results[1] = this.parseConversationMessages(results[1]); | ||||
| 
 | ||||
|                 return results[0].concat(results[1]); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
| @ -113,7 +157,59 @@ export class AddonMessagesOfflineProvider { | ||||
|      */ | ||||
|     getAllMessages(siteId?: string): Promise<any> { | ||||
|         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  {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 !!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. | ||||
|      * | ||||
| @ -169,7 +315,13 @@ export class AddonMessagesOfflineProvider { | ||||
|                 data = { deviceoffline: value ? 1 : 0 }; | ||||
| 
 | ||||
|             messages.forEach((message) => { | ||||
|                 promises.push(db.insertRecord(AddonMessagesOfflineProvider.MESSAGES_TABLE, data)); | ||||
|                 if (message.conversationid) { | ||||
|                     promises.push(db.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); | ||||
|  | ||||
| @ -33,6 +33,7 @@ export class AddonMessagesProvider { | ||||
|     static NEW_MESSAGE_EVENT = 'addon_messages_new_message_event'; | ||||
|     static READ_CHANGED_EVENT = 'addon_messages_read_changed_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 POLL_INTERVAL = 10000; | ||||
|     static PUSH_SIMULATION_COMPONENT = 'AddonMessagesPushSimulation'; | ||||
| @ -115,7 +116,11 @@ export class AddonMessagesProvider { | ||||
|         } | ||||
| 
 | ||||
|         // It's an offline message.
 | ||||
|         return this.messagesOffline.deleteMessage(message.touserid, message.smallmessage, message.timecreated); | ||||
|         if (message.conversationid) { | ||||
|             return this.messagesOffline.deleteConversationMessage(message.conversationid, message.text, message.timecreated); | ||||
|         } else { | ||||
|             return this.messagesOffline.deleteMessage(message.touserid, message.smallmessage, message.timecreated); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -127,18 +132,56 @@ export class AddonMessagesProvider { | ||||
|      * @return {Promise<any>}   Promise resolved when the message has been deleted. | ||||
|      */ | ||||
|     deleteMessageOnline(id: number, read: number, userId?: number): Promise<any> { | ||||
|         userId = userId || this.sitesProvider.getCurrentSiteUserId(); | ||||
|         const params = { | ||||
|         const params: any = { | ||||
|             messageid: id, | ||||
|             userid: userId, | ||||
|             read: read | ||||
|             userid: userId || this.sitesProvider.getCurrentSiteUserId() | ||||
|         }; | ||||
| 
 | ||||
|         if (typeof read != 'undefined') { | ||||
|             params.read = read; | ||||
|         } | ||||
| 
 | ||||
|         return this.sitesProvider.getCurrentSite().write('core_message_delete_message', params).then(() => { | ||||
|             return this.invalidateDiscussionCache(userId); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 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. | ||||
|      * | ||||
| @ -187,6 +230,50 @@ export class AddonMessagesProvider { | ||||
|         return this.ROOT_CACHE_KEY + 'discussions'; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get cache key for get conversations. | ||||
|      * | ||||
|      * @param {number} userId User ID. | ||||
|      * @param {number} conversationId Conversation ID. | ||||
|      * @return {string} Cache key. | ||||
|      */ | ||||
|     protected getCacheKeyForConversation(userId: number, conversationId: number): string { | ||||
|         return this.ROOT_CACHE_KEY + 'conversation:' + userId + ':' + conversationId; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get cache key for get conversations between users. | ||||
|      * | ||||
|      * @param {number} userId User ID. | ||||
|      * @param {number} otherUserId Other user ID. | ||||
|      * @return {string} Cache key. | ||||
|      */ | ||||
|     protected getCacheKeyForConversationBetweenUsers(userId: number, otherUserId: number): string { | ||||
|         return this.ROOT_CACHE_KEY + 'conversationBetweenUsers:' + userId + ':' + otherUserId; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get cache key for get conversation 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. | ||||
|      * | ||||
| @ -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. | ||||
|      * 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 {number} [userId] User ID. If not defined, current user in the site. | ||||
|      * @return {Promise<any>} Promise resolved with the conversations. | ||||
|      * @since 3.6 | ||||
|      */ | ||||
|     getConversations(type?: number, favourites?: boolean, limitFrom: number = 0, siteId?: string, userId?: number) | ||||
|             : Promise<{conversations: any[], canLoadMore: boolean}> { | ||||
| @ -333,32 +645,8 @@ export class AddonMessagesProvider { | ||||
| 
 | ||||
|             return site.read('core_message_get_conversations', params, preSets).then((response) => { | ||||
|                 // Format the conversations, adding some calculated fields.
 | ||||
|                 const conversations = response.conversations.map((conversation) => { | ||||
|                     const numMessages = conversation.messages.length, | ||||
|                         lastMessage = numMessages ? conversation.messages[numMessages - 1] : null; | ||||
| 
 | ||||
|                     conversation.lastmessage = lastMessage ? lastMessage.text : null; | ||||
|                     conversation.lastmessagedate = lastMessage ? lastMessage.timecreated : null; | ||||
|                     conversation.sentfromcurrentuser = lastMessage ? lastMessage.useridfrom == userId : null; | ||||
| 
 | ||||
|                     if (conversation.type == AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) { | ||||
|                         const otherUser = conversation.members.reduce((carry, member) => { | ||||
|                             if (!carry && member.id != userId) { | ||||
|                                 carry = member; | ||||
|                             } | ||||
| 
 | ||||
|                             return carry; | ||||
|                         }, null); | ||||
| 
 | ||||
|                         conversation.name = conversation.name ? conversation.name : otherUser.fullname; | ||||
|                         conversation.imageurl = conversation.imageurl ? conversation.imageurl : otherUser.profileimageurl; | ||||
|                         conversation.userid = otherUser.id; | ||||
|                         conversation.showonlinestatus = otherUser.showonlinestatus; | ||||
|                         conversation.isonline = otherUser.isonline; | ||||
|                         conversation.isblocked = otherUser.isblocked; | ||||
|                     } | ||||
| 
 | ||||
|                     return conversation; | ||||
|                 const conversations = response.conversations.slice(0, this.LIMIT_MESSAGES).map((conversation) => { | ||||
|                     return this.formatConversation(conversation, userId); | ||||
|                 }); | ||||
| 
 | ||||
|                 return { | ||||
| @ -813,7 +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 {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); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 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. | ||||
|      * | ||||
| @ -1144,8 +1515,11 @@ export class AddonMessagesProvider { | ||||
|             } | ||||
| 
 | ||||
|             // Online and no messages stored. Send it to server.
 | ||||
|             return this.sendMessageOnline(toUserId, message).then(() => { | ||||
|                 return { sent: true }; | ||||
|             return this.sendMessageOnline(toUserId, 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.
 | ||||
| @ -1185,6 +1559,8 @@ export class AddonMessagesProvider { | ||||
| 
 | ||||
|             return this.invalidateDiscussionCache(toUserId, siteId).catch(() => { | ||||
|                 // 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. | ||||
|      * | ||||
|      * @param {any} messages Array of messages containing the key 'timecreated'. | ||||
|      * @return {any} Messages sorted with most recent last. | ||||
|      * @param {any[]} messages Array of messages containing the key 'timecreated'. | ||||
|      * @return {any[]} Messages sorted with most recent last. | ||||
|      */ | ||||
|     sortMessages(messages: any): any { | ||||
|     sortMessages(messages: any[]): any[] { | ||||
|         return messages.sort((a, b) => { | ||||
|             // Pending messages last.
 | ||||
|             if (a.pending && !b.pending) { | ||||
| @ -1238,17 +1744,17 @@ export class AddonMessagesProvider { | ||||
|     /** | ||||
|      * Store the last received message if it's newer than the last stored. | ||||
|      * | ||||
|      * @param  {number} userIdFrom ID of the useridfrom retrieved, 0 for all users. | ||||
|      * @param  {number} convIdOrUserIdFrom Conversation ID (3.6+) or ID of the useridfrom retrieved (3.5-), 0 for all users. | ||||
|      * @param  {any} message       Last message received. | ||||
|      * @param  {string} [siteId]   Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>}      Promise resolved when done. | ||||
|      */ | ||||
|     protected storeLastReceivedMessageIfNeeded(userIdFrom: number, message: any, siteId?: string): Promise<any> { | ||||
|     protected storeLastReceivedMessageIfNeeded(convIdOrUserIdFrom: number, message: any, siteId?: string): Promise<any> { | ||||
|         const component = AddonMessagesProvider.PUSH_SIMULATION_COMPONENT; | ||||
| 
 | ||||
|         // Get the last received message.
 | ||||
|         return this.emulatorHelper.getLastReceivedNotification(component, siteId).then((lastMessage) => { | ||||
|             if (userIdFrom > 0 && (!message || !lastMessage)) { | ||||
|             if (convIdOrUserIdFrom > 0 && (!message || !lastMessage)) { | ||||
|                 // Seeing a single discussion. No received message or cannot know if it really is the last received message. Stop.
 | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
| @ -42,6 +42,21 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider { | ||||
|         super('AddonMessagesSync', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the ID of a discussion sync. | ||||
|      * | ||||
|      * @param {number} conversationId Conversation ID. | ||||
|      * @param {number} userId User ID talking to (if no conversation ID). | ||||
|      * @return {string} Sync ID. | ||||
|      */ | ||||
|     protected getSyncId(conversationId: number, userId: number): string { | ||||
|         if (conversationId) { | ||||
|             return 'conversationid:' + conversationId; | ||||
|         } else { | ||||
|             return 'userid:' + userId; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Try to synchronize all the discussions in a certain site or in all sites. | ||||
|      * | ||||
| @ -70,22 +85,39 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider { | ||||
| 
 | ||||
|         return promise.then((messages) => { | ||||
|             const userIds = [], | ||||
|                 conversationIds = [], | ||||
|                 promises = []; | ||||
| 
 | ||||
|             // Get all the discussions to be synced.
 | ||||
|             // Get all the conversations to be synced.
 | ||||
|             messages.forEach((message) => { | ||||
|                 if (userIds.indexOf(message.touserid) == -1) { | ||||
|                 if (message.conversationid) { | ||||
|                     if (conversationIds.indexOf(message.conversationid) == -1) { | ||||
|                         conversationIds.push(message.conversationid); | ||||
|                     } | ||||
|                 } else if (userIds.indexOf(message.touserid) == -1) { | ||||
|                     userIds.push(message.touserid); | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
|             // Sync all discussions.
 | ||||
|             userIds.forEach((userId) => { | ||||
|                 promises.push(this.syncDiscussion(userId, siteId).then((warnings) => { | ||||
|             // Sync all conversations.
 | ||||
|             conversationIds.forEach((conversationId) => { | ||||
|                 promises.push(this.syncDiscussion(conversationId, undefined, siteId).then((warnings) => { | ||||
|                     if (typeof warnings != 'undefined') { | ||||
|                         // Sync successful, send event.
 | ||||
|                         this.eventsProvider.trigger(AddonMessagesSyncProvider.AUTO_SYNCED, { | ||||
|                             userid: userId, | ||||
|                             conversationId: conversationId, | ||||
|                             warnings: warnings | ||||
|                         }, siteId); | ||||
|                     } | ||||
|                 })); | ||||
|             }); | ||||
| 
 | ||||
|             userIds.forEach((userId) => { | ||||
|                 promises.push(this.syncDiscussion(undefined, userId, siteId).then((warnings) => { | ||||
|                     if (typeof warnings != 'undefined') { | ||||
|                         // Sync successful, send event.
 | ||||
|                         this.eventsProvider.trigger(AddonMessagesSyncProvider.AUTO_SYNCED, { | ||||
|                             userId: userId, | ||||
|                             warnings: warnings | ||||
|                         }, siteId); | ||||
|                     } | ||||
| @ -99,24 +131,39 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider { | ||||
|     /** | ||||
|      * Synchronize a discussion. | ||||
|      * | ||||
|      * @param  {number} userId   User ID of the discussion. | ||||
|      * @param  {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<any>}    Promise resolved if sync is successful, rejected otherwise. | ||||
|      * @param {number} conversationId Conversation ID. | ||||
|      * @param {number} userId User ID talking to (if no conversation ID). | ||||
|      * @return {Promise<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(); | ||||
| 
 | ||||
|         if (this.isSyncing(userId, siteId)) { | ||||
|             // There's already a sync ongoing for this SCORM, return the promise.
 | ||||
|             return this.getOngoingSync(userId, siteId); | ||||
|         const syncId = this.getSyncId(conversationId, userId), | ||||
|             groupMessagingEnabled = this.messagesProvider.isGroupMessagingEnabled(); | ||||
| 
 | ||||
|         if (this.isSyncing(syncId, siteId)) { | ||||
|             // There's already a sync ongoing for this conversation, return the promise.
 | ||||
|             return this.getOngoingSync(syncId, siteId); | ||||
|         } | ||||
| 
 | ||||
|         const warnings = []; | ||||
| 
 | ||||
|         this.logger.debug(`Try to sync discussion with user '${userId}'`); | ||||
|         if (conversationId) { | ||||
|             this.logger.debug(`Try to sync conversation '${conversationId}'`); | ||||
|         } else { | ||||
|             this.logger.debug(`Try to sync discussion with user '${userId}'`); | ||||
|         } | ||||
| 
 | ||||
|         // Get offline messages to be sent.
 | ||||
|         const syncPromise = this.messagesOffline.getMessages(userId, siteId).then((messages) => { | ||||
|         let syncPromise; | ||||
| 
 | ||||
|         if (conversationId) { | ||||
|             syncPromise = this.messagesOffline.getConversationMessages(conversationId, siteId); | ||||
|         } else { | ||||
|             syncPromise = this.messagesOffline.getMessages(userId, siteId); | ||||
|         } | ||||
| 
 | ||||
|         syncPromise = syncPromise.then((messages) => { | ||||
|             if (!messages.length) { | ||||
|                 // Nothing to sync.
 | ||||
|                 return []; | ||||
| @ -134,12 +181,19 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider { | ||||
|             messages = this.messagesProvider.sortMessages(messages); | ||||
| 
 | ||||
|             // Send the messages.
 | ||||
|             // We don't use AddonMessagesProvider#sendMessagesOnline because there's a problem with display order.
 | ||||
|             // @todo Use AddonMessagesProvider#sendMessagesOnline once the display order is fixed.
 | ||||
|             // Send them 1 by 1 to simulate web's behaviour and to make sure we know which message has failed.
 | ||||
|             messages.forEach((message, index) => { | ||||
|                 // Chain message sending. If 1 message fails to be sent we'll stop sending.
 | ||||
|                 promise = promise.then(() => { | ||||
|                     return this.messagesProvider.sendMessageOnline(userId, message.smallmessage, siteId).catch((error) => { | ||||
|                     let subPromise; | ||||
| 
 | ||||
|                     if (conversationId) { | ||||
|                         subPromise = this.messagesProvider.sendMessageToConversationOnline(conversationId, message.text, siteId); | ||||
|                     } else { | ||||
|                         subPromise = this.messagesProvider.sendMessageOnline(userId, message.smallmessage, siteId); | ||||
|                     } | ||||
| 
 | ||||
|                     return subPromise.catch((error) => { | ||||
|                         if (this.utils.isWebServiceError(error)) { | ||||
|                             // Error returned by WS. Store the error to show a warning but keep sending messages.
 | ||||
|                             if (errors.indexOf(error) == -1) { | ||||
| @ -158,22 +212,63 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider { | ||||
|                         return Promise.reject(error); | ||||
|                     }).then(() => { | ||||
|                         // Message was sent, delete it from local DB.
 | ||||
|                         return this.messagesOffline.deleteMessage(userId, message.smallmessage, message.timecreated, siteId); | ||||
|                         if (conversationId) { | ||||
|                             return this.messagesOffline.deleteConversationMessage(conversationId, message.text, | ||||
|                                     message.timecreated, siteId); | ||||
|                         } else { | ||||
|                             return this.messagesOffline.deleteMessage(userId, message.smallmessage, message.timecreated, siteId); | ||||
|                         } | ||||
|                     }).then(() => { | ||||
|                         // All done. Wait 1 second to ensure timecreated of messages is different.
 | ||||
|                         if (index < messages.length - 1) { | ||||
|                             return setTimeout(() => {return; }, 1000); | ||||
|                         // In some Moodle versions, wait 1 second to make sure timecreated is different.
 | ||||
|                         // This is because there was a bug where messages with the same timecreated had a wrong order.
 | ||||
|                         if (!groupMessagingEnabled && index < messages.length - 1) { | ||||
|                             return new Promise((resolve, reject): any => { | ||||
|                                 setTimeout(resolve, 1000); | ||||
|                             }); | ||||
|                         } | ||||
|                     }); | ||||
|                 }); | ||||
|             }); | ||||
| 
 | ||||
|             return promise.then(() => { | ||||
|                 return errors; | ||||
|             }); | ||||
|             return promise; | ||||
|         }).then((errors) => { | ||||
|             if (errors && errors.length) { | ||||
|                 // At least an error occurred, get user full name and add errors to warnings array.
 | ||||
|             return this.handleSyncErrors(conversationId, userId, errors, warnings); | ||||
|         }).then(() => { | ||||
|             // All done, return the warnings.
 | ||||
|             return warnings; | ||||
|         }); | ||||
| 
 | ||||
|         return this.addOngoingSync(syncId, syncPromise, siteId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Handle sync errors. | ||||
|      * | ||||
|      * @param {number} conversationId Conversation ID. | ||||
|      * @param {number} userId User ID talking to (if no conversation ID). | ||||
|      * @param {any[]} errors List of errors. | ||||
|      * @param {any[]} warnings Array where to place the warnings. | ||||
|      * @return {Promise<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(() => { | ||||
|                     // Ignore errors.
 | ||||
|                     return {}; | ||||
| @ -181,16 +276,26 @@ export class AddonMessagesSyncProvider extends CoreSyncBaseProvider { | ||||
|                     errors.forEach((error) => { | ||||
|                         warnings.push(this.translate.instant('addon.messages.warningmessagenotsent', { | ||||
|                             user: user.fullname ? user.fullname : userId, | ||||
|                             error: error | ||||
|                             error: this.textUtils.getErrorMessageFromError(error) | ||||
|                         })); | ||||
|                     }); | ||||
|                 }); | ||||
|             } | ||||
|         }).then(() => { | ||||
|             // All done, return the warnings.
 | ||||
|             return warnings; | ||||
|         }); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|         return this.addOngoingSync(userId, syncPromise, siteId); | ||||
|     /** | ||||
|      * If there's an ongoing sync for a certain conversation, wait for it to end. | ||||
|      * If there's no sync ongoing the promise will be resolved right away. | ||||
|      * | ||||
|      * @param {number} conversationId Conversation ID. | ||||
|      * @param {number} userId User ID talking to (if no conversation ID). | ||||
|      * @param {string} [siteId] Site ID. If not defined, current site. | ||||
|      * @return {Promise<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); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -165,7 +165,9 @@ | ||||
|     "addon.messages.errorwhileretrievingcontacts": "Error while retrieving contacts from the server.", | ||||
|     "addon.messages.errorwhileretrievingdiscussions": "Error while retrieving discussions from the server.", | ||||
|     "addon.messages.errorwhileretrievingmessages": "Error while retrieving messages from the server.", | ||||
|     "addon.messages.groupinfo": "Group info", | ||||
|     "addon.messages.groupmessages": "Group messages", | ||||
|     "addon.messages.info": "Info", | ||||
|     "addon.messages.message": "Message", | ||||
|     "addon.messages.messagenotsent": "The message was not sent. Please try again later.", | ||||
|     "addon.messages.messagepreferences": "Message preferences", | ||||
| @ -176,8 +178,10 @@ | ||||
|     "addon.messages.nogroupmessages": "No group messages", | ||||
|     "addon.messages.nomessages": "No messages", | ||||
|     "addon.messages.nousersfound": "No users found", | ||||
|     "addon.messages.numparticipants": "{{$a}} participants", | ||||
|     "addon.messages.removecontact": "Remove contact", | ||||
|     "addon.messages.removecontactconfirm": "Contact will be removed from your contacts list.", | ||||
|     "addon.messages.showdeletemessages": "Show delete messages", | ||||
|     "addon.messages.type_blocked": "Blocked", | ||||
|     "addon.messages.type_offline": "Offline", | ||||
|     "addon.messages.type_online": "Online", | ||||
| @ -185,6 +189,7 @@ | ||||
|     "addon.messages.type_strangers": "Others", | ||||
|     "addon.messages.unblockuser": "Unblock user", | ||||
|     "addon.messages.unblockuserconfirm": "Are you sure you want to unblock {{$a}}?", | ||||
|     "addon.messages.warningconversationmessagenotsent": "Couldn't send message(s) to conversation {{conversation}}. {{error}}", | ||||
|     "addon.messages.warningmessagenotsent": "Couldn't send message(s) to user {{user}}. {{error}}", | ||||
|     "addon.messages.you": "You:", | ||||
|     "addon.mod_assign.acceptsubmissionstatement": "Please accept the submission statement.", | ||||
|  | ||||
| @ -15,6 +15,7 @@ | ||||
| import { Component, Input, OnInit, OnChanges, SimpleChange } from '@angular/core'; | ||||
| import { NavController } from 'ionic-angular'; | ||||
| import { CoreSitesProvider } from '@providers/sites'; | ||||
| import { CoreUtilsProvider } from '@providers/utils/utils'; | ||||
| 
 | ||||
| /** | ||||
|  * Component to display a "user avatar". | ||||
| @ -41,7 +42,7 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges { | ||||
|     protected myUser = false; | ||||
|     protected currentUserId: number; | ||||
| 
 | ||||
|     constructor(private navCtrl: NavController, private sitesProvider: CoreSitesProvider) { | ||||
|     constructor(private navCtrl: NavController, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider) { | ||||
|         this.currentUserId = this.sitesProvider.getCurrentSiteUserId(); | ||||
|     } | ||||
| 
 | ||||
| @ -75,7 +76,7 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges { | ||||
| 
 | ||||
|         this.fullname = this.fullname || (this.user && (this.user.fullname || this.user.userfullname)); | ||||
| 
 | ||||
|         this.userId = this.userId || (this.user && this.user.userid); | ||||
|         this.userId = this.userId || (this.user && (this.user.userid || this.user.id)); | ||||
|         this.courseId = this.courseId || (this.user && this.user.courseid); | ||||
| 
 | ||||
|         // If not available we cannot ensure the avatar is from the current user.
 | ||||
| @ -89,9 +90,18 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges { | ||||
|      * @return boolean | ||||
|      */ | ||||
|     isOnline(): boolean { | ||||
|         const time = new Date().getTime() - this.timetoshowusers; | ||||
|         if (this.myUser || this.utils.isFalseOrZero(this.user.isonline)) { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         return !this.myUser && ((this.user.lastaccess && this.user.lastaccess * 1000 >= time) || this.user.isonline); | ||||
|         if (this.user.lastaccess) { | ||||
|             // If the time has passed, don't show the online status.
 | ||||
|             const time = new Date().getTime() - this.timetoshowusers; | ||||
| 
 | ||||
|             return this.user.lastaccess * 1000 >= time; | ||||
|         } else { | ||||
|             return this.user.isonline; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user