MOBILE-3833 notifications: Open notifications on a different page
parent
90ddcd7827
commit
ef1222b148
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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 {}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue