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;
|
||||
}
|
||||
}
|
|
@ -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,26 +156,37 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
|
|||
ionViewDidLoad(): void {
|
||||
// Disable the profile button if we're already coming from a profile.
|
||||
const backViewPage = this.navCtrl.getPrevious() && this.navCtrl.getPrevious().component.name;
|
||||
this.showProfileLink = !backViewPage || backViewPage !== 'CoreUserProfilePage';
|
||||
this.showInfo = !backViewPage || backViewPage !== 'CoreUserProfilePage';
|
||||
|
||||
if (!this.groupMessagingEnabled && this.userId) {
|
||||
// Get the user profile to retrieve the user fullname and image.
|
||||
this.userProvider.getProfile(this.userId, undefined, true).then((user) => {
|
||||
if (!this.title) {
|
||||
this.title = user.fullname;
|
||||
}
|
||||
this.profileLink = user.profileimageurl;
|
||||
this.conversationImage = user.profileimageurl;
|
||||
});
|
||||
}
|
||||
|
||||
// Synchronize messages if needed.
|
||||
this.messagesSync.syncDiscussion(this.userId).catch(() => {
|
||||
this.messagesSync.syncDiscussion(this.conversationId, this.userId).catch(() => {
|
||||
// Ignore errors.
|
||||
}).then((warnings) => {
|
||||
if (warnings && warnings[0]) {
|
||||
this.domUtils.showErrorModal(warnings[0]);
|
||||
}
|
||||
|
||||
if (this.groupMessagingEnabled) {
|
||||
// Get the conversation ID if it exists and we don't have it yet.
|
||||
return this.getConversation(this.conversationId, this.userId).then((exists) => {
|
||||
if (exists) {
|
||||
// Fetch the messages for the first time.
|
||||
return this.fetchData().then(() => {
|
||||
return this.fetchMessages();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Fetch the messages for the first time.
|
||||
return this.fetchMessages().then(() => {
|
||||
if (!this.title && this.messages.length) {
|
||||
// Didn't receive the fullname via argument. Try to get it from messages.
|
||||
// It's possible that name cannot be resolved when no messages were yet exchanged.
|
||||
|
@ -172,13 +196,15 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
|
|||
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,21 +243,50 @@ 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.
|
||||
if (this.groupMessagingEnabled) {
|
||||
return this.messagesProvider.invalidateConversationMessages(this.conversationId).catch(() => {
|
||||
// Ignore errors.
|
||||
}).then(() => {
|
||||
return this.getConversationMessages(this.pagesLoaded);
|
||||
});
|
||||
} else {
|
||||
return this.messagesProvider.invalidateDiscussionCache(this.userId).catch(() => {
|
||||
// Ignore errors.
|
||||
});
|
||||
}).then(() => {
|
||||
return this.getDiscussion(this.pagesLoaded);
|
||||
return this.getDiscussionMessages(this.pagesLoaded);
|
||||
});
|
||||
}
|
||||
}).then((messages) => {
|
||||
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 Promise.resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we are at the bottom to scroll it after render.
|
||||
|
@ -240,7 +295,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
|
|||
|
||||
if (this.messagesBeingSent > 0) {
|
||||
// Ignore polling due to a race condition.
|
||||
return Promise.reject(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Add new messages to the list and mark the messages that should still be displayed.
|
||||
|
@ -256,13 +311,108 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
|
|||
// Sort the messages.
|
||||
this.messagesProvider.sortMessages(this.messages);
|
||||
|
||||
// Calculate which messages need to display the date or user data.
|
||||
this.messages.forEach((message, index): any => {
|
||||
message.showDate = this.showDate(message, this.messages[index - 1]);
|
||||
message.showUserData = this.showUserData(message, this.messages[index - 1]);
|
||||
});
|
||||
|
||||
// Notify that there can be a new message.
|
||||
this.notifyNewMessage();
|
||||
|
||||
// Mark retrieved messages as read if they are not.
|
||||
this.markMessagesAsRead();
|
||||
}).finally(() => {
|
||||
this.fetching = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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,7 +480,11 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
|
|||
|
||||
if (this.messagesProvider.isMarkAllMessagesReadEnabled()) {
|
||||
let messageUnreadFound = false;
|
||||
// Mark all messages at a time if one messages is unread.
|
||||
|
||||
// Mark all messages at a time if there is any unread message.
|
||||
if (this.groupMessagingEnabled) {
|
||||
messageUnreadFound = this.conversation && this.conversation.unreadcount > 0 && this.conversationId > 0;
|
||||
} else {
|
||||
for (const x in this.messages) {
|
||||
const message = this.messages[x];
|
||||
// If an unread message is found, mark all messages as read.
|
||||
|
@ -339,14 +493,26 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
|
|||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (messageUnreadFound) {
|
||||
this.setUnreadLabelPosition();
|
||||
promises.push(this.messagesProvider.markAllMessagesRead(this.userId).then(() => {
|
||||
readChanged = true;
|
||||
|
||||
let promise;
|
||||
|
||||
if (this.groupMessagingEnabled) {
|
||||
promise = this.messagesProvider.markAllConversationMessagesRead(this.conversationId);
|
||||
} else {
|
||||
promise = this.messagesProvider.markAllMessagesRead(this.userId).then(() => {
|
||||
// Mark all messages as read.
|
||||
this.messages.forEach((message) => {
|
||||
message.read = 1;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
promises.push(promise.then(() => {
|
||||
readChanged = true;
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
|
@ -366,6 +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,15 +579,32 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
|
|||
return;
|
||||
}
|
||||
|
||||
if (this.groupMessagingEnabled) {
|
||||
// Use the unreadcount from the conversation to calculate where should the label be placed.
|
||||
if (this.conversation && this.conversation.unreadcount > 0 && this.messages) {
|
||||
// Iterate over messages to find the right message using the unreadcount. Skip offline messages and own messages.
|
||||
let found = 0;
|
||||
|
||||
for (let i = this.messages.length - 1; i >= 0; i--) {
|
||||
const message = this.messages[i];
|
||||
if (!message.pending && message.useridfrom != this.currentUserId) {
|
||||
found++;
|
||||
if (found == this.conversation.unreadcount) {
|
||||
this.unreadMessageFrom = parseInt(message.id, 10);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let previousMessageRead = false;
|
||||
|
||||
for (const x in this.messages) {
|
||||
const message = this.messages[x];
|
||||
if (message.useridfrom != this.currentUserId) {
|
||||
// Place unread from message label only once.
|
||||
message.unreadFrom = message.read == 0 && previousMessageRead;
|
||||
const unreadFrom = message.read == 0 && previousMessageRead;
|
||||
|
||||
if (message.unreadFrom) {
|
||||
if (unreadFrom) {
|
||||
// Save where the label is placed.
|
||||
this.unreadMessageFrom = parseInt(message.id, 10);
|
||||
break;
|
||||
|
@ -428,6 +613,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy {
|
|||
previousMessageRead = message.read != 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Do not update the message unread from label on next refresh.
|
||||
if (this.unreadMessageFrom == 0) {
|
||||
|
@ -450,15 +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) {
|
||||
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.fetchData();
|
||||
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,9 +88,8 @@ 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);
|
||||
const conversation = this.findConversation(data.conversationId, data.userId);
|
||||
|
||||
if (typeof conversation == 'undefined') {
|
||||
// Probably a new conversation, refresh the list.
|
||||
|
@ -91,10 +97,20 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
|
|||
this.refreshData().finally(() => {
|
||||
this.loaded = true;
|
||||
});
|
||||
} else {
|
||||
} 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;
|
||||
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 {
|
||||
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;
|
||||
});
|
||||
} 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,13 +491,22 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
|
|||
// Already expanded, close it.
|
||||
option.expanded = false;
|
||||
} else {
|
||||
// Collapse all and expand the clicked one.
|
||||
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,8 +116,12 @@ export class AddonMessagesProvider {
|
|||
}
|
||||
|
||||
// It's an offline message.
|
||||
if (message.conversationid) {
|
||||
return this.messagesOffline.deleteConversationMessage(message.conversationid, message.text, message.timecreated);
|
||||
} else {
|
||||
return this.messagesOffline.deleteMessage(message.touserid, message.smallmessage, message.timecreated);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a message from the server.
|
||||
|
@ -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.
|
||||
* @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 = [];
|
||||
|
||||
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.
|
||||
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) => {
|
||||
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) {
|
||||
// At least an error occurred, get user full name and add errors to warnings array.
|
||||
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 {
|
||||
if (this.myUser || this.utils.isFalseOrZero(this.user.isonline)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.user.lastaccess) {
|
||||
// If the time has passed, don't show the online status.
|
||||
const time = new Date().getTime() - this.timetoshowusers;
|
||||
|
||||
return !this.myUser && ((this.user.lastaccess && this.user.lastaccess * 1000 >= time) || this.user.isonline);
|
||||
return this.user.lastaccess * 1000 >= time;
|
||||
} else {
|
||||
return this.user.isonline;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue