MOBILE-3631 messages: Messages main page and basic services
parent
69bc2c7480
commit
f9b6a66e75
|
@ -22,12 +22,14 @@ import { AddonBadgesModule } from './badges/badges.module';
|
||||||
import { AddonCalendarModule } from './calendar/calendar.module';
|
import { AddonCalendarModule } from './calendar/calendar.module';
|
||||||
import { AddonNotificationsModule } from './notifications/notifications.module';
|
import { AddonNotificationsModule } from './notifications/notifications.module';
|
||||||
import { AddonMessageOutputModule } from './messageoutput/messageoutput.module';
|
import { AddonMessageOutputModule } from './messageoutput/messageoutput.module';
|
||||||
|
import { AddonMessagesModule } from './messages/messages.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
AddonBlockModule,
|
AddonBlockModule,
|
||||||
AddonBadgesModule,
|
AddonBadgesModule,
|
||||||
AddonCalendarModule,
|
AddonCalendarModule,
|
||||||
|
AddonMessagesModule,
|
||||||
AddonPrivateFilesModule,
|
AddonPrivateFilesModule,
|
||||||
AddonFilterModule,
|
AddonFilterModule,
|
||||||
AddonUserProfileFieldModule,
|
AddonUserProfileFieldModule,
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
{
|
||||||
|
"acceptandaddcontact": "Accept and add to contacts",
|
||||||
|
"addcontact": "Add contact",
|
||||||
|
"addcontactconfirm": "Are you sure you want to add {{$a}} to your contacts?",
|
||||||
|
"addtofavourites": "Star conversation",
|
||||||
|
"addtoyourcontacts": "Add to contacts",
|
||||||
|
"blocknoncontacts": "Prevent non-contacts from messaging me",
|
||||||
|
"blockuser": "Block user",
|
||||||
|
"blockuserconfirm": "Are you sure you want to block {{$a}}?",
|
||||||
|
"contactableprivacy": "Accept messages from:",
|
||||||
|
"contactableprivacy_coursemember": "My contacts and anyone in my courses",
|
||||||
|
"contactableprivacy_onlycontacts": "My contacts only",
|
||||||
|
"contactableprivacy_site": "Anyone on the site",
|
||||||
|
"contactblocked": "Contact blocked",
|
||||||
|
"contactlistempty": "The contact list is empty",
|
||||||
|
"contactname": "Contact name",
|
||||||
|
"contactrequestsent": "Contact request sent",
|
||||||
|
"contacts": "Contacts",
|
||||||
|
"conversationactions": "Conversation actions menu",
|
||||||
|
"decline": "Decline",
|
||||||
|
"deleteallconfirm": "Are you sure you would like to delete this entire conversation? This will not delete it for other conversation participants.",
|
||||||
|
"deleteallselfconfirm": "Are you sure you would like to delete this entire personal conversation?",
|
||||||
|
"deleteconversation": "Delete conversation",
|
||||||
|
"deleteforeveryone": "Delete for me and for everyone else",
|
||||||
|
"deletemessage": "Delete message",
|
||||||
|
"deletemessageconfirmation": "Are you sure you want to delete this message? It will only be deleted from your messaging history and will still be viewable by the user who sent or received the message.",
|
||||||
|
"errordeletemessage": "Error while deleting the message.",
|
||||||
|
"errorwhileretrievingcontacts": "Error while retrieving contacts from the server.",
|
||||||
|
"errorwhileretrievingdiscussions": "Error while retrieving discussions from the server.",
|
||||||
|
"errorwhileretrievingmessages": "Error while retrieving messages from the server.",
|
||||||
|
"errorwhileretrievingusers": "Error while retrieving users from the server.",
|
||||||
|
"groupconversations": "Group",
|
||||||
|
"groupinfo": "Group info",
|
||||||
|
"individualconversations": "Private",
|
||||||
|
"info": "User info",
|
||||||
|
"isnotinyourcontacts": "{{$a}} is not in your contacts",
|
||||||
|
"message": "Message",
|
||||||
|
"messagenotsent": "The message was not sent. Please try again later.",
|
||||||
|
"messagepreferences": "Message preferences",
|
||||||
|
"messages": "Messages",
|
||||||
|
"muteconversation": "Mute",
|
||||||
|
"mutedconversation": "Muted conversation",
|
||||||
|
"newmessage": "New message",
|
||||||
|
"newmessages": "New messages",
|
||||||
|
"nocontactrequests": "No contact requests",
|
||||||
|
"nocontactsgetstarted": "No contacts",
|
||||||
|
"nofavourites": "No starred conversations",
|
||||||
|
"nogroupconversations": "No group conversations",
|
||||||
|
"noindividualconversations": "No private conversations",
|
||||||
|
"nomessagesfound": "No messages were found",
|
||||||
|
"noncontacts": "Non-contacts",
|
||||||
|
"nousersfound": "No users found",
|
||||||
|
"numparticipants": "{{$a}} participants",
|
||||||
|
"removecontact": "Remove contact",
|
||||||
|
"removecontactconfirm": "Are you sure you want to remove {{$a}} from your contacts?",
|
||||||
|
"removefromfavourites": "Unstar conversation",
|
||||||
|
"removefromyourcontacts": "Remove from contacts",
|
||||||
|
"requests": "Requests",
|
||||||
|
"requirecontacttomessage": "You need to request {{$a}} to add you as a contact to be able to message them.",
|
||||||
|
"searchcombined": "Search people and messages",
|
||||||
|
"selfconversation": "Personal space",
|
||||||
|
"selfconversationdefaultmessage": "Save draft messages, links, notes etc. to access later.",
|
||||||
|
"sendcontactrequest": "Send contact request",
|
||||||
|
"showdeletemessages": "Show delete messages",
|
||||||
|
"type_blocked": "Blocked",
|
||||||
|
"type_offline": "Offline",
|
||||||
|
"type_online": "Online",
|
||||||
|
"type_search": "Search results",
|
||||||
|
"type_strangers": "Others",
|
||||||
|
"unabletomessage": "You are unable to message this user",
|
||||||
|
"unblockuser": "Unblock user",
|
||||||
|
"unblockuserconfirm": "Are you sure you want to unblock {{$a}}?",
|
||||||
|
"unmuteconversation": "Unmute",
|
||||||
|
"useentertosend": "Use enter to send",
|
||||||
|
"useentertosenddescdesktop": "If disabled, you can use Ctrl+Enter to send the message.",
|
||||||
|
"useentertosenddescmac": "If disabled, you can use Cmd+Enter to send the message.",
|
||||||
|
"userwouldliketocontactyou": "{{$a}} would like to contact you",
|
||||||
|
"warningconversationmessagenotsent": "Couldn't send message(s) to conversation {{conversation}}. {{error}}",
|
||||||
|
"warningmessagenotsent": "Couldn't send message(s) to user {{user}}. {{error}}",
|
||||||
|
"wouldliketocontactyou": "Would like to contact you",
|
||||||
|
"you": "You:",
|
||||||
|
"youhaveblockeduser": "You have blocked this user.",
|
||||||
|
"yourcontactrequestpending": "Your contact request is pending with {{$a}}"
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
:host {
|
||||||
|
.addon-messages-conversation-item,
|
||||||
|
.addon-message-discussion {
|
||||||
|
h2 {
|
||||||
|
core-format-text {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-icon {
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.note {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
margin: 4px 8px;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addon-message-last-message {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addon-message-last-message-user {
|
||||||
|
white-space: nowrap;
|
||||||
|
color: var(--ion-text-color);
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addon-message-last-message-text {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.addon-message-discussion {
|
||||||
|
h2 {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context([dir=rtl]) {
|
||||||
|
.addon-messages-conversation-item,
|
||||||
|
.addon-message-discussion {
|
||||||
|
h2 ion-icon {
|
||||||
|
margin-right: 2px;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note {
|
||||||
|
left: 0;
|
||||||
|
right: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addon-message-last-message-user {
|
||||||
|
margin-left: 2px;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
// (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 { Injector, NgModule } from '@angular/core';
|
||||||
|
import { RouterModule, ROUTES, Routes } from '@angular/router';
|
||||||
|
|
||||||
|
import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module';
|
||||||
|
import { AddonMessagesIndexRoutingModule } from './pages/index-35/messages-index-routing.module';
|
||||||
|
|
||||||
|
function buildRoutes(injector: Injector): Routes {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
path: 'index', // 3.5 or lower.
|
||||||
|
loadChildren: () => import('./pages/index-35/index.module').then( m => m.AddonMessagesIndex35PageModule),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'group-conversations', // 3.6 or greater.
|
||||||
|
loadChildren: () => import('./pages/group-conversations/group-conversations.module')
|
||||||
|
.then(m => m.AddonMessagesGroupConversationsPageModule),
|
||||||
|
},
|
||||||
|
...buildTabMainRoutes(injector, {
|
||||||
|
redirectTo: 'index',
|
||||||
|
pathMatch: 'full',
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3.5 or lower.
|
||||||
|
const indexTabRoutes: Routes = [
|
||||||
|
{
|
||||||
|
path: 'discussions',
|
||||||
|
loadChildren: () => import('./pages/discussions-35/discussions.module').then(m => m.AddonMessagesDiscussions35PageModule),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'contacts',
|
||||||
|
loadChildren: () => import('./pages/contacts-35/contacts.module').then(m => m.AddonMessagesContacts35PageModule),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [AddonMessagesIndexRoutingModule.forChild({ children: indexTabRoutes })],
|
||||||
|
exports: [RouterModule],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: ROUTES,
|
||||||
|
multi: true,
|
||||||
|
deps: [Injector],
|
||||||
|
useFactory: buildRoutes,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AddonMessagesLazyModule { }
|
|
@ -0,0 +1,129 @@
|
||||||
|
// (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 { APP_INITIALIZER, NgModule } from '@angular/core';
|
||||||
|
import { Routes } from '@angular/router';
|
||||||
|
|
||||||
|
import { CoreMainMenuRoutingModule } from '@features/mainmenu/mainmenu-routing.module';
|
||||||
|
import { MESSAGES_OFFLINE_SITE_SCHEMA } from './services/database/messages';
|
||||||
|
import { CORE_SITE_SCHEMAS } from '@services/sites';
|
||||||
|
import { CoreMainMenuDelegate } from '@features/mainmenu/services/mainmenu-delegate';
|
||||||
|
import { AddonMessagesMainMenuHandler, AddonMessagesMainMenuHandlerService } from './services/handlers/mainmenu';
|
||||||
|
import { CoreCronDelegate } from '@services/cron';
|
||||||
|
|
||||||
|
const mainMenuChildrenRoutes: Routes = [
|
||||||
|
{
|
||||||
|
path: AddonMessagesMainMenuHandlerService.PAGE_NAME,
|
||||||
|
loadChildren: () => import('./messages-lazy.module').then(m => m.AddonMessagesLazyModule),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CoreMainMenuRoutingModule.forChild({ children: mainMenuChildrenRoutes }),
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: CORE_SITE_SCHEMAS,
|
||||||
|
useValue: [MESSAGES_OFFLINE_SITE_SCHEMA],
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: APP_INITIALIZER,
|
||||||
|
multi: true,
|
||||||
|
deps: [],
|
||||||
|
useFactory: () => () => {
|
||||||
|
CoreMainMenuDelegate.instance.registerHandler(AddonMessagesMainMenuHandler.instance);
|
||||||
|
CoreCronDelegate.instance.register(AddonMessagesMainMenuHandler.instance);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AddonMessagesModule {
|
||||||
|
|
||||||
|
/* constructor(
|
||||||
|
contentLinksDelegate: CoreContentLinksDelegate,
|
||||||
|
indexLinkHandler: AddonMessagesIndexLinkHandler,
|
||||||
|
discussionLinkHandler: AddonMessagesDiscussionLinkHandler,
|
||||||
|
sendMessageHandler: AddonMessagesSendMessageUserHandler,
|
||||||
|
userDelegate: CoreUserDelegate,
|
||||||
|
cronDelegate: CoreCronDelegate,
|
||||||
|
syncHandler: AddonMessagesSyncCronHandler,
|
||||||
|
network: Network,
|
||||||
|
zone: NgZone,
|
||||||
|
messagesSync: AddonMessagesSyncProvider,
|
||||||
|
messagesProvider: AddonMessagesProvider,
|
||||||
|
sitesProvider: CoreSitesProvider,
|
||||||
|
linkHelper: CoreContentLinksHelperProvider,
|
||||||
|
settingsHandler: AddonMessagesSettingsHandler,
|
||||||
|
settingsDelegate: CoreSettingsDelegate,
|
||||||
|
pushNotificationsDelegate: CorePushNotificationsDelegate,
|
||||||
|
addContactHandler: AddonMessagesAddContactUserHandler,
|
||||||
|
blockContactHandler: AddonMessagesBlockContactUserHandler,
|
||||||
|
contactRequestLinkHandler: AddonMessagesContactRequestLinkHandler,
|
||||||
|
pushClickHandler: AddonMessagesPushClickHandler,
|
||||||
|
) {
|
||||||
|
// Register handlers.
|
||||||
|
contentLinksDelegate.registerHandler(indexLinkHandler);
|
||||||
|
contentLinksDelegate.registerHandler(discussionLinkHandler);
|
||||||
|
contentLinksDelegate.registerHandler(contactRequestLinkHandler);
|
||||||
|
userDelegate.registerHandler(sendMessageHandler);
|
||||||
|
userDelegate.registerHandler(addContactHandler);
|
||||||
|
userDelegate.registerHandler(blockContactHandler);
|
||||||
|
cronDelegate.register(syncHandler);
|
||||||
|
settingsDelegate.registerHandler(settingsHandler);
|
||||||
|
pushNotificationsDelegate.registerClickHandler(pushClickHandler);
|
||||||
|
|
||||||
|
// Sync some discussions when device goes online.
|
||||||
|
network.onConnect().subscribe(() => {
|
||||||
|
// Execute the callback in the Angular zone, so change detection doesn't stop working.
|
||||||
|
zone.run(() => {
|
||||||
|
messagesSync.syncAllDiscussions(undefined, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const notificationClicked = (notification: any): void => {
|
||||||
|
messagesProvider.isMessagingEnabledForSite(notification.site).then(() => {
|
||||||
|
sitesProvider.isFeatureDisabled('CoreMainMenuDelegate_AddonMessages', notification.site).then((disabled) => {
|
||||||
|
if (disabled) {
|
||||||
|
// Messages are disabled, stop.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
messagesProvider.invalidateDiscussionsCache(notification.site).finally(() => {
|
||||||
|
// Check if group messaging is enabled, to determine which page should be loaded.
|
||||||
|
messagesProvider.isGroupMessagingEnabledInSite(notification.site).then((enabled) => {
|
||||||
|
const pageParams: any = {};
|
||||||
|
let pageName = 'AddonMessagesIndexPage';
|
||||||
|
if (enabled) {
|
||||||
|
pageName = 'AddonMessagesGroupConversationsPage';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have enough information to open the conversation.
|
||||||
|
if (notification.convid && enabled) {
|
||||||
|
pageParams.conversationId = Number(notification.convid);
|
||||||
|
} else if (notification.userfromid || notification.useridfrom) {
|
||||||
|
pageParams.discussionUserId = Number(notification.userfromid || notification.useridfrom);
|
||||||
|
}
|
||||||
|
|
||||||
|
linkHelper.goInSite(undefined, pageName, pageParams, notification.site);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}*/
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
<ion-content>
|
||||||
|
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshData($event)">
|
||||||
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
|
</ion-refresher>
|
||||||
|
|
||||||
|
<core-search-box (onSubmit)="search($event)" (onClear)="clearSearch($event)"
|
||||||
|
[placeholder]="'addon.messages.contactname' | translate" autocorrect="off" spellcheck="false" lengthCheck="2"
|
||||||
|
[disabled]="!loaded" searchArea="AddonMessagesContacts"></core-search-box>
|
||||||
|
|
||||||
|
<core-loading [hideUntil]="loaded" [message]="loadingMessage">
|
||||||
|
<core-empty-box *ngIf="!hasContacts && searchString == ''" icon="fas-address-book"
|
||||||
|
[message]="'addon.messages.contactlistempty' | translate"></core-empty-box>
|
||||||
|
|
||||||
|
<core-empty-box *ngIf="!hasContacts && searchString != ''" icon="fas-address-book"
|
||||||
|
[message]="'addon.messages.nousersfound' | translate"></core-empty-box>
|
||||||
|
|
||||||
|
<ion-list *ngFor="let contactType of contactTypes" class="ion-no-margin">
|
||||||
|
<ng-container *ngIf="contacts[contactType] && (contacts[contactType].length > 0 || contactType === searchType)">
|
||||||
|
<ion-item-divider>
|
||||||
|
<ion-label><h2>{{ 'addon.messages.type_' + contactType | translate }}</h2></ion-label>
|
||||||
|
<ion-note slot="end" class="ion-padding-end"><ion-badge>{{ contacts[contactType].length }}</ion-badge></ion-note>
|
||||||
|
</ion-item-divider>
|
||||||
|
<ng-container *ngFor="let contact of contacts[contactType]">
|
||||||
|
<!-- Don't show deleted users -->
|
||||||
|
<ion-item class="ion-text-wrap" *ngIf="contact.profileimageurl || contact.profileimageurlsmall"
|
||||||
|
[title]="contact.fullname" (click)="gotoDiscussion(contact.id)"
|
||||||
|
[class.core-split-item-selected]="contact.id == discussionUserId" class="addon-messages-conversation-item">
|
||||||
|
<core-user-avatar [user]="contact" slot="start" [checkOnline]="contact.showonlinestatus"></core-user-avatar>
|
||||||
|
<ion-label><h2>{{ contact.fullname }}</h2></ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</ion-list>
|
||||||
|
</core-loading>
|
||||||
|
</ion-content>
|
|
@ -0,0 +1,47 @@
|
||||||
|
// (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 { NgModule } from '@angular/core';
|
||||||
|
import { IonicModule } from '@ionic/angular';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
|
import { CoreSharedModule } from '@/core/shared.module';
|
||||||
|
import { CoreSearchComponentsModule } from '@features/search/components/components.module';
|
||||||
|
|
||||||
|
import { AddonMessagesContacts35Page } from './contacts.page';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: AddonMessagesContacts35Page,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forChild(routes),
|
||||||
|
CommonModule,
|
||||||
|
IonicModule,
|
||||||
|
TranslateModule.forChild(),
|
||||||
|
CoreSharedModule,
|
||||||
|
CoreSearchComponentsModule,
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
AddonMessagesContacts35Page,
|
||||||
|
],
|
||||||
|
exports: [RouterModule],
|
||||||
|
})
|
||||||
|
export class AddonMessagesContacts35PageModule {}
|
|
@ -0,0 +1,264 @@
|
||||||
|
// (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, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { IonRefresher } from '@ionic/angular';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import {
|
||||||
|
AddonMessagesProvider,
|
||||||
|
AddonMessagesGetContactsResult,
|
||||||
|
AddonMessagesSearchContactsContact,
|
||||||
|
AddonMessagesGetContactsContact,
|
||||||
|
AddonMessages,
|
||||||
|
AddonMessagesSplitViewLoadIndexEventData,
|
||||||
|
AddonMessagesMemberInfoChangedEventData,
|
||||||
|
} from '../../services/messages';
|
||||||
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
import { CoreApp } from '@services/app';
|
||||||
|
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { Translate } from '@singletons';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page that displays the list of contacts.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'addon-messages-contacts',
|
||||||
|
templateUrl: 'contacts.html',
|
||||||
|
styleUrls: ['../../messages-common.scss'],
|
||||||
|
})
|
||||||
|
export class AddonMessagesContacts35Page implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
protected currentUserId: number;
|
||||||
|
protected searchingMessages: string;
|
||||||
|
protected loadingMessages: string;
|
||||||
|
protected siteId: string;
|
||||||
|
protected noSearchTypes = ['online', 'offline', 'blocked', 'strangers'];
|
||||||
|
protected memberInfoObserver: CoreEventObserver;
|
||||||
|
|
||||||
|
loaded = false;
|
||||||
|
discussionUserId?: number;
|
||||||
|
contactTypes = ['online', 'offline', 'blocked', 'strangers'];
|
||||||
|
searchType = 'search';
|
||||||
|
loadingMessage = '';
|
||||||
|
hasContacts = false;
|
||||||
|
contacts: AddonMessagesGetContactsFormatted = {
|
||||||
|
online: [],
|
||||||
|
offline: [],
|
||||||
|
strangers: [],
|
||||||
|
search: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
searchString = '';
|
||||||
|
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected route: ActivatedRoute,
|
||||||
|
) {
|
||||||
|
this.currentUserId = CoreSites.instance.getCurrentSiteUserId();
|
||||||
|
this.siteId = CoreSites.instance.getCurrentSiteId();
|
||||||
|
this.searchingMessages = Translate.instance.instant('core.searching');
|
||||||
|
this.loadingMessages = Translate.instance.instant('core.loading');
|
||||||
|
this.loadingMessage = this.loadingMessages;
|
||||||
|
|
||||||
|
// Refresh the list when a contact request is confirmed.
|
||||||
|
this.memberInfoObserver = CoreEvents.on<AddonMessagesMemberInfoChangedEventData>(
|
||||||
|
AddonMessagesProvider.MEMBER_INFO_CHANGED_EVENT,
|
||||||
|
(data) => {
|
||||||
|
if (data.contactRequestConfirmed) {
|
||||||
|
this.refreshData();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
CoreSites.instance.getCurrentSiteId(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component loaded.
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.route.queryParams.subscribe(async params => {
|
||||||
|
this.discussionUserId = params['discussionUserId'] || undefined;
|
||||||
|
|
||||||
|
if (this.discussionUserId) {
|
||||||
|
// There is a discussion to load, open the discussion in a new state.
|
||||||
|
this.gotoDiscussion(this.discussionUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.fetchData();
|
||||||
|
if (!this.discussionUserId && this.hasContacts) {
|
||||||
|
let contact: AddonMessagesGetContactsContact | undefined;
|
||||||
|
for (const x in this.contacts) {
|
||||||
|
if (this.contacts[x].length > 0) {
|
||||||
|
contact = this.contacts[x][0];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contact) {
|
||||||
|
// Take first and load it.
|
||||||
|
this.gotoDiscussion(contact.id, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.loaded = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh the data.
|
||||||
|
*
|
||||||
|
* @param refresher Refresher.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async refreshData(refresher?: CustomEvent<IonRefresher>): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (this.searchString) {
|
||||||
|
// User has searched, update the search.
|
||||||
|
await this.performSearch(this.searchString);
|
||||||
|
} else {
|
||||||
|
// Update contacts.
|
||||||
|
await AddonMessages.instance.invalidateAllContactsCache(this.currentUserId);
|
||||||
|
await this.fetchData();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
refresher?.detail.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch contacts.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async fetchData(): Promise<void> {
|
||||||
|
this.loadingMessage = this.loadingMessages;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const contacts = await AddonMessages.instance.getAllContacts();
|
||||||
|
for (const x in contacts) {
|
||||||
|
if (contacts[x].length > 0) {
|
||||||
|
this.contacts[x] = this.sortUsers(contacts[x]);
|
||||||
|
} else {
|
||||||
|
this.contacts[x] = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clearSearch();
|
||||||
|
} catch (error) {
|
||||||
|
CoreDomUtils.instance.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingcontacts', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort user list by fullname
|
||||||
|
*
|
||||||
|
* @param list List to sort.
|
||||||
|
* @return Sorted list.
|
||||||
|
*/
|
||||||
|
protected sortUsers(list: AddonMessagesSearchContactsContact[]): AddonMessagesSearchContactsContact[] {
|
||||||
|
return list.sort((a, b) => {
|
||||||
|
const compareA = a.fullname.toLowerCase();
|
||||||
|
const compareB = b.fullname.toLowerCase();
|
||||||
|
|
||||||
|
return compareA.localeCompare(compareB);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear search and show all contacts again.
|
||||||
|
*/
|
||||||
|
clearSearch(): void {
|
||||||
|
this.searchString = ''; // Reset searched string.
|
||||||
|
this.contactTypes = this.noSearchTypes;
|
||||||
|
|
||||||
|
this.hasContacts = false;
|
||||||
|
for (const x in this.contacts) {
|
||||||
|
if (this.contacts[x].length > 0) {
|
||||||
|
this.hasContacts = true;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search users from the UI.
|
||||||
|
*
|
||||||
|
* @param query Text to search for.
|
||||||
|
* @return Resolved when done.
|
||||||
|
*/
|
||||||
|
search(query: string): Promise<void> {
|
||||||
|
CoreApp.instance.closeKeyboard();
|
||||||
|
|
||||||
|
this.loaded = false;
|
||||||
|
this.loadingMessage = this.searchingMessages;
|
||||||
|
|
||||||
|
return this.performSearch(query).finally(() => {
|
||||||
|
this.loaded = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform the search of users.
|
||||||
|
*
|
||||||
|
* @param query Text to search for.
|
||||||
|
* @return Resolved when done.
|
||||||
|
*/
|
||||||
|
protected async performSearch(query: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const result = await AddonMessages.instance.searchContacts(query);
|
||||||
|
this.hasContacts = result.length > 0;
|
||||||
|
this.searchString = query;
|
||||||
|
this.contactTypes = ['search'];
|
||||||
|
|
||||||
|
this.contacts.search = this.sortUsers(result);
|
||||||
|
} catch (error) {
|
||||||
|
CoreDomUtils.instance.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingcontacts', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to a particular discussion.
|
||||||
|
*
|
||||||
|
* @param discussionUserId Discussion Id to load.
|
||||||
|
* @param onlyWithSplitView Only go to Discussion if split view is on.
|
||||||
|
*/
|
||||||
|
gotoDiscussion(discussionUserId: number, onlyWithSplitView: boolean = false): void {
|
||||||
|
this.discussionUserId = discussionUserId;
|
||||||
|
|
||||||
|
const params: AddonMessagesSplitViewLoadIndexEventData = {
|
||||||
|
discussion: discussionUserId,
|
||||||
|
onlyWithSplitView: onlyWithSplitView,
|
||||||
|
};
|
||||||
|
CoreEvents.trigger(AddonMessagesProvider.SPLIT_VIEW_LOAD_INDEX_EVENT, params, this.siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component destroyed.
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.memberInfoObserver?.off();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contacts with some calculated data.
|
||||||
|
*/
|
||||||
|
export type AddonMessagesGetContactsFormatted = AddonMessagesGetContactsResult & {
|
||||||
|
search?: AddonMessagesSearchContactsContact[]; // Calculated in the app. Result of searching users.
|
||||||
|
};
|
|
@ -0,0 +1,56 @@
|
||||||
|
<ion-content>
|
||||||
|
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshData($event)">
|
||||||
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
|
</ion-refresher>
|
||||||
|
|
||||||
|
<core-search-box *ngIf="search.enabled" (onSubmit)="searchMessage($event)" (onClear)="clearSearch($event)"
|
||||||
|
[placeholder]=" 'addon.messages.message' | translate" autocorrect="off" spellcheck="false" lengthCheck="2"
|
||||||
|
[disabled]="!loaded" searchArea="AddonMessagesDiscussions"></core-search-box>
|
||||||
|
|
||||||
|
<core-loading [hideUntil]="loaded" [message]="loadingMessage">
|
||||||
|
|
||||||
|
<ion-list *ngIf="search.showResults" class="ion-no-margin">
|
||||||
|
<ion-item-divider>
|
||||||
|
<ion-label>
|
||||||
|
<h2>{{ 'core.searchresults' | translate }}</h2>
|
||||||
|
</ion-label>
|
||||||
|
<ion-note slot="end" class="ion-padding-end"><ion-badge>{{ search.results.length }}</ion-badge></ion-note>
|
||||||
|
</ion-item-divider>
|
||||||
|
<ion-item class="ion-text-wrap" *ngFor="let result of search.results" [title]="result.fullname"
|
||||||
|
(click)="gotoDiscussion(result.userid, result.messageid)"
|
||||||
|
[class.core-split-item-selected]="result.userid == discussionUserId" class="addon-message-discussion">
|
||||||
|
<core-user-avatar [user]="result" slot="start" [checkOnline]="result.showonlinestatus"></core-user-avatar>
|
||||||
|
<ion-label>
|
||||||
|
<h2>{{ result.fullname }}</h2>
|
||||||
|
<p><core-format-text clean="true" singleLine="true" [text]="result.lastmessage" contextLevel="system"
|
||||||
|
[contextInstanceId]="0"></core-format-text></p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-list>
|
||||||
|
|
||||||
|
<ion-list *ngIf="!search.showResults" class="ion-no-margin">
|
||||||
|
<ion-item class="ion-text-wrap" *ngFor="let discussion of discussions" [title]="discussion.fullname"
|
||||||
|
(click)="gotoDiscussion(discussion.message!.user)"
|
||||||
|
[class.core-split-item-selected]="discussion.message!.user == discussionUserId" class="addon-message-discussion">
|
||||||
|
<core-user-avatar [user]="discussion" slot="start" checkOnline="false"></core-user-avatar>
|
||||||
|
<ion-label>
|
||||||
|
<h2>{{ discussion.fullname }}</h2>
|
||||||
|
<ion-note *ngIf="discussion.message!.timecreated > 0 || discussion.unread">
|
||||||
|
<span *ngIf="discussion.unread" class="core-primary-circle"></span>
|
||||||
|
<span *ngIf="discussion.message!.timecreated > 0">{{discussion.message!.timecreated / 1000 | coreDateDayOrTime}}</span>
|
||||||
|
</ion-note>
|
||||||
|
<p>
|
||||||
|
<core-format-text clean="true" singleLine="true" [text]="discussion.message!.message" contextLevel="system" [contextInstanceId]="0">
|
||||||
|
</core-format-text>
|
||||||
|
</p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-list>
|
||||||
|
|
||||||
|
<core-empty-box *ngIf="(!discussions || discussions.length <= 0) && !search.showResults" icon="far-comments"
|
||||||
|
[message]="'addon.messages.nomessagesfound' | translate"></core-empty-box>
|
||||||
|
|
||||||
|
<core-empty-box *ngIf="(!search.results || search.results.length <= 0) && search.showResults" icon="search"
|
||||||
|
[message]="'core.noresults' | translate"></core-empty-box>
|
||||||
|
</core-loading>
|
||||||
|
</ion-content>
|
|
@ -0,0 +1,47 @@
|
||||||
|
// (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 { NgModule } from '@angular/core';
|
||||||
|
import { IonicModule } from '@ionic/angular';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
|
import { CoreSharedModule } from '@/core/shared.module';
|
||||||
|
import { CoreSearchComponentsModule } from '@features/search/components/components.module';
|
||||||
|
|
||||||
|
import { AddonMessagesDiscussions35Page } from './discussions.page';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: AddonMessagesDiscussions35Page,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forChild(routes),
|
||||||
|
CommonModule,
|
||||||
|
IonicModule,
|
||||||
|
TranslateModule.forChild(),
|
||||||
|
CoreSharedModule,
|
||||||
|
CoreSearchComponentsModule,
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
AddonMessagesDiscussions35Page,
|
||||||
|
],
|
||||||
|
exports: [RouterModule],
|
||||||
|
})
|
||||||
|
export class AddonMessagesDiscussions35PageModule {}
|
|
@ -0,0 +1,284 @@
|
||||||
|
// (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, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import {
|
||||||
|
AddonMessages,
|
||||||
|
AddonMessagesDiscussion,
|
||||||
|
AddonMessagesMessageAreaContact,
|
||||||
|
AddonMessagesProvider,
|
||||||
|
AddonMessagesSplitViewLoadIndexEventData,
|
||||||
|
} from '../../services/messages';
|
||||||
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { CoreApp } from '@services/app';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { CorePushNotificationsNotificationBasicData } from '@features/pushnotifications/services/pushnotifications';
|
||||||
|
import { CorePushNotificationsDelegate } from '@features/pushnotifications/services/push-delegate';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
import { Translate, Platform } from '@singletons';
|
||||||
|
import { IonRefresher } from '@ionic/angular';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page that displays the list of discussions.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'addon-messages-discussions',
|
||||||
|
templateUrl: 'discussions.html',
|
||||||
|
styleUrls: ['../../messages-common.scss'],
|
||||||
|
})
|
||||||
|
export class AddonMessagesDiscussions35Page implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
protected newMessagesObserver: CoreEventObserver;
|
||||||
|
protected readChangedObserver: CoreEventObserver;
|
||||||
|
protected cronObserver: CoreEventObserver;
|
||||||
|
protected appResumeSubscription: Subscription;
|
||||||
|
protected pushObserver: Subscription;
|
||||||
|
protected loadingMessages: string;
|
||||||
|
protected siteId: string;
|
||||||
|
|
||||||
|
loaded = false;
|
||||||
|
loadingMessage = '';
|
||||||
|
discussions: AddonMessagesDiscussion[] = [];
|
||||||
|
discussionUserId?: number;
|
||||||
|
|
||||||
|
search: {
|
||||||
|
enabled: boolean;
|
||||||
|
showResults: boolean;
|
||||||
|
results: AddonMessagesMessageAreaContact[];
|
||||||
|
loading: string;
|
||||||
|
text: string;
|
||||||
|
} = {
|
||||||
|
enabled: false,
|
||||||
|
showResults: false,
|
||||||
|
results: [],
|
||||||
|
loading: '',
|
||||||
|
text: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected route: ActivatedRoute,
|
||||||
|
) {
|
||||||
|
|
||||||
|
this.search.loading = Translate.instance.instant('core.searching');
|
||||||
|
this.loadingMessages = Translate.instance.instant('core.loading');
|
||||||
|
this.siteId = CoreSites.instance.getCurrentSiteId();
|
||||||
|
|
||||||
|
// Update discussions when new message is received.
|
||||||
|
this.newMessagesObserver = CoreEvents.on(AddonMessagesProvider.NEW_MESSAGE_EVENT, (data: any) => {
|
||||||
|
if (data.userId && this.discussions) {
|
||||||
|
const discussion = this.discussions.find((disc) => disc.message!.user == data.userId);
|
||||||
|
|
||||||
|
if (typeof discussion == 'undefined') {
|
||||||
|
this.loaded = false;
|
||||||
|
this.refreshData().finally(() => {
|
||||||
|
this.loaded = true;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// An existing discussion has a new message, update the last message.
|
||||||
|
discussion.message!.message = data.message;
|
||||||
|
discussion.message!.timecreated = data.timecreated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, this.siteId);
|
||||||
|
|
||||||
|
// Update discussions when a message is read.
|
||||||
|
this.readChangedObserver = CoreEvents.on(AddonMessagesProvider.READ_CHANGED_EVENT, (data: any) => {
|
||||||
|
if (data.userId && this.discussions) {
|
||||||
|
const discussion = this.discussions.find((disc) => disc.message!.user == data.userId);
|
||||||
|
|
||||||
|
if (typeof discussion != 'undefined') {
|
||||||
|
// A discussion has been read reset counter.
|
||||||
|
discussion.unread = false;
|
||||||
|
|
||||||
|
// Conversations changed, invalidate them and refresh unread counts.
|
||||||
|
AddonMessages.instance.invalidateConversations(this.siteId);
|
||||||
|
AddonMessages.instance.refreshUnreadConversationCounts(this.siteId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, this.siteId);
|
||||||
|
|
||||||
|
// Update unread conversation counts.
|
||||||
|
this.cronObserver = CoreEvents.on(AddonMessagesProvider.UNREAD_CONVERSATION_COUNTS_EVENT, () => {
|
||||||
|
AddonMessages.instance.refreshUnreadConversationCounts(this.siteId);
|
||||||
|
}, this.siteId);
|
||||||
|
|
||||||
|
// Refresh the view when the app is resumed.
|
||||||
|
this.appResumeSubscription = Platform.instance.resume.subscribe(() => {
|
||||||
|
if (!this.loaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.loaded = false;
|
||||||
|
this.refreshData();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// If a message push notification is received, refresh the view.
|
||||||
|
this.pushObserver = CorePushNotificationsDelegate.instance.on<CorePushNotificationsNotificationBasicData>('receive')
|
||||||
|
.subscribe((notification) => {
|
||||||
|
// New message received. If it's from current site, refresh the data.
|
||||||
|
if (CoreUtils.instance.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(undefined, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component loaded.
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.route.queryParams.subscribe(async params => {
|
||||||
|
this.discussionUserId = params['discussionUserId'] || undefined;
|
||||||
|
|
||||||
|
if (this.discussionUserId) {
|
||||||
|
// There is a discussion to load, open the discussion in a new state.
|
||||||
|
this.gotoDiscussion(this.discussionUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.fetchData();
|
||||||
|
|
||||||
|
if (!this.discussionUserId && this.discussions.length > 0) {
|
||||||
|
// Take first and load it.
|
||||||
|
this.gotoDiscussion(this.discussions[0].message!.user, undefined, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh the data.
|
||||||
|
*
|
||||||
|
* @param refresher Refresher.
|
||||||
|
* @param refreshUnreadCounts Whteher to refresh unread counts.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async refreshData(refresher?: CustomEvent<IonRefresher>, refreshUnreadCounts: boolean = true): Promise<void> {
|
||||||
|
const promises: Promise<void>[] = [];
|
||||||
|
promises.push(AddonMessages.instance.invalidateDiscussionsCache(this.siteId));
|
||||||
|
|
||||||
|
if (refreshUnreadCounts) {
|
||||||
|
promises.push(AddonMessages.instance.invalidateUnreadConversationCounts(this.siteId));
|
||||||
|
}
|
||||||
|
|
||||||
|
await CoreUtils.instance.allPromises(promises).finally(() => this.fetchData().finally(() => {
|
||||||
|
if (refresher) {
|
||||||
|
refresher?.detail.complete();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch discussions.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async fetchData(): Promise<void> {
|
||||||
|
this.loadingMessage = this.loadingMessages;
|
||||||
|
this.search.enabled = AddonMessages.instance.isSearchMessagesEnabled();
|
||||||
|
|
||||||
|
const promises: Promise<unknown>[] = [];
|
||||||
|
|
||||||
|
promises.push(AddonMessages.instance.getDiscussions(this.siteId).then((discussions) => {
|
||||||
|
// Convert to an array for sorting.
|
||||||
|
const discussionsSorted: AddonMessagesDiscussion[] = [];
|
||||||
|
for (const userId in discussions) {
|
||||||
|
discussions[userId].unread = !!discussions[userId].unread;
|
||||||
|
|
||||||
|
discussionsSorted.push(discussions[userId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.discussions = discussionsSorted.sort((a, b) => (b.message?.timecreated || 0) - (a.message?.timecreated || 0));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}));
|
||||||
|
|
||||||
|
promises.push(AddonMessages.instance.getUnreadConversationCounts(this.siteId));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all(promises);
|
||||||
|
} catch (error) {
|
||||||
|
CoreDomUtils.instance.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingdiscussions', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear search and show discussions again.
|
||||||
|
*/
|
||||||
|
clearSearch(): void {
|
||||||
|
this.loaded = false;
|
||||||
|
this.search.showResults = false;
|
||||||
|
this.search.text = ''; // Reset searched string.
|
||||||
|
this.fetchData().finally(() => {
|
||||||
|
this.loaded = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search messages cotaining text.
|
||||||
|
*
|
||||||
|
* @param query Text to search for.
|
||||||
|
* @return Resolved when done.
|
||||||
|
*/
|
||||||
|
async searchMessage(query: string): Promise<void> {
|
||||||
|
CoreApp.instance.closeKeyboard();
|
||||||
|
this.loaded = false;
|
||||||
|
this.loadingMessage = this.search.loading;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const searchResults = await AddonMessages.instance.searchMessages(query, undefined, undefined, undefined, this.siteId);
|
||||||
|
this.search.showResults = true;
|
||||||
|
this.search.results = searchResults.messages;
|
||||||
|
} catch (error) {
|
||||||
|
CoreDomUtils.instance.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingmessages', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to a particular discussion.
|
||||||
|
*
|
||||||
|
* @param discussionUserId Discussion Id to load.
|
||||||
|
* @param messageId Message to scroll after loading the discussion. Used when searching.
|
||||||
|
* @param onlyWithSplitView Only go to Discussion if split view is on.
|
||||||
|
*/
|
||||||
|
gotoDiscussion(discussionUserId: number, messageId?: number, onlyWithSplitView: boolean = false): void {
|
||||||
|
this.discussionUserId = discussionUserId;
|
||||||
|
|
||||||
|
const params: AddonMessagesSplitViewLoadIndexEventData = {
|
||||||
|
discussion: discussionUserId,
|
||||||
|
onlyWithSplitView: onlyWithSplitView,
|
||||||
|
};
|
||||||
|
if (messageId) {
|
||||||
|
params.message = messageId;
|
||||||
|
}
|
||||||
|
CoreEvents.trigger(AddonMessagesProvider.SPLIT_VIEW_LOAD_INDEX_EVENT, params, this.siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component destroyed.
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.newMessagesObserver?.off();
|
||||||
|
this.readChangedObserver?.off();
|
||||||
|
this.cronObserver?.off();
|
||||||
|
this.appResumeSubscription?.unsubscribe();
|
||||||
|
this.pushObserver?.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,127 @@
|
||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-buttons slot="start">
|
||||||
|
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||||
|
</ion-buttons>
|
||||||
|
<ion-title>{{ 'addon.messages.messages' | translate }}</ion-title>
|
||||||
|
<ion-buttons slot="end">
|
||||||
|
<ion-button (click)="gotoSearch()" [attr.aria-label]="'addon.messages.search' | translate">
|
||||||
|
<ion-icon name="fas-search" slot="icon-only"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
<ion-button (click)="gotoSettings($event)" [attr.aria-label]="'addon.messages.messagepreferences' | translate">
|
||||||
|
<ion-icon name="fas-cog" slot="icon-only"></ion-icon>
|
||||||
|
</ion-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-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<!-- @todo <core-split-view> -->
|
||||||
|
<ion-content class="core-expand-max">
|
||||||
|
<ion-refresher slot="fixed" [disabled]="!loaded || !currentListEl" (ionRefresh)="refreshData($event)">
|
||||||
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
|
</ion-refresher>
|
||||||
|
|
||||||
|
<core-loading [hideUntil]="loaded" [message]="loadingMessage">
|
||||||
|
<ion-list>
|
||||||
|
<ion-item class="ion-text-wrap" (click)="gotoContacts($event)" [attr.aria-label]="'addon.messages.contacts' | translate"
|
||||||
|
class="addon-message-discussion">
|
||||||
|
<ion-icon name="fas-address-book" slot="start"></ion-icon>
|
||||||
|
<ion-label><h2>{{ 'addon.messages.contacts' | translate }}</h2></ion-label>
|
||||||
|
<ion-badge *ngIf="contactRequestsCount > 0" slot="end">{{contactRequestsCount}}</ion-badge>
|
||||||
|
</ion-item>
|
||||||
|
<!-- Favourite conversations. -->
|
||||||
|
<ion-item-divider class="ion-text-wrap" (click)="toggle(favourites)" class="core-expandable" sticky="true">
|
||||||
|
<ion-icon *ngIf="!favourites.expanded" name="fas-caret-right" slot="start"></ion-icon>
|
||||||
|
<ion-icon *ngIf="favourites.expanded" name="fas-caret-down" slot="start"></ion-icon>
|
||||||
|
<ion-label>{{ 'core.favourites' | translate }} ({{ favourites.count }})</ion-label>
|
||||||
|
<ion-badge slot="end" *ngIf="favourites.unread">{{ favourites.unread }}</ion-badge>
|
||||||
|
</ion-item-divider>
|
||||||
|
<div [hidden]="!favourites.conversations || !favourites.expanded || favourites.loading" #favlist>
|
||||||
|
<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)" [error]="favourites.loadMoreError"></core-infinite-loading>
|
||||||
|
<ion-item class="ion-text-wrap" *ngIf="favourites.conversations && favourites.conversations.length == 0">
|
||||||
|
<ion-label><p>{{ 'addon.messages.nofavourites' | translate }}</p></ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</div>
|
||||||
|
<ion-item class="ion-text-center" *ngIf="favourites.loading">
|
||||||
|
<ion-label><ion-spinner></ion-spinner></ion-label>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<!-- Group conversations. -->
|
||||||
|
<ion-item-divider class="ion-text-wrap" (click)="toggle(group)" class="core-expandable" sticky="true">
|
||||||
|
<ion-icon *ngIf="!group.expanded" name="fas-caret-right" slot="start"></ion-icon>
|
||||||
|
<ion-icon *ngIf="group.expanded" name="fas-caret-down" slot="start"></ion-icon>
|
||||||
|
<ion-label>{{ 'addon.messages.groupconversations' | translate }} ({{ group.count }})</ion-label>
|
||||||
|
<ion-badge slot="end" *ngIf="group.unread">{{ group.unread }}</ion-badge>
|
||||||
|
</ion-item-divider>
|
||||||
|
<div [hidden]="!group.conversations || !group.expanded || group.loading" #grouplist>
|
||||||
|
<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)" [error]="group.loadMoreError"></core-infinite-loading>
|
||||||
|
<ion-item class="ion-text-wrap" *ngIf="group.conversations && group.conversations.length == 0">
|
||||||
|
<ion-label><p>{{ 'addon.messages.nogroupconversations' | translate }}</p></ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</div>
|
||||||
|
<ion-item class="ion-text-center" *ngIf="group.loading">
|
||||||
|
<ion-label><ion-spinner></ion-spinner></ion-label>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<ion-item-divider class="ion-text-wrap" (click)="toggle(individual)" class="core-expandable" sticky="true">
|
||||||
|
<ion-icon *ngIf="!individual.expanded" name="fas-caret-right" slot="start"></ion-icon>
|
||||||
|
<ion-icon *ngIf="individual.expanded" name="fas-caret-down" slot="start"></ion-icon>
|
||||||
|
<ion-label>{{ 'addon.messages.individualconversations' | translate }} ({{ individual.count }})</ion-label>
|
||||||
|
<ion-badge slot="end" *ngIf="individual.unread">{{ individual.unread }}</ion-badge>
|
||||||
|
</ion-item-divider>
|
||||||
|
<div [hidden]="!individual.conversations || !individual.expanded || individual.loading" #indlist>
|
||||||
|
<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)" [error]="individual.loadMoreError"></core-infinite-loading>
|
||||||
|
<ion-item class="ion-text-wrap" *ngIf="individual.conversations && individual.conversations.length == 0">
|
||||||
|
<ion-label><p>{{ 'addon.messages.noindividualconversations' | translate }}</p></ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</div>
|
||||||
|
<ion-item class="ion-text-center" *ngIf="individual.loading">
|
||||||
|
<ion-label><ion-spinner></ion-spinner></ion-label>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
</ion-list>
|
||||||
|
</core-loading>
|
||||||
|
</ion-content>
|
||||||
|
|
||||||
|
<!-- Template to render a list of conversations. -->
|
||||||
|
<ng-template #conversationsTemplate let-conversations="conversations">
|
||||||
|
<ion-item class="ion-text-wrap" *ngFor="let conversation of conversations" [title]="conversation.name" (click)="gotoConversation(conversation.id, conversation.userid)" [class.core-split-item-selected]="(conversation.id && conversation.id == selectedConversationId) || (conversation.userid && conversation.userid == selectedUserId)" class="addon-message-discussion" id="addon-message-conversation-{{ conversation.id ? conversation.id : 'user-' + conversation.userid }}">
|
||||||
|
<!-- Group conversation image. -->
|
||||||
|
<ion-avatar slot="start" *ngIf="conversation.type == typeGroup">
|
||||||
|
<img [src]="conversation.imageurl" [alt]="conversation.name" core-external-content onError="this.src='assets/img/group-avatar.png'">
|
||||||
|
</ion-avatar>
|
||||||
|
|
||||||
|
<!-- Avatar for individual conversations. -->
|
||||||
|
<core-user-avatar *ngIf="conversation.type != typeGroup" core-user-avatar [user]="conversation.otherUser"
|
||||||
|
[linkProfile]="false" [checkOnline]="conversation.showonlinestatus" slot="start"></core-user-avatar>
|
||||||
|
|
||||||
|
<ion-label>
|
||||||
|
<h2>
|
||||||
|
<core-format-text [text]="conversation.name" contextLevel="system" [contextInstanceId]="0"></core-format-text>
|
||||||
|
<ion-icon name="fas-user-slash" *ngIf="conversation.isblocked" [title]="'addon.messages.contactblocked' | translate">
|
||||||
|
</ion-icon>
|
||||||
|
<ion-icon *ngIf="conversation.ismuted" name="fas-volume-mute" [title]="'addon.messages.mutedconversation' | translate">
|
||||||
|
</ion-icon>
|
||||||
|
</h2>
|
||||||
|
<ion-note *ngIf="conversation.lastmessagedate > 0 || conversation.unreadcount">
|
||||||
|
<ion-badge *ngIf="conversation.unreadcount > 0">{{ conversation.unreadcount }}</ion-badge>
|
||||||
|
<span *ngIf="conversation.lastmessagedate > 0">{{conversation.lastmessagedate | coreDateDayOrTime}}</span>
|
||||||
|
</ion-note>
|
||||||
|
<p *ngIf="conversation.subname"><core-format-text [text]="conversation.subname" contextLevel="system" [contextInstanceId]="0"></core-format-text></p>
|
||||||
|
<p class="addon-message-last-message">
|
||||||
|
<span *ngIf="conversation.sentfromcurrentuser" class="addon-message-last-message-user">{{ 'addon.messages.you' | translate }}</span>
|
||||||
|
<span *ngIf="!conversation.sentfromcurrentuser && conversation.type == typeGroup && conversation.members[0]" class="addon-message-last-message-user">{{ conversation.members[0].fullname + ':' }}</span>
|
||||||
|
<core-format-text clean="true" singleLine="true" [text]="conversation.lastmessage" class="addon-message-last-message-text" contextLevel="system" [contextInstanceId]="0"></core-format-text>
|
||||||
|
</p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ng-template>
|
|
@ -0,0 +1,45 @@
|
||||||
|
// (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 { NgModule } from '@angular/core';
|
||||||
|
import { IonicModule } from '@ionic/angular';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
|
import { CoreSharedModule } from '@/core/shared.module';
|
||||||
|
|
||||||
|
import { AddonMessagesGroupConversationsPage } from './group-conversations.page';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: AddonMessagesGroupConversationsPage,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forChild(routes),
|
||||||
|
CommonModule,
|
||||||
|
IonicModule,
|
||||||
|
TranslateModule.forChild(),
|
||||||
|
CoreSharedModule,
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
AddonMessagesGroupConversationsPage,
|
||||||
|
],
|
||||||
|
exports: [RouterModule],
|
||||||
|
})
|
||||||
|
export class AddonMessagesGroupConversationsPageModule {}
|
|
@ -0,0 +1,810 @@
|
||||||
|
// (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 { IonContent, IonRefresher } from '@ionic/angular';
|
||||||
|
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import {
|
||||||
|
AddonMessagesProvider,
|
||||||
|
AddonMessagesConversationFormatted,
|
||||||
|
AddonMessages,
|
||||||
|
AddonMessagesMemberInfoChangedEventData,
|
||||||
|
AddonMessagesContactRequestCountEventData,
|
||||||
|
AddonMessagesUnreadConversationCountsEventData,
|
||||||
|
} from '../../services/messages';
|
||||||
|
import { AddonMessagesOffline } from '../../services/messages-offline';
|
||||||
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
import { CoreUser } from '@features/user/services/user';
|
||||||
|
import { CorePushNotificationsDelegate } from '@features/pushnotifications/services/push-delegate';
|
||||||
|
import { Platform, Translate } from '@singletons';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
import { CorePushNotificationsNotificationBasicData } from '@features/pushnotifications/services/pushnotifications';
|
||||||
|
import { ActivatedRoute, Params } from '@angular/router';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
import {
|
||||||
|
AddonMessagesOfflineConversationMessagesDBRecordFormatted,
|
||||||
|
AddonMessagesOfflineMessagesDBRecordFormatted,
|
||||||
|
} from '@addons/messages/services/database/messages';
|
||||||
|
// import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page that displays the list of conversations, including group conversations.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'page-addon-messages-group-conversations',
|
||||||
|
templateUrl: 'group-conversations.html',
|
||||||
|
styleUrls: ['../../messages-common.scss'],
|
||||||
|
})
|
||||||
|
export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
// @ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent;
|
||||||
|
@ViewChild(IonContent) content?: IonContent;
|
||||||
|
@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: undefined,
|
||||||
|
favourites: true,
|
||||||
|
count: 0,
|
||||||
|
unread: 0,
|
||||||
|
conversations: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
group: AddonMessagesGroupConversationOption = {
|
||||||
|
type: AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_GROUP,
|
||||||
|
favourites: false,
|
||||||
|
count: 0,
|
||||||
|
unread: 0,
|
||||||
|
conversations: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
individual: AddonMessagesGroupConversationOption = {
|
||||||
|
type: AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
|
||||||
|
favourites: false,
|
||||||
|
count: 0,
|
||||||
|
unread: 0,
|
||||||
|
conversations: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
typeGroup = AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_GROUP;
|
||||||
|
currentListEl?: HTMLElement;
|
||||||
|
|
||||||
|
protected siteId: string;
|
||||||
|
protected currentUserId: number;
|
||||||
|
protected conversationId?: number;
|
||||||
|
protected discussionUserId?: number;
|
||||||
|
protected newMessagesObserver: CoreEventObserver;
|
||||||
|
protected pushObserver: Subscription;
|
||||||
|
protected appResumeSubscription: Subscription;
|
||||||
|
protected readChangedObserver: CoreEventObserver;
|
||||||
|
protected cronObserver: CoreEventObserver;
|
||||||
|
protected openConversationObserver: CoreEventObserver;
|
||||||
|
protected updateConversationListObserver: CoreEventObserver;
|
||||||
|
protected contactRequestsCountObserver: CoreEventObserver;
|
||||||
|
protected memberInfoObserver: CoreEventObserver;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected route: ActivatedRoute,
|
||||||
|
) {
|
||||||
|
|
||||||
|
this.loadingMessage = Translate.instance.instant('core.loading');
|
||||||
|
this.siteId = CoreSites.instance.getCurrentSiteId();
|
||||||
|
this.currentUserId = CoreSites.instance.getCurrentSiteUserId();
|
||||||
|
|
||||||
|
// Update conversations when new message is received.
|
||||||
|
this.newMessagesObserver = CoreEvents.on(AddonMessagesProvider.NEW_MESSAGE_EVENT, (data: any) => {
|
||||||
|
// Check if the new message belongs to the option that is currently expanded.
|
||||||
|
const expandedOption = this.getExpandedOption();
|
||||||
|
const 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;
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (conversation.lastmessage != data.message || conversation.lastmessagedate != data.timecreated / 1000) {
|
||||||
|
const isNewer = data.timecreated / 1000 > (conversation.lastmessagedate || 0);
|
||||||
|
|
||||||
|
// 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 = AddonMessages.instance.sortConversations(option.conversations || []);
|
||||||
|
|
||||||
|
if (isNewer) {
|
||||||
|
// The last message is newer than the previous one, scroll to top to keep viewing the conversation.
|
||||||
|
this.content?.scrollToTop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, this.siteId);
|
||||||
|
|
||||||
|
// Update conversations when a message is read.
|
||||||
|
this.readChangedObserver = CoreEvents.on(AddonMessagesProvider.READ_CHANGED_EVENT, (data: any) => {
|
||||||
|
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.
|
||||||
|
AddonMessages.instance.invalidateConversations(this.siteId);
|
||||||
|
AddonMessages.instance.refreshUnreadConversationCounts(this.siteId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, this.siteId);
|
||||||
|
|
||||||
|
// Load a discussion if we receive an event to do so.
|
||||||
|
this.openConversationObserver = CoreEvents.on(AddonMessagesProvider.OPEN_CONVERSATION_EVENT, (data: any) => {
|
||||||
|
if (data.conversationId || data.userId) {
|
||||||
|
this.gotoConversation(data.conversationId, data.userId);
|
||||||
|
}
|
||||||
|
}, this.siteId);
|
||||||
|
|
||||||
|
// Refresh the view when the app is resumed.
|
||||||
|
this.appResumeSubscription = Platform.instance.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 = CoreEvents.on(AddonMessagesProvider.UPDATE_CONVERSATION_LIST_EVENT, (data: any) => {
|
||||||
|
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 = CorePushNotificationsDelegate.instance.on<CorePushNotificationsNotificationBasicData>('receive')
|
||||||
|
.subscribe((notification) => {
|
||||||
|
// New message received. If it's from current site, refresh the data.
|
||||||
|
if (CoreUtils.instance.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(undefined, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update unread conversation counts.
|
||||||
|
this.cronObserver = CoreEvents.on<AddonMessagesUnreadConversationCountsEventData>(
|
||||||
|
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 = CoreEvents.on<AddonMessagesContactRequestCountEventData>(
|
||||||
|
AddonMessagesProvider.CONTACT_REQUESTS_COUNT_EVENT,
|
||||||
|
(data) => {
|
||||||
|
this.contactRequestsCount = data.count;
|
||||||
|
},
|
||||||
|
this.siteId,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update block status of a user.
|
||||||
|
this.memberInfoObserver = CoreEvents.on<AddonMessagesMemberInfoChangedEventData>(
|
||||||
|
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 {
|
||||||
|
this.route.queryParams.subscribe(async params => {
|
||||||
|
// Conversation to load.
|
||||||
|
this.conversationId = params['conversationId'] || undefined;
|
||||||
|
this.discussionUserId = !this.conversationId && (params['discussionUserId'] || undefined);
|
||||||
|
|
||||||
|
if (this.conversationId || this.discussionUserId) {
|
||||||
|
// There is a discussion to load, open the discussion in a new state.
|
||||||
|
this.gotoConversation(this.conversationId, this.discussionUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.fetchData();
|
||||||
|
/* @todo if (!this.conversationId && !this.discussionUserId && this.splitviewCtrl.isOn()) {
|
||||||
|
// Load the first conversation.
|
||||||
|
let conversation: AddonMessagesConversationForList;
|
||||||
|
const expandedOption = this.getExpandedOption();
|
||||||
|
|
||||||
|
if (expandedOption && expandedOption.conversations.length) {
|
||||||
|
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 async fetchData(refreshUnreadCounts: boolean = true): Promise<void> {
|
||||||
|
// Load the amount of conversations and contact requests.
|
||||||
|
const promises: Promise<unknown>[] = [];
|
||||||
|
|
||||||
|
promises.push(this.fetchConversationCounts());
|
||||||
|
|
||||||
|
// View updated by the events observers.
|
||||||
|
promises.push(AddonMessages.instance.getContactRequestsCount(this.siteId));
|
||||||
|
if (refreshUnreadCounts) {
|
||||||
|
promises.push(AddonMessages.instance.refreshUnreadConversationCounts(this.siteId));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all(promises);
|
||||||
|
|
||||||
|
// The expanded status hasn't been initialized. Do it now.
|
||||||
|
if (typeof this.favourites.expanded == 'undefined' && 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: Promise<void>[] = [];
|
||||||
|
|
||||||
|
promises.push(this.fetchDataForOption(this.favourites, false));
|
||||||
|
promises.push(this.fetchDataForOption(this.group, false));
|
||||||
|
promises.push(this.fetchDataForOption(this.individual, false));
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
await this.expandOption(option);
|
||||||
|
|
||||||
|
this.loaded = true;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the data for the expanded option.
|
||||||
|
await this.fetchDataForExpandedOption();
|
||||||
|
} catch (error) {
|
||||||
|
CoreDomUtils.instance.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingdiscussions', true);
|
||||||
|
}
|
||||||
|
this.loaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch data for the expanded option.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async fetchDataForExpandedOption(): Promise<void> {
|
||||||
|
// Calculate which option should be expanded initially.
|
||||||
|
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();
|
||||||
|
|
||||||
|
const expandedOption = this.getExpandedOption();
|
||||||
|
|
||||||
|
if (expandedOption) {
|
||||||
|
await this.fetchDataForOption(expandedOption, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
async fetchDataForOption(
|
||||||
|
option: AddonMessagesGroupConversationOption,
|
||||||
|
loadingMore = false,
|
||||||
|
getCounts = false,
|
||||||
|
): Promise<void> {
|
||||||
|
option.loadMoreError = false;
|
||||||
|
|
||||||
|
const limitFrom = loadingMore ? option.conversations.length : 0;
|
||||||
|
const promises: Promise<unknown>[] = [];
|
||||||
|
|
||||||
|
let data: { conversations: AddonMessagesConversationForList[]; canLoadMore: boolean } = {
|
||||||
|
conversations: [],
|
||||||
|
canLoadMore: false,
|
||||||
|
};
|
||||||
|
let offlineMessages:
|
||||||
|
(AddonMessagesOfflineConversationMessagesDBRecordFormatted | AddonMessagesOfflineMessagesDBRecordFormatted)[] = [];
|
||||||
|
|
||||||
|
// Get the conversations and, if needed, the offline messages. Always try to get the latest data.
|
||||||
|
promises.push(AddonMessages.instance.invalidateConversations(this.siteId).then(async () => {
|
||||||
|
data = await AddonMessages.instance.getConversations(option.type, option.favourites, limitFrom, this.siteId);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!loadingMore) {
|
||||||
|
promises.push(AddonMessagesOffline.instance.getAllMessages().then((messages) => {
|
||||||
|
offlineMessages = messages;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getCounts) {
|
||||||
|
promises.push(this.fetchConversationCounts());
|
||||||
|
promises.push(AddonMessages.instance.refreshUnreadConversationCounts(this.siteId));
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
|
||||||
|
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) {
|
||||||
|
await this.loadOfflineMessages(option, offlineMessages);
|
||||||
|
|
||||||
|
// Sort the conversations, the offline messages could affect the order.
|
||||||
|
option.conversations = AddonMessages.instance.sortConversations(option.conversations);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch conversation counts.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async fetchConversationCounts(): Promise<void> {
|
||||||
|
// Always try to get the latest data.
|
||||||
|
await AddonMessages.instance.invalidateConversationCounts(this.siteId);
|
||||||
|
|
||||||
|
const counts = await AddonMessages.instance.getConversationCounts(this.siteId);
|
||||||
|
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 | undefined {
|
||||||
|
|
||||||
|
if (conversationId) {
|
||||||
|
const conversations: AddonMessagesConversationForList[] = option
|
||||||
|
? option.conversations
|
||||||
|
: (this.favourites.conversations.concat(this.group.conversations).concat(this.individual.conversations));
|
||||||
|
|
||||||
|
return conversations.find((conv) => conv.id == conversationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const conversations = option
|
||||||
|
? option.conversations
|
||||||
|
: this.favourites.conversations.concat(this.individual.conversations);
|
||||||
|
|
||||||
|
return conversations.find((conv) => conv.userid == userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the option that is currently expanded, undefined if they are all collapsed.
|
||||||
|
*
|
||||||
|
* @return Option currently expanded.
|
||||||
|
*/
|
||||||
|
protected getExpandedOption(): AddonMessagesGroupConversationOption | undefined {
|
||||||
|
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 {
|
||||||
|
// @todo this.splitviewCtrl.getMasterNav().push('AddonMessagesContactsPage');
|
||||||
|
CoreNavigator.instance.navigateToSitePath('contacts');
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: Params = {
|
||||||
|
conversationId: conversationId,
|
||||||
|
userId: userId,
|
||||||
|
};
|
||||||
|
if (messageId) {
|
||||||
|
params.message = messageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @todo this.splitviewCtrl.push
|
||||||
|
CoreNavigator.instance.navigateToSitePath('discussion', { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to message settings.
|
||||||
|
*/
|
||||||
|
gotoSettings(): void {
|
||||||
|
// @todo this.splitviewCtrl.push
|
||||||
|
CoreNavigator.instance.navigateToSitePath('settings');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
async loadMoreConversations(option: AddonMessagesGroupConversationOption, infiniteComplete?: () => void): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.fetchDataForOption(option, true);
|
||||||
|
} catch (error) {
|
||||||
|
CoreDomUtils.instance.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingdiscussions', true);
|
||||||
|
option.loadMoreError = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 async loadOfflineMessages(
|
||||||
|
option: AddonMessagesGroupConversationOption,
|
||||||
|
messages: (AddonMessagesOfflineConversationMessagesDBRecordFormatted | AddonMessagesOfflineMessagesDBRecordFormatted)[],
|
||||||
|
): Promise<void> {
|
||||||
|
const promises: Promise<void>[] = [];
|
||||||
|
|
||||||
|
messages.forEach((message) => {
|
||||||
|
if ('conversationid' in message) {
|
||||||
|
// 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 || 0) <= 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 = {
|
||||||
|
id: message.conversationid,
|
||||||
|
type: message.conversation?.type || AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
|
||||||
|
membercount: message.conversation?.membercount || 0,
|
||||||
|
ismuted: message.conversation?.ismuted || false,
|
||||||
|
isfavourite: message.conversation?.isfavourite || false,
|
||||||
|
isread: message.conversation?.isread || false,
|
||||||
|
members: message.conversation?.members || [],
|
||||||
|
messages: message.conversation?.messages || [],
|
||||||
|
candeletemessagesforallusers: message.conversation?.candeletemessagesforallusers || false,
|
||||||
|
userid: 0, // Faked data.
|
||||||
|
name: message.conversation?.name,
|
||||||
|
imageurl: message.conversation?.imageurl || '',
|
||||||
|
}; message.conversation || {};
|
||||||
|
|
||||||
|
if (this.getConversationOption(conversation) == option) {
|
||||||
|
// Message belongs to current option, add the conversation.
|
||||||
|
this.addLastOfflineMessage(conversation, message);
|
||||||
|
this.addOfflineConversation(conversation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (option.type == AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_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 || 0) <= 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(CoreUser.instance.getProfile(message.touserid, undefined, true).catch(() => {
|
||||||
|
// User not found.
|
||||||
|
}).then((user) => {
|
||||||
|
const conversation: AddonMessagesConversationForList = {
|
||||||
|
id: 0,
|
||||||
|
type: AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
|
||||||
|
membercount: 0, // Faked data.
|
||||||
|
ismuted: false, // Faked data.
|
||||||
|
isfavourite: false, // Faked data.
|
||||||
|
isread: false, // Faked data.
|
||||||
|
members: [], // Faked data.
|
||||||
|
messages: [], // Faked data.
|
||||||
|
candeletemessagesforallusers: false,
|
||||||
|
userid: message.touserid,
|
||||||
|
name: user ? user.fullname : String(message.touserid),
|
||||||
|
imageurl: user ? user.profileimageurl : '',
|
||||||
|
};
|
||||||
|
|
||||||
|
this.addLastOfflineMessage(conversation, message);
|
||||||
|
this.addOfflineConversation(conversation);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an offline conversation into the right list of conversations.
|
||||||
|
*
|
||||||
|
* @param conversation Offline conversation to add.
|
||||||
|
*/
|
||||||
|
protected addOfflineConversation(conversation: AddonMessagesConversationForList): 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: AddonMessagesConversationForList,
|
||||||
|
message: AddonMessagesOfflineConversationMessagesDBRecordFormatted | AddonMessagesOfflineMessagesDBRecordFormatted,
|
||||||
|
): 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conversation.type == AddonMessagesProvider.MESSAGE_CONVERSATION_TYPE_GROUP) {
|
||||||
|
return this.group;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.individual;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh the data.
|
||||||
|
*
|
||||||
|
* @param refresher Refresher.
|
||||||
|
* @param refreshUnreadCounts Whether to refresh unread counts.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async refreshData(refresher?: CustomEvent<IonRefresher>, refreshUnreadCounts: boolean = true): Promise<void> {
|
||||||
|
// Don't invalidate conversations and so, they always try to get latest data.
|
||||||
|
try {
|
||||||
|
await AddonMessages.instance.invalidateContactRequestsCountCache(this.siteId);
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
await this.fetchData(refreshUnreadCounts);
|
||||||
|
} finally {
|
||||||
|
if (refresher) {
|
||||||
|
refresher?.detail.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) => {
|
||||||
|
CoreDomUtils.instance.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 async expandOption(option: AddonMessagesGroupConversationOption, getCounts = false): 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;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.fetchDataForOption(option, false, getCounts);
|
||||||
|
|
||||||
|
this.loadCurrentListElement();
|
||||||
|
} catch (error) {
|
||||||
|
option.expanded = false;
|
||||||
|
|
||||||
|
throw 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 {
|
||||||
|
CoreNavigator.instance.navigateToSitePath('search');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page destroyed.
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.newMessagesObserver?.off();
|
||||||
|
this.appResumeSubscription?.unsubscribe();
|
||||||
|
this.pushObserver?.unsubscribe();
|
||||||
|
this.readChangedObserver?.off();
|
||||||
|
this.cronObserver?.off();
|
||||||
|
this.openConversationObserver?.off();
|
||||||
|
this.updateConversationListObserver?.off();
|
||||||
|
this.contactRequestsCountObserver?.off();
|
||||||
|
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.
|
||||||
|
};
|
|
@ -0,0 +1,16 @@
|
||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-buttons slot="start">
|
||||||
|
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||||
|
</ion-buttons>
|
||||||
|
<ion-title>{{ 'addon.messages.messages' | translate }}</ion-title>
|
||||||
|
<ion-buttons slot="end">
|
||||||
|
<!-- 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-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<!-- @todo <core-split-view> -->
|
||||||
|
<ion-content>
|
||||||
|
<core-tabs [tabs]="tabs" hideUntil="true"></core-tabs>
|
||||||
|
</ion-content>
|
|
@ -0,0 +1,64 @@
|
||||||
|
// (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 { Injector, NgModule } from '@angular/core';
|
||||||
|
import { IonicModule } from '@ionic/angular';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { RouterModule, ROUTES, Routes } from '@angular/router';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
|
import { CoreSharedModule } from '@/core/shared.module';
|
||||||
|
|
||||||
|
import { AddonMessagesIndex35Page } from './index.page';
|
||||||
|
import { ADDON_MESSAGES_INDEX_ROUTES } from './messages-index-routing.module';
|
||||||
|
import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module';
|
||||||
|
import { resolveModuleRoutes } from '@/app/app-routing.module';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: AddonMessagesIndex35Page,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function buildRoutes(injector: Injector): Routes {
|
||||||
|
const routes = resolveModuleRoutes(injector, ADDON_MESSAGES_INDEX_ROUTES);
|
||||||
|
|
||||||
|
return [
|
||||||
|
...buildTabMainRoutes(injector, {
|
||||||
|
path: '',
|
||||||
|
component: AddonMessagesIndex35Page,
|
||||||
|
children: routes.children,
|
||||||
|
}),
|
||||||
|
...routes.siblings,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forChild(routes),
|
||||||
|
CommonModule,
|
||||||
|
IonicModule,
|
||||||
|
TranslateModule.forChild(),
|
||||||
|
CoreSharedModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: ROUTES, multi: true, useFactory: buildRoutes, deps: [Injector] },
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
AddonMessagesIndex35Page,
|
||||||
|
],
|
||||||
|
exports: [RouterModule],
|
||||||
|
})
|
||||||
|
export class AddonMessagesIndex35PageModule {}
|
|
@ -0,0 +1,102 @@
|
||||||
|
// (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, OnDestroy } from '@angular/core';
|
||||||
|
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreTab } from '@components/tabs/tabs';
|
||||||
|
import { Params } from '@angular/router';
|
||||||
|
import { AddonMessagesProvider, AddonMessagesSplitViewLoadIndexEventData } from '../../services/messages';
|
||||||
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
// import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page that displays the messages index page.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'page-addon-messages-index',
|
||||||
|
templateUrl: 'index.html',
|
||||||
|
})
|
||||||
|
export class AddonMessagesIndex35Page implements OnDestroy {
|
||||||
|
|
||||||
|
// @ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent;
|
||||||
|
|
||||||
|
tabs: CoreTab[] = [
|
||||||
|
{
|
||||||
|
id: 'discussions-35',
|
||||||
|
class: '',
|
||||||
|
title: 'addon.messages.messages',
|
||||||
|
icon: 'fas-comments',
|
||||||
|
enabled: true,
|
||||||
|
page: 'main/messages/index/discussions',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'contacts-35',
|
||||||
|
class: '',
|
||||||
|
title: 'addon.messages.contacts',
|
||||||
|
icon: 'fas-address-book',
|
||||||
|
enabled: true,
|
||||||
|
page: 'main/messages/index/contacts',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
protected loadSplitViewObserver?: CoreEventObserver;
|
||||||
|
protected siteId: string;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
|
||||||
|
this.siteId = CoreSites.instance.getCurrentSiteId();
|
||||||
|
|
||||||
|
// Update split view or navigate.
|
||||||
|
this.loadSplitViewObserver = CoreEvents.on<AddonMessagesSplitViewLoadIndexEventData>(
|
||||||
|
AddonMessagesProvider.SPLIT_VIEW_LOAD_INDEX_EVENT,
|
||||||
|
(data) => {
|
||||||
|
if (data.discussion /* @todo && (this.splitviewCtrl.isOn() || !data.onlyWithSplitView)*/) {
|
||||||
|
this.gotoDiscussion(data.discussion, data.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
this.siteId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to a particular discussion.
|
||||||
|
*
|
||||||
|
* @param discussionUserId Discussion Id to load.
|
||||||
|
* @param messageId Message to scroll after loading the discussion. Used when searching.
|
||||||
|
*/
|
||||||
|
gotoDiscussion(discussionUserId: number, messageId?: number): void {
|
||||||
|
const params: Params = {
|
||||||
|
userId: discussionUserId,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (messageId) {
|
||||||
|
params.message = messageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @todo
|
||||||
|
// this.splitviewCtrl.push('discussion', { params });
|
||||||
|
CoreNavigator.instance.navigateToSitePath('discussion', { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page destroyed.
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.loadSplitViewObserver?.off();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
// (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 { InjectionToken, ModuleWithProviders, NgModule } from '@angular/core';
|
||||||
|
|
||||||
|
import { ModuleRoutesConfig } from '@/app/app-routing.module';
|
||||||
|
|
||||||
|
export const ADDON_MESSAGES_INDEX_ROUTES = new InjectionToken('ADDON_MESSAGES_INDEX_ROUTES');
|
||||||
|
|
||||||
|
@NgModule()
|
||||||
|
export class AddonMessagesIndexRoutingModule {
|
||||||
|
|
||||||
|
static forChild(routes: ModuleRoutesConfig): ModuleWithProviders<AddonMessagesIndexRoutingModule> {
|
||||||
|
return {
|
||||||
|
ngModule: AddonMessagesIndexRoutingModule,
|
||||||
|
providers: [
|
||||||
|
{ provide: ADDON_MESSAGES_INDEX_ROUTES, multi: true, useValue: routes },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,111 @@
|
||||||
|
// (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 { CoreSiteSchema } from '@services/sites';
|
||||||
|
import { AddonMessagesConversation } from '../messages';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database variables for AddonMessagesOffline service.
|
||||||
|
*/
|
||||||
|
export const MESSAGES_TABLE = 'addon_messages_offline_messages'; // When group messaging isn't available or new conversation starts.
|
||||||
|
export const CONVERSATION_MESSAGES_TABLE = 'addon_messages_offline_conversation_messages'; // Conversation messages.
|
||||||
|
export const MESSAGES_OFFLINE_SITE_SCHEMA: CoreSiteSchema = {
|
||||||
|
name: 'AddonMessagesOfflineProvider',
|
||||||
|
version: 1,
|
||||||
|
tables: [
|
||||||
|
{
|
||||||
|
name: MESSAGES_TABLE,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'touserid',
|
||||||
|
type: 'INTEGER',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'useridfrom',
|
||||||
|
type: 'INTEGER',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'smallmessage',
|
||||||
|
type: 'TEXT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'timecreated',
|
||||||
|
type: 'INTEGER',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'deviceoffline', // If message was stored because device was offline.
|
||||||
|
type: 'INTEGER',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
primaryKeys: ['touserid', 'smallmessage', 'timecreated'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 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'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AddonMessagesOfflineMessagesDBRecord = {
|
||||||
|
touserid: number;
|
||||||
|
useridfrom: number;
|
||||||
|
smallmessage: string;
|
||||||
|
timecreated: number;
|
||||||
|
deviceoffline: number; // If message was stored because device was offline.
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AddonMessagesOfflineMessagesDBRecordFormatted = AddonMessagesOfflineMessagesDBRecord & {
|
||||||
|
pending?: boolean; // Will be likely true.
|
||||||
|
text?: string; // Copy of smallmessage.
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AddonMessagesOfflineConversationMessagesDBRecord = {
|
||||||
|
conversationid: number;
|
||||||
|
text: string;
|
||||||
|
timecreated: number;
|
||||||
|
deviceoffline: number; // If message was stored because device was offline.
|
||||||
|
conversation: string; // Data about the conversation.
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AddonMessagesOfflineConversationMessagesDBRecordFormatted =
|
||||||
|
Omit<AddonMessagesOfflineConversationMessagesDBRecord, 'conversation'> &
|
||||||
|
{
|
||||||
|
conversation?: AddonMessagesConversation; // Data about the conversation.
|
||||||
|
pending: boolean; // Will be always true.
|
||||||
|
useridfrom?: number; // User Id who send the message, will be likely us.
|
||||||
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,223 @@
|
||||||
|
// (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 { Injectable } from '@angular/core';
|
||||||
|
import {
|
||||||
|
AddonMessagesProvider,
|
||||||
|
AddonMessages,
|
||||||
|
AddonMessagesUnreadConversationCountsEventData,
|
||||||
|
AddonMessagesContactRequestCountEventData,
|
||||||
|
} from '../messages';
|
||||||
|
import { CoreMainMenuHandler, CoreMainMenuHandlerToDisplay } from '@features/mainmenu/services/mainmenu-delegate';
|
||||||
|
import { CoreCronHandler } from '@services/cron';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreEvents } from '@singletons/events';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import {
|
||||||
|
CorePushNotifications,
|
||||||
|
CorePushNotificationsNotificationBasicData,
|
||||||
|
} from '@features/pushnotifications/services/pushnotifications';
|
||||||
|
import { CorePushNotificationsDelegate } from '@features/pushnotifications/services/push-delegate';
|
||||||
|
import { makeSingleton } from '@singletons';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to inject an option into main menu.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class AddonMessagesMainMenuHandlerService implements CoreMainMenuHandler, CoreCronHandler {
|
||||||
|
|
||||||
|
static readonly PAGE_NAME = 'messages';
|
||||||
|
|
||||||
|
name = 'AddonMessages';
|
||||||
|
priority = 800;
|
||||||
|
|
||||||
|
protected handler: CoreMainMenuHandlerToDisplay = {
|
||||||
|
icon: 'fas-comments',
|
||||||
|
title: 'addon.messages.messages',
|
||||||
|
page: AddonMessages.instance.getMainMessagesPagePath(),
|
||||||
|
class: 'addon-messages-handler',
|
||||||
|
showBadge: true, // Do not check isMessageCountEnabled because we'll use fallback it not enabled.
|
||||||
|
badge: '',
|
||||||
|
loading: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
protected unreadCount = 0;
|
||||||
|
protected contactRequestsCount = 0;
|
||||||
|
protected orMore = false;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
|
||||||
|
CoreEvents.on<AddonMessagesUnreadConversationCountsEventData>(
|
||||||
|
AddonMessagesProvider.UNREAD_CONVERSATION_COUNTS_EVENT,
|
||||||
|
(data) => {
|
||||||
|
this.unreadCount = data.favourites + data.individual + data.group + data.self;
|
||||||
|
this.orMore = !!data.orMore;
|
||||||
|
this.updateBadge(data.siteId!);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
CoreEvents.on<AddonMessagesContactRequestCountEventData>(AddonMessagesProvider.CONTACT_REQUESTS_COUNT_EVENT, (data) => {
|
||||||
|
this.contactRequestsCount = data.count;
|
||||||
|
this.updateBadge(data.siteId!);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset info on logout.
|
||||||
|
CoreEvents.on(CoreEvents.LOGOUT, () => {
|
||||||
|
this.unreadCount = 0;
|
||||||
|
this.contactRequestsCount = 0;
|
||||||
|
this.orMore = false;
|
||||||
|
this.handler.badge = '';
|
||||||
|
this.handler.loading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// If a message push notification is received, refresh the count.
|
||||||
|
CorePushNotificationsDelegate.instance.on<CorePushNotificationsNotificationBasicData>('receive').subscribe(
|
||||||
|
(notification) => {
|
||||||
|
// New message received. If it's from current site, refresh the data.
|
||||||
|
const isMessage = CoreUtils.instance.isFalseOrZero(notification.notif) ||
|
||||||
|
notification.name == 'messagecontactrequests';
|
||||||
|
if (isMessage && CoreSites.instance.isCurrentSite(notification.site)) {
|
||||||
|
this.refreshBadge(notification.site);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Register Badge counter.
|
||||||
|
CorePushNotificationsDelegate.instance.registerCounterHandler('AddonMessages');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the handler is enabled on a site level.
|
||||||
|
*
|
||||||
|
* @return Whether or not the handler is enabled on a site level.
|
||||||
|
*/
|
||||||
|
isEnabled(): Promise<boolean> {
|
||||||
|
return AddonMessages.instance.isPluginEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the data needed to render the handler.
|
||||||
|
*
|
||||||
|
* @return Data needed to render the handler.
|
||||||
|
*/
|
||||||
|
getDisplayData(): CoreMainMenuHandlerToDisplay {
|
||||||
|
this.handler.page = AddonMessages.instance.getMainMessagesPagePath();
|
||||||
|
|
||||||
|
if (this.handler.loading) {
|
||||||
|
this.refreshBadge();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes badge number.
|
||||||
|
*
|
||||||
|
* @param siteId Site ID or current Site if undefined.
|
||||||
|
* @param unreadOnly If true only the unread conversations count is refreshed.
|
||||||
|
* @return Resolve when done.
|
||||||
|
*/
|
||||||
|
async refreshBadge(siteId?: string, unreadOnly?: boolean): Promise<void> {
|
||||||
|
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||||
|
if (!siteId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const promises: Promise<unknown>[] = [];
|
||||||
|
|
||||||
|
promises.push(AddonMessages.instance.refreshUnreadConversationCounts(siteId).catch(() => {
|
||||||
|
this.unreadCount = 0;
|
||||||
|
this.orMore = false;
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Refresh the number of contact requests in 3.6+ sites.
|
||||||
|
if (!unreadOnly && AddonMessages.instance.isGroupMessagingEnabled()) {
|
||||||
|
promises.push(AddonMessages.instance.refreshContactRequestsCount(siteId).catch(() => {
|
||||||
|
this.contactRequestsCount = 0;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises).finally(() => {
|
||||||
|
this.updateBadge(siteId!);
|
||||||
|
this.handler.loading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update badge number and push notifications counter from loaded data.
|
||||||
|
*
|
||||||
|
* @param siteId Site ID.
|
||||||
|
*/
|
||||||
|
updateBadge(siteId: string): void {
|
||||||
|
const totalCount = this.unreadCount + (this.contactRequestsCount || 0);
|
||||||
|
if (totalCount > 0) {
|
||||||
|
this.handler.badge = totalCount + (this.orMore ? '+' : '');
|
||||||
|
} else {
|
||||||
|
this.handler.badge = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update push notifications badge.
|
||||||
|
CorePushNotifications.instance.updateAddonCounter('AddonMessages', totalCount, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the process.
|
||||||
|
* Receives the ID of the site affected, undefined for all sites.
|
||||||
|
*
|
||||||
|
* @param siteId ID of the site affected, undefined for all sites.
|
||||||
|
* @return Promise resolved when done, rejected if failure.
|
||||||
|
*/
|
||||||
|
async execute(siteId?: string): Promise<void> {
|
||||||
|
if (!CoreSites.instance.isCurrentSite(siteId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.refreshBadge();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the time between consecutive executions.
|
||||||
|
*
|
||||||
|
* @return Time between consecutive executions (in ms).
|
||||||
|
*/
|
||||||
|
getInterval(): number {
|
||||||
|
if (!this.isSync()) {
|
||||||
|
return 300000; // We have a WS to check the number, check it every 5 minutes.
|
||||||
|
}
|
||||||
|
|
||||||
|
return 600000; // Check it every 10 minutes.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether it's a synchronization process or not.
|
||||||
|
*
|
||||||
|
* @return True if is a sync process, false otherwise.
|
||||||
|
*/
|
||||||
|
isSync(): boolean {
|
||||||
|
// This is done to use only wifi if using the fallback function.
|
||||||
|
return !AddonMessages.instance.isMessageCountEnabled() && !AddonMessages.instance.isGroupMessagingEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the process should be executed during a manual sync.
|
||||||
|
*
|
||||||
|
* @return True if is a manual sync process, false otherwise.
|
||||||
|
*/
|
||||||
|
canManualSync(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AddonMessagesMainMenuHandler extends makeSingleton(AddonMessagesMainMenuHandlerService) {}
|
|
@ -0,0 +1,383 @@
|
||||||
|
// (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 { Injectable } from '@angular/core';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreApp } from '@services/app';
|
||||||
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
|
import {
|
||||||
|
AddonMessagesOfflineConversationMessagesDBRecord,
|
||||||
|
AddonMessagesOfflineConversationMessagesDBRecordFormatted,
|
||||||
|
AddonMessagesOfflineMessagesDBRecord,
|
||||||
|
AddonMessagesOfflineMessagesDBRecordFormatted,
|
||||||
|
CONVERSATION_MESSAGES_TABLE,
|
||||||
|
MESSAGES_TABLE,
|
||||||
|
} from './database/messages';
|
||||||
|
import { makeSingleton } from '@singletons';
|
||||||
|
import { AddonMessagesConversation } from './messages';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to handle Offline messages.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class AddonMessagesOfflineProvider {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a message.
|
||||||
|
*
|
||||||
|
* @param conversationId Conversation ID.
|
||||||
|
* @param message The message.
|
||||||
|
* @param timeCreated The time the message was created.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved if stored, rejected if failure.
|
||||||
|
*/
|
||||||
|
async deleteConversationMessage(conversationId: number, message: string, timeCreated: number, siteId?: string): Promise<void> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
await site.getDb().deleteRecords(CONVERSATION_MESSAGES_TABLE, {
|
||||||
|
conversationid: conversationId,
|
||||||
|
text: message,
|
||||||
|
timecreated: timeCreated,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete all the messages in a conversation.
|
||||||
|
*
|
||||||
|
* @param conversationId Conversation ID.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved if stored, rejected if failure.
|
||||||
|
*/
|
||||||
|
async deleteConversationMessages(conversationId: number, siteId?: string): Promise<void> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
await site.getDb().deleteRecords(CONVERSATION_MESSAGES_TABLE, {
|
||||||
|
conversationid: conversationId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a message.
|
||||||
|
*
|
||||||
|
* @param toUserId User ID to send the message to.
|
||||||
|
* @param message The message.
|
||||||
|
* @param timeCreated The time the message was created.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved if stored, rejected if failure.
|
||||||
|
*/
|
||||||
|
async deleteMessage(toUserId: number, message: string, timeCreated: number, siteId?: string): Promise<void> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
await site.getDb().deleteRecords(MESSAGES_TABLE, {
|
||||||
|
touserid: toUserId,
|
||||||
|
smallmessage: message,
|
||||||
|
timecreated: timeCreated,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all messages where deviceoffline is set to 1.
|
||||||
|
*
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with messages.
|
||||||
|
*/
|
||||||
|
async getAllDeviceOfflineMessages(
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<(AddonMessagesOfflineConversationMessagesDBRecordFormatted | AddonMessagesOfflineMessagesDBRecordFormatted)[]> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
const promises: [
|
||||||
|
Promise<AddonMessagesOfflineMessagesDBRecord[]>,
|
||||||
|
Promise<AddonMessagesOfflineConversationMessagesDBRecord[]>,
|
||||||
|
] = [
|
||||||
|
site.getDb().getRecords(MESSAGES_TABLE, { deviceoffline: 1 }),
|
||||||
|
site.getDb().getRecords(CONVERSATION_MESSAGES_TABLE, { deviceoffline: 1 }),
|
||||||
|
];
|
||||||
|
|
||||||
|
const [
|
||||||
|
messages,
|
||||||
|
conversations,
|
||||||
|
]: [
|
||||||
|
AddonMessagesOfflineMessagesDBRecord[],
|
||||||
|
AddonMessagesOfflineConversationMessagesDBRecord[],
|
||||||
|
] = await Promise.all(promises);
|
||||||
|
|
||||||
|
|
||||||
|
const messageResult:
|
||||||
|
(AddonMessagesOfflineConversationMessagesDBRecordFormatted | AddonMessagesOfflineMessagesDBRecordFormatted)[] =
|
||||||
|
this.parseMessages(messages);
|
||||||
|
const formattedConv = this.parseConversationMessages(conversations);
|
||||||
|
|
||||||
|
return messageResult.concat(formattedConv);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all offline messages.
|
||||||
|
*
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with messages.
|
||||||
|
*/
|
||||||
|
async getAllMessages(
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<(AddonMessagesOfflineConversationMessagesDBRecordFormatted | AddonMessagesOfflineMessagesDBRecordFormatted)[]> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
const promises: [
|
||||||
|
Promise<AddonMessagesOfflineMessagesDBRecord[]>,
|
||||||
|
Promise<AddonMessagesOfflineConversationMessagesDBRecord[]>,
|
||||||
|
] = [
|
||||||
|
site.getDb().getAllRecords(MESSAGES_TABLE),
|
||||||
|
site.getDb().getAllRecords(CONVERSATION_MESSAGES_TABLE),
|
||||||
|
];
|
||||||
|
|
||||||
|
const [
|
||||||
|
messages,
|
||||||
|
conversations,
|
||||||
|
]: [
|
||||||
|
AddonMessagesOfflineMessagesDBRecord[],
|
||||||
|
AddonMessagesOfflineConversationMessagesDBRecord[],
|
||||||
|
] = await Promise.all(promises);
|
||||||
|
|
||||||
|
|
||||||
|
const messageResult:
|
||||||
|
(AddonMessagesOfflineConversationMessagesDBRecordFormatted | AddonMessagesOfflineMessagesDBRecordFormatted)[] =
|
||||||
|
this.parseMessages(messages);
|
||||||
|
const formattedConv = this.parseConversationMessages(conversations);
|
||||||
|
|
||||||
|
return messageResult.concat(formattedConv);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get offline messages to send to a certain user.
|
||||||
|
*
|
||||||
|
* @param conversationId Conversation ID.
|
||||||
|
* @param userIdFrom To add to the conversation messages when parsing.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with messages.
|
||||||
|
*/
|
||||||
|
async getConversationMessages(
|
||||||
|
conversationId: number,
|
||||||
|
userIdFrom?: number,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<AddonMessagesOfflineConversationMessagesDBRecordFormatted[]> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
const messages: AddonMessagesOfflineConversationMessagesDBRecord[] = await site.getDb().getRecords(
|
||||||
|
CONVERSATION_MESSAGES_TABLE,
|
||||||
|
{ conversationid: conversationId },
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.parseConversationMessages(messages, userIdFrom);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get offline messages to send to a certain user.
|
||||||
|
*
|
||||||
|
* @param toUserId User ID to get messages to.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with messages.
|
||||||
|
*/
|
||||||
|
async getMessages(toUserId: number, siteId?: string): Promise<AddonMessagesOfflineMessagesDBRecordFormatted[]> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
const messages: AddonMessagesOfflineMessagesDBRecord[] =
|
||||||
|
await site.getDb().getRecords(MESSAGES_TABLE, { touserid: toUserId });
|
||||||
|
|
||||||
|
return this.parseMessages(messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if there are offline messages to send to a conversation.
|
||||||
|
*
|
||||||
|
* @param conversationId Conversation ID.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with boolean: true if has offline messages, false otherwise.
|
||||||
|
*/
|
||||||
|
async hasConversationMessages(conversationId: number, siteId?: string): Promise<boolean> {
|
||||||
|
const messages = await this.getConversationMessages(conversationId, undefined, siteId);
|
||||||
|
|
||||||
|
return !!messages.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if there are offline messages to send to a certain user.
|
||||||
|
*
|
||||||
|
* @param toUserId User ID to check.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with boolean: true if has offline messages, false otherwise.
|
||||||
|
*/
|
||||||
|
async hasMessages(toUserId: number, siteId?: string): Promise<boolean> {
|
||||||
|
const messages = await this.getMessages(toUserId, siteId);
|
||||||
|
|
||||||
|
return !!messages.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse some fields of each offline conversation messages.
|
||||||
|
*
|
||||||
|
* @param messages List of messages to parse.
|
||||||
|
* @param userIdFrom To add to the conversation messages when parsin.
|
||||||
|
* @return Parsed messages.
|
||||||
|
*/
|
||||||
|
protected parseConversationMessages(
|
||||||
|
messages: AddonMessagesOfflineConversationMessagesDBRecord[],
|
||||||
|
userIdFrom?: number,
|
||||||
|
): AddonMessagesOfflineConversationMessagesDBRecordFormatted[] {
|
||||||
|
if (!messages) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages.map((message) => {
|
||||||
|
const parsedMessage: AddonMessagesOfflineConversationMessagesDBRecordFormatted = {
|
||||||
|
conversationid: message.conversationid,
|
||||||
|
text: message.text,
|
||||||
|
timecreated: message.timecreated,
|
||||||
|
deviceoffline: message.deviceoffline,
|
||||||
|
conversation: message.conversation ? CoreTextUtils.instance.parseJSON(message.conversation, undefined) : undefined,
|
||||||
|
pending: true,
|
||||||
|
useridfrom: userIdFrom,
|
||||||
|
};
|
||||||
|
|
||||||
|
return parsedMessage;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse some fields of each offline messages.
|
||||||
|
*
|
||||||
|
* @param messages List of messages to parse.
|
||||||
|
* @return Parsed messages.
|
||||||
|
*/
|
||||||
|
protected parseMessages(
|
||||||
|
messages: AddonMessagesOfflineMessagesDBRecord[],
|
||||||
|
): AddonMessagesOfflineMessagesDBRecordFormatted[] {
|
||||||
|
if (!messages) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages.map((message) => {
|
||||||
|
const parsedMessage: AddonMessagesOfflineMessagesDBRecordFormatted = {
|
||||||
|
touserid: message.touserid,
|
||||||
|
useridfrom: message.useridfrom,
|
||||||
|
smallmessage: message.smallmessage,
|
||||||
|
timecreated: message.timecreated,
|
||||||
|
deviceoffline: message.deviceoffline,
|
||||||
|
pending: true,
|
||||||
|
text: message.smallmessage,
|
||||||
|
};
|
||||||
|
|
||||||
|
return parsedMessage;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a conversation message to be sent later.
|
||||||
|
*
|
||||||
|
* @param conversation Conversation.
|
||||||
|
* @param message The message to send.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved if stored, rejected if failure.
|
||||||
|
*/
|
||||||
|
async saveConversationMessage(
|
||||||
|
conversation: AddonMessagesConversation,
|
||||||
|
message: string,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<AddonMessagesOfflineConversationMessagesDBRecord> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
const entry: AddonMessagesOfflineConversationMessagesDBRecord = {
|
||||||
|
conversationid: conversation.id,
|
||||||
|
text: message,
|
||||||
|
timecreated: Date.now(),
|
||||||
|
deviceoffline: CoreApp.instance.isOnline() ? 0 : 1,
|
||||||
|
conversation: JSON.stringify({
|
||||||
|
name: conversation.name || '',
|
||||||
|
subname: conversation.subname || '',
|
||||||
|
imageurl: conversation.imageurl || '',
|
||||||
|
isfavourite: conversation.isfavourite ? 1 : 0,
|
||||||
|
type: conversation.type,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
await site.getDb().insertRecord(CONVERSATION_MESSAGES_TABLE, entry);
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a message to be sent later.
|
||||||
|
*
|
||||||
|
* @param toUserId User ID recipient of the message.
|
||||||
|
* @param message The message to send.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved if stored, rejected if failure.
|
||||||
|
*/
|
||||||
|
async saveMessage(toUserId: number, message: string, siteId?: string): Promise<AddonMessagesOfflineMessagesDBRecord> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
const entry: AddonMessagesOfflineMessagesDBRecord = {
|
||||||
|
touserid: toUserId,
|
||||||
|
useridfrom: site.getUserId(),
|
||||||
|
smallmessage: message,
|
||||||
|
timecreated: new Date().getTime(),
|
||||||
|
deviceoffline: CoreApp.instance.isOnline() ? 0 : 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
await site.getDb().insertRecord(MESSAGES_TABLE, entry);
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set deviceoffline for a group of messages.
|
||||||
|
*
|
||||||
|
* @param messages Messages to update. Should be the same entry as retrieved from the DB.
|
||||||
|
* @param value Value to set.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved if stored, rejected if failure.
|
||||||
|
*/
|
||||||
|
async setMessagesDeviceOffline(
|
||||||
|
messages: (AddonMessagesOfflineConversationMessagesDBRecord | AddonMessagesOfflineMessagesDBRecord)[],
|
||||||
|
value: boolean,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
|
|
||||||
|
const db = site.getDb();
|
||||||
|
|
||||||
|
const promises: Promise<number>[] = [];
|
||||||
|
const data = { deviceoffline: value ? 1 : 0 };
|
||||||
|
|
||||||
|
messages.forEach((message: AddonMessagesOfflineConversationMessagesDBRecord | AddonMessagesOfflineMessagesDBRecord) => {
|
||||||
|
if ('conversationid' in message) {
|
||||||
|
promises.push(db.updateRecords(
|
||||||
|
CONVERSATION_MESSAGES_TABLE,
|
||||||
|
data,
|
||||||
|
{ conversationid: message.conversationid, text: message.text, timecreated: message.timecreated },
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
promises.push(db.updateRecords(
|
||||||
|
MESSAGES_TABLE,
|
||||||
|
data,
|
||||||
|
{ touserid: message.touserid, smallmessage: message.smallmessage, timecreated: message.timecreated },
|
||||||
|
));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AddonMessagesOffline extends makeSingleton(AddonMessagesOfflineProvider) {}
|
File diff suppressed because it is too large
Load Diff
|
@ -84,7 +84,11 @@ export class CoreEvents {
|
||||||
* @param siteId Site where to trigger the event. Undefined won't check the site.
|
* @param siteId Site where to trigger the event. Undefined won't check the site.
|
||||||
* @return Observer to stop listening.
|
* @return Observer to stop listening.
|
||||||
*/
|
*/
|
||||||
static on<T = unknown>(eventName: string, callBack: (value: T) => void, siteId?: string): CoreEventObserver {
|
static on<T = unknown>(
|
||||||
|
eventName: string,
|
||||||
|
callBack: (value: T & { siteId?: string }) => void,
|
||||||
|
siteId?: string,
|
||||||
|
): CoreEventObserver {
|
||||||
// If it's a unique event and has been triggered already, call the callBack.
|
// If it's a unique event and has been triggered already, call the callBack.
|
||||||
// We don't need to create an observer because the event won't be triggered again.
|
// We don't need to create an observer because the event won't be triggered again.
|
||||||
if (this.uniqueEvents[eventName]) {
|
if (this.uniqueEvents[eventName]) {
|
||||||
|
|
|
@ -186,7 +186,7 @@
|
||||||
|
|
||||||
--core-large-avatar-size: var(--custom-large-avatar-size, 90px);
|
--core-large-avatar-size: var(--custom-large-avatar-size, 90px);
|
||||||
|
|
||||||
--core-avatar-size: var(--custom-avatar-size, 64px);
|
--core-avatar-size: var(--custom-avatar-size, 40px);
|
||||||
|
|
||||||
--addon-calendar-event-category-color: var(--custom-calendar-event-category-color, var(--purple));
|
--addon-calendar-event-category-color: var(--custom-calendar-event-category-color, var(--purple));
|
||||||
--addon-calendar-event-course-color: var(--custom-calendar-event-course-color, var(--red));
|
--addon-calendar-event-course-color: var(--custom-calendar-event-course-color, var(--red));
|
||||||
|
|
Loading…
Reference in New Issue