diff --git a/src/addons/notifications/classes/notifications-source.ts b/src/addons/notifications/classes/notifications-source.ts new file mode 100644 index 000000000..6a49c913d --- /dev/null +++ b/src/addons/notifications/classes/notifications-source.ts @@ -0,0 +1,47 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { CoreRoutedItemsManagerSource } from '@classes/items-management/routed-items-manager-source'; +import { AddonNotifications } from '../services/notifications'; +import { AddonNotificationsHelper, AddonNotificationsNotificationToRender } from '../services/notifications-helper'; + +/** + * Provides a list of notifications + */ +export class AddonsNotificationsNotificationsSource extends CoreRoutedItemsManagerSource { + + /** + * @inheritdoc + */ + protected async loadPageItems(page: number): Promise<{ + items: AddonNotificationsNotificationToRender[]; + hasMoreItems: boolean; + }> { + // TODO this should be refactored to avoid using the existing items. + const { notifications, canLoadMore } = await AddonNotifications.getNotifications(page === 0 ? [] : this.getItems() ?? []); + + return { + items: notifications.map(notification => AddonNotificationsHelper.formatNotificationText(notification)), + hasMoreItems: canLoadMore, + }; + } + + /** + * @inheritdoc + */ + getItemPath(notification: AddonNotificationsNotificationToRender): string { + return notification.id.toString(); + } + +} diff --git a/src/addons/notifications/notifications-lazy.module.ts b/src/addons/notifications/notifications-lazy.module.ts index 11879c29a..31928d31d 100644 --- a/src/addons/notifications/notifications-lazy.module.ts +++ b/src/addons/notifications/notifications-lazy.module.ts @@ -12,10 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { conditionalRoutes } from '@/app/app-routing.module'; import { Injector, NgModule } from '@angular/core'; import { RouterModule, ROUTES, Routes } from '@angular/router'; import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module'; +import { CoreScreen } from '@services/screen'; import { AddonNotificationsMainMenuHandlerService } from './services/handlers/mainmenu'; function buildRoutes(injector: Injector): Routes { @@ -27,6 +29,13 @@ function buildRoutes(injector: Injector): Routes { }, loadChildren: () => import('./pages/list/list.module').then(m => m.AddonNotificationsListPageModule), }, + ...conditionalRoutes([ + { + path: 'list/:id', + loadChildren: () => import('./pages/notification/notification.module') + .then(m => m.AddonNotificationsNotificationPageModule), + }, + ], () => CoreScreen.isMobile), { path: 'notification', loadChildren: () => import('./pages/notification/notification.module') diff --git a/src/addons/notifications/pages/list/list.html b/src/addons/notifications/pages/list/list.html index f208df1ae..1a2cd5589 100644 --- a/src/addons/notifications/pages/list/list.html +++ b/src/addons/notifications/pages/list/list.html @@ -11,66 +11,72 @@ - - - - - + + + + + + - - -
- -
- - -
+ - -
- -
- - -
+ +
+ +
+ + +
- -

- - -

-

{{ notification.timecreated | coreTimeAgo }} · {{ - notification.userfromfullname }} -

-
- - - -
+ +
+ +
+ + +
- - - - -
+ +

+ + +

+

{{ notification.timecreated | coreTimeAgo }} · {{ + notification.userfromfullname }} +

+
+ + + + + + + + + +
-
- - - - - {{ 'addon.notifications.markallread' | translate }} - -
+
+ + + + + {{ 'addon.notifications.markallread' | translate }} + +
+
diff --git a/src/addons/notifications/pages/list/list.module.ts b/src/addons/notifications/pages/list/list.module.ts index bf8eb18ec..62ffcd9c7 100644 --- a/src/addons/notifications/pages/list/list.module.ts +++ b/src/addons/notifications/pages/list/list.module.ts @@ -18,11 +18,20 @@ import { RouterModule, Routes } from '@angular/router'; import { CoreSharedModule } from '@/core/shared.module'; import { AddonNotificationsListPage } from './list'; import { CoreMainMenuComponentsModule } from '@features/mainmenu/components/components.module'; +import { conditionalRoutes } from '@/app/app-routing.module'; +import { CoreScreen } from '@services/screen'; const routes: Routes = [ { path: '', component: AddonNotificationsListPage, + children: conditionalRoutes([ + { + path: ':id', + loadChildren: () => import('../../pages/notification/notification.module') + .then(m => m.AddonNotificationsNotificationPageModule), + }, + ], () => CoreScreen.isTablet), }, ]; diff --git a/src/addons/notifications/pages/list/list.ts b/src/addons/notifications/pages/list/list.ts index d8b29c4de..9d144a5dd 100644 --- a/src/addons/notifications/pages/list/list.ts +++ b/src/addons/notifications/pages/list/list.ts @@ -12,26 +12,26 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { AfterViewInit, Component, OnDestroy, ViewChild } from '@angular/core'; import { IonRefresher } from '@ionic/angular'; import { Subscription } from 'rxjs'; -import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; -import { CoreEvents, CoreEventObserver } from '@singletons/events'; +import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { - AddonNotifications, - AddonNotificationsProvider, + AddonNotifications, AddonNotificationsProvider, } from '../../services/notifications'; -import { CorePushNotificationsDelegate } from '@features/pushnotifications/services/push-delegate'; -import { - AddonNotificationsHelper, - AddonNotificationsNotificationToRender, -} from '@addons/notifications/services/notifications-helper'; -import { CoreMainMenuDeepLinkManager } from '@features/mainmenu/classes/deep-link-manager'; import { CoreNavigator } from '@services/navigator'; +import { CoreSplitViewComponent } from '@components/split-view/split-view'; +import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; +import { CorePushNotificationsDelegate } from '@features/pushnotifications/services/push-delegate'; +import { CoreSites } from '@services/sites'; +import { CoreMainMenuDeepLinkManager } from '@features/mainmenu/classes/deep-link-manager'; import { CoreTimeUtils } from '@services/utils/time'; +import { AddonsNotificationsNotificationsSource } from '@addons/notifications/classes/notifications-source'; +import { CoreListItemsManager } from '@classes/items-management/list-items-manager'; +import { AddonNotificationsNotificationToRender } from '@addons/notifications/services/notifications-helper'; /** * Page that displays the list of notifications. @@ -41,12 +41,11 @@ import { CoreTimeUtils } from '@services/utils/time'; templateUrl: 'list.html', styleUrls: ['list.scss', '../../notifications.scss'], }) -export class AddonNotificationsListPage implements OnInit, OnDestroy { +export class AddonNotificationsListPage implements AfterViewInit, OnDestroy { - notifications: AddonNotificationsNotificationToRender[] = []; - notificationsLoaded = false; - canLoadMore = false; - loadMoreError = false; + @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; + notifications!: CoreListItemsManager; + fetchMoreNotificationsFailed = false; canMarkAllNotificationsAsRead = false; loadingMarkAllNotificationsAsRead = false; @@ -56,18 +55,33 @@ export class AddonNotificationsListPage implements OnInit, OnDestroy { protected pushObserver?: Subscription; protected pendingRefresh = false; + constructor() { + try { + const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource( + AddonsNotificationsNotificationsSource, + [], + ); + + this.notifications = new CoreListItemsManager(source, AddonNotificationsListPage); + } catch(error) { + CoreDomUtils.showErrorModal(error); + CoreNavigator.back(); + } + } + /** * @inheritdoc */ - ngOnInit(): void { - this.fetchNotifications(); + async ngAfterViewInit(): Promise { + await this.fetchInitialNotifications(); + + this.notifications.start(this.splitView); this.cronObserver = CoreEvents.on(AddonNotificationsProvider.READ_CRON_EVENT, () => { if (!this.isCurrentView) { return; } - this.notificationsLoaded = false; this.refreshNotifications(); }, CoreSites.getCurrentSiteId()); @@ -83,7 +97,6 @@ export class AddonNotificationsListPage implements OnInit, OnDestroy { return; } - this.notificationsLoaded = false; this.refreshNotifications(); }); @@ -92,7 +105,7 @@ export class AddonNotificationsListPage implements OnInit, OnDestroy { return; } - const notification = this.notifications.find((notification) => notification.id === data.id); + const notification = this.notifications.items.find((notification) => notification.id === data.id); if (!notification) { return; } @@ -109,34 +122,47 @@ export class AddonNotificationsListPage implements OnInit, OnDestroy { /** * Convenience function to get notifications. Gets unread notifications first. * - * @param refresh Whether we're refreshing data. - * @return Resolved when done. + * @param reload Whether to reload the list or load the next page. */ - protected async fetchNotifications(refresh?: boolean): Promise { - this.loadMoreError = false; + protected async fetchNotifications(reload: boolean): Promise { + reload + ? await this.notifications.reload() + : await this.notifications.load(); + this.fetchMoreNotificationsFailed = false; + this.loadMarkAllAsReadButton(); + } + + /** + * Obtain the initial batch of notifications. + */ + private async fetchInitialNotifications(): Promise { try { - const result = await AddonNotifications.getNotifications(refresh ? [] : this.notifications); - - const notifications = result.notifications - .map((notification) => AddonNotificationsHelper.formatNotificationText(notification)); - - if (refresh) { - this.notifications = notifications; - } else { - this.notifications = this.notifications.concat(notifications); - } - this.canLoadMore = result.canLoadMore; - - await this.loadMarkAllAsReadButton(); + await this.fetchNotifications(true); } catch (error) { - CoreDomUtils.showErrorModalDefault(error, 'addon.notifications.errorgetnotifications', true); - this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading. - } finally { - this.notificationsLoaded = true; + CoreDomUtils.showErrorModalDefault(error, 'Error loading notifications'); + + this.notifications.reset(); } } + /** + * Load a new batch of Notifications. + * + * @param complete Completion callback. + */ + async fetchMoreNotifications(complete: () => void): Promise { + try { + await this.fetchNotifications(false); + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'Error loading more notifications'); + + this.fetchMoreNotificationsFailed = true; + } + + complete(); + } + /** * Mark all notifications as read. * @@ -151,9 +177,6 @@ export class AddonNotificationsListPage implements OnInit, OnDestroy { time: CoreTimeUtils.timestamp(), }, CoreSites.getCurrentSiteId()); - // All marked as read, refresh the list. - this.notificationsLoaded = false; - await this.refreshNotifications(); } @@ -179,38 +202,12 @@ export class AddonNotificationsListPage implements OnInit, OnDestroy { * Refresh notifications. * * @param refresher Refresher. - * @return Promise Promise resolved when done. */ async refreshNotifications(refresher?: IonRefresher): Promise { await CoreUtils.ignoreErrors(AddonNotifications.invalidateNotificationsList()); + await CoreUtils.ignoreErrors(this.fetchNotifications(true)); - try { - await this.fetchNotifications(true); - } finally { - refresher?.complete(); - } - } - - /** - * Load more results. - * - * @param infiniteComplete Infinite scroll complete function. Only used from core-infinite-loading. - */ - async loadMoreNotifications(infiniteComplete?: () => void): Promise { - try { - await this.fetchNotifications(); - } finally { - infiniteComplete?.(); - } - } - - /** - * Open Notification page. - * - * @param notification Notification to open. - */ - openNotification(notification: AddonNotificationsNotificationToRender): void { - CoreNavigator.navigate('../notification', { params: { notification } }); + refresher?.complete(); } /** @@ -224,7 +221,6 @@ export class AddonNotificationsListPage implements OnInit, OnDestroy { } this.pendingRefresh = false; - this.notificationsLoaded = false; this.refreshNotifications(); } @@ -243,6 +239,7 @@ export class AddonNotificationsListPage implements OnInit, OnDestroy { this.cronObserver?.off(); this.readObserver?.off(); this.pushObserver?.unsubscribe(); + this.notifications?.destroy(); } } diff --git a/src/addons/notifications/pages/notification/notification.html b/src/addons/notifications/pages/notification/notification.html index 9c2254c43..2814eeaa8 100644 --- a/src/addons/notifications/pages/notification/notification.html +++ b/src/addons/notifications/pages/notification/notification.html @@ -8,7 +8,7 @@ - +
diff --git a/src/addons/notifications/pages/notification/notification.ts b/src/addons/notifications/pages/notification/notification.ts index 23282d713..5b5993f2f 100644 --- a/src/addons/notifications/pages/notification/notification.ts +++ b/src/addons/notifications/pages/notification/notification.ts @@ -12,18 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { AddonsNotificationsNotificationsSource } from '@addons/notifications/classes/notifications-source'; import { AddonNotificationsNotificationData } from '@addons/notifications/services/handlers/push-click'; -import { AddonNotifications } from '@addons/notifications/services/notifications'; import { AddonNotificationsHelper, AddonNotificationsNotificationToRender, } from '@addons/notifications/services/notifications-helper'; -import { Component, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { ActivatedRouteSnapshot } from '@angular/router'; +import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker'; +import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager'; import { CoreContentLinksAction, CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; import { CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; -import { CoreUtils } from '@services/utils/utils'; /** * Page to render a notification. @@ -33,8 +35,9 @@ import { CoreUtils } from '@services/utils/utils'; templateUrl: 'notification.html', styleUrls: ['../../notifications.scss', 'notification.scss'], }) -export class AddonNotificationsNotificationPage implements OnInit { +export class AddonNotificationsNotificationPage implements OnInit, OnDestroy { + notifications?: AddonNotificationSwipeItemsManager; subject = ''; // Notification subject. content = ''; // Notification content. userIdFrom = -1; // User ID who sent the notification. @@ -58,28 +61,14 @@ export class AddonNotificationsNotificationPage implements OnInit { let notification: AddonNotificationsNotification; try { - notification = CoreNavigator.getRequiredRouteParam('notification'); + notification = this.getNotification(); } catch (error) { CoreDomUtils.showErrorModal(error); - CoreNavigator.back(); return; } - if (!('subject' in notification)) { - // Try to find the notification using the WebService, it contains a better message. - const notifId = Number(notification.savedmessageid); - const result = await CoreUtils.ignoreErrors( - AddonNotifications.getNotifications([], { siteId: notification.site }), - ); - - const foundNotification = result?.notifications.find(notif => notif.id === notifId); - if (foundNotification) { - notification = AddonNotificationsHelper.formatNotificationText(foundNotification); - } - } - if ('subject' in notification) { this.subject = notification.subject; this.content = notification.mobiletext || notification.fullmessagehtml; @@ -95,7 +84,6 @@ export class AddonNotificationsNotificationPage implements OnInit { } } this.timecreated = notification.timecreated; - } else { this.subject = notification.title || ''; this.content = notification.message || ''; @@ -110,6 +98,51 @@ export class AddonNotificationsNotificationPage implements OnInit { this.loaded = true; } + /** + * Get notification. + * + * @returns notification. + */ + getNotification(): AddonNotificationsNotification { + const id = CoreNavigator.getRouteNumberParam('id'); + const notification = id ? this.getNotificationById(id) : undefined; + + return notification ?? CoreNavigator.getRequiredRouteParam('notification'); + } + + /** + * Obtain notification by passed id. + * + * @param notificationId Notification id. + * @return Found notification. + */ + getNotificationById(notificationId: number): AddonNotificationsNotification | undefined { + const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource( + AddonsNotificationsNotificationsSource, + [], + ); + const notification = source.getItems()?.find(({ id }) => id === notificationId); + + if (!notification) { + return; + } + + this.loadNotifications(source); + + return notification; + } + + /** + * Load notifications from source. + * + * @param source Notifications source + */ + async loadNotifications(source: AddonsNotificationsNotificationsSource): Promise { + this.notifications = new AddonNotificationSwipeItemsManager(source); + + await this.notifications.start(); + } + /** * Load notification actions * @@ -171,6 +204,27 @@ export class AddonNotificationsNotificationPage implements OnInit { site.openInBrowserWithAutoLogin(url); } + /** + * @inheritdoc + */ + ngOnDestroy(): void { + this.notifications?.destroy(); + } + +} + +/** + * Helper to manage swiping within a collection of notifications. + */ +class AddonNotificationSwipeItemsManager extends CoreSwipeNavigationItemsManager { + + /** + * @inheritdoc + */ + protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot): string | null { + return route.params.id; + } + } type AddonNotificationsNotification = AddonNotificationsNotificationToRender | AddonNotificationsNotificationData; diff --git a/src/core/initializers/prepare-devtools.ts b/src/core/initializers/prepare-devtools.ts index dbd3df0b1..7e0d4d255 100644 --- a/src/core/initializers/prepare-devtools.ts +++ b/src/core/initializers/prepare-devtools.ts @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { CorePushNotifications, CorePushNotificationsProvider } from '@features/pushnotifications/services/pushnotifications'; import { CoreApp, CoreAppProvider } from '@services/app'; import { CoreConfig, CoreConfigProvider } from '@services/config'; import { CoreDB, CoreDbProvider } from '@services/db'; @@ -25,6 +26,7 @@ type DevelopmentWindow = Window & { configProvider?: CoreConfigProvider; dbProvider?: CoreDbProvider; urlSchemes?: CoreCustomURLSchemesProvider; + pushNotifications?: CorePushNotificationsProvider; }; function initializeDevelopmentWindow(window: DevelopmentWindow) { @@ -33,6 +35,7 @@ function initializeDevelopmentWindow(window: DevelopmentWindow) { window.configProvider = CoreConfig.instance; window.dbProvider = CoreDB.instance; window.urlSchemes = CoreCustomURLSchemes.instance; + window.pushNotifications = CorePushNotifications.instance; } export default function(): void {