diff --git a/src/addons/notifications/components/actions/actions.ts b/src/addons/notifications/components/actions/actions.ts deleted file mode 100644 index 9ee3b221a..000000000 --- a/src/addons/notifications/components/actions/actions.ts +++ /dev/null @@ -1,90 +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, Input, OnInit } from '@angular/core'; - -import { CoreSites } from '@services/sites'; -import { CoreContentLinksDelegate, CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate'; - -/** - * Component that displays the actions for a notification. - */ -@Component({ - selector: 'addon-notifications-actions', - templateUrl: 'addon-notifications-actions.html', -}) -export class AddonNotificationsActionsComponent implements OnInit { - - @Input() contextUrl?: string; - @Input() courseId?: number; - @Input() data?: Record; // Extra data to handle the URL. - - actions: CoreContentLinksAction[] = []; - - /** - * Component being initialized. - */ - async ngOnInit(): Promise { - if (!this.contextUrl && (!this.data || !this.data.appurl)) { - // No URL, nothing to do. - return; - } - - let actions: CoreContentLinksAction[] = []; - - // Treat appurl first if any. - if (this.data?.appurl) { - actions = await CoreContentLinksDelegate.getActionsFor( - this.data.appurl, - this.courseId, - undefined, - this.data, - ); - } - - if (!actions.length && this.contextUrl) { - // No appurl or cannot handle it. Try with contextUrl. - actions = await CoreContentLinksDelegate.getActionsFor(this.contextUrl, this.courseId, undefined, this.data); - } - - if (!actions.length) { - // URL is not supported. Add an action to open it in browser. - actions.push({ - message: 'core.view', - icon: 'fas-eye', - action: this.openInBrowser.bind(this), - }); - } - - this.actions = actions; - } - - /** - * Default action. Open in browser. - * - * @param siteId Site ID to use. - */ - protected async openInBrowser(siteId?: string): Promise { - const url = this.data?.appurl || this.contextUrl; - - if (!url) { - return; - } - - const site = await CoreSites.getSite(siteId); - - site.openInBrowserWithAutoLogin(url); - } - -} diff --git a/src/addons/notifications/components/actions/addon-notifications-actions.html b/src/addons/notifications/components/actions/addon-notifications-actions.html deleted file mode 100644 index 348a2d701..000000000 --- a/src/addons/notifications/components/actions/addon-notifications-actions.html +++ /dev/null @@ -1,8 +0,0 @@ - - - - - {{ action.message | translate }} - - - diff --git a/src/addons/notifications/components/components.module.ts b/src/addons/notifications/components/components.module.ts deleted file mode 100644 index 06e36bdcd..000000000 --- a/src/addons/notifications/components/components.module.ts +++ /dev/null @@ -1,31 +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 { CoreSharedModule } from '@/core/shared.module'; - -import { AddonNotificationsActionsComponent } from './actions/actions'; - -@NgModule({ - declarations: [ - AddonNotificationsActionsComponent, - ], - imports: [ - CoreSharedModule, - ], - exports: [ - AddonNotificationsActionsComponent, - ], -}) -export class AddonNotificationsComponentsModule {} diff --git a/src/addons/notifications/notifications.scss b/src/addons/notifications/notifications.scss index 5285ea7f0..0583e27fa 100644 --- a/src/addons/notifications/notifications.scss +++ b/src/addons/notifications/notifications.scss @@ -2,6 +2,7 @@ :host { --extra-icon-size: 16px; + --icon-size: 24px; ::ng-deep core-user-avatar { .core-avatar-extra-img, @@ -28,71 +29,18 @@ } } - .core-notification-icon { - width: var(--core-avatar-size); - height: var(--core-avatar-size); - @include margin(6px, 8px, 6px, 0px); + div.core-notification-icon { + img { + width: var(--icon-size); + height: var(--icon-size); + } + padding: 0.7rem; background: var(--background-color); border-radius: var(--small-radius); } - .item core-format-text ::ng-deep { - .forumpost { - border: 1px solid var(--gray-200); - width: 100%; - margin: 0 0 1em 0; - - td { - padding: 10px; - } - - .header { - background-color: var(--light); - - .picture { - width: 48px; - text-align: center; - @include padding-horizontal(4px, 0px); - padding-top: 8px; - - img { - width: 44px !important; - } - } - } - - .subject { - font-weight: 700; - margin-bottom: 1rem; - } - } - - a { - text-decoration: none; - } - - .userpicture { - border-radius: 50%; - } - - .mdl-right { - text-align: end; - a { - display: none; - } - font { - font-size: 0.9em; - } - } - - .commands { - display: none; - } - - hr { - margin-top: 1.5rem; - margin-bottom: 1.5rem; - background-color: var(--gray-200); - } + .core-notification-icon { + --module-icon-size: var(--icon-size); + @include margin(6px, 8px, 6px, 0px); } } diff --git a/src/addons/notifications/pages/list/list.html b/src/addons/notifications/pages/list/list.html index bac0b73c5..955271699 100644 --- a/src/addons/notifications/pages/list/list.html +++ b/src/addons/notifications/pages/list/list.html @@ -11,69 +11,60 @@ - + -
-
- - - - - {{ 'addon.notifications.markallread' | translate }} - -
+
+ + + + + {{ 'addon.notifications.markallread' | translate }} + +
- - - -
- -
- - -
+ : 'addon.notifications.unreadnotification' | translate: {$a: notification.subject}" + (click)="openNotification(notification)" button [detail]="true" lines="true"> + +
+ +
+ + +
- + +
+ +
+ + +
- -

- - -

-

{{ notification.userfromfullname }}

-
- - {{ notification.timecreated | coreDateDayOrTime }} - - - - -
- - - - - - - - -
-
+ +

+ + +

+

{{ notification.userfromfullname }}

+
+ + {{ notification.timecreated | coreDateDayOrTime }} + + + + + diff --git a/src/addons/notifications/pages/list/list.module.ts b/src/addons/notifications/pages/list/list.module.ts index b592079e7..bf8eb18ec 100644 --- a/src/addons/notifications/pages/list/list.module.ts +++ b/src/addons/notifications/pages/list/list.module.ts @@ -16,7 +16,6 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { CoreSharedModule } from '@/core/shared.module'; -import { AddonNotificationsComponentsModule } from '../../components/components.module'; import { AddonNotificationsListPage } from './list'; import { CoreMainMenuComponentsModule } from '@features/mainmenu/components/components.module'; @@ -31,7 +30,6 @@ const routes: Routes = [ imports: [ RouterModule.forChild(routes), CoreSharedModule, - AddonNotificationsComponentsModule, CoreMainMenuComponentsModule, ], declarations: [ diff --git a/src/addons/notifications/pages/list/list.ts b/src/addons/notifications/pages/list/list.ts index f508814bf..5919309e9 100644 --- a/src/addons/notifications/pages/list/list.ts +++ b/src/addons/notifications/pages/list/list.ts @@ -30,6 +30,8 @@ import { AddonNotificationsNotificationToRender, } from '@addons/notifications/services/notifications-helper'; import { CoreMainMenuDeepLinkManager } from '@features/mainmenu/classes/deep-link-manager'; +import { CoreNavigator } from '@services/navigator'; +import { CoreTimeUtils } from '@services/utils/time'; /** * Page that displays the list of notifications. @@ -50,6 +52,7 @@ export class AddonNotificationsListPage implements OnInit, OnDestroy { protected isCurrentView?: boolean; protected cronObserver?: CoreEventObserver; + protected readObserver?: CoreEventObserver; protected pushObserver?: Subscription; protected pendingRefresh = false; @@ -84,6 +87,21 @@ export class AddonNotificationsListPage implements OnInit, OnDestroy { this.refreshNotifications(); }); + this.readObserver = CoreEvents.on(AddonNotificationsProvider.READ_CHANGED_EVENT, (data) => { + if (!data.id) { + return; + } + + const notification = this.notifications.find((notification) => notification.id === data.id); + if (!notification) { + return; + } + + notification.read = true; + notification.timeread = data.time; + this.loadMarkAllAsReadButton(); + }); + const deepLinkManager = new CoreMainMenuDeepLinkManager(); deepLinkManager.treatLink(); } @@ -91,7 +109,7 @@ export class AddonNotificationsListPage implements OnInit, OnDestroy { /** * Convenience function to get notifications. Gets unread notifications first. * - * @param refreh Whether we're refreshing data. + * @param refresh Whether we're refreshing data. * @return Resolved when done. */ protected async fetchNotifications(refresh?: boolean): Promise { @@ -110,7 +128,7 @@ export class AddonNotificationsListPage implements OnInit, OnDestroy { } this.canLoadMore = result.canLoadMore; - this.markNotificationsAsRead(notifications); + await this.loadMarkAllAsReadButton(); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.notifications.errorgetnotifications', true); this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading. @@ -129,7 +147,9 @@ export class AddonNotificationsListPage implements OnInit, OnDestroy { await CoreUtils.ignoreErrors(AddonNotifications.markAllNotificationsAsRead()); - CoreEvents.trigger(AddonNotificationsProvider.READ_CHANGED_EVENT, {}, CoreSites.getCurrentSiteId()); + CoreEvents.trigger(AddonNotificationsProvider.READ_CHANGED_EVENT, { + time: CoreTimeUtils.timestamp(), + }, CoreSites.getCurrentSiteId()); // All marked as read, refresh the list. this.notificationsLoaded = false; @@ -140,26 +160,25 @@ export class AddonNotificationsListPage implements OnInit, OnDestroy { /** * Mark notifications as read. * - * @param notifications Array of notification objects. + * @param notification Array of notification objects. + * @return Promise resolved when done. */ - protected async markNotificationsAsRead(notifications: AddonNotificationsNotificationToRender[]): Promise { - if (notifications.length > 0) { - const promises = notifications.map(async (notification) => { - if (notification.read) { - // Already read, don't mark it. - return; - } + protected async markNotificationAsRead(notification: AddonNotificationsNotificationToRender): Promise { + const updated = await AddonNotificationsHelper.markNotificationAsRead(notification); - await AddonNotifications.markNotificationRead(notification.id); - }); - - await CoreUtils.ignoreErrors(Promise.all(promises)); - - await CoreUtils.ignoreErrors(AddonNotifications.invalidateNotificationsList()); - - CoreEvents.trigger(AddonNotificationsProvider.READ_CHANGED_EVENT, {}, CoreSites.getCurrentSiteId()); + if (!updated) { + return; } + await this.loadMarkAllAsReadButton(); + } + + /** + * Load mark all notifications as read button. + * + * @return Promise resolved when done. + */ + protected async loadMarkAllAsReadButton(): Promise { // Check if mark all as read should be displayed (there are unread notifications). try { this.loadingMarkAllNotificationsAsRead = true; @@ -201,6 +220,15 @@ export class AddonNotificationsListPage implements OnInit, OnDestroy { } } + /** + * Open Notification page. + * + * @param notification Notification to open. + */ + openNotification(notification: AddonNotificationsNotificationToRender): void { + CoreNavigator.navigate('../notification', { params: { notification } }); + } + /** * User entered the page. */ @@ -229,6 +257,7 @@ export class AddonNotificationsListPage implements OnInit, OnDestroy { */ ngOnDestroy(): void { this.cronObserver?.off(); + this.readObserver?.off(); this.pushObserver?.unsubscribe(); } diff --git a/src/addons/notifications/pages/notification/notification.html b/src/addons/notifications/pages/notification/notification.html index c0566dee0..b5acae1bd 100644 --- a/src/addons/notifications/pages/notification/notification.html +++ b/src/addons/notifications/pages/notification/notification.html @@ -4,34 +4,61 @@ -

{{ 'addon.notifications.notifications' | translate }}

+

{{ 'addon.notifications.notifications' | translate }}

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

{{ subject }}

-

{{ userFromFullName }}

-
-
- - - - - - + +
+ +
+ + +
+ + + {{ userFromFullName }} + + + + + + + {{ timecreated | coreDateDayOrTime }} + + + + + + + + +
+
+ +
+
+ + + {{ action.message | translate }} + +
+
diff --git a/src/addons/notifications/pages/notification/notification.module.ts b/src/addons/notifications/pages/notification/notification.module.ts index b316d8bd4..e16e2860b 100644 --- a/src/addons/notifications/pages/notification/notification.module.ts +++ b/src/addons/notifications/pages/notification/notification.module.ts @@ -16,7 +16,6 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { CoreSharedModule } from '@/core/shared.module'; -import { AddonNotificationsComponentsModule } from '../../components/components.module'; import { AddonNotificationsNotificationPage } from './notification'; const routes: Routes = [ @@ -30,7 +29,6 @@ const routes: Routes = [ imports: [ RouterModule.forChild(routes), CoreSharedModule, - AddonNotificationsComponentsModule, ], declarations: [ AddonNotificationsNotificationPage, diff --git a/src/addons/notifications/pages/notification/notification.scss b/src/addons/notifications/pages/notification/notification.scss new file mode 100644 index 000000000..e296eadb9 --- /dev/null +++ b/src/addons/notifications/pages/notification/notification.scss @@ -0,0 +1,68 @@ +@import "~theme/globals"; + +:host { + + h2 { + font-weight: bold; + } + + core-format-text ::ng-deep { + .forumpost { + border: 1px solid var(--gray-200); + width: 100%; + margin: 0 0 1em 0; + + td { + padding: 10px; + } + + .header { + background-color: var(--light); + + .picture { + width: 48px; + text-align: center; + @include padding-horizontal(4px, 0px); + padding-top: 8px; + + img { + width: 44px !important; + } + } + } + + .subject { + font-weight: 700; + margin-bottom: 1rem; + } + } + + a { + text-decoration: none; + } + + .userpicture { + border-radius: 50%; + } + + .mdl-right { + text-align: end; + a { + display: none; + } + font { + font-size: 0.9em; + } + } + + .commands { + display: none; + } + + hr { + margin-top: 1.5rem; + margin-bottom: 1.5rem; + background-color: var(--gray-200); + } + } +} diff --git a/src/addons/notifications/pages/notification/notification.ts b/src/addons/notifications/pages/notification/notification.ts index 9fb04f294..23282d713 100644 --- a/src/addons/notifications/pages/notification/notification.ts +++ b/src/addons/notifications/pages/notification/notification.ts @@ -19,7 +19,9 @@ import { AddonNotificationsNotificationToRender, } from '@addons/notifications/services/notifications-helper'; import { Component, OnInit } from '@angular/core'; +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'; @@ -29,7 +31,7 @@ import { CoreUtils } from '@services/utils/utils'; @Component({ selector: 'page-addon-notifications-notification', templateUrl: 'notification.html', - styleUrls: ['../../notifications.scss'], + styleUrls: ['../../notifications.scss', 'notification.scss'], }) export class AddonNotificationsNotificationPage implements OnInit { @@ -41,12 +43,19 @@ export class AddonNotificationsNotificationPage implements OnInit { iconUrl?: string; // Icon URL. modname?: string; // Module name. loaded = false; + timecreated = 0; + + // Actions data. + actions: CoreContentLinksAction[] = []; + contextUrl?: string; + courseId?: number; + actionsData?: Record; // Extra data to handle the URL. /** * @inheritdoc */ async ngOnInit(): Promise { - let notification: AddonNotificationsNotificationToRender | AddonNotificationsNotificationData; + let notification: AddonNotificationsNotification; try { notification = CoreNavigator.getRequiredRouteParam('notification'); @@ -85,6 +94,8 @@ export class AddonNotificationsNotificationPage implements OnInit { this.modname = modname; } } + this.timecreated = notification.timecreated; + } else { this.subject = notification.title || ''; this.content = notification.message || ''; @@ -93,7 +104,73 @@ export class AddonNotificationsNotificationPage implements OnInit { this.userFromFullName = notification.userfromfullname; } + await this.loadActions(notification); + AddonNotificationsHelper.markNotificationAsRead(notification); + this.loaded = true; } + /** + * Load notification actions + * + * @param notification Notification. + * @return Promise resolved when done. + */ + async loadActions(notification: AddonNotificationsNotification): Promise { + if (!notification.contexturl && (!notification.customdata || !notification.customdata.appurl)) { + // No URL, nothing to do. + return; + } + + let actions: CoreContentLinksAction[] = []; + this.actionsData = notification.customdata; + this.contextUrl = notification.contexturl; + this.courseId = 'courseid' in notification ? notification.courseid : undefined; + + // Treat appurl first if any. + if (this.actionsData?.appurl) { + actions = await CoreContentLinksDelegate.getActionsFor( + this.actionsData.appurl, + this.courseId, + undefined, + this.actionsData, + ); + } + + if (!actions.length && this.contextUrl) { + // No appurl or cannot handle it. Try with contextUrl. + actions = await CoreContentLinksDelegate.getActionsFor(this.contextUrl, this.courseId, undefined, this.actionsData); + } + + if (!actions.length) { + // URL is not supported. Add an action to open it in browser. + actions.push({ + message: 'core.view', + icon: 'fas-eye', + action: this.openInBrowser.bind(this), + }); + } + + this.actions = actions; + } + + /** + * Default action. Open in browser. + * + * @param siteId Site ID to use. + */ + protected async openInBrowser(siteId?: string): Promise { + const url = this.actionsData?.appurl || this.contextUrl; + + if (!url) { + return; + } + + const site = await CoreSites.getSite(siteId); + + site.openInBrowserWithAutoLogin(url); + } + } + +type AddonNotificationsNotification = AddonNotificationsNotificationToRender | AddonNotificationsNotificationData; diff --git a/src/addons/notifications/services/handlers/push-click.ts b/src/addons/notifications/services/handlers/push-click.ts index 2312b4e75..3fc1c4d0d 100644 --- a/src/addons/notifications/services/handlers/push-click.ts +++ b/src/addons/notifications/services/handlers/push-click.ts @@ -18,12 +18,12 @@ import { CoreNavigator } from '@services/navigator'; import { CoreTextUtils } from '@services/utils/text'; import { CoreUtils } from '@services/utils/utils'; import { makeSingleton } from '@singletons'; -import { CoreEvents } from '@singletons/events'; import { CorePushNotificationsClickHandler } from '@features/pushnotifications/services/push-delegate'; import { CorePushNotificationsNotificationBasicData } from '@features/pushnotifications/services/pushnotifications'; import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper'; -import { AddonNotifications, AddonNotificationsProvider } from '../notifications'; +import { AddonNotifications } from '../notifications'; import { AddonNotificationsMainMenuHandlerService } from './mainmenu'; +import { AddonNotificationsHelper } from '../notifications-helper'; /** * Handler for non-messaging push notifications clicks. @@ -64,15 +64,7 @@ export class AddonNotificationsPushClickHandlerService implements CorePushNotifi * @return Promise resolved when done. */ protected async markAsRead(notification: AddonNotificationsNotificationData): Promise { - const notifId = notification.savedmessageid || notification.id; - - if (!notifId) { - return; - } - - await CoreUtils.ignoreErrors(AddonNotifications.markNotificationRead(notifId, notification.site)); - - CoreEvents.trigger(AddonNotificationsProvider.READ_CHANGED_EVENT, {}, notification.site); + await CoreUtils.ignoreErrors(AddonNotificationsHelper.markNotificationAsRead(notification)); } /** diff --git a/src/addons/notifications/services/notifications-helper.ts b/src/addons/notifications/services/notifications-helper.ts index 8eb24bfdc..38d79ddc6 100644 --- a/src/addons/notifications/services/notifications-helper.ts +++ b/src/addons/notifications/services/notifications-helper.ts @@ -18,13 +18,18 @@ import { CoreUtils } from '@services/utils/utils'; import { makeSingleton } from '@singletons'; import { AddonMessageOutputDelegate } from '@addons/messageoutput/services/messageoutput-delegate'; import { + AddonNotifications, AddonNotificationsNotificationMessageFormatted, AddonNotificationsPreferences, AddonNotificationsPreferencesComponent, AddonNotificationsPreferencesNotification, AddonNotificationsPreferencesNotificationProcessor, AddonNotificationsPreferencesProcessor, + AddonNotificationsProvider, } from './notifications'; +import { CoreEvents } from '@singletons/events'; +import { AddonNotificationsNotificationData } from './handlers/push-click'; +import { CoreTimeUtils } from '@services/utils/time'; /** * Service that provides some helper functions for notifications. @@ -115,6 +120,46 @@ export class AddonNotificationsHelperProvider { return result; } + /** + * Mark notification as read, trigger event and invalidate data. + * + * @param notification Notification object. + * @return Promise resolved when done. + */ + async markNotificationAsRead( + notification: AddonNotificationsNotificationMessageFormatted | AddonNotificationsNotificationData, + siteId?: string, + ): Promise { + if ('read' in notification && (notification.read || notification.timeread > 0)) { + // Already read, don't mark it. + return false; + } + + const notifId = 'savedmessageid' in notification ? notification.savedmessageid || notification.id : notification.id; + if (!notifId) { + return false; + } + + siteId = 'site' in notification ? notification.site : siteId; + + await CoreUtils.ignoreErrors(AddonNotifications.markNotificationRead(notifId, siteId)); + + const time = CoreTimeUtils.timestamp(); + if ('read' in notification) { + notification.read = true; + notification.timeread = time; + } + + await CoreUtils.ignoreErrors(AddonNotifications.invalidateNotificationsList()); + + CoreEvents.trigger(AddonNotificationsProvider.READ_CHANGED_EVENT, { + id: notifId, + time, + }, siteId); + + return true; + } + } export const AddonNotificationsHelper = makeSingleton(AddonNotificationsHelperProvider); diff --git a/src/addons/notifications/services/notifications.ts b/src/addons/notifications/services/notifications.ts index 1fab101db..d6426b01a 100644 --- a/src/addons/notifications/services/notifications.ts +++ b/src/addons/notifications/services/notifications.ts @@ -24,6 +24,19 @@ import { CoreLogger } from '@singletons/logger'; import { makeSingleton } from '@singletons'; import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; +declare module '@singletons/events' { + + /** + * Augment CoreEventsData interface with events specific to this service. + * + * @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation + */ + export interface CoreEventsData { + [AddonNotificationsProvider.READ_CHANGED_EVENT]: AddonNotificationsReadChangedEvent; + } + +} + const ROOT_CACHE_KEY = 'mmaNotifications:'; /** @@ -577,3 +590,11 @@ export enum AddonNotificationsGetReadType { READ = 1, BOTH = 2, } + +/** + * Event triggered when one or more notifications are read. + */ +export type AddonNotificationsReadChangedEvent = { + id?: number; // Set to the single id notification read. Undefined if multiple. + time: number; // Time of the change. +};