MOBILE-3833 notifications: Open notifications on a different page

main
Pau Ferrer Ocaña 2022-03-24 16:35:42 +01:00
parent 90ddcd7827
commit ef1222b148
14 changed files with 367 additions and 302 deletions

View File

@ -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<string, unknown>; // Extra data to handle the URL.
actions: CoreContentLinksAction[] = [];
/**
* Component being initialized.
*/
async ngOnInit(): Promise<void> {
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(
<string> 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<void> {
const url = <string> this.data?.appurl || this.contextUrl;
if (!url) {
return;
}
const site = await CoreSites.getSite(siteId);
site.openInBrowserWithAutoLogin(url);
}
}

View File

@ -1,8 +0,0 @@
<ion-row *ngIf="actions && actions.length > 0" class="justify-content-around">
<ion-col *ngFor="let action of actions">
<ion-button fill="clear" expand="block" (click)="action.action()">
<ion-icon slot="start" name="{{action.icon}}" aria-hidden="true"></ion-icon>
{{ action.message | translate }}
</ion-button>
</ion-col>
</ion-row>

View File

@ -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 {}

View File

@ -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);
}
}

View File

@ -11,69 +11,60 @@
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-content class="limited-width">
<ion-refresher slot="fixed" [disabled]="!notificationsLoaded" (ionRefresh)="refreshNotifications($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="notificationsLoaded">
<div class="list-item-limited-width">
<div class="ion-padding" *ngIf="canMarkAllNotificationsAsRead">
<ion-button [disabled]="loadingMarkAllNotificationsAsRead" expand="block" (click)="markAllNotificationsAsRead()"
fill="outline">
<ion-icon slot="start" name="fas-check" aria-hidden="true" *ngIf="!loadingMarkAllNotificationsAsRead"></ion-icon>
<ion-spinner slot="start" [attr.aria-label]="'core.loading' | translate" *ngIf="loadingMarkAllNotificationsAsRead">
</ion-spinner>
{{ 'addon.notifications.markallread' | translate }}
</ion-button>
</div>
<div class="ion-padding" *ngIf="canMarkAllNotificationsAsRead">
<ion-button [disabled]="loadingMarkAllNotificationsAsRead" expand="block" (click)="markAllNotificationsAsRead()" fill="outline">
<ion-icon slot="start" name="fas-check" aria-hidden="true" *ngIf="!loadingMarkAllNotificationsAsRead"></ion-icon>
<ion-spinner slot="start" [attr.aria-label]="'core.loading' | translate" *ngIf="loadingMarkAllNotificationsAsRead">
</ion-spinner>
{{ 'addon.notifications.markallread' | translate }}
</ion-button>
</div>
<ion-card *ngFor="let notification of notifications">
<ion-item class="ion-text-wrap" [attr.aria-label]="
<ion-item *ngFor="let notification of notifications" class="ion-text-wrap" [attr.aria-label]="
notification.timeread
? notification.subject
: 'addon.notifications.unreadnotification' | translate: {$a: notification.subject}">
<core-user-avatar *ngIf="notification.useridfrom > 0" [user]="notification" slot="start"
[profileUrl]="notification.profileimageurlfrom" [fullname]="notification.userfromfullname"
[userId]="notification.useridfrom">
<div class="core-avatar-extra-img" *ngIf="notification.iconurl && !notification.modname">
<img [src]="notification.iconurl" alt="" role="presentation">
</div>
<core-mod-icon *ngIf="notification.modname" [modicon]="notification.iconurl" [modname]="notification.modname"
[showAlt]="false">
</core-mod-icon>
</core-user-avatar>
: 'addon.notifications.unreadnotification' | translate: {$a: notification.subject}"
(click)="openNotification(notification)" button [detail]="true" lines="true">
<core-user-avatar *ngIf="notification.useridfrom > 0" [user]="notification" slot="start"
[profileUrl]="notification.profileimageurlfrom" [fullname]="notification.userfromfullname"
[userId]="notification.useridfrom">
<div class="core-avatar-extra-img" *ngIf="notification.iconurl && !notification.modname">
<img [src]="notification.iconurl" alt="" role="presentation">
</div>
<core-mod-icon *ngIf="notification.modname" [modicon]="notification.iconurl" [modname]="notification.modname"
[showAlt]="false">
</core-mod-icon>
</core-user-avatar>
<img *ngIf="notification.useridfrom <= 0 && notification.iconurl" [src]="notification.iconurl" alt=""
role="presentation" class="core-notification-icon" slot="start">
<ng-container *ngIf="notification.useridfrom <= 0 && notification.iconurl">
<div class="core-notification-icon" *ngIf="!notification.modname" slot="start">
<img [src]="notification.iconurl" alt="" role="presentation">
</div>
<core-mod-icon *ngIf="notification.modname" [modicon]="notification.iconurl" [modname]="notification.modname"
[showAlt]="false" class="core-notification-icon" slot="start">
</core-mod-icon>
</ng-container>
<ion-label>
<p class="item-heading">
<core-format-text [text]="notification.subject" contextLevel="system" [contextInstanceId]="0"
[wsNotFiltered]="true">
</core-format-text>
</p>
<p *ngIf="notification.useridfrom > 0">{{ notification.userfromfullname }}</p>
</ion-label>
<ion-note slot="end" class="ion-float-end ion-text-end">
{{ notification.timecreated | coreDateDayOrTime }}
<span *ngIf="!notification.timeread">
<ion-icon name="fas-circle" color="primary" aria-hidden="true"></ion-icon>
</span>
</ion-note>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>
<core-format-text [text]="notification.mobiletext | coreCreateLinks" contextLevel="system" [contextInstanceId]="0"
collapsible-item>
</core-format-text>
</ion-label>
</ion-item>
<addon-notifications-actions [contextUrl]="notification.contexturl" [courseId]="notification.courseid"
[data]="notification.customdata">
</addon-notifications-actions>
</ion-card>
</div>
<ion-label>
<p class="item-heading">
<core-format-text [text]="notification.subject" contextLevel="system" [contextInstanceId]="0" [wsNotFiltered]="true">
</core-format-text>
</p>
<p *ngIf="notification.useridfrom > 0">{{ notification.userfromfullname }}</p>
</ion-label>
<ion-note slot="end">
{{ notification.timecreated | coreDateDayOrTime }}
<span *ngIf="!notification.timeread">
<ion-icon name="fas-circle" color="primary" aria-hidden="true"></ion-icon>
</span>
</ion-note>
</ion-item>
<core-empty-box *ngIf="!notifications || notifications.length <= 0" icon="far-bell"
[message]="'addon.notifications.therearentnotificationsyet' | translate">

View File

@ -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: [

View File

@ -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<void> {
@ -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<void> {
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<void> {
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<void> {
// 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();
}

View File

@ -4,34 +4,61 @@
<ion-back-button [text]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>
<h2>{{ 'addon.notifications.notifications' | translate }}</h2>
<h1>{{ 'addon.notifications.notifications' | translate }}</h1>
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<core-loading [hideUntil]="loaded">
<ion-item class="ion-text-wrap" [attr.aria-label]="subject">
<core-user-avatar *ngIf="userIdFrom > 0" slot="start" [userId]="userIdFrom" [profileUrl]="profileImageUrlFrom"
[fullname]="userFromFullName">
<div class="core-avatar-extra-img" *ngIf="iconUrl && !modname">
<img [src]="iconUrl" alt="" role="presentation">
</div>
<core-mod-icon *ngIf="modname" [modicon]="iconUrl" [modname]="modname" [showAlt]="false">
</core-mod-icon>
</core-user-avatar>
<div class="list-item-limited-width">
<ion-card>
<img *ngIf="userIdFrom <= 0 && iconUrl" [src]="iconUrl" alt="" role="presentation" class="core-notification-icon" slot="start">
<ion-item class="ion-text-wrap">
<core-user-avatar *ngIf="userIdFrom > 0" slot="start" [userId]="userIdFrom" [profileUrl]="profileImageUrlFrom"
[fullname]="userFromFullName">
<div class="core-avatar-extra-img" *ngIf="iconUrl && !modname">
<img [src]="iconUrl" alt="" role="presentation">
</div>
<core-mod-icon *ngIf="modname" [modicon]="iconUrl" [modname]="modname" [showAlt]="false">
</core-mod-icon>
</core-user-avatar>
<ion-label>
<p class="item-heading">{{ subject }}</p>
<p *ngIf="userIdFrom > 0">{{ userFromFullName }}</p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>
<core-format-text [text]="content | coreCreateLinks" contextLevel="system" [contextInstanceId]="0">
</core-format-text>
</ion-label>
</ion-item>
<ng-container *ngIf="userIdFrom <= 0 && iconUrl">
<div class="core-notification-icon" *ngIf="!modname" slot="start">
<img [src]="iconUrl" alt="" role="presentation">
</div>
<core-mod-icon *ngIf="modname" [modicon]="iconUrl" [modname]="modname" [showAlt]="false"
class="core-notification-icon" slot="start">
</core-mod-icon>
</ng-container>
<ion-label>
<ion-card-subtitle *ngIf="userIdFrom > 0">{{ userFromFullName }}</ion-card-subtitle>
<ion-card-title>
<core-format-text [text]="subject" contextLevel="system" [contextInstanceId]="0" [wsNotFiltered]="true">
</core-format-text>
</ion-card-title>
</ion-label>
<ion-note slot="end" *ngIf="timecreated">
{{ timecreated | coreDateDayOrTime }}
</ion-note>
</ion-item>
<ion-item class="ion-text-wrap">
<ion-label>
<core-format-text [text]="content | coreCreateLinks" contextLevel="system" [contextInstanceId]="0">
</core-format-text>
</ion-label>
</ion-item>
</ion-card>
</div>
<div collapsible-footer appearOnBottom *ngIf="loaded && actions && actions.length > 0" slot="fixed">
<div class="list-item-limited-width adaptable-buttons-row">
<ion-button expand="block" (click)="action.action()" *ngFor="let action of actions">
<ion-icon slot="start" name="{{action.icon}}" aria-hidden="true"></ion-icon>
{{ action.message | translate }}
</ion-button>
</div>
</div>
</core-loading>
</ion-content>

View File

@ -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,

View File

@ -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);
}
}
}

View File

@ -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<string, unknown>; // Extra data to handle the URL.
/**
* @inheritdoc
*/
async ngOnInit(): Promise<void> {
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<void> {
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(
<string> 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<void> {
const url = <string> this.actionsData?.appurl || this.contextUrl;
if (!url) {
return;
}
const site = await CoreSites.getSite(siteId);
site.openInBrowserWithAutoLogin(url);
}
}
type AddonNotificationsNotification = AddonNotificationsNotificationToRender | AddonNotificationsNotificationData;

View File

@ -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<void> {
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));
}
/**

View File

@ -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<boolean> {
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);

View File

@ -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.
};