MOBILE-3631 messages: Fix split view on pages with tabs
This commit is contained in:
		
							parent
							
								
									210518d549
								
							
						
					
					
						commit
						44552cfe3b
					
				| @ -16,8 +16,6 @@ import { Injector, NgModule } from '@angular/core'; | ||||
| import { Route, RouterModule, ROUTES, Routes } from '@angular/router'; | ||||
| 
 | ||||
| import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module'; | ||||
| import { AddonMessagesContactsRoutingModule } from './pages/contacts/messages-contacts-routing.module'; | ||||
| import { AddonMessagesIndexRoutingModule } from './pages/index-35/messages-index-routing.module'; | ||||
| import { AddonMessagesSettingsHandlerService } from './services/handlers/settings'; | ||||
| 
 | ||||
| export const discussionRoute: Route = { | ||||
| @ -30,7 +28,12 @@ function buildRoutes(injector: Injector): Routes { | ||||
|     return [ | ||||
|         { | ||||
|             path: 'index', // 3.5 or lower.
 | ||||
|             loadChildren: () => import('./pages/index-35/index.module').then( m => m.AddonMessagesIndex35PageModule), | ||||
|             loadChildren: () => | ||||
|                 import('./pages/discussions-35/discussions.module').then(m => m.AddonMessagesDiscussions35PageModule), | ||||
|         }, | ||||
|         { | ||||
|             path: 'contacts-35', // 3.5 or lower.
 | ||||
|             loadChildren: () => import('./pages/contacts-35/contacts.module').then(m => m.AddonMessagesContacts35PageModule), | ||||
|         }, | ||||
|         { | ||||
|             path: 'group-conversations', // 3.6 or greater.
 | ||||
| @ -60,37 +63,7 @@ function buildRoutes(injector: Injector): Routes { | ||||
|     ]; | ||||
| } | ||||
| 
 | ||||
| // 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), | ||||
|     }, | ||||
| ]; | ||||
| 
 | ||||
| // 3.6 or greater.
 | ||||
| const contactsTabRoutes: Routes = [ | ||||
|     { | ||||
|         path: 'confirmed', | ||||
|         loadChildren: () => import('./pages/contacts-confirmed/contacts-confirmed.module') | ||||
|             .then(m => m.AddonMessagesContactsConfirmedPageModule), | ||||
|     }, | ||||
|     { | ||||
|         path: 'requests', | ||||
|         loadChildren: () => import('./pages/contacts-requests/contacts-requests.module') | ||||
|             .then(m => m.AddonMessagesContactsRequestsPageModule), | ||||
|     }, | ||||
| ]; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     imports: [ | ||||
|         AddonMessagesIndexRoutingModule.forChild({ children: indexTabRoutes }), | ||||
|         AddonMessagesContactsRoutingModule.forChild({ children: contactsTabRoutes }), | ||||
|     ], | ||||
|     exports: [RouterModule], | ||||
|     providers: [ | ||||
|         { | ||||
|  | ||||
| @ -1,35 +1,49 @@ | ||||
| <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.contacts' | 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> | ||||
| <ion-content> | ||||
|     <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshData($event)"> | ||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|     </ion-refresher> | ||||
|     <core-split-view> | ||||
|         <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-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-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> | ||||
|             <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 addon-messages-conversation-item" *ngIf="contact.profileimageurl || contact.profileimageurlsmall" | ||||
|                         [title]="contact.fullname" (click)="gotoDiscussion(contact.id)" detail | ||||
|                         [class.core-selected-item]="contact.id == discussionUserId"> | ||||
|                         <core-user-avatar [user]="contact" slot="start" [checkOnline]="contact.showonlinestatus"></core-user-avatar> | ||||
|                         <ion-label><h2>{{ contact.fullname }}</h2></ion-label> | ||||
|                     </ion-item> | ||||
|             <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 addon-messages-conversation-item" *ngIf="contact.profileimageurl || contact.profileimageurlsmall" | ||||
|                             [title]="contact.fullname" (click)="gotoDiscussion(contact.id)" detail | ||||
|                             [class.core-selected-item]="contact.id == discussionUserId"> | ||||
|                             <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> | ||||
|             </ng-container> | ||||
|         </ion-list> | ||||
|     </core-loading> | ||||
|             </ion-list> | ||||
|         </core-loading> | ||||
|     </core-split-view> | ||||
| </ion-content> | ||||
|  | ||||
| @ -17,6 +17,9 @@ import { IonicModule } from '@ionic/angular'; | ||||
| import { TranslateModule } from '@ngx-translate/core'; | ||||
| import { RouterModule, Routes } from '@angular/router'; | ||||
| import { CommonModule } from '@angular/common'; | ||||
| import { conditionalRoutes } from '@/app/app-routing.module'; | ||||
| import { discussionRoute } from '@addons/messages/messages-lazy.module'; | ||||
| import { CoreScreen } from '@services/screen'; | ||||
| 
 | ||||
| import { CoreSharedModule } from '@/core/shared.module'; | ||||
| import { CoreSearchComponentsModule } from '@features/search/components/components.module'; | ||||
| @ -25,9 +28,21 @@ import { AddonMessagesContacts35Page } from './contacts.page'; | ||||
| 
 | ||||
| const routes: Routes = [ | ||||
|     { | ||||
|         path: '', | ||||
|         matcher: segments => { | ||||
|             const matches = CoreScreen.instance.isMobile ? segments.length === 0 : true; | ||||
| 
 | ||||
|             return matches ? { consumed: [] } : null; | ||||
|         }, | ||||
|         component: AddonMessagesContacts35Page, | ||||
|         children: conditionalRoutes([ | ||||
|             { | ||||
|                 path: '', | ||||
|                 pathMatch: 'full', | ||||
|             }, | ||||
|             discussionRoute, | ||||
|         ], () => CoreScreen.instance.isTablet), | ||||
|     }, | ||||
|     ...conditionalRoutes([discussionRoute], () => CoreScreen.instance.isMobile), | ||||
| ]; | ||||
| 
 | ||||
| @NgModule({ | ||||
|  | ||||
| @ -21,14 +21,15 @@ import { | ||||
|     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 { ActivatedRoute, Params } from '@angular/router'; | ||||
| import { Translate } from '@singletons'; | ||||
| import { CoreScreen } from '@services/screen'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that displays the list of contacts. | ||||
| @ -89,7 +90,15 @@ export class AddonMessagesContacts35Page implements OnInit, OnDestroy { | ||||
|      */ | ||||
|     ngOnInit(): void { | ||||
|         this.route.queryParams.subscribe(async params => { | ||||
|             this.discussionUserId = params['discussionUserId'] || undefined; | ||||
|             const discussionUserId = params['discussionUserId'] | ||||
|                 ? parseInt(params['discussionUserId'], 10) | ||||
|                 : (params['userId'] ? parseInt(params['userId'], 10) : undefined); | ||||
| 
 | ||||
|             if (this.loaded && this.discussionUserId == discussionUserId) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             this.discussionUserId = discussionUserId; | ||||
| 
 | ||||
|             if (this.discussionUserId) { | ||||
|                 // There is a discussion to load, open the discussion in a new state.
 | ||||
| @ -98,7 +107,7 @@ export class AddonMessagesContacts35Page implements OnInit, OnDestroy { | ||||
| 
 | ||||
|             try { | ||||
|                 await this.fetchData(); | ||||
|                 if (!this.discussionUserId && this.hasContacts) { | ||||
|                 if (!this.discussionUserId && this.hasContacts && CoreScreen.instance.isTablet) { | ||||
|                     let contact: AddonMessagesGetContactsContact | undefined; | ||||
|                     for (const x in this.contacts) { | ||||
|                         if (this.contacts[x].length > 0) { | ||||
| @ -109,7 +118,7 @@ export class AddonMessagesContacts35Page implements OnInit, OnDestroy { | ||||
| 
 | ||||
|                     if (contact) { | ||||
|                         // Take first and load it.
 | ||||
|                         this.gotoDiscussion(contact.id, true); | ||||
|                         this.gotoDiscussion(contact.id); | ||||
|                     } | ||||
|                 } | ||||
|             } finally { | ||||
| @ -235,16 +244,18 @@ export class AddonMessagesContacts35Page implements OnInit, OnDestroy { | ||||
|      * 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 { | ||||
|     gotoDiscussion(discussionUserId: number): void { | ||||
|         this.discussionUserId = discussionUserId; | ||||
| 
 | ||||
|         const params: AddonMessagesSplitViewLoadIndexEventData = { | ||||
|             discussion: discussionUserId, | ||||
|             onlyWithSplitView: onlyWithSplitView, | ||||
|         const params: Params = { | ||||
|             userId: discussionUserId, | ||||
|         }; | ||||
|         CoreEvents.trigger(AddonMessagesProvider.SPLIT_VIEW_LOAD_INDEX_EVENT, params, this.siteId); | ||||
| 
 | ||||
|         const splitViewLoaded = CoreNavigator.instance.isSplitViewOutletLoaded('**/messages/contacts-35/discussion'); | ||||
|         const path = (splitViewLoaded ? '../' : '') + 'discussion'; | ||||
| 
 | ||||
|         CoreNavigator.instance.navigate(path, { params }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -1,30 +0,0 @@ | ||||
| <ion-content> | ||||
|     <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshData($event)"> | ||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|     </ion-refresher> | ||||
|     <core-loading [hideUntil]="loaded" class="core-loading-center"> | ||||
|         <ion-list  class="ion-no-margin"> | ||||
|             <ion-item class="ion-text-wrap addon-messages-conversation-item" *ngFor="let contact of contacts" | ||||
|                 [title]="contact.fullname" (click)="selectUser(contact.id)" detail | ||||
|                 [class.core-selected-item]="contact.id == selectedUserId"> | ||||
|                 <core-user-avatar slot="start" core-user-avatar [user]="contact" [checkOnline]="contact.showonlinestatus" | ||||
|                     [linkProfile]="false"></core-user-avatar> | ||||
|                 <ion-label> | ||||
|                     <h2> | ||||
|                         <core-format-text [text]="contact.fullname" contextLevel="system" [contextInstanceId]="0"></core-format-text> | ||||
|                         <ion-icon *ngIf="contact.isblocked" name="fas-user-slash" slot="end"> | ||||
|                         </ion-icon> | ||||
|                     </h2> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|         </ion-list> | ||||
| 
 | ||||
|         <core-empty-box *ngIf="!contacts.length" icon="far-address-book" | ||||
|             [message]="'addon.messages.nocontactsgetstarted' | translate"> | ||||
|         </core-empty-box> | ||||
| 
 | ||||
|         <core-infinite-loading [enabled]="canLoadMore" (action)="loadMore($event)" [error]="loadMoreError" | ||||
|             position="bottom"> | ||||
|         </core-infinite-loading> | ||||
|     </core-loading> | ||||
| </ion-content> | ||||
| @ -1,45 +0,0 @@ | ||||
| // (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 { AddonMessagesContactsConfirmedPage } from './contacts-confirmed.page'; | ||||
| 
 | ||||
| const routes: Routes = [ | ||||
|     { | ||||
|         path: '', | ||||
|         component: AddonMessagesContactsConfirmedPage, | ||||
|     }, | ||||
| ]; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     imports: [ | ||||
|         RouterModule.forChild(routes), | ||||
|         CommonModule, | ||||
|         IonicModule, | ||||
|         TranslateModule.forChild(), | ||||
|         CoreSharedModule, | ||||
|     ], | ||||
|     declarations: [ | ||||
|         AddonMessagesContactsConfirmedPage, | ||||
|     ], | ||||
|     exports: [RouterModule], | ||||
| }) | ||||
| export class AddonMessagesContactsConfirmedPageModule {} | ||||
| @ -1,162 +0,0 @@ | ||||
| // (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 { | ||||
|     AddonMessagesProvider, | ||||
|     AddonMessagesConversationMember, | ||||
|     AddonMessages, | ||||
|     AddonMessagesMemberInfoChangedEventData, | ||||
|     AddonMessagesSplitViewLoadContactsEventData, | ||||
| } from '../../services/messages'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { IonRefresher } from '@ionic/angular'; | ||||
| import { CoreScreen } from '@services/screen'; | ||||
| 
 | ||||
| /** | ||||
|  * Component that displays the list of confirmed contacts. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'addon-messages-confirmed-contacts', | ||||
|     templateUrl: 'contacts-confirmed.html', | ||||
|     styleUrls: ['../../messages-common.scss'], | ||||
| }) | ||||
| export class AddonMessagesContactsConfirmedPage implements OnInit, OnDestroy { | ||||
| 
 | ||||
|     loaded = false; | ||||
|     canLoadMore = false; | ||||
|     loadMoreError = false; | ||||
|     contacts: AddonMessagesConversationMember[] = []; | ||||
|     selectedUserId?: number; | ||||
| 
 | ||||
|     protected memberInfoObserver: CoreEventObserver; | ||||
| 
 | ||||
|     constructor() { | ||||
|         // Update block status of a user.
 | ||||
|         this.memberInfoObserver = CoreEvents.on<AddonMessagesMemberInfoChangedEventData>( | ||||
|             AddonMessagesProvider.MEMBER_INFO_CHANGED_EVENT, | ||||
|             (data) => { | ||||
|                 if (data.userBlocked || data.userUnblocked) { | ||||
|                     const user = this.contacts.find((user) => user.id == data.userId); | ||||
|                     if (user) { | ||||
|                         user.isblocked = !!data.userBlocked; | ||||
|                     } | ||||
|                 } else if (data.contactRemoved) { | ||||
|                     const index = this.contacts.findIndex((contact) => contact.id == data.userId); | ||||
|                     if (index >= 0) { | ||||
|                         this.contacts.splice(index, 1); | ||||
|                     } | ||||
|                 } else if (data.contactRequestConfirmed) { | ||||
|                     this.refreshData(); | ||||
|                 } | ||||
|             }, | ||||
|             CoreSites.instance.getCurrentSiteId(), | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component loaded. | ||||
|      */ | ||||
|     async ngOnInit(): Promise<void> { | ||||
|         try { | ||||
|             await this.fetchData(); | ||||
|             if (this.contacts.length && CoreScreen.instance.isTablet) { | ||||
|                 this.selectUser(this.contacts[0].id, true); | ||||
|             } | ||||
|         } finally { | ||||
|             this.loaded = true; | ||||
|         } | ||||
|         // Workaround for infinite scrolling.
 | ||||
|         // @todo this.content.resize();
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch contacts. | ||||
|      * | ||||
|      * @param refresh True if we are refreshing contacts, false if we are loading more. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async fetchData(refresh: boolean = false): Promise<void> { | ||||
|         this.loadMoreError = false; | ||||
| 
 | ||||
|         const limitFrom = refresh ? 0 : this.contacts.length; | ||||
| 
 | ||||
|         if (limitFrom === 0) { | ||||
|             // Always try to get latest data from server.
 | ||||
|             await AddonMessages.instance.invalidateUserContacts(); | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             const result = await AddonMessages.instance.getUserContacts(limitFrom); | ||||
|             this.contacts = refresh ? result.contacts : this.contacts.concat(result.contacts); | ||||
|             this.canLoadMore = result.canLoadMore; | ||||
|         } catch (error) { | ||||
|             this.loadMoreError = true; | ||||
|             CoreDomUtils.instance.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingcontacts', true); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Refresh contacts. | ||||
|      * | ||||
|      * @param refresher Refresher. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async refreshData(refresher?: CustomEvent<IonRefresher>): Promise<void> { | ||||
|         // No need to invalidate contacts, we always try to get the latest.
 | ||||
|         await this.fetchData(true).finally(() => { | ||||
|             refresher?.detail.complete(); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load more contacts. | ||||
|      * | ||||
|      * @param infiniteComplete Infinite scroll complete function. Only used from core-infinite-loading. | ||||
|      * @return Resolved when done. | ||||
|      */ | ||||
|     async loadMore(infiniteComplete?: () => void): Promise<void> { | ||||
|         await this.fetchData().finally(() => { | ||||
|             infiniteComplete && infiniteComplete(); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Notify that a contact has been selected. | ||||
|      * | ||||
|      * @param userId User id. | ||||
|      * @param onInit Whether the contact is selected on initial load. | ||||
|      */ | ||||
|     selectUser(userId: number, onInit: boolean = false): void { | ||||
|         this.selectedUserId = userId; | ||||
| 
 | ||||
|         CoreEvents.trigger<AddonMessagesSplitViewLoadContactsEventData>( | ||||
|             AddonMessagesProvider.SPLIT_VIEW_LOAD_CONTACTS_EVENT, | ||||
|             { | ||||
|                 userId, | ||||
|                 onInit, | ||||
|             }, | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component destroyed. | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         this.memberInfoObserver?.off(); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -1,24 +0,0 @@ | ||||
| <ion-content> | ||||
|     <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshData($event)"> | ||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|     </ion-refresher> | ||||
|     <core-loading [hideUntil]="loaded" class="core-loading-center"> | ||||
|         <ion-list  class="ion-no-margin"> | ||||
|             <ion-item class="ion-text-wrap addon-messages-conversation-item" *ngFor="let request of requests" | ||||
|                 [title]="request.fullname" (click)="selectUser(request.id)" | ||||
|                 [class.core-selected-item]="request.id == selectedUserId" detail> | ||||
|                 <core-user-avatar slot="start" [user]="request" [linkProfile]="false"></core-user-avatar> | ||||
|                 <ion-label> | ||||
|                     <core-format-text [text]="request.fullname" contextLevel="system" [contextInstanceId]="0"></core-format-text> | ||||
|                     <p *ngIf="!request.iscontact"> | ||||
|                         {{ 'addon.messages.wouldliketocontactyou' | translate }} | ||||
|                     </p> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|         </ion-list> | ||||
|         <core-empty-box *ngIf="!requests.length" icon="far-address-book" [message]="'addon.messages.nocontactrequests' | translate"> | ||||
|         </core-empty-box> | ||||
|         <core-infinite-loading [enabled]="canLoadMore" (action)="loadMore($event)" [error]="loadMoreError" position="bottom"> | ||||
|         </core-infinite-loading> | ||||
|     </core-loading> | ||||
| </ion-content> | ||||
| @ -1,45 +0,0 @@ | ||||
| // (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 { AddonMessagesContactsRequestsPage } from './contacts-requests.page'; | ||||
| 
 | ||||
| const routes: Routes = [ | ||||
|     { | ||||
|         path: '', | ||||
|         component: AddonMessagesContactsRequestsPage, | ||||
|     }, | ||||
| ]; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     imports: [ | ||||
|         RouterModule.forChild(routes), | ||||
|         CommonModule, | ||||
|         IonicModule, | ||||
|         TranslateModule.forChild(), | ||||
|         CoreSharedModule, | ||||
|     ], | ||||
|     declarations: [ | ||||
|         AddonMessagesContactsRequestsPage, | ||||
|     ], | ||||
|     exports: [RouterModule], | ||||
| }) | ||||
| export class AddonMessagesContactsRequestsPageModule {} | ||||
| @ -1,160 +0,0 @@ | ||||
| // (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 { | ||||
|     AddonMessagesProvider, | ||||
|     AddonMessagesConversationMember, | ||||
|     AddonMessagesSplitViewLoadContactsEventData, | ||||
|     AddonMessages, | ||||
|     AddonMessagesMemberInfoChangedEventData, | ||||
| } from '../../services/messages'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { IonRefresher } from '@ionic/angular'; | ||||
| import { CoreScreen } from '@services/screen'; | ||||
| 
 | ||||
| /** | ||||
|  * Component that displays the list of contact requests. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'addon-messages-contact-requests', | ||||
|     templateUrl: 'contacts-requests.html', | ||||
|     styleUrls: ['../../messages-common.scss'], | ||||
| }) | ||||
| export class AddonMessagesContactsRequestsPage implements OnInit, OnDestroy { | ||||
| 
 | ||||
|     loaded = false; | ||||
|     canLoadMore = false; | ||||
|     loadMoreError = false; | ||||
|     requests: AddonMessagesConversationMember[] = []; | ||||
|     selectedUserId?: number; | ||||
| 
 | ||||
|     protected memberInfoObserver: CoreEventObserver; | ||||
| 
 | ||||
|     constructor() { | ||||
| 
 | ||||
|         // Hide the "Would like to contact you" message when a contact request is confirmed.
 | ||||
|         this.memberInfoObserver = CoreEvents.on<AddonMessagesMemberInfoChangedEventData>( | ||||
|             AddonMessagesProvider.MEMBER_INFO_CHANGED_EVENT, | ||||
|             (data) => { | ||||
|                 if (data.contactRequestConfirmed || data.contactRequestDeclined) { | ||||
|                     const index = this.requests.findIndex((request) => request.id == data.userId); | ||||
|                     if (index >= 0) { | ||||
|                         this.requests.splice(index, 1); | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
| 
 | ||||
|             CoreSites.instance.getCurrentSiteId(), | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component loaded. | ||||
|      */ | ||||
|     async ngOnInit(): Promise<void> { | ||||
|         try { | ||||
|             await this.fetchData(); | ||||
|             if (this.requests.length && CoreScreen.instance.isTablet) { | ||||
|                 this.selectUser(this.requests[0].id, true); | ||||
|             } | ||||
|         } finally { | ||||
|             this.loaded = true; | ||||
|         } | ||||
|         // Workaround for infinite scrolling.
 | ||||
|         // @todo this.content.resize();
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch contact requests. | ||||
|      * | ||||
|      * @param refresh True if we are refreshing contact requests, false if we are loading more. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async fetchData(refresh: boolean = false): Promise<void> { | ||||
|         this.loadMoreError = false; | ||||
| 
 | ||||
|         const limitFrom = refresh ? 0 : this.requests.length; | ||||
| 
 | ||||
|         if (limitFrom === 0) { | ||||
|             // Always try to get latest data from server.
 | ||||
|             await AddonMessages.instance.invalidateContactRequestsCache(); | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             const result = await AddonMessages.instance.getContactRequests(limitFrom); | ||||
|             this.requests = refresh ? result.requests : this.requests.concat(result.requests); | ||||
|             this.canLoadMore = result.canLoadMore; | ||||
|         } catch (error) { | ||||
|             this.loadMoreError = true; | ||||
|             CoreDomUtils.instance.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingcontacts', true); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Refresh contact requests. | ||||
|      * | ||||
|      * @param refresher Refresher. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async refreshData(refresher?: CustomEvent<IonRefresher>): Promise<void> { | ||||
|         // Refresh the number of contacts requests to update badges.
 | ||||
|         AddonMessages.instance.refreshContactRequestsCount(); | ||||
| 
 | ||||
|         // No need to invalidate contact requests, we always try to get the latest.
 | ||||
|         await this.fetchData(true).finally(() => { | ||||
|             refresher?.detail.complete(); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load more contact requests. | ||||
|      * | ||||
|      * @param infiniteComplete Infinite scroll complete function. Only used from core-infinite-loading. | ||||
|      * @return Resolved when done. | ||||
|      */ | ||||
|     async loadMore(infiniteComplete?: () => void): Promise<void> { | ||||
|         await this.fetchData().finally(() => { | ||||
|             infiniteComplete && infiniteComplete(); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Notify that a contact has been selected. | ||||
|      * | ||||
|      * @param userId User id. | ||||
|      * @param onInit Whether the contact is selected on initial load. | ||||
|      */ | ||||
|     selectUser(userId: number, onInit: boolean = false): void { | ||||
|         this.selectedUserId = userId; | ||||
| 
 | ||||
|         const data: AddonMessagesSplitViewLoadContactsEventData = { | ||||
|             userId, | ||||
|             onInit, | ||||
|         }; | ||||
| 
 | ||||
|         CoreEvents.trigger(AddonMessagesProvider.SPLIT_VIEW_LOAD_CONTACTS_EVENT, data); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component destroyed. | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         this.memberInfoObserver?.off(); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -15,6 +15,77 @@ | ||||
| </ion-header> | ||||
| <ion-content> | ||||
|     <core-split-view> | ||||
|         <core-tabs [tabs]="tabs" hideUntil="true"></core-tabs> | ||||
|         <ion-tab-bar class="core-tabs-bar"> | ||||
|             <ion-row> | ||||
|                 <ion-col class="tab-slide" [attr.aria-selected]="selected == 'confirmed'" (click)="selectTab('confirmed', $event)"> | ||||
|                     <ion-label>{{ 'addon.messages.contacts' | translate}}</ion-label> | ||||
|                 </ion-col> | ||||
|                 <ion-col class="tab-slide" [attr.aria-selected]="selected != 'confirmed'" (click)="selectTab('requests', $event)"> | ||||
|                     <ion-label> | ||||
|                         {{ 'addon.messages.requests' | translate}} | ||||
|                         <ion-badge *ngIf="requestsBadge">{{ requestsBadge }}</ion-badge> | ||||
|                     </ion-label> | ||||
|                 </ion-col> | ||||
|             </ion-row> | ||||
|         </ion-tab-bar> | ||||
|         <div *ngIf="selected == 'confirmed'"> | ||||
|             <ion-refresher slot="fixed" [disabled]="!confirmedLoaded" (ionRefresh)="refreshData($event)"> | ||||
|                 <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|             </ion-refresher> | ||||
|             <core-loading [hideUntil]="confirmedLoaded" class="core-loading-center"> | ||||
|                 <ion-list  class="ion-no-margin"> | ||||
|                     <ion-item class="ion-text-wrap addon-messages-conversation-item" *ngFor="let contact of confirmedContacts" | ||||
|                         [title]="contact.fullname" (click)="selectUser(contact.id)" detail | ||||
|                         [class.core-selected-item]="contact.id == selectedUserId"> | ||||
|                         <core-user-avatar slot="start" core-user-avatar [user]="contact" [checkOnline]="contact.showonlinestatus" | ||||
|                             [linkProfile]="false"></core-user-avatar> | ||||
|                         <ion-label> | ||||
|                             <h2> | ||||
|                                 <core-format-text [text]="contact.fullname" contextLevel="system" [contextInstanceId]="0"> | ||||
|                                 </core-format-text> | ||||
|                                 <ion-icon *ngIf="contact.isblocked" name="fas-user-slash" slot="end"> | ||||
|                                 </ion-icon> | ||||
|                             </h2> | ||||
|                         </ion-label> | ||||
|                     </ion-item> | ||||
|                 </ion-list> | ||||
| 
 | ||||
|                 <core-empty-box *ngIf="!confirmedContacts.length" icon="far-address-book" | ||||
|                     [message]="'addon.messages.nocontactsgetstarted' | translate"> | ||||
|                 </core-empty-box> | ||||
| 
 | ||||
|                 <core-infinite-loading [enabled]="confirmedCanLoadMore" (action)="loadMore($event)" [error]="confirmedLoadMoreError" | ||||
|                     position="bottom"> | ||||
|                 </core-infinite-loading> | ||||
|             </core-loading> | ||||
|         </div> | ||||
|         <div  *ngIf="selected != 'confirmed'"> | ||||
|             <ion-refresher slot="fixed" [disabled]="!requestsLoaded" (ionRefresh)="refreshData($event)"> | ||||
|                 <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|             </ion-refresher> | ||||
|             <core-loading [hideUntil]="requestsLoaded" class="core-loading-center"> | ||||
|                 <ion-list  class="ion-no-margin"> | ||||
|                     <ion-item class="ion-text-wrap addon-messages-conversation-item" *ngFor="let request of requests" | ||||
|                         [title]="request.fullname" (click)="selectUser(request.id)" | ||||
|                         [class.core-selected-item]="request.id == selectedUserId" detail> | ||||
|                         <core-user-avatar slot="start" [user]="request" [linkProfile]="false"></core-user-avatar> | ||||
|                         <ion-label> | ||||
|                             <core-format-text [text]="request.fullname" contextLevel="system" [contextInstanceId]="0"> | ||||
|                             </core-format-text> | ||||
|                             <p *ngIf="!request.iscontact"> | ||||
|                                 {{ 'addon.messages.wouldliketocontactyou' | translate }} | ||||
|                             </p> | ||||
|                         </ion-label> | ||||
|                     </ion-item> | ||||
|                 </ion-list> | ||||
|                 <core-empty-box *ngIf="!requests.length" icon="far-address-book" | ||||
|                     [message]="'addon.messages.nocontactrequests' | translate"> | ||||
|                 </core-empty-box> | ||||
|                 <core-infinite-loading [enabled]="requestsCanLoadMore" (action)="loadMore($event)" [error]="requestsLoadMoreError" | ||||
|                     position="bottom"> | ||||
|                 </core-infinite-loading> | ||||
|             </core-loading> | ||||
|         </div> | ||||
| 
 | ||||
|     </core-split-view> | ||||
| </ion-content> | ||||
|  | ||||
| @ -12,22 +12,19 @@ | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injector, NgModule } from '@angular/core'; | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { IonicModule } from '@ionic/angular'; | ||||
| import { TranslateModule } from '@ngx-translate/core'; | ||||
| import { RouterModule, ROUTES, Routes } from '@angular/router'; | ||||
| import { RouterModule, Routes } from '@angular/router'; | ||||
| import { CommonModule } from '@angular/common'; | ||||
| import { conditionalRoutes, resolveModuleRoutes } from '@/app/app-routing.module'; | ||||
| import { conditionalRoutes } from '@/app/app-routing.module'; | ||||
| import { discussionRoute } from '@addons/messages/messages-lazy.module'; | ||||
| import { CoreScreen } from '@services/screen'; | ||||
| 
 | ||||
| import { CoreSharedModule } from '@/core/shared.module'; | ||||
| 
 | ||||
| import { ADDON_MESSAGES_CONTACTS_ROUTES } from './messages-contacts-routing.module'; | ||||
| import { AddonMessagesContactsPage } from './contacts.page'; | ||||
| import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module'; | ||||
| 
 | ||||
| // @todo mix both routes to get messages/contacts/requests/discussion and messages/contacts/confirmed/discussion working
 | ||||
| const routes: Routes = [ | ||||
|     { | ||||
|         matcher: segments => { | ||||
| @ -47,19 +44,6 @@ const routes: Routes = [ | ||||
|     ...conditionalRoutes([discussionRoute], () => CoreScreen.instance.isMobile), | ||||
| ]; | ||||
| 
 | ||||
| function buildRoutes(injector: Injector): Routes { | ||||
|     const routes = resolveModuleRoutes(injector, ADDON_MESSAGES_CONTACTS_ROUTES); | ||||
| 
 | ||||
|     return [ | ||||
|         ...buildTabMainRoutes(injector, { | ||||
|             path: '', | ||||
|             component: AddonMessagesContactsPage, | ||||
|             children: routes.children, | ||||
|         }), | ||||
|         ...routes.siblings, | ||||
|     ]; | ||||
| } | ||||
| 
 | ||||
| @NgModule({ | ||||
|     imports: [ | ||||
|         RouterModule.forChild(routes), | ||||
| @ -68,9 +52,6 @@ function buildRoutes(injector: Injector): Routes { | ||||
|         TranslateModule.forChild(), | ||||
|         CoreSharedModule, | ||||
|     ], | ||||
|     providers: [ | ||||
|         { provide: ROUTES, multi: true, useFactory: buildRoutes, deps: [Injector] }, | ||||
|     ], | ||||
|     declarations: [ | ||||
|         AddonMessagesContactsPage, | ||||
|     ], | ||||
|  | ||||
| @ -18,12 +18,14 @@ import { CoreSites } from '@services/sites'; | ||||
| import { | ||||
|     AddonMessages, | ||||
|     AddonMessagesContactRequestCountEventData, | ||||
|     AddonMessagesConversationMember, | ||||
|     AddonMessagesMemberInfoChangedEventData, | ||||
|     AddonMessagesProvider, | ||||
|     AddonMessagesSplitViewLoadContactsEventData, | ||||
| } from '../../services/messages'; | ||||
| import { CoreTab } from '@components/tabs/tabs'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { CoreScreen } from '@services/screen'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { IonRefresher } from '@ionic/angular'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that displays contacts and contact requests. | ||||
| @ -31,56 +33,71 @@ import { CoreScreen } from '@services/screen'; | ||||
| @Component({ | ||||
|     selector: 'page-addon-messages-contacts', | ||||
|     templateUrl: 'contacts.html', | ||||
|     styleUrls: [ | ||||
|         'tabs.scss', | ||||
|         '../../messages-common.scss', | ||||
|     ], | ||||
| }) | ||||
| export class AddonMessagesContactsPage implements OnInit, OnDestroy { | ||||
| 
 | ||||
|     protected contactsTab: CoreTab =         { | ||||
|         id: 'contacts-confirmed', | ||||
|         class: '', | ||||
|         title: 'addon.messages.contacts', | ||||
|         icon: 'fas-address-book', | ||||
|         enabled: true, | ||||
|         page: '/main/messages/contacts/confirmed', | ||||
|     }; | ||||
|     selected = 'confirmed'; | ||||
|     requestsBadge = ''; | ||||
|     selectedUserId?: number; // User id of the conversation opened in the split view.
 | ||||
| 
 | ||||
|     protected requestsTab: CoreTab = { | ||||
|         id: 'contact-requests', | ||||
|         class: '', | ||||
|         title: 'addon.messages.requests', | ||||
|         icon: 'fas-user-plus', | ||||
|         enabled: true, | ||||
|         page: '/main/messages/contacts/requests', | ||||
|         badge: '', | ||||
|     }; | ||||
|     confirmedLoaded = false; | ||||
|     confirmedCanLoadMore = false; | ||||
|     confirmedLoadMoreError = false; | ||||
|     confirmedContacts: AddonMessagesConversationMember[] = []; | ||||
| 
 | ||||
|     tabs: CoreTab[] = []; | ||||
|     requestsLoaded = false; | ||||
|     requestsCanLoadMore = false; | ||||
|     requestsLoadMoreError = false; | ||||
|     requests: AddonMessagesConversationMember[] = []; | ||||
| 
 | ||||
|     protected siteId: string; | ||||
|     protected contactRequestsCountObserver: CoreEventObserver; | ||||
|     protected splitViewObserver: CoreEventObserver; | ||||
|     protected selectedUserId?: number; // User id of the conversation opened in the split view.
 | ||||
|     protected memberInfoObserver: CoreEventObserver; | ||||
| 
 | ||||
| 
 | ||||
|     constructor() { | ||||
| 
 | ||||
|         this.tabs = [this.contactsTab, this.requestsTab]; | ||||
| 
 | ||||
|         this.siteId = CoreSites.instance.getCurrentSiteId(); | ||||
| 
 | ||||
|         // Update the contact requests badge.
 | ||||
|         this.contactRequestsCountObserver = CoreEvents.on<AddonMessagesContactRequestCountEventData>( | ||||
|             AddonMessagesProvider.CONTACT_REQUESTS_COUNT_EVENT, | ||||
|             (data) => { | ||||
|                 this.requestsTab.badge = data.count > 0 ? String(data.count) : ''; | ||||
|                 this.requestsBadge = data.count > 0 ? String(data.count) : ''; | ||||
|             }, | ||||
|             this.siteId, | ||||
|         ); | ||||
| 
 | ||||
|         // Update the contact requests badge.
 | ||||
|         this.splitViewObserver = CoreEvents.on<AddonMessagesSplitViewLoadContactsEventData>( | ||||
|             AddonMessagesProvider.SPLIT_VIEW_LOAD_CONTACTS_EVENT, | ||||
|         // Update block status of a user.
 | ||||
|         this.memberInfoObserver = CoreEvents.on<AddonMessagesMemberInfoChangedEventData>( | ||||
|             AddonMessagesProvider.MEMBER_INFO_CHANGED_EVENT, | ||||
|             (data) => { | ||||
|                 this.selectUser(data.userId, data.onInit); | ||||
|                 if (data.userBlocked || data.userUnblocked) { | ||||
|                     const user = this.confirmedContacts.find((user) => user.id == data.userId); | ||||
|                     if (user) { | ||||
|                         user.isblocked = !!data.userBlocked; | ||||
|                     } | ||||
|                 } else if (data.contactRemoved) { | ||||
|                     const index = this.confirmedContacts.findIndex((contact) => contact.id == data.userId); | ||||
|                     if (index >= 0) { | ||||
|                         this.confirmedContacts.splice(index, 1); | ||||
|                     } | ||||
|                 } else if (data.contactRequestConfirmed) { | ||||
|                     this.refreshData(); | ||||
|                 } | ||||
| 
 | ||||
|                 if (data.contactRequestConfirmed || data.contactRequestDeclined) { | ||||
|                     const index = this.requests.findIndex((request) => request.id == data.userId); | ||||
|                     if (index >= 0) { | ||||
|                         this.requests.splice(index, 1); | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             CoreSites.instance.getCurrentSiteId(), | ||||
|         ); | ||||
| 
 | ||||
|     } | ||||
| @ -88,8 +105,123 @@ export class AddonMessagesContactsPage implements OnInit, OnDestroy { | ||||
|     /** | ||||
|      * Page being initialized. | ||||
|      */ | ||||
|     ngOnInit(): void { | ||||
|     async ngOnInit(): Promise<void> { | ||||
|         AddonMessages.instance.getContactRequestsCount(this.siteId); // Badge already updated by the observer.
 | ||||
| 
 | ||||
|         if (this.selected == 'confirmed') { | ||||
|             try { | ||||
| 
 | ||||
|                 await this.confirmedFetchData(); | ||||
|                 if (this.confirmedContacts.length && CoreScreen.instance.isTablet) { | ||||
|                     this.selectUser(this.confirmedContacts[0].id, true); | ||||
|                 } | ||||
|             } finally { | ||||
|                 this.confirmedLoaded = true; | ||||
|             } | ||||
|         } else { | ||||
|             try { | ||||
|                 await this.requestsFetchData(); | ||||
|                 if (this.requests.length && CoreScreen.instance.isTablet) { | ||||
|                     this.selectUser(this.requests[0].id, true); | ||||
|                 } | ||||
|             } finally { | ||||
|                 this.requestsLoaded = true; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch contacts. | ||||
|      * | ||||
|      * @param refresh True if we are refreshing contacts, false if we are loading more. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async confirmedFetchData(refresh: boolean = false): Promise<void> { | ||||
|         this.confirmedLoadMoreError = false; | ||||
| 
 | ||||
|         const limitFrom = refresh ? 0 : this.confirmedContacts.length; | ||||
| 
 | ||||
|         if (limitFrom === 0) { | ||||
|             // Always try to get latest data from server.
 | ||||
|             await AddonMessages.instance.invalidateUserContacts(); | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             const result = await AddonMessages.instance.getUserContacts(limitFrom); | ||||
|             this.confirmedContacts = refresh ? result.contacts : this.confirmedContacts.concat(result.contacts); | ||||
|             this.confirmedCanLoadMore = result.canLoadMore; | ||||
|         } catch (error) { | ||||
|             this.confirmedLoadMoreError = true; | ||||
|             CoreDomUtils.instance.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingcontacts', true); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch contact requests. | ||||
|      * | ||||
|      * @param refresh True if we are refreshing contact requests, false if we are loading more. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async requestsFetchData(refresh: boolean = false): Promise<void> { | ||||
|         this.requestsLoadMoreError = false; | ||||
| 
 | ||||
|         const limitFrom = refresh ? 0 : this.requests.length; | ||||
| 
 | ||||
|         if (limitFrom === 0) { | ||||
|             // Always try to get latest data from server.
 | ||||
|             await AddonMessages.instance.invalidateContactRequestsCache(); | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             const result = await AddonMessages.instance.getContactRequests(limitFrom); | ||||
|             this.requests = refresh ? result.requests : this.requests.concat(result.requests); | ||||
|             this.requestsCanLoadMore = result.canLoadMore; | ||||
|         } catch (error) { | ||||
|             this.requestsLoadMoreError = true; | ||||
|             CoreDomUtils.instance.showErrorModalDefault(error, 'addon.messages.errorwhileretrievingcontacts', true); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Refresh contacts or requests. | ||||
|      * | ||||
|      * @param refresher Refresher. | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async refreshData(refresher?: CustomEvent<IonRefresher>): Promise<void> { | ||||
|         try { | ||||
|             if (this.selected == 'confirmed') { | ||||
|                 // No need to invalidate contacts, we always try to get the latest.
 | ||||
|                 await this.confirmedFetchData(true); | ||||
|             } else { | ||||
|                 // Refresh the number of contacts requests to update badges.
 | ||||
|                 AddonMessages.instance.refreshContactRequestsCount(); | ||||
| 
 | ||||
|                 // No need to invalidate contact requests, we always try to get the latest.
 | ||||
|                 await this.requestsFetchData(true); | ||||
|             } | ||||
|         } finally { | ||||
|             refresher?.detail.complete(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load more contacts or requests. | ||||
|      * | ||||
|      * @param infiniteComplete Infinite scroll complete function. Only used from core-infinite-loading. | ||||
|      * @return Resolved when done. | ||||
|      */ | ||||
|     async loadMore(infiniteComplete?: () => void): Promise<void> { | ||||
|         try { | ||||
|             if (this.selected == 'confirmed') { | ||||
|                 // No need to invalidate contacts, we always try to get the latest.
 | ||||
|                 await this.confirmedFetchData(); | ||||
|             } else { | ||||
|                 await this.requestsFetchData(); | ||||
|             } | ||||
|         } finally { | ||||
|             infiniteComplete && infiniteComplete(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -99,6 +231,14 @@ export class AddonMessagesContactsPage implements OnInit, OnDestroy { | ||||
|         CoreNavigator.instance.navigateToSitePath('search'); | ||||
|     } | ||||
| 
 | ||||
|     selectTab(selected: string): void { | ||||
|         this.selected = selected; | ||||
| 
 | ||||
|         if ((this.selected == 'confirmed' && !this.confirmedLoaded) || (this.selected != 'confirmed' && !this.requestsLoaded)) { | ||||
|             this.ngOnInit(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set the selected user and open the conversation in the split view if needed. | ||||
|      * | ||||
| @ -118,14 +258,8 @@ export class AddonMessagesContactsPage implements OnInit, OnDestroy { | ||||
| 
 | ||||
|         this.selectedUserId = userId; | ||||
| 
 | ||||
|         // @todo it does not seem to work load anything.
 | ||||
|         let path = 'discussion'; | ||||
|         if (CoreScreen.instance.isMobile) { | ||||
|             path = '../../' + path; | ||||
|         } else { | ||||
|             const splitViewLoaded = CoreNavigator.instance.isSplitViewOutletLoaded('**/messages/contacts/**/discussion'); | ||||
|             path = (splitViewLoaded ? '../' : '') + path; | ||||
|         } | ||||
|         const splitViewLoaded = CoreNavigator.instance.isSplitViewOutletLoaded('**/messages/contacts/discussion'); | ||||
|         const path = (splitViewLoaded ? '../' : '') + 'discussion'; | ||||
| 
 | ||||
|         CoreNavigator.instance.navigate(path, { params : { userId } }); | ||||
|     } | ||||
| @ -135,7 +269,6 @@ export class AddonMessagesContactsPage implements OnInit, OnDestroy { | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         this.contactRequestsCountObserver?.off(); | ||||
|         this.splitViewObserver?.off(); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -1,33 +0,0 @@ | ||||
| // (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_CONTACTS_ROUTES = new InjectionToken('ADDON_MESSAGES_CONTACTS_ROUTES'); | ||||
| 
 | ||||
| @NgModule() | ||||
| export class AddonMessagesContactsRoutingModule { | ||||
| 
 | ||||
|     static forChild(routes: ModuleRoutesConfig): ModuleWithProviders<AddonMessagesContactsRoutingModule> { | ||||
|         return { | ||||
|             ngModule: AddonMessagesContactsRoutingModule, | ||||
|             providers: [ | ||||
|                 { provide: ADDON_MESSAGES_CONTACTS_ROUTES, multi: true, useValue: routes }, | ||||
|             ], | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										48
									
								
								src/addons/messages/pages/contacts/tabs.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/addons/messages/pages/contacts/tabs.scss
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,48 @@ | ||||
| :host { | ||||
|     ion-tab-bar.core-tabs-bar { | ||||
|         position: relative; | ||||
|         width: 100%; | ||||
|         background: var(--core-tabs-background); | ||||
|         color: var(--core-tab-color); | ||||
|         -webkit-filter: drop-shadow(0px 3px 3px rgba(var(--drop-shadow))); | ||||
|         filter: drop-shadow(0px 3px 3px rgba(var(--drop-shadow))); | ||||
|         border: 0; | ||||
| 
 | ||||
|         ion-row { | ||||
|             width: 100%; | ||||
|         } | ||||
| 
 | ||||
|         .tab-slide { | ||||
|             border-bottom: 2px solid transparent; | ||||
|             min-width: 100px; | ||||
|             min-height: 56px; | ||||
|             cursor: pointer; | ||||
|             overflow: hidden; | ||||
|             display: flex; | ||||
|             align-items: center; | ||||
|             justify-content: center; | ||||
|             padding: 0; | ||||
|             margin-bottom: 1px; | ||||
| 
 | ||||
|             ion-label { | ||||
|                 font-size: 16px; | ||||
|                 font-weight: 400; | ||||
|                 text-overflow: ellipsis; | ||||
|                 white-space: nowrap; | ||||
|                 overflow: hidden; | ||||
|                 word-wrap: break-word; | ||||
|                 max-width: 100%; | ||||
|                 line-height: 1.2em; | ||||
|                 margin: 16px auto; | ||||
|             } | ||||
| 
 | ||||
|             &[aria-selected=true] { | ||||
|                 color: var(--core-tab-border-color-active); | ||||
|                 border-bottom-color: var(--core-tab-color-active); | ||||
|                 ion-tab-button { | ||||
|                     color: var(--core-tab-border-color-active); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -1,56 +1,78 @@ | ||||
| <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> | ||||
| <ion-content> | ||||
|     <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshData($event)"> | ||||
|         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|     </ion-refresher> | ||||
|     <core-split-view> | ||||
|         <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-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"> | ||||
|         <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-selected-item]="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   class="ion-no-margin"> | ||||
| 
 | ||||
|         <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-selected-item]="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> | ||||
|                 <ion-item class="ion-text-wrap addon-message-discussion" (click)="gotoContacts($event)" | ||||
|                     [attr.aria-label]="'addon.messages.contacts' | translate" detail> | ||||
|                     <ion-icon name="fas-address-book" slot="start"></ion-icon> | ||||
|                     <ion-label><h2>{{ 'addon.messages.contacts' | translate }}</h2></ion-label> | ||||
|                 </ion-item> | ||||
| 
 | ||||
|         <core-empty-box *ngIf="(!discussions || discussions.length <= 0) && !search.showResults" icon="far-comments" | ||||
|         [message]="'addon.messages.nomessagesfound' | translate"></core-empty-box> | ||||
|                 <ng-container *ngIf="search.showResults"> | ||||
|                     <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-selected-item]="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> | ||||
|                 </ng-container> | ||||
|                 <ng-container *ngIf="!search.showResults"> | ||||
|                     <ion-item class="ion-text-wrap" *ngFor="let discussion of discussions" [title]="discussion.fullname" | ||||
|                     (click)="gotoDiscussion(discussion.message!.user)" | ||||
|                     [class.core-selected-item]="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> | ||||
|                 </ng-container> | ||||
|             </ion-list> | ||||
| 
 | ||||
|         <core-empty-box *ngIf="(!search.results || search.results.length <= 0) && search.showResults" icon="search" | ||||
|         [message]="'core.noresults' | translate"></core-empty-box> | ||||
|     </core-loading> | ||||
|             <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> | ||||
|     </core-split-view> | ||||
| </ion-content> | ||||
|  | ||||
| @ -17,6 +17,9 @@ import { IonicModule } from '@ionic/angular'; | ||||
| import { TranslateModule } from '@ngx-translate/core'; | ||||
| import { RouterModule, Routes } from '@angular/router'; | ||||
| import { CommonModule } from '@angular/common'; | ||||
| import { CoreScreen } from '@services/screen'; | ||||
| import { conditionalRoutes } from '@/app/app-routing.module'; | ||||
| import { discussionRoute } from '@addons/messages/messages-lazy.module'; | ||||
| 
 | ||||
| import { CoreSharedModule } from '@/core/shared.module'; | ||||
| import { CoreSearchComponentsModule } from '@features/search/components/components.module'; | ||||
| @ -25,9 +28,21 @@ import { AddonMessagesDiscussions35Page } from './discussions.page'; | ||||
| 
 | ||||
| const routes: Routes = [ | ||||
|     { | ||||
|         path: '', | ||||
|         matcher: segments => { | ||||
|             const matches = CoreScreen.instance.isMobile ? segments.length === 0 : true; | ||||
| 
 | ||||
|             return matches ? { consumed: [] } : null; | ||||
|         }, | ||||
|         component: AddonMessagesDiscussions35Page, | ||||
|         children: conditionalRoutes([ | ||||
|             { | ||||
|                 path: '', | ||||
|                 pathMatch: 'full', | ||||
|             }, | ||||
|             discussionRoute, | ||||
|         ], () => CoreScreen.instance.isTablet), | ||||
|     }, | ||||
|     ...conditionalRoutes([discussionRoute], () => CoreScreen.instance.isMobile), | ||||
| ]; | ||||
| 
 | ||||
| @NgModule({ | ||||
|  | ||||
| @ -22,17 +22,18 @@ import { | ||||
|     AddonMessagesNewMessagedEventData, | ||||
|     AddonMessagesProvider, | ||||
|     AddonMessagesReadChangedEventData, | ||||
|     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 { ActivatedRoute, Params } 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'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { CoreScreen } from '@services/screen'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that displays the list of discussions. | ||||
| @ -46,7 +47,6 @@ export class AddonMessagesDiscussions35Page implements OnInit, OnDestroy { | ||||
| 
 | ||||
|     protected newMessagesObserver: CoreEventObserver; | ||||
|     protected readChangedObserver: CoreEventObserver; | ||||
|     protected cronObserver: CoreEventObserver; | ||||
|     protected appResumeSubscription: Subscription; | ||||
|     protected pushObserver: Subscription; | ||||
|     protected loadingMessages: string; | ||||
| @ -121,11 +121,6 @@ export class AddonMessagesDiscussions35Page implements OnInit, OnDestroy { | ||||
|             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) { | ||||
| @ -152,7 +147,15 @@ export class AddonMessagesDiscussions35Page implements OnInit, OnDestroy { | ||||
|      */ | ||||
|     ngOnInit(): void { | ||||
|         this.route.queryParams.subscribe(async params => { | ||||
|             this.discussionUserId = params['discussionUserId'] || undefined; | ||||
|             const discussionUserId = params['discussionUserId'] | ||||
|                 ? parseInt(params['discussionUserId'], 10) | ||||
|                 : (params['userId'] ? parseInt(params['userId'], 10) : undefined); | ||||
| 
 | ||||
|             if (this.loaded && this.discussionUserId == discussionUserId) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             this.discussionUserId = discussionUserId; | ||||
| 
 | ||||
|             if (this.discussionUserId) { | ||||
|                 // There is a discussion to load, open the discussion in a new state.
 | ||||
| @ -161,9 +164,9 @@ export class AddonMessagesDiscussions35Page implements OnInit, OnDestroy { | ||||
| 
 | ||||
|             await this.fetchData(); | ||||
| 
 | ||||
|             if (!this.discussionUserId && this.discussions.length > 0) { | ||||
|             if (!this.discussionUserId && this.discussions.length > 0 && CoreScreen.instance.isTablet) { | ||||
|                 // Take first and load it.
 | ||||
|                 this.gotoDiscussion(this.discussions[0].message!.user, undefined, true); | ||||
|                 this.gotoDiscussion(this.discussions[0].message!.user); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| @ -267,17 +270,34 @@ export class AddonMessagesDiscussions35Page implements OnInit, OnDestroy { | ||||
|      * @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 { | ||||
|     gotoDiscussion(discussionUserId: number, messageId?: number): void { | ||||
|         this.discussionUserId = discussionUserId; | ||||
| 
 | ||||
|         const params: AddonMessagesSplitViewLoadIndexEventData = { | ||||
|             discussion: discussionUserId, | ||||
|             onlyWithSplitView: onlyWithSplitView, | ||||
|         const params: Params = { | ||||
|             userId: discussionUserId, | ||||
|         }; | ||||
| 
 | ||||
|         if (messageId) { | ||||
|             params.message = messageId; | ||||
|         } | ||||
|         CoreEvents.trigger(AddonMessagesProvider.SPLIT_VIEW_LOAD_INDEX_EVENT, params, this.siteId); | ||||
| 
 | ||||
|         const splitViewLoaded = CoreNavigator.instance.isSplitViewOutletLoaded('**/messages/index/discussion'); | ||||
|         const path = (splitViewLoaded ? '../' : '') + 'discussion'; | ||||
| 
 | ||||
|         CoreNavigator.instance.navigate(path, { params }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Navigate to contacts view. | ||||
|      */ | ||||
|     gotoContacts(): void { | ||||
|         const params: Params = {}; | ||||
| 
 | ||||
|         if (CoreScreen.instance.isTablet && this.discussionUserId) { | ||||
|             params.discussionUserId = this.discussionUserId; | ||||
|         } | ||||
| 
 | ||||
|         CoreNavigator.instance.navigateToSitePath('contacts-35', { params }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -286,7 +306,6 @@ export class AddonMessagesDiscussions35Page implements OnInit, OnDestroy { | ||||
|     ngOnDestroy(): void { | ||||
|         this.newMessagesObserver?.off(); | ||||
|         this.readChangedObserver?.off(); | ||||
|         this.cronObserver?.off(); | ||||
|         this.appResumeSubscription?.unsubscribe(); | ||||
|         this.pushObserver?.unsubscribe(); | ||||
|     } | ||||
|  | ||||
| @ -25,8 +25,8 @@ | ||||
| 
 | ||||
|         <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-item class="ion-text-wrap addon-message-discussion" (click)="gotoContacts($event)" | ||||
|                     [attr.aria-label]="'addon.messages.contacts' | translate" detail> | ||||
|                     <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> | ||||
|  | ||||
| @ -511,7 +511,6 @@ export class AddonMessagesGroupConversationsPage implements OnInit, OnDestroy { | ||||
|      */ | ||||
|     gotoContacts(): void { | ||||
|         CoreNavigator.instance.navigateToSitePath('contacts'); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -1,17 +0,0 @@ | ||||
| <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> | ||||
| <ion-content> | ||||
|     <core-split-view> | ||||
|         <core-tabs [tabs]="tabs" hideUntil="true"></core-tabs> | ||||
|     </core-split-view> | ||||
| </ion-content> | ||||
| @ -1,79 +0,0 @@ | ||||
| // (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 { conditionalRoutes, resolveModuleRoutes } from '@/app/app-routing.module'; | ||||
| import { discussionRoute } from '@addons/messages/messages-lazy.module'; | ||||
| import { CoreScreen } from '@services/screen'; | ||||
| 
 | ||||
| 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'; | ||||
| 
 | ||||
| // @todo mix both routes to get messages/index/discussions/discussion and messages/index/contacts/discussion working
 | ||||
| const routes: Routes = [ | ||||
|     { | ||||
|         matcher: segments => { | ||||
|             const matches = CoreScreen.instance.isMobile ? segments.length === 0 : true; | ||||
| 
 | ||||
|             return matches ? { consumed: [] } : null; | ||||
|         }, | ||||
|         component: AddonMessagesIndex35Page, | ||||
|         children: conditionalRoutes([ | ||||
|             { | ||||
|                 path: '', | ||||
|                 pathMatch: 'full', | ||||
|             }, | ||||
|             discussionRoute, | ||||
|         ], () => CoreScreen.instance.isTablet), | ||||
|     }, | ||||
|     ...conditionalRoutes([discussionRoute], () => CoreScreen.instance.isMobile), | ||||
| ]; | ||||
| 
 | ||||
| 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 {} | ||||
| @ -1,104 +0,0 @@ | ||||
| // (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 { CoreScreen } from '@services/screen'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that displays the messages index page. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'page-addon-messages-index', | ||||
|     templateUrl: 'index.html', | ||||
| }) | ||||
| export class AddonMessagesIndex35Page implements OnDestroy { | ||||
| 
 | ||||
|     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 && (CoreScreen.instance.isTablet || !data.onlyWithSplitView)) { | ||||
|                     this.gotoDiscussion(data.discussion, data.message); | ||||
|                 } | ||||
|             }, | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 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; | ||||
|         } | ||||
| 
 | ||||
|         let path = 'discussion'; | ||||
|         if (CoreScreen.instance.isMobile) { | ||||
|             path = '../../' + path; | ||||
|         } else { | ||||
|             const splitViewLoaded = CoreNavigator.instance.isSplitViewOutletLoaded('**/messages/index/**/discussion'); | ||||
|             path = (splitViewLoaded ? '../' : '') + path; | ||||
|         } | ||||
| 
 | ||||
|         CoreNavigator.instance.navigate(path, { params }); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Page destroyed. | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         this.loadSplitViewObserver?.off(); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -1,33 +0,0 @@ | ||||
| // (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 }, | ||||
|             ], | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -42,8 +42,6 @@ export class AddonMessagesProvider { | ||||
|     static readonly NEW_MESSAGE_EVENT = 'addon_messages_new_message_event'; | ||||
|     static readonly READ_CHANGED_EVENT = 'addon_messages_read_changed_event'; | ||||
|     static readonly OPEN_CONVERSATION_EVENT = 'addon_messages_open_conversation_event'; // Notify a conversation should be opened.
 | ||||
|     static readonly SPLIT_VIEW_LOAD_INDEX_EVENT = 'addon_messages_split_view_load_index_event'; // Used on 3.5 or lower.
 | ||||
|     static readonly SPLIT_VIEW_LOAD_CONTACTS_EVENT = 'addon_messages_split_view_load_contacts_event'; // Used on 3.6 or greater.
 | ||||
|     static readonly UPDATE_CONVERSATION_LIST_EVENT = 'addon_messages_update_conversation_list_event'; | ||||
|     static readonly MEMBER_INFO_CHANGED_EVENT = 'addon_messages_member_changed_event'; | ||||
|     static readonly UNREAD_CONVERSATION_COUNTS_EVENT = 'addon_messages_unread_conversation_counts_event'; | ||||
| @ -3696,23 +3694,6 @@ export type AddonMessagesMemberInfoChangedEventData = { | ||||
|     contactRemoved?: boolean; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Data sent by SPLIT_VIEW_LOAD_INDEX_EVENT event. Used on 3.5 or lower. | ||||
|  */ | ||||
| export type AddonMessagesSplitViewLoadIndexEventData = { | ||||
|     discussion: number; | ||||
|     onlyWithSplitView?: boolean; | ||||
|     message?: number; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Data sent by SPLIT_VIEW_LOAD_CONTACTS_EVENT event. Used on 3.6 or greater. | ||||
|  */ | ||||
| export type AddonMessagesSplitViewLoadContactsEventData = { | ||||
|     userId: number; | ||||
|     onInit: boolean; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Data sent by READ_CHANGED_EVENT event. | ||||
|  */ | ||||
|  | ||||
| @ -135,14 +135,21 @@ | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     --core-tabs-background: var(--custom-tabs-background, var(--white)); | ||||
|     --core-tab-background: var(--custom-tab-background, var(--core-tabs-background)); | ||||
|     --core-tab-color: var(--custom-tab-color, var(--gray-dark)); | ||||
|     --core-tab-border-color: var(--custom-tab-border-color, var(--gray)); | ||||
|     --core-tab-color-active: var(--custom-tab-color-active, var(--core-color)); | ||||
|     --core-tab-border-color-active: var(--custom-tab-border-color-active, var(--core-color)); | ||||
| 
 | ||||
|     core-tabs { | ||||
|         --background: var(--custom-tabs-background, var(--white)); | ||||
|         --background: var(--core-tabs-background); | ||||
|         ion-slide { | ||||
|             --background: var(--custom-tab-background, var(--white)); | ||||
|             --color: var(--custom-tab-background, var(--gray-dark)); | ||||
|             --border-color: var(--custom-tab-border-color, var(--gray)); | ||||
|             --color-active: var(--custom-tab-color-active, var(--color)); | ||||
|             --border-color-active: var(--custom-tab-border-color-active, var(--core-color)); | ||||
|             --background: var(--core-tab-background); | ||||
|             --color: var(--core-tab-color); | ||||
|             --border-color: var(--core-tab-border-colo); | ||||
|             --color-active: var(--core-tab-color-active); | ||||
|             --border-color-active: var(--core-tab-border-color-active); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @ -276,14 +283,10 @@ | ||||
|         --contrast-background: var(--ion-background-color); | ||||
|     } | ||||
| 
 | ||||
|     core-tabs { | ||||
|         --background: var(--custom-tabs-background, #3a3a3a); | ||||
|         ion-slide { | ||||
|             --background: var(--custom-tab-background, #3a3a3a); | ||||
|             --color: var(--custom-tab-background, var(--white)); | ||||
|             --border-color: var(--custom-tab-border-color, var(--gray-light)); | ||||
|         } | ||||
|     } | ||||
|     --core-tabs-background: var(--custom-tabs-background, #3a3a3a); | ||||
|     --core-tab-background: var(--custom-tab-background, #3a3a3a); | ||||
|     --core-tab-color: var(--custom-tab-color, var(--white)); | ||||
|     --core-tab-border-color: var(--custom-tab-border-color, var(--gray-light)); | ||||
| 
 | ||||
|     core-progress-bar { | ||||
|         --text-color: var(--custom-progress-text-color, var(--gray-lighter)); | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user