746 lines
30 KiB
TypeScript

// (C) Copyright 2015 Moodle Pty Ltd.
//
// 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, OnDestroy, ViewChild, ElementRef } from '@angular/core';
import { IonicPage, Platform, NavController, NavParams, Content } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
import {
AddonMessagesProvider, AddonMessagesConversationFormatted, AddonMessagesConversationMessage
} from '../../providers/messages';
import { AddonMessagesOfflineProvider } from '../../providers/messages-offline';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CorePushNotificationsDelegate } from '@core/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.
*/
@IonicPage({ segment: 'addon-messages-group-conversations' })
@Component({
selector: 'page-addon-messages-group-conversations',
templateUrl: 'group-conversations.html',
})
export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
@ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent;
@ViewChild(Content) content: Content;
@ViewChild('favlist') favListEl: ElementRef;
@ViewChild('grouplist') groupListEl: ElementRef;
@ViewChild('indlist') indListEl: ElementRef;
loaded = false;
loadingMessage: string;
selectedConversationId: number;
selectedUserId: number;
contactRequestsCount = 0;
favourites: AddonMessagesGroupConversationOption = {
type: null,
favourites: true,
count: 0,
unread: 0,
};
group: AddonMessagesGroupConversationOption = {
type: AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_GROUP,
favourites: false,
count: 0,
unread: 0
};
individual: AddonMessagesGroupConversationOption = {
type: AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
favourites: false,
count: 0,
unread: 0
};
typeGroup = AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_GROUP;
currentListEl: HTMLElement;
siteHomeId: number;
protected loadingString: string;
protected siteId: string;
protected currentUserId: number;
protected conversationId: number;
protected discussionUserId: number;
protected newMessagesObserver: any;
protected pushObserver: any;
protected appResumeSubscription: any;
protected readChangedObserver: any;
protected cronObserver: any;
protected openConversationObserver: any;
protected updateConversationListObserver: any;
protected contactRequestsCountObserver: any;
protected memberInfoObserver: any;
constructor(eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, translate: TranslateService,
private messagesProvider: AddonMessagesProvider, private domUtils: CoreDomUtilsProvider, navParams: NavParams,
private navCtrl: NavController, platform: Platform, private utils: CoreUtilsProvider,
pushNotificationsDelegate: CorePushNotificationsDelegate, private messagesOffline: AddonMessagesOfflineProvider,
private userProvider: CoreUserProvider) {
this.loadingString = translate.instant('core.loading');
this.siteId = sitesProvider.getCurrentSiteId();
this.currentUserId = sitesProvider.getCurrentSiteUserId();
// Conversation to load.
this.conversationId = navParams.get('conversationId') || false;
this.discussionUserId = !this.conversationId && (navParams.get('discussionUserId') || false);
this.siteHomeId = sitesProvider.getCurrentSiteHomeId();
// Update conversations when new message is received.
this.newMessagesObserver = eventsProvider.on(AddonMessagesProvider.NEW_MESSAGE_EVENT, (data) => {
// Check if the new message belongs to the option that is currently expanded.
const expandedOption = this.getExpandedOption(),
messageOption = this.getConversationOption(data);
if (expandedOption != messageOption) {
return; // Message doesn't belong to current list, stop.
}
// Search the conversation to update.
const conversation = this.findConversation(data.conversationId, data.userId, expandedOption);
if (typeof conversation == 'undefined') {
// Probably a new conversation, refresh the list.
this.loaded = false;
this.refreshData().finally(() => {
this.loaded = true;
});
} else if (conversation.lastmessage != data.message || conversation.lastmessagedate != data.timecreated / 1000) {
const isNewer = data.timecreated / 1000 > conversation.lastmessagedate;
// An existing conversation has a new message, update the last message.
conversation.lastmessage = data.message;
conversation.lastmessagedate = data.timecreated / 1000;
// Sort the affected list.
const option = this.getConversationOption(conversation);
option.conversations = this.messagesProvider.sortConversations(option.conversations);
if (isNewer) {
// The last message is newer than the previous one, scroll to top to keep viewing the conversation.
this.domUtils.scrollToTop(this.content);
}
}
}, this.siteId);
// Update conversations when a message is read.
this.readChangedObserver = eventsProvider.on(AddonMessagesProvider.READ_CHANGED_EVENT, (data) => {
if (data.conversationId) {
const conversation = this.findConversation(data.conversationId);
if (typeof conversation != 'undefined') {
// A conversation has been read reset counter.
conversation.unreadcount = 0;
// Conversations changed, invalidate them and refresh unread counts.
this.messagesProvider.invalidateConversations(this.siteId);
this.messagesProvider.refreshUnreadConversationCounts(this.siteId);
}
}
}, this.siteId);
// Load a discussion if we receive an event to do so.
this.openConversationObserver = eventsProvider.on(AddonMessagesProvider.OPEN_CONVERSATION_EVENT, (data) => {
if (data.conversationId || data.userId) {
this.gotoConversation(data.conversationId, data.userId);
}
}, this.siteId);
// Refresh the view when the app is resumed.
this.appResumeSubscription = platform.resume.subscribe(() => {
if (!this.loaded) {
return;
}
this.loaded = false;
this.refreshData().finally(() => {
this.loaded = true;
});
});
// Update conversations if we receive an event to do so.
this.updateConversationListObserver = eventsProvider.on(AddonMessagesProvider.UPDATE_CONVERSATION_LIST_EVENT, (data) => {
if (data && data.action == 'mute') {
// If the conversation is displayed, change its muted value.
const expandedOption = this.getExpandedOption();
if (expandedOption && expandedOption.conversations) {
const conversation = this.findConversation(data.conversationId, undefined, expandedOption);
if (conversation) {
conversation.ismuted = data.value;
}
}
return;
}
this.refreshData();
}, this.siteId);
// If a message push notification is received, refresh the view.
this.pushObserver = pushNotificationsDelegate.on('receive').subscribe((notification) => {
// New message received. If it's from current site, refresh the data.
if (utils.isFalseOrZero(notification.notif) && notification.site == this.siteId) {
// Don't refresh unread counts, it's refreshed from the main menu handler in this case.
this.refreshData(null, false);
}
});
// Update unread conversation counts.
this.cronObserver = eventsProvider.on(AddonMessagesProvider.UNREAD_CONVERSATION_COUNTS_EVENT, (data) => {
this.favourites.unread = data.favourites;
this.individual.unread = data.individual + data.self; // Self is only returned if it's not favourite.
this.group.unread = data.group;
}, this.siteId);
// Update the contact requests badge.
this.contactRequestsCountObserver = eventsProvider.on(AddonMessagesProvider.CONTACT_REQUESTS_COUNT_EVENT, (data) => {
this.contactRequestsCount = data.count;
}, this.siteId);
// Update block status of a user.
this.memberInfoObserver = eventsProvider.on(AddonMessagesProvider.MEMBER_INFO_CHANGED_EVENT, (data) => {
if (!data.userBlocked && !data.userUnblocked) {
// The block status has not changed, ignore.
return;
}
const expandedOption = this.getExpandedOption();
if (expandedOption == this.individual || expandedOption == this.favourites) {
if (!expandedOption.conversations || expandedOption.conversations.length <= 0) {
return;
}
const conversation = this.findConversation(undefined, data.userId, expandedOption);
if (conversation) {
conversation.isblocked = data.userBlocked;
}
}
}, this.siteId);
}
/**
* Component loaded.
*/
ngOnInit(): void {
if (this.conversationId || this.discussionUserId) {
// There is a discussion to load, open the discussion in a new state.
this.gotoConversation(this.conversationId, this.discussionUserId);
}
this.fetchData().then(() => {
if (!this.conversationId && !this.discussionUserId && this.splitviewCtrl.isOn()) {
// Load the first conversation.
let conversation;
const expandedOption = this.getExpandedOption();
if (expandedOption) {
conversation = expandedOption.conversations[0];
}
if (conversation) {
this.gotoConversation(conversation.id);
}
}
});
}
/**
* Fetch conversations.
*
* @param refreshUnreadCounts Whether to refresh unread counts.
* @return Promise resolved when done.
*/
protected fetchData(refreshUnreadCounts: boolean = true): Promise<any> {
this.loadingMessage = this.loadingString;
// Load the amount of conversations and contact requests.
const promises = [];
promises.push(this.fetchConversationCounts());
// View updated by the events observers.
promises.push(this.messagesProvider.getContactRequestsCount(this.siteId));
if (refreshUnreadCounts) {
promises.push(this.messagesProvider.refreshUnreadConversationCounts(this.siteId));
}
return Promise.all(promises).then(() => {
if (typeof this.favourites.expanded == 'undefined') {
// The expanded status hasn't been initialized. Do it now.
if (this.conversationId || this.discussionUserId) {
// A certain conversation should be opened.
// We don't know which option it belongs to, so we need to fetch the data for all of them.
const promises = [];
promises.push(this.fetchDataForOption(this.favourites, false));
promises.push(this.fetchDataForOption(this.group, false));
promises.push(this.fetchDataForOption(this.individual, false));
return Promise.all(promises).then(() => {
// All conversations have been loaded, find the one we need to load and expand its option.
const conversation = this.findConversation(this.conversationId, this.discussionUserId);
if (conversation) {
const option = this.getConversationOption(conversation);
return this.expandOption(option);
} else {
// Conversation not found, just open the default option.
this.calculateExpandedStatus();
// Now load the data for the expanded option.
return this.fetchDataForExpandedOption();
}
});
}
// No conversation specified or not found, determine which one should be expanded.
this.calculateExpandedStatus();
}
// Now load the data for the expanded option.
return this.fetchDataForExpandedOption();
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingdiscussions', true);
}).finally(() => {
this.loaded = true;
});
}
/**
* Calculate which option should be expanded initially.
*/
protected calculateExpandedStatus(): void {
this.favourites.expanded = this.favourites.count != 0 && !this.group.unread && !this.individual.unread;
this.group.expanded = !this.favourites.expanded && this.group.count != 0 && !this.individual.unread;
this.individual.expanded = !this.favourites.expanded && !this.group.expanded;
this.loadCurrentListElement();
}
/**
* Fetch data for the expanded option.
*
* @return Promise resolved when done.
*/
protected fetchDataForExpandedOption(): Promise<void> {
const expandedOption = this.getExpandedOption();
if (expandedOption) {
return this.fetchDataForOption(expandedOption, false);
}
return Promise.resolve();
}
/**
* Fetch data for a certain option.
*
* @param option The option to fetch data for.
* @param loadingMore Whether we are loading more data or just the first ones.
* @param getCounts Whether to get counts data.
* @return Promise resolved when done.
*/
fetchDataForOption(option: AddonMessagesGroupConversationOption, loadingMore?: boolean, getCounts?: boolean): Promise<void> {
option.loadMoreError = false;
const limitFrom = loadingMore ? option.conversations.length : 0,
promises = [];
let data: {conversations: AddonMessagesConversationForList[], canLoadMore: boolean},
offlineMessages;
// Get the conversations and, if needed, the offline messages. Always try to get the latest data.
promises.push(this.messagesProvider.invalidateConversations(this.siteId).catch(() => {
// Shouldn't happen.
}).then(() => {
return this.messagesProvider.getConversations(option.type, option.favourites, limitFrom, this.siteId);
}).then((result) => {
data = result;
}));
if (!loadingMore) {
promises.push(this.messagesOffline.getAllMessages().then((data) => {
offlineMessages = data;
}));
}
if (getCounts) {
promises.push(this.fetchConversationCounts());
promises.push(this.messagesProvider.refreshUnreadConversationCounts(this.siteId));
}
return Promise.all(promises).then(() => {
if (loadingMore) {
option.conversations = option.conversations.concat(data.conversations);
option.canLoadMore = data.canLoadMore;
} else {
option.conversations = data.conversations;
option.canLoadMore = data.canLoadMore;
if (offlineMessages && offlineMessages.length) {
return this.loadOfflineMessages(option, offlineMessages).then(() => {
// Sort the conversations, the offline messages could affect the order.
option.conversations = this.messagesProvider.sortConversations(option.conversations);
});
}
}
});
}
/**
* Fetch conversation counts.
*
* @return Promise resolved when done.
*/
protected fetchConversationCounts(): Promise<void> {
// Always try to get the latest data.
return this.messagesProvider.invalidateConversationCounts(this.siteId).catch(() => {
// Shouldn't happen.
}).then(() => {
return this.messagesProvider.getConversationCounts(this.siteId);
}).then((counts) => {
this.favourites.count = counts.favourites;
this.individual.count = counts.individual + counts.self; // Self is only returned if it's not favourite.
this.group.count = counts.group;
});
}
/**
* Find a conversation in the list of loaded conversations.
*
* @param conversationId The conversation ID to search.
* @param userId User ID to search (if no conversationId).
* @param option The option to search in. If not defined, search in all options.
* @return Conversation.
*/
protected findConversation(conversationId: number, userId?: number, option?: AddonMessagesGroupConversationOption)
: AddonMessagesConversationForList {
if (conversationId) {
const conversations = option ? (option.conversations || []) : ((this.favourites.conversations || [])
.concat(this.group.conversations || []).concat(this.individual.conversations || []));
return conversations.find((conv) => {
return conv.id == conversationId;
});
}
const conversations = option ? (option.conversations || []) :
((this.favourites.conversations || []).concat(this.individual.conversations || []));
return conversations.find((conv) => {
return conv.userid == userId;
});
}
/**
* Get the option that is currently expanded, undefined if they are all collapsed.
*
* @return Option currently expanded.
*/
protected getExpandedOption(): AddonMessagesGroupConversationOption {
if (this.favourites.expanded) {
return this.favourites;
} else if (this.group.expanded) {
return this.group;
} else if (this.individual.expanded) {
return this.individual;
}
}
/**
* Navigate to contacts view.
*/
gotoContacts(): void {
this.splitviewCtrl.getMasterNav().push('AddonMessagesContactsPage');
}
/**
* Navigate to a particular conversation.
*
* @param conversationId Conversation Id to load.
* @param userId User of the conversation. Only if there is no conversationId.
* @param messageId Message to scroll after loading the discussion. Used when searching.
*/
gotoConversation(conversationId: number, userId?: number, messageId?: number): void {
this.selectedConversationId = conversationId;
this.selectedUserId = userId;
const params = {
conversationId: conversationId,
userId: userId
};
if (messageId) {
params['message'] = messageId;
}
this.splitviewCtrl.push('AddonMessagesDiscussionPage', params);
}
/**
* Navigate to message settings.
*/
gotoSettings(): void {
this.splitviewCtrl.push('AddonMessagesSettingsPage');
}
/**
* Function to load more conversations.
*
* @param option The option to fetch data for.
* @param infiniteComplete Infinite scroll complete function. Only used from core-infinite-loading.
* @return Promise resolved when done.
*/
loadMoreConversations(option: AddonMessagesGroupConversationOption, infiniteComplete?: any): Promise<void> {
return this.fetchDataForOption(option, true).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingdiscussions', true);
option.loadMoreError = true;
}).finally(() => {
infiniteComplete && infiniteComplete();
});
}
/**
* Load offline messages into the conversations.
*
* @param option The option where the messages should be loaded.
* @param messages Offline messages.
* @return Promise resolved when done.
*/
protected loadOfflineMessages(option: AddonMessagesGroupConversationOption, messages: any[]): Promise<any> {
const promises = [];
messages.forEach((message) => {
if (message.conversationid) {
// It's an existing conversation. Search it in the current option.
let conversation = this.findConversation(message.conversationid, undefined, option);
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 could be an old one or the message could belong to another option.
conversation = message.conversation || {};
conversation.id = message.conversationid;
if (this.getConversationOption(conversation) == option) {
// Message belongs to current option, add the conversation.
this.addLastOfflineMessage(conversation, message);
this.addOfflineConversation(conversation);
}
}
} else if (option == this.individual) {
// It's 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, option);
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 if it belongs to the current option.
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 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 conversation Conversation where to put the last message.
* @param message Offline message to add.
*/
protected addLastOfflineMessage(conversation: any, message: AddonMessagesConversationMessage): 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 conversation Conversation to check.
* @return Option object.
*/
protected getConversationOption(conversation: AddonMessagesConversationForList): AddonMessagesGroupConversationOption {
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.
*
* @param refresher Refresher.
* @param refreshUnreadCounts Whether to refresh unread counts.
* @return Promise resolved when done.
*/
refreshData(refresher?: any, refreshUnreadCounts: boolean = true): Promise<void> {
// Don't invalidate conversations and so, they always try to get latest data.
const promises = [
this.messagesProvider.invalidateContactRequestsCountCache(this.siteId)
];
return this.utils.allPromises(promises).finally(() => {
return this.fetchData(refreshUnreadCounts).finally(() => {
if (refresher) {
refresher.complete();
}
});
});
}
/**
* Toogle the visibility of an option (expand/collapse).
*
* @param option The option to expand/collapse.
*/
toggle(option: AddonMessagesGroupConversationOption): void {
if (option.expanded) {
// Already expanded, close it.
option.expanded = false;
this.loadCurrentListElement();
} else {
// Pass getCounts=true to update the counts everytime the user expands an option.
this.expandOption(option, true).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingdiscussions', true);
});
}
}
/**
* Expand a certain option.
*
* @param option The option to expand.
* @param getCounts Whether to get counts data.
* @return Promise resolved when done.
*/
protected expandOption(option: AddonMessagesGroupConversationOption, getCounts?: boolean): Promise<void> {
// Collapse all and expand the right one.
this.favourites.expanded = false;
this.group.expanded = false;
this.individual.expanded = false;
option.expanded = true;
option.loading = true;
return this.fetchDataForOption(option, false, getCounts).then(() => {
this.loadCurrentListElement();
}).catch((error) => {
option.expanded = false;
return Promise.reject(error);
}).finally(() => {
option.loading = false;
});
}
/**
* Load the current list element based on the expanded list.
*/
protected loadCurrentListElement(): void {
if (this.favourites.expanded) {
this.currentListEl = this.favListEl && this.favListEl.nativeElement;
} else if (this.group.expanded) {
this.currentListEl = this.groupListEl && this.groupListEl.nativeElement;
} else if (this.individual.expanded) {
this.currentListEl = this.indListEl && this.indListEl.nativeElement;
} else {
this.currentListEl = undefined;
}
}
/**
* Navigate to the search page.
*/
gotoSearch(): void {
this.navCtrl.push('AddonMessagesSearchPage');
}
/**
* Page destroyed.
*/
ngOnDestroy(): void {
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();
this.updateConversationListObserver && this.updateConversationListObserver.off();
this.contactRequestsCountObserver && this.contactRequestsCountObserver.off();
this.memberInfoObserver && this.memberInfoObserver.off();
}
}
/**
* Conversation options.
*/
export type AddonMessagesGroupConversationOption = {
type: number; // Option type.
favourites: boolean; // Whether it contains favourites conversations.
count: number; // Number of conversations.
unread?: number; // Number of unread conversations.
expanded?: boolean; // Whether the option is currently expanded.
loading?: boolean; // Whether the option is being loaded.
canLoadMore?: boolean; // Whether it can load more data.
loadMoreError?: boolean; // Whether there was an error loading more conversations.
conversations?: AddonMessagesConversationForList[]; // List of conversations.
};
/**
* Formatted conversation with some calculated data for the list.
*/
export type AddonMessagesConversationForList = AddonMessagesConversationFormatted & {
lastmessagepending?: boolean; // Calculated in the app. Whether last message is pending to be sent.
};