Merge pull request #3661 from dpalou/MOBILE-4270

Mobile 4270
main
Pau Ferrer Ocaña 2023-05-08 13:42:58 +02:00 committed by GitHub
commit f39a63dc7e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 198 additions and 150 deletions

View File

@ -522,7 +522,7 @@
"addon.mod_choice.savemychoice": "choice", "addon.mod_choice.savemychoice": "choice",
"addon.mod_choice.userchoosethisoption": "choice", "addon.mod_choice.userchoosethisoption": "choice",
"addon.mod_choice.yourselection": "choice", "addon.mod_choice.yourselection": "choice",
"addon.mod_data.actions": "data", "addon.mod_data.actionsmenu": "data",
"addon.mod_data.addentries": "data", "addon.mod_data.addentries": "data",
"addon.mod_data.advancedsearch": "data", "addon.mod_data.advancedsearch": "data",
"addon.mod_data.alttext": "data", "addon.mod_data.alttext": "data",
@ -2200,6 +2200,7 @@
"core.nopermissionerror": "local_moodlemobileapp", "core.nopermissionerror": "local_moodlemobileapp",
"core.nopermissions": "error", "core.nopermissions": "error",
"core.nopermissiontoaccesspage": "error", "core.nopermissiontoaccesspage": "error",
"core.noreplyname": "moodle",
"core.noresults": "moodle", "core.noresults": "moodle",
"core.noselection": "form", "core.noselection": "form",
"core.notapplicable": "local_moodlemobileapp", "core.notapplicable": "local_moodlemobileapp",

View File

@ -1,5 +1,5 @@
<ion-button size="small" *ngIf="action == 'actionsmenu'" fill="clear" (click)="actionsMenu()" <ion-button size="small" *ngIf="action == 'actionsmenu'" fill="clear" (click)="actionsMenu()"
[attr.aria-label]="'addon.mod_data.actions' | translate"> [attr.aria-label]="'addon.mod_data.actionsmenu' | translate">
<ion-icon name="fas-ellipsis-vertical" slot="icon-only" aria-hidden="true"></ion-icon> <ion-icon name="fas-ellipsis-vertical" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button> </ion-button>

View File

@ -1,5 +1,5 @@
{ {
"actions": "Actions menu", "actionsmenu": "Actions menu",
"addentries": "Add entries", "addentries": "Add entries",
"advancedsearch": "Advanced search", "advancedsearch": "Advanced search",
"alttext": "Alternative text", "alttext": "Alternative text",

View File

@ -13,8 +13,10 @@
// limitations under the License. // limitations under the License.
import { AddonNotificationsNotificationsSource } from '@addons/notifications/classes/notifications-source'; import { AddonNotificationsNotificationsSource } from '@addons/notifications/classes/notifications-source';
import { AddonNotificationsGetReadType } from '@addons/notifications/services/notifications'; import {
import { AddonNotificationsNotificationToRender } from '@addons/notifications/services/notifications-helper'; AddonNotificationsGetReadType,
AddonNotificationsNotificationMessageFormatted,
} from '@addons/notifications/services/notifications';
/** /**
* Provides a list of notifications using legacy web services. * Provides a list of notifications using legacy web services.
@ -25,10 +27,10 @@ export class AddonLegacyNotificationsNotificationsSource extends AddonNotificati
* @inheritdoc * @inheritdoc
*/ */
protected async loadPageItems(page: number): Promise<{ protected async loadPageItems(page: number): Promise<{
items: AddonNotificationsNotificationToRender[]; items: AddonNotificationsNotificationMessageFormatted[];
hasMoreItems: boolean; hasMoreItems: boolean;
}> { }> {
let items: AddonNotificationsNotificationToRender[] = []; let items: AddonNotificationsNotificationMessageFormatted[] = [];
let hasMoreItems = false; let hasMoreItems = false;
let pageUnreadCount = 0; let pageUnreadCount = 0;
const pageLength = this.getPageLength(); const pageLength = this.getPageLength();

View File

@ -15,22 +15,23 @@
import { import {
AddonNotifications, AddonNotifications,
AddonNotificationsGetReadType, AddonNotificationsGetReadType,
AddonNotificationsNotificationMessageFormatted,
AddonNotificationsProvider, AddonNotificationsProvider,
} from '@addons/notifications/services/notifications'; } from '@addons/notifications/services/notifications';
import { AddonNotificationsNotificationToRender } from '@addons/notifications/services/notifications-helper';
import { CoreRoutedItemsManagerSource } from '@classes/items-management/routed-items-manager-source'; import { CoreRoutedItemsManagerSource } from '@classes/items-management/routed-items-manager-source';
/** /**
* Provides a list of notifications. * Provides a list of notifications.
*/ */
export class AddonNotificationsNotificationsSource extends CoreRoutedItemsManagerSource<AddonNotificationsNotificationToRender> { export class AddonNotificationsNotificationsSource
extends CoreRoutedItemsManagerSource<AddonNotificationsNotificationMessageFormatted> {
protected totals: Record<string, number> = {}; protected totals: Record<string, number> = {};
/** /**
* @inheritdoc * @inheritdoc
*/ */
getItemPath(notification: AddonNotificationsNotificationToRender): string { getItemPath(notification: AddonNotificationsNotificationMessageFormatted): string {
return notification.id.toString(); return notification.id.toString();
} }
@ -47,7 +48,7 @@ export class AddonNotificationsNotificationsSource extends CoreRoutedItemsManage
* @inheritdoc * @inheritdoc
*/ */
protected async loadPageItems(page: number): Promise<{ protected async loadPageItems(page: number): Promise<{
items: AddonNotificationsNotificationToRender[]; items: AddonNotificationsNotificationMessageFormatted[];
hasMoreItems: boolean; hasMoreItems: boolean;
}> { }> {
const results = await this.loadNotifications(AddonNotificationsGetReadType.BOTH, page * this.getPageLength()); const results = await this.loadNotifications(AddonNotificationsGetReadType.BOTH, page * this.getPageLength());
@ -67,7 +68,7 @@ export class AddonNotificationsNotificationsSource extends CoreRoutedItemsManage
* @returns Notifications and whether there are any more. * @returns Notifications and whether there are any more.
*/ */
protected async loadNotifications(type: AddonNotificationsGetReadType, offset: number, limit?: number): Promise<{ protected async loadNotifications(type: AddonNotificationsGetReadType, offset: number, limit?: number): Promise<{
notifications: AddonNotificationsNotificationToRender[]; notifications: AddonNotificationsNotificationMessageFormatted[];
hasMoreNotifications: boolean; hasMoreNotifications: boolean;
}> { }> {
limit = limit ?? this.getPageLength(); limit = limit ?? this.getPageLength();
@ -94,7 +95,7 @@ export class AddonNotificationsNotificationsSource extends CoreRoutedItemsManage
/** /**
* @inheritdoc * @inheritdoc
*/ */
protected setItems(notifications: AddonNotificationsNotificationToRender[], hasMoreItems: boolean): void { protected setItems(notifications: AddonNotificationsNotificationMessageFormatted[], hasMoreItems: boolean): void {
const sortedNotifications = notifications.slice(0); const sortedNotifications = notifications.slice(0);
sortedNotifications.sort((a, b) => a.timecreated < b.timecreated ? 1 : -1); sortedNotifications.sort((a, b) => a.timecreated < b.timecreated ? 1 : -1);

View File

@ -34,6 +34,9 @@
width: var(--icon-size); width: var(--icon-size);
height: var(--icon-size); height: var(--icon-size);
} }
ion-icon {
font-size: var(--icon-size);
}
padding: 0.7rem; padding: 0.7rem;
background: var(--background-color); background: var(--background-color);
border-radius: var(--small-radius); border-radius: var(--small-radius);
@ -43,4 +46,11 @@
--module-icon-size: var(--icon-size); --module-icon-size: var(--icon-size);
@include margin(6px, 8px, 6px, 0px); @include margin(6px, 8px, 6px, 0px);
} }
.core-notification-img {
@include margin(6px, 8px, 6px, 0px);
width: var(--core-avatar-size);
height: var(--core-avatar-size);
object-fit: cover;
}
} }

View File

@ -28,21 +28,18 @@
<core-user-avatar *ngIf="notification.useridfrom > 0" [user]="notification" slot="start" <core-user-avatar *ngIf="notification.useridfrom > 0" [user]="notification" slot="start"
[profileUrl]="notification.profileimageurlfrom" [fullname]="notification.userfromfullname" [profileUrl]="notification.profileimageurlfrom" [fullname]="notification.userfromfullname"
[userId]="notification.useridfrom"> [userId]="notification.useridfrom">
<div class="core-avatar-extra-img" *ngIf="notification.iconurl && !notification.modname"> <div class="core-avatar-extra-img" *ngIf="notification.iconurl">
<img [src]="notification.iconurl" alt="" role="presentation"> <img [src]="notification.iconurl" alt="" role="presentation">
</div> </div>
<core-mod-icon *ngIf="notification.modname" [modicon]="notification.iconurl" [modname]="notification.modname"
[showAlt]="false">
</core-mod-icon>
</core-user-avatar> </core-user-avatar>
<ng-container *ngIf="notification.useridfrom <= 0 && notification.iconurl"> <ng-container *ngIf="notification.useridfrom <= 0">
<div class="core-notification-icon" *ngIf="!notification.modname" slot="start"> <img *ngIf="notification.imgUrl" class="core-notification-img" [src]="notification.imgUrl" core-external-content alt=""
<img [src]="notification.iconurl" alt="" role="presentation"> role="presentation" slot="start">
<div class="core-notification-icon" *ngIf="!notification.imgUrl" slot="start">
<img *ngIf="notification.iconurl" [src]="notification.iconurl" core-external-content alt="" role="presentation">
<ion-icon *ngIf="!notification.iconurl" name="fas-bell" aria-hidden="true"></ion-icon>
</div> </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> </ng-container>
<ion-label> <ion-label>

View File

@ -20,7 +20,7 @@ import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { import {
AddonNotifications, AddonNotificationsProvider, AddonNotifications, AddonNotificationsNotificationMessageFormatted, AddonNotificationsProvider,
} from '../../services/notifications'; } from '../../services/notifications';
import { CoreNavigator } from '@services/navigator'; import { CoreNavigator } from '@services/navigator';
import { CoreSplitViewComponent } from '@components/split-view/split-view'; import { CoreSplitViewComponent } from '@components/split-view/split-view';
@ -31,7 +31,6 @@ import { CoreMainMenuDeepLinkManager } from '@features/mainmenu/classes/deep-lin
import { CoreTimeUtils } from '@services/utils/time'; import { CoreTimeUtils } from '@services/utils/time';
import { AddonNotificationsNotificationsSource } from '@addons/notifications/classes/notifications-source'; import { AddonNotificationsNotificationsSource } from '@addons/notifications/classes/notifications-source';
import { CoreListItemsManager } from '@classes/items-management/list-items-manager'; import { CoreListItemsManager } from '@classes/items-management/list-items-manager';
import { AddonNotificationsNotificationToRender } from '@addons/notifications/services/notifications-helper';
import { AddonLegacyNotificationsNotificationsSource } from '@addons/notifications/classes/legacy-notifications-source'; import { AddonLegacyNotificationsNotificationsSource } from '@addons/notifications/classes/legacy-notifications-source';
/** /**
@ -45,7 +44,7 @@ import { AddonLegacyNotificationsNotificationsSource } from '@addons/notificatio
export class AddonNotificationsListPage implements AfterViewInit, OnDestroy { export class AddonNotificationsListPage implements AfterViewInit, OnDestroy {
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
notifications!: CoreListItemsManager<AddonNotificationsNotificationToRender, AddonNotificationsNotificationsSource>; notifications!: CoreListItemsManager<AddonNotificationsNotificationMessageFormatted, AddonNotificationsNotificationsSource>;
fetchMoreNotificationsFailed = false; fetchMoreNotificationsFailed = false;
canMarkAllNotificationsAsRead = false; canMarkAllNotificationsAsRead = false;
loadingMarkAllNotificationsAsRead = false; loadingMarkAllNotificationsAsRead = false;

View File

@ -10,40 +10,40 @@
</ion-header> </ion-header>
<ion-content [core-swipe-navigation]="notifications"> <ion-content [core-swipe-navigation]="notifications">
<core-loading [hideUntil]="loaded"> <core-loading [hideUntil]="loaded">
<div class="list-item-limited-width"> <div class="list-item-limited-width" *ngIf="notification">
<ion-item class="ion-text-wrap core-notification-title" lines="full"> <ion-item class="ion-text-wrap core-notification-title" lines="full">
<core-user-avatar *ngIf="userIdFrom > 0" slot="start" [userId]="userIdFrom" [profileUrl]="profileImageUrlFrom" <core-user-avatar *ngIf="notification.useridfrom > 0" slot="start" [userId]="notification.useridfrom"
[fullname]="userFromFullName"> [profileUrl]="notification.profileimageurlfrom" [fullname]="notification.userfromfullname">
<div class="core-avatar-extra-img" *ngIf="iconUrl && !modname"> <div class="core-avatar-extra-img" *ngIf="notification.iconurl">
<img [src]="iconUrl" alt="" role="presentation"> <img [src]="notification.iconurl" alt="" role="presentation">
</div> </div>
<core-mod-icon *ngIf="modname" [modicon]="iconUrl" [modname]="modname" [showAlt]="false">
</core-mod-icon>
</core-user-avatar> </core-user-avatar>
<ng-container *ngIf="userIdFrom <= 0 && iconUrl"> <ng-container *ngIf="notification.useridfrom <= 0">
<div class="core-notification-icon" *ngIf="!modname" slot="start"> <img *ngIf="notification.imgUrl" class="core-notification-img" [src]="notification.imgUrl" core-external-content alt=""
<img [src]="iconUrl" alt="" role="presentation"> role="presentation" slot="start">
<div class="core-notification-icon" *ngIf="!notification.imgUrl" slot="start">
<img *ngIf="notification.iconurl" [src]="notification.iconurl" core-external-content alt="" role="presentation">
<ion-icon *ngIf="!notification.iconurl" name="fas-bell" aria-hidden="true"></ion-icon>
</div> </div>
<core-mod-icon *ngIf="modname" [modicon]="iconUrl" [modname]="modname" [showAlt]="false" class="core-notification-icon"
slot="start">
</core-mod-icon>
</ng-container> </ng-container>
<ion-label> <ion-label>
<p class="item-heading"> <p class="item-heading">
<core-format-text [text]="subject" contextLevel="system" [contextInstanceId]="0" [wsNotFiltered]="true"> <core-format-text [text]="notification.subject" contextLevel="system" [contextInstanceId]="0"
[wsNotFiltered]="true">
</core-format-text> </core-format-text>
</p> </p>
<p *ngIf="timecreated > 0">{{ timecreated | coreTimeAgo }}<ng-container *ngIf="userIdFrom > 0"> · {{ <p *ngIf="notification.timecreated > 0">
userFromFullName }}</ng-container> {{ notification.timecreated | coreTimeAgo }}
<ng-container *ngIf="notification.useridfrom > 0"> · {{ notification.userfromfullname }}</ng-container>
</p> </p>
</ion-label> </ion-label>
</ion-item> </ion-item>
<ion-item class="ion-text-wrap core-notification-body"> <ion-item class="ion-text-wrap core-notification-body">
<ion-label> <ion-label>
<core-format-text [text]="content | coreCreateLinks" contextLevel="system" [contextInstanceId]="0"> <core-format-text [text]="notification.mobiletext | coreCreateLinks" contextLevel="system" [contextInstanceId]="0">
</core-format-text> </core-format-text>
</ion-label> </ion-label>
</ion-item> </ion-item>

View File

@ -14,10 +14,10 @@
import { AddonLegacyNotificationsNotificationsSource } from '@addons/notifications/classes/legacy-notifications-source'; import { AddonLegacyNotificationsNotificationsSource } from '@addons/notifications/classes/legacy-notifications-source';
import { AddonNotificationsNotificationsSource } from '@addons/notifications/classes/notifications-source'; import { AddonNotificationsNotificationsSource } from '@addons/notifications/classes/notifications-source';
import { AddonNotificationsNotificationData } from '@addons/notifications/services/handlers/push-click'; import { AddonNotificationsPushNotification } from '@addons/notifications/services/handlers/push-click';
import { AddonNotifications, AddonNotificationsNotificationMessageFormatted } from '@addons/notifications/services/notifications';
import { import {
AddonNotificationsHelper, AddonNotificationsHelper,
AddonNotificationsNotificationToRender,
} from '@addons/notifications/services/notifications-helper'; } from '@addons/notifications/services/notifications-helper';
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRouteSnapshot } from '@angular/router'; import { ActivatedRouteSnapshot } from '@angular/router';
@ -39,21 +39,15 @@ import { CoreDomUtils } from '@services/utils/dom';
export class AddonNotificationsNotificationPage implements OnInit, OnDestroy { export class AddonNotificationsNotificationPage implements OnInit, OnDestroy {
notifications?: AddonNotificationSwipeItemsManager; notifications?: AddonNotificationSwipeItemsManager;
subject = ''; // Notification subject. notification?: AddonNotificationsNotificationMessageFormatted;
content = ''; // Notification content.
userIdFrom = -1; // User ID who sent the notification.
profileImageUrlFrom?: string; // Avatar of the user who sent the notification. profileImageUrlFrom?: string; // Avatar of the user who sent the notification.
userFromFullName?: string; // Name of the user who sent the notification.
iconUrl?: string; // Icon URL.
modname?: string; // Module name.
loaded = false; loaded = false;
timecreated = 0;
// Actions data. // Actions data.
actions: CoreContentLinksAction[] = []; actions: CoreContentLinksAction[] = [];
contextUrl?: string; protected contextUrl?: string;
courseId?: number; protected courseId?: number;
actionsData?: Record<string, unknown>; // Extra data to handle the URL. protected actionsData?: Record<string, string|number>; // Extra data to handle the URL.
/** /**
* @inheritdoc * @inheritdoc
@ -70,31 +64,11 @@ export class AddonNotificationsNotificationPage implements OnInit, OnDestroy {
return; return;
} }
if ('subject' in notification) { this.notification = 'subject' in notification ?
this.subject = notification.subject; notification :
this.content = notification.mobiletext || notification.fullmessagehtml; await AddonNotifications.convertPushToMessage(notification);
this.userIdFrom = notification.useridfrom;
this.profileImageUrlFrom = notification.profileimageurlfrom;
this.userFromFullName = notification.userfromfullname;
this.iconUrl = notification.iconurl;
if (notification.moodlecomponent?.startsWith('mod_') && notification.iconurl) {
const modname = notification.moodlecomponent.substring(4);
if (notification.iconurl.match('/theme/image.php/[^/]+/' + modname + '/[-0-9]*/') ||
notification.iconurl.match('/theme/image.php/[^/]+/' + notification.moodlecomponent + '/[-0-9]*/')) {
this.modname = modname;
}
}
this.timecreated = notification.timecreated;
} else {
this.subject = notification.title || '';
this.content = notification.message || '';
this.userIdFrom = notification.userfromid ? Number(notification.userfromid) : -1;
this.profileImageUrlFrom = notification.senderImage;
this.userFromFullName = notification.userfromfullname;
this.timecreated = Number(notification.date ?? 0);
}
await this.loadActions(notification); await this.loadActions(this.notification);
AddonNotificationsHelper.markNotificationAsRead(notification); AddonNotificationsHelper.markNotificationAsRead(notification);
this.loaded = true; this.loaded = true;
@ -153,7 +127,7 @@ export class AddonNotificationsNotificationPage implements OnInit, OnDestroy {
* @param notification Notification. * @param notification Notification.
* @returns Promise resolved when done. * @returns Promise resolved when done.
*/ */
async loadActions(notification: AddonNotificationsNotification): Promise<void> { async loadActions(notification: AddonNotificationsNotificationMessageFormatted): Promise<void> {
if (!notification.contexturl && (!notification.customdata || !notification.customdata.appurl)) { if (!notification.contexturl && (!notification.customdata || !notification.customdata.appurl)) {
// No URL, nothing to do. // No URL, nothing to do.
return; return;
@ -161,7 +135,7 @@ export class AddonNotificationsNotificationPage implements OnInit, OnDestroy {
let actions: CoreContentLinksAction[] = []; let actions: CoreContentLinksAction[] = [];
this.actionsData = notification.customdata; this.actionsData = notification.customdata;
this.contextUrl = notification.contexturl; this.contextUrl = notification.contexturl || undefined;
this.courseId = 'courseid' in notification ? notification.courseid : undefined; this.courseId = 'courseid' in notification ? notification.courseid : undefined;
// Treat appurl first if any. // Treat appurl first if any.
@ -231,4 +205,4 @@ class AddonNotificationSwipeItemsManager extends CoreSwipeNavigationItemsManager
} }
type AddonNotificationsNotification = AddonNotificationsNotificationToRender | AddonNotificationsNotificationData; type AddonNotificationsNotification = AddonNotificationsNotificationMessageFormatted | AddonNotificationsPushNotification;

View File

@ -41,7 +41,7 @@ export class AddonNotificationsPushClickHandlerService implements CorePushNotifi
* @param notification The notification to check. * @param notification The notification to check.
* @returns Whether the notification click is handled by this handler * @returns Whether the notification click is handled by this handler
*/ */
async handles(notification: AddonNotificationsNotificationData): Promise<boolean> { async handles(notification: AddonNotificationsPushNotification): Promise<boolean> {
if (!notification.moodlecomponent) { if (!notification.moodlecomponent) {
// The notification doesn't come from Moodle. Handle it. // The notification doesn't come from Moodle. Handle it.
return true; return true;
@ -63,7 +63,7 @@ export class AddonNotificationsPushClickHandlerService implements CorePushNotifi
* @param notification Notification to mark. * @param notification Notification to mark.
* @returns Promise resolved when done. * @returns Promise resolved when done.
*/ */
protected async markAsRead(notification: AddonNotificationsNotificationData): Promise<void> { protected async markAsRead(notification: AddonNotificationsPushNotification): Promise<void> {
await CoreUtils.ignoreErrors(AddonNotificationsHelper.markNotificationAsRead(notification)); await CoreUtils.ignoreErrors(AddonNotificationsHelper.markNotificationAsRead(notification));
} }
@ -73,7 +73,7 @@ export class AddonNotificationsPushClickHandlerService implements CorePushNotifi
* @param notification The notification to check. * @param notification The notification to check.
* @returns Promise resolved when done. * @returns Promise resolved when done.
*/ */
async handleClick(notification: AddonNotificationsNotificationData): Promise<void> { async handleClick(notification: AddonNotificationsPushNotification): Promise<void> {
if (notification.customdata?.extendedtext) { if (notification.customdata?.extendedtext) {
// Display the text in a modal. // Display the text in a modal.
@ -137,7 +137,7 @@ export class AddonNotificationsPushClickHandlerService implements CorePushNotifi
export const AddonNotificationsPushClickHandler = makeSingleton(AddonNotificationsPushClickHandlerService); export const AddonNotificationsPushClickHandler = makeSingleton(AddonNotificationsPushClickHandlerService);
export type AddonNotificationsNotificationData = CorePushNotificationsNotificationBasicData & { export type AddonNotificationsPushNotification = CorePushNotificationsNotificationBasicData & {
contexturl?: string; // URL related to the notification. contexturl?: string; // URL related to the notification.
savedmessageid?: number; // Notification ID (optional). savedmessageid?: number; // Notification ID (optional).
id?: number; // Notification ID (optional). id?: number; // Notification ID (optional).

View File

@ -28,7 +28,7 @@ import {
AddonNotificationsProvider, AddonNotificationsProvider,
} from './notifications'; } from './notifications';
import { CoreEvents } from '@singletons/events'; import { CoreEvents } from '@singletons/events';
import { AddonNotificationsNotificationData } from './handlers/push-click'; import { AddonNotificationsPushNotification } from './handlers/push-click';
import { CoreTimeUtils } from '@services/utils/time'; import { CoreTimeUtils } from '@services/utils/time';
/** /**
@ -42,23 +42,12 @@ export class AddonNotificationsHelperProvider {
* *
* @param notification The notification object. * @param notification The notification object.
* @returns The notification formatted to render. * @returns The notification formatted to render.
* @deprecated since 4.2. This function isn't needed anymore.
*/ */
formatNotificationText( formatNotificationText(
notification: AddonNotificationsNotificationMessageFormatted, notification: AddonNotificationsNotificationMessageFormatted,
): AddonNotificationsNotificationToRender { ): AddonNotificationsNotificationMessageFormatted {
const formattedNotification: AddonNotificationsNotificationToRender = notification; return notification;
if (notification.moodlecomponent?.startsWith('mod_') && notification.iconurl) {
const modname = notification.moodlecomponent.substring(4);
if (notification.iconurl.match('/theme/image.php/[^/]+/' + modname + '/[-0-9]*/') ||
notification.iconurl.match('/theme/image.php/[^/]+/' + notification.moodlecomponent + '/[-0-9]*/')) {
formattedNotification.modname = modname;
}
} else {
formattedNotification.iconurl = formattedNotification.iconurl || undefined; // Make sure the property exists.
}
return formattedNotification;
} }
/** /**
@ -128,7 +117,7 @@ export class AddonNotificationsHelperProvider {
* @returns Promise resolved when done. * @returns Promise resolved when done.
*/ */
async markNotificationAsRead( async markNotificationAsRead(
notification: AddonNotificationsNotificationMessageFormatted | AddonNotificationsNotificationData, notification: AddonNotificationsNotificationMessageFormatted | AddonNotificationsPushNotification,
siteId?: string, siteId?: string,
): Promise<boolean> { ): Promise<boolean> {
if ('read' in notification && (notification.read || notification.timeread > 0)) { if ('read' in notification && (notification.read || notification.timeread > 0)) {
@ -197,11 +186,3 @@ type AddonNotificationsPreferencesNotificationProcessorFormatted = AddonNotifica
export type AddonNotificationsPreferencesProcessorFormatted = AddonNotificationsPreferencesProcessor & { export type AddonNotificationsPreferencesProcessorFormatted = AddonNotificationsPreferencesProcessor & {
supported?: boolean; // Calculated in the app. Whether the processor is supported in the app. supported?: boolean; // Calculated in the app. Whether the processor is supported in the app.
}; };
/**
* Notification with some calculated data to render it.
*/
export type AddonNotificationsNotificationToRender = AddonNotificationsNotificationMessageFormatted & {
iconurl?: string;
modname?: string;
};

View File

@ -18,11 +18,12 @@ import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites';
import { CoreWSExternalWarning } from '@services/ws'; import { CoreWSExternalWarning } from '@services/ws';
import { CoreTextUtils } from '@services/utils/text'; import { CoreTextUtils } from '@services/utils/text';
import { CoreTimeUtils } from '@services/utils/time'; import { CoreTimeUtils } from '@services/utils/time';
import { CoreUser } from '@features/user/services/user'; import { CoreUser, USER_NOREPLY_USER } from '@features/user/services/user';
import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
import { CoreLogger } from '@singletons/logger'; import { CoreLogger } from '@singletons/logger';
import { makeSingleton } from '@singletons'; import { Translate, makeSingleton } from '@singletons';
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
import { AddonNotificationsPushNotification } from './handlers/push-click';
declare module '@singletons/events' { declare module '@singletons/events' {
@ -56,6 +57,48 @@ export class AddonNotificationsProvider {
this.logger = CoreLogger.getInstance('AddonNotificationsProvider'); this.logger = CoreLogger.getInstance('AddonNotificationsProvider');
} }
/**
* Convert a push notification data to use the same format as the get_messages WS.
*
* @param notification Push notification to convert.
* @returns Converted notification.
*/
async convertPushToMessage(
notification: AddonNotificationsPushNotification,
): Promise<AddonNotificationsNotificationMessageFormatted> {
const message = notification.message ?? '';
const siteInfo = CoreSites.getCurrentSite()?.getInfo();
if (notification.senderImage && notification.customdata && !notification.customdata.notificationiconurl) {
notification.customdata.notificationiconurl = notification.senderImage;
}
const notificationMessage: AddonNotificationsNotificationMessage = {
id: notification.id ?? 0,
useridfrom: notification.userfromid ? Number(notification.userfromid) : USER_NOREPLY_USER,
userfromfullname: notification.userfromfullname ?? Translate.instant('core.noreplyname'),
useridto: notification.usertoid ? Number(notification.usertoid) : (siteInfo?.userid ?? 0),
usertofullname: siteInfo?.fullname ?? '',
subject: notification.title ?? '',
text: message,
fullmessage: message,
fullmessageformat: 1,
fullmessagehtml: message,
smallmessage: message,
notification: Number(notification.notif ?? 1),
contexturl: notification.contexturl || null,
contexturlname: null,
timecreated: Number(notification.date ?? 0),
timeread: 0,
component: notification.moodlecomponent,
customdata: notification.customdata ? JSON.stringify(notification.customdata) : undefined,
};
const formatted = await this.formatNotificationsData([notificationMessage]);
return formatted[0];
}
/** /**
* Function to format notification data. * Function to format notification data.
* *
@ -76,7 +119,7 @@ export class AddonNotificationsProvider {
notification.read = notification.timeread > 0; notification.read = notification.timeread > 0;
if (typeof notification.customdata == 'string') { if (typeof notification.customdata == 'string') {
notification.customdata = CoreTextUtils.parseJSON<Record<string, unknown>>(notification.customdata, {}); notification.customdata = CoreTextUtils.parseJSON<Record<string, string|number>>(notification.customdata, {});
} }
// Try to set courseid the notification belongs to. // Try to set courseid the notification belongs to.
@ -91,13 +134,16 @@ export class AddonNotificationsProvider {
if (!notification.iconurl) { if (!notification.iconurl) {
// The iconurl is only returned in 4.0 or above. Calculate it if not present. // The iconurl is only returned in 4.0 or above. Calculate it if not present.
if (notification.component && notification.component.startsWith('mod_')) { if (notification.moodlecomponent && notification.moodlecomponent.startsWith('mod_')) {
notification.iconurl = await CoreCourseModuleDelegate.getModuleIconSrc( notification.iconurl = await CoreCourseModuleDelegate.getModuleIconSrc(
notification.component.replace('mod_', ''), notification.moodlecomponent.replace('mod_', ''),
); );
} }
} }
const imgUrl = notification.customdata?.notificationpictureurl || notification.customdata?.notificationiconurl;
notification.imgUrl = imgUrl ? String(imgUrl) : undefined;
if (notification.useridfrom > 0) { if (notification.useridfrom > 0) {
// Try to get the profile picture of the user. // Try to get the profile picture of the user.
try { try {
@ -502,8 +548,8 @@ export type AddonNotificationsNotificationMessage = {
fullmessagehtml: string; // The message in html. fullmessagehtml: string; // The message in html.
smallmessage: string; // The shorten message. smallmessage: string; // The shorten message.
notification: number; // Is a notification?. notification: number; // Is a notification?.
contexturl: string; // Context URL. contexturl: string | null; // Context URL.
contexturlname: string; // Context URL link name. contexturlname: string | null; // Context URL link name.
timecreated: number; // Time created. timecreated: number; // Time created.
timeread: number; // Time read. timeread: number; // Time read.
usertofullname: string; // User to full name. usertofullname: string; // User to full name.
@ -532,15 +578,16 @@ export type AddonNotificationsGetUserNotificationPreferencesResult = {
* Calculated data for messages returned by core_message_get_messages. * Calculated data for messages returned by core_message_get_messages.
*/ */
export type AddonNotificationsNotificationCalculatedData = { export type AddonNotificationsNotificationCalculatedData = {
mobiletext?: string; // Calculated in the app. Text to display for the notification. mobiletext: string; // Calculated in the app. Text to display for the notification.
moodlecomponent?: string; // Calculated in the app. Moodle's component. moodlecomponent?: string; // Calculated in the app. Moodle's component.
notif?: number; // Calculated in the app. Whether it's a notification. notif: number; // Calculated in the app. Whether it's a notification.
notification?: number; // Calculated in the app in some cases. Whether it's a notification. notification: number; // Calculated in the app in some cases. Whether it's a notification.
read?: boolean; // Calculated in the app. Whether the notifications is read. read: boolean; // Calculated in the app. Whether the notifications is read.
courseid?: number; // Calculated in the app. Course the notification belongs to. courseid?: number; // Calculated in the app. Course the notification belongs to.
profileimageurlfrom?: string; // Calculated in the app. Avatar of user that sent the notification. profileimageurlfrom?: string; // Calculated in the app. Avatar of user that sent the notification.
userfromfullname?: string; // Calculated in the app in some cases. User from full name. userfromfullname?: string; // Calculated in the app in some cases. User from full name.
customdata?: Record<string, unknown>; // Parsed custom data. customdata?: Record<string, string|number>; // Parsed custom data.
imgUrl?: string; // Calculated in the app. URL of the image to use if the notification has no real user from.
}; };
/** /**

View File

@ -280,7 +280,7 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncDirec
button.setAttribute('aria-label', label); button.setAttribute('aria-label', label);
// Add an ion-icon item to apply the right styles, but the ion-icon component won't be executed. // Add an ion-icon item to apply the right styles, but the ion-icon component won't be executed.
button.innerHTML = '<ion-icon name="fas-up-right-and-down-left-from-center" aria-hidden="true" \ button.innerHTML = '<ion-icon name="fas-up-right-and-down-left-from-center" aria-hidden="true" \
src="assets/fonts/font-awesome/solid/expand-alt.svg">\ src="assets/fonts/font-awesome/solid/up-right-and-down-left-from-center.svg">\
</ion-icon>'; </ion-icon>';
button.addEventListener('click', (e: Event) => { button.addEventListener('click', (e: Event) => {

View File

@ -187,11 +187,7 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
this.comments = comments.concat(this.comments); this.comments = comments.concat(this.comments);
this.comments.forEach((comment, index) => { this.comments.forEach((comment, index) => this.calculateCommentData(comment, this.comments[index - 1]));
comment.showDate = this.showDate(comment, this.comments[index - 1]);
comment.showUserData = this.showUserData(comment, this.comments[index - 1]);
comment.showTail = this.showTail(comment, this.comments[index + 1]);
});
this.canDeleteComments = this.addDeleteCommentsAvailable && this.canDeleteComments = this.addDeleteCommentsAvailable &&
(this.hasOffline || this.comments.some((comment) => !!comment.delete)); (this.hasOffline || this.comments.some((comment) => !!comment.delete));
@ -216,6 +212,18 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
} }
/**
* Calculate some comment data.
*
* @param comment Comment.
* @param prevComment Previous comment.
*/
protected calculateCommentData(comment: CoreCommentsDataToDisplay, prevComment?: CoreCommentsDataToDisplay): void {
comment.showDate = this.showDate(comment, prevComment);
comment.showUserData = this.showUserData(comment, prevComment);
comment.showTail = this.showTail(comment, prevComment);
}
/** /**
* Function to load more commemts. * Function to load more commemts.
* *
@ -245,17 +253,15 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
this.refreshIcon = CoreConstants.ICON_LOADING; this.refreshIcon = CoreConstants.ICON_LOADING;
this.syncIcon = CoreConstants.ICON_LOADING; this.syncIcon = CoreConstants.ICON_LOADING;
try { await CoreUtils.ignoreErrors(this.invalidateComments());
await this.invalidateComments();
} finally {
this.page = 0;
this.comments = [];
try { this.page = 0;
await this.fetchComments(true, showErrors); this.comments = [];
} finally {
refresher?.complete(); try {
} await this.fetchComments(true, showErrors);
} finally {
refresher?.complete();
} }
} }
@ -325,13 +331,11 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
if (commentsResponse) { if (commentsResponse) {
this.invalidateComments(); this.invalidateComments();
const addedComments = await this.loadCommentProfile(commentsResponse); const addedComment = await this.loadCommentProfile(commentsResponse);
addedComments.showDate = this.showDate(addedComments, this.comments[this.comments.length - 1]); this.calculateCommentData(addedComment, this.comments[this.comments.length - 1]);
addedComments.showUserData = this.showUserData(addedComments, this.comments[this.comments.length - 1]);
addedComments.showTail = this.showTail(addedComments, this.comments[this.comments.length + 1]);
// Add the comment to the top. // Add the comment to the top.
this.comments = this.comments.concat([addedComments]); this.comments = this.comments.concat([addedComment]);
this.canDeleteComments = this.addDeleteCommentsAvailable; this.canDeleteComments = this.addDeleteCommentsAvailable;
CoreEvents.trigger(CoreCommentsProvider.COMMENTS_COUNT_CHANGED_EVENT, { CoreEvents.trigger(CoreCommentsProvider.COMMENTS_COUNT_CHANGED_EVENT, {
@ -343,6 +347,8 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
countChange: 1, countChange: 1,
}, CoreSites.getCurrentSiteId()); }, CoreSites.getCurrentSiteId());
this.refreshInBackground();
} else if (commentsResponse === false) { } else if (commentsResponse === false) {
// Comments added in offline mode. // Comments added in offline mode.
await this.loadOfflineData(); await this.loadOfflineData();
@ -410,6 +416,8 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
area: this.area, area: this.area,
countChange: -1, countChange: -1,
}, CoreSites.getCurrentSiteId()); }, CoreSites.getCurrentSiteId());
this.refreshInBackground();
} }
} else { } else {
this.loadOfflineData(); this.loadOfflineData();
@ -602,6 +610,28 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy {
this.showDelete = !this.showDelete; this.showDelete = !this.showDelete;
} }
/**
* Refresh cached data in background.
*/
protected async refreshInBackground(): Promise<void> {
await CoreUtils.ignoreErrors(this.invalidateComments());
const promises: Promise<unknown>[] = [];
for (let i = 0; i <= this.page; i++) {
promises.push(CoreComments.getComments(
this.contextLevel,
this.instanceId,
this.componentName,
this.itemId,
this.area,
i,
));
}
await Promise.all(promises);
}
/** /**
* @inheritdoc * @inheritdoc
*/ */

View File

@ -460,7 +460,7 @@ export class CorePushNotificationsProvider {
title: notification.title, title: notification.title,
message: notification.message, message: notification.message,
customdata: typeof rawData.customdata == 'string' ? customdata: typeof rawData.customdata == 'string' ?
CoreTextUtils.parseJSON<Record<string, unknown>>(rawData.customdata, {}) : rawData.customdata, CoreTextUtils.parseJSON<Record<string, string|number>>(rawData.customdata, {}) : rawData.customdata,
}); });
let site: CoreSite | undefined; let site: CoreSite | undefined;
@ -964,7 +964,7 @@ export type CorePushNotificationsNotificationBasicRawData = {
export type CorePushNotificationsNotificationBasicData = Omit<CorePushNotificationsNotificationBasicRawData, 'customdata'> & { export type CorePushNotificationsNotificationBasicData = Omit<CorePushNotificationsNotificationBasicRawData, 'customdata'> & {
title?: string; // Notification title. title?: string; // Notification title.
message?: string; // Notification message. message?: string; // Notification message.
customdata?: Record<string, unknown>; // Parsed custom data. customdata?: Record<string, string|number>; // Parsed custom data.
}; };
/** /**

View File

@ -806,7 +806,7 @@ export class CoreQuestionHelperProvider {
newIcon.className = 'core-correct-icon ion-color ion-color-success questioncorrectnessicon'; newIcon.className = 'core-correct-icon ion-color ion-color-success questioncorrectnessicon';
} else { } else {
newIcon.setAttribute('name', 'fas-xmark'); newIcon.setAttribute('name', 'fas-xmark');
newIcon.setAttribute('src', 'assets/fonts/font-awesome/solid/times.svg'); newIcon.setAttribute('src', 'assets/fonts/font-awesome/solid/xmark.svg');
newIcon.className = 'core-correct-icon ion-color ion-color-danger questioncorrectnessicon'; newIcon.className = 'core-correct-icon ion-color ion-color-danger questioncorrectnessicon';
} }

View File

@ -45,7 +45,7 @@ function buildRoutes(injector: Injector): Routes {
}, },
{ {
...indexAreaRoute, ...indexAreaRoute,
path: `${indexAreaRoute.path}/index`, path: `index/${indexAreaRoute.path}`,
}, },
]; ];

View File

@ -4,7 +4,7 @@
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher> </ion-refresher>
<core-search-box [disabled]="searchInProgress" [spellcheck]="false" [autoFocus]="true" [lengthCheck]="1" autocorrect="off" <core-search-box [disabled]="searchInProgress" [spellcheck]="false" [lengthCheck]="1" autocorrect="off"
searchArea="CoreUserParticipants" (onSubmit)="search($event)" (onClear)="clearSearch()"> searchArea="CoreUserParticipants" (onSubmit)="search($event)" (onClear)="clearSearch()">
</core-search-box> </core-search-box>

View File

@ -59,6 +59,11 @@ export const USER_PROFILE_PICTURE_UPDATED = 'CoreUserProfilePictureUpdated';
*/ */
export const USER_PROFILE_SERVER_TIMEZONE = '99'; export const USER_PROFILE_SERVER_TIMEZONE = '99';
/**
* Fake ID for a "no reply" user.
*/
export const USER_NOREPLY_USER = -10;
/** /**
* Service to provide user functionalities. * Service to provide user functionalities.
*/ */

View File

@ -216,6 +216,7 @@
"nopermissionerror": "Sorry, but you do not currently have permissions to do that", "nopermissionerror": "Sorry, but you do not currently have permissions to do that",
"nopermissions": "Sorry, but you do not currently have permissions to do that ({{$a}}).", "nopermissions": "Sorry, but you do not currently have permissions to do that ({{$a}}).",
"nopermissiontoaccesspage": "You don't have permission to access this page.", "nopermissiontoaccesspage": "You don't have permission to access this page.",
"noreplyname": "Do not reply to this email",
"noresults": "No results", "noresults": "No results",
"noselection": "No selection", "noselection": "No selection",
"notapplicable": "n/a", "notapplicable": "n/a",

View File

@ -451,8 +451,8 @@ div.core-iframe-network-error {
width: 50%; width: 50%;
height: 50%; height: 50%;
background-color: var(--danger); background-color: var(--danger);
-webkit-mask: url("/assets/fonts/font-awesome/solid/exclamation-triangle.svg") no-repeat 50% 50%; -webkit-mask: url("/assets/fonts/font-awesome/solid/triangle-exclamation.svg") no-repeat 50% 50%;
mask: url("/assets/fonts/font-awesome/solid/exclamation-triangle.svg") no-repeat 50% 50%; mask: url("/assets/fonts/font-awesome/solid/triangle-exclamation.svg") no-repeat 50% 50%;
} }
} }