commit
						f39a63dc7e
					
				| @ -522,7 +522,7 @@ | ||||
|   "addon.mod_choice.savemychoice": "choice", | ||||
|   "addon.mod_choice.userchoosethisoption": "choice", | ||||
|   "addon.mod_choice.yourselection": "choice", | ||||
|   "addon.mod_data.actions": "data", | ||||
|   "addon.mod_data.actionsmenu": "data", | ||||
|   "addon.mod_data.addentries": "data", | ||||
|   "addon.mod_data.advancedsearch": "data", | ||||
|   "addon.mod_data.alttext": "data", | ||||
| @ -2200,6 +2200,7 @@ | ||||
|   "core.nopermissionerror": "local_moodlemobileapp", | ||||
|   "core.nopermissions": "error", | ||||
|   "core.nopermissiontoaccesspage": "error", | ||||
|   "core.noreplyname": "moodle", | ||||
|   "core.noresults": "moodle", | ||||
|   "core.noselection": "form", | ||||
|   "core.notapplicable": "local_moodlemobileapp", | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| <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-button> | ||||
| 
 | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| { | ||||
|     "actions": "Actions menu", | ||||
|     "actionsmenu": "Actions menu", | ||||
|     "addentries": "Add entries", | ||||
|     "advancedsearch": "Advanced search", | ||||
|     "alttext": "Alternative text", | ||||
|  | ||||
| @ -13,8 +13,10 @@ | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { AddonNotificationsNotificationsSource } from '@addons/notifications/classes/notifications-source'; | ||||
| import { AddonNotificationsGetReadType } from '@addons/notifications/services/notifications'; | ||||
| import { AddonNotificationsNotificationToRender } from '@addons/notifications/services/notifications-helper'; | ||||
| import { | ||||
|     AddonNotificationsGetReadType, | ||||
|     AddonNotificationsNotificationMessageFormatted, | ||||
| } from '@addons/notifications/services/notifications'; | ||||
| 
 | ||||
| /** | ||||
|  * Provides a list of notifications using legacy web services. | ||||
| @ -25,10 +27,10 @@ export class AddonLegacyNotificationsNotificationsSource extends AddonNotificati | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     protected async loadPageItems(page: number): Promise<{ | ||||
|         items: AddonNotificationsNotificationToRender[]; | ||||
|         items: AddonNotificationsNotificationMessageFormatted[]; | ||||
|         hasMoreItems: boolean; | ||||
|     }> { | ||||
|         let items: AddonNotificationsNotificationToRender[] = []; | ||||
|         let items: AddonNotificationsNotificationMessageFormatted[] = []; | ||||
|         let hasMoreItems = false; | ||||
|         let pageUnreadCount = 0; | ||||
|         const pageLength = this.getPageLength(); | ||||
|  | ||||
| @ -15,22 +15,23 @@ | ||||
| import { | ||||
|     AddonNotifications, | ||||
|     AddonNotificationsGetReadType, | ||||
|     AddonNotificationsNotificationMessageFormatted, | ||||
|     AddonNotificationsProvider, | ||||
| } from '@addons/notifications/services/notifications'; | ||||
| import { AddonNotificationsNotificationToRender } from '@addons/notifications/services/notifications-helper'; | ||||
| import { CoreRoutedItemsManagerSource } from '@classes/items-management/routed-items-manager-source'; | ||||
| 
 | ||||
| /** | ||||
|  * Provides a list of notifications. | ||||
|  */ | ||||
| export class AddonNotificationsNotificationsSource extends CoreRoutedItemsManagerSource<AddonNotificationsNotificationToRender> { | ||||
| export class AddonNotificationsNotificationsSource | ||||
|     extends CoreRoutedItemsManagerSource<AddonNotificationsNotificationMessageFormatted> { | ||||
| 
 | ||||
|     protected totals: Record<string, number> = {}; | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     getItemPath(notification: AddonNotificationsNotificationToRender): string { | ||||
|     getItemPath(notification: AddonNotificationsNotificationMessageFormatted): string { | ||||
|         return notification.id.toString(); | ||||
|     } | ||||
| 
 | ||||
| @ -47,7 +48,7 @@ export class AddonNotificationsNotificationsSource extends CoreRoutedItemsManage | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     protected async loadPageItems(page: number): Promise<{ | ||||
|         items: AddonNotificationsNotificationToRender[]; | ||||
|         items: AddonNotificationsNotificationMessageFormatted[]; | ||||
|         hasMoreItems: boolean; | ||||
|     }> { | ||||
|         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. | ||||
|      */ | ||||
|     protected async loadNotifications(type: AddonNotificationsGetReadType, offset: number, limit?: number): Promise<{ | ||||
|         notifications: AddonNotificationsNotificationToRender[]; | ||||
|         notifications: AddonNotificationsNotificationMessageFormatted[]; | ||||
|         hasMoreNotifications: boolean; | ||||
|     }> { | ||||
|         limit = limit ?? this.getPageLength(); | ||||
| @ -94,7 +95,7 @@ export class AddonNotificationsNotificationsSource extends CoreRoutedItemsManage | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     protected setItems(notifications: AddonNotificationsNotificationToRender[], hasMoreItems: boolean): void { | ||||
|     protected setItems(notifications: AddonNotificationsNotificationMessageFormatted[], hasMoreItems: boolean): void { | ||||
|         const sortedNotifications = notifications.slice(0); | ||||
| 
 | ||||
|         sortedNotifications.sort((a, b) => a.timecreated < b.timecreated ? 1 : -1); | ||||
|  | ||||
| @ -34,6 +34,9 @@ | ||||
|             width: var(--icon-size); | ||||
|             height: var(--icon-size); | ||||
|         } | ||||
|         ion-icon { | ||||
|             font-size: var(--icon-size); | ||||
|         } | ||||
|         padding: 0.7rem; | ||||
|         background: var(--background-color); | ||||
|         border-radius: var(--small-radius); | ||||
| @ -43,4 +46,11 @@ | ||||
|         --module-icon-size: var(--icon-size); | ||||
|         @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; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -28,21 +28,18 @@ | ||||
|                 <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"> | ||||
|                     <div class="core-avatar-extra-img" *ngIf="notification.iconurl"> | ||||
|                         <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> | ||||
| 
 | ||||
|                 <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"> | ||||
|                 <ng-container *ngIf="notification.useridfrom <= 0"> | ||||
|                     <img *ngIf="notification.imgUrl" class="core-notification-img" [src]="notification.imgUrl" core-external-content alt="" | ||||
|                         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> | ||||
|                     <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> | ||||
|  | ||||
| @ -20,7 +20,7 @@ import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { CoreEventObserver, CoreEvents } from '@singletons/events'; | ||||
| import { | ||||
|     AddonNotifications, AddonNotificationsProvider, | ||||
|     AddonNotifications, AddonNotificationsNotificationMessageFormatted, AddonNotificationsProvider, | ||||
| } from '../../services/notifications'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| 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 { AddonNotificationsNotificationsSource } from '@addons/notifications/classes/notifications-source'; | ||||
| 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'; | ||||
| 
 | ||||
| /** | ||||
| @ -45,7 +44,7 @@ import { AddonLegacyNotificationsNotificationsSource } from '@addons/notificatio | ||||
| export class AddonNotificationsListPage implements AfterViewInit, OnDestroy { | ||||
| 
 | ||||
|     @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent; | ||||
|     notifications!: CoreListItemsManager<AddonNotificationsNotificationToRender, AddonNotificationsNotificationsSource>; | ||||
|     notifications!: CoreListItemsManager<AddonNotificationsNotificationMessageFormatted, AddonNotificationsNotificationsSource>; | ||||
|     fetchMoreNotificationsFailed = false; | ||||
|     canMarkAllNotificationsAsRead = false; | ||||
|     loadingMarkAllNotificationsAsRead = false; | ||||
|  | ||||
| @ -10,40 +10,40 @@ | ||||
| </ion-header> | ||||
| <ion-content [core-swipe-navigation]="notifications"> | ||||
|     <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"> | ||||
|                 <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"> | ||||
|                 <core-user-avatar *ngIf="notification.useridfrom > 0" slot="start" [userId]="notification.useridfrom" | ||||
|                     [profileUrl]="notification.profileimageurlfrom" [fullname]="notification.userfromfullname"> | ||||
|                     <div class="core-avatar-extra-img" *ngIf="notification.iconurl"> | ||||
|                         <img [src]="notification.iconurl" alt="" role="presentation"> | ||||
|                     </div> | ||||
|                     <core-mod-icon *ngIf="modname" [modicon]="iconUrl" [modname]="modname" [showAlt]="false"> | ||||
|                     </core-mod-icon> | ||||
|                 </core-user-avatar> | ||||
| 
 | ||||
|                 <ng-container *ngIf="userIdFrom <= 0 && iconUrl"> | ||||
|                     <div class="core-notification-icon" *ngIf="!modname" slot="start"> | ||||
|                         <img [src]="iconUrl" alt="" role="presentation"> | ||||
|                 <ng-container *ngIf="notification.useridfrom <= 0"> | ||||
|                     <img *ngIf="notification.imgUrl" class="core-notification-img" [src]="notification.imgUrl" core-external-content alt="" | ||||
|                         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> | ||||
|                     <core-mod-icon *ngIf="modname" [modicon]="iconUrl" [modname]="modname" [showAlt]="false" class="core-notification-icon" | ||||
|                         slot="start"> | ||||
|                     </core-mod-icon> | ||||
|                 </ng-container> | ||||
| 
 | ||||
|                 <ion-label> | ||||
|                     <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> | ||||
|                     </p> | ||||
|                     <p *ngIf="timecreated > 0">{{ timecreated | coreTimeAgo }}<ng-container *ngIf="userIdFrom > 0"> · {{ | ||||
|                             userFromFullName }}</ng-container> | ||||
|                     <p *ngIf="notification.timecreated > 0"> | ||||
|                         {{ notification.timecreated | coreTimeAgo }} | ||||
|                         <ng-container *ngIf="notification.useridfrom > 0"> · {{ notification.userfromfullname }}</ng-container> | ||||
|                     </p> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|             <ion-item class="ion-text-wrap core-notification-body"> | ||||
|                 <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> | ||||
|                 </ion-label> | ||||
|             </ion-item> | ||||
|  | ||||
| @ -14,10 +14,10 @@ | ||||
| 
 | ||||
| import { AddonLegacyNotificationsNotificationsSource } from '@addons/notifications/classes/legacy-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 { | ||||
|     AddonNotificationsHelper, | ||||
|     AddonNotificationsNotificationToRender, | ||||
| } from '@addons/notifications/services/notifications-helper'; | ||||
| import { Component, OnDestroy, OnInit } from '@angular/core'; | ||||
| import { ActivatedRouteSnapshot } from '@angular/router'; | ||||
| @ -39,21 +39,15 @@ import { CoreDomUtils } from '@services/utils/dom'; | ||||
| export class AddonNotificationsNotificationPage implements OnInit, OnDestroy { | ||||
| 
 | ||||
|     notifications?: AddonNotificationSwipeItemsManager; | ||||
|     subject = ''; // Notification subject.
 | ||||
|     content = ''; // Notification content.
 | ||||
|     userIdFrom = -1; // User ID who sent the notification.
 | ||||
|     notification?: AddonNotificationsNotificationMessageFormatted; | ||||
|     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; | ||||
|     timecreated = 0; | ||||
| 
 | ||||
|     // Actions data.
 | ||||
|     actions: CoreContentLinksAction[] = []; | ||||
|     contextUrl?: string; | ||||
|     courseId?: number; | ||||
|     actionsData?: Record<string, unknown>; // Extra data to handle the URL.
 | ||||
|     protected contextUrl?: string; | ||||
|     protected courseId?: number; | ||||
|     protected actionsData?: Record<string, string|number>; // Extra data to handle the URL.
 | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
| @ -70,31 +64,11 @@ export class AddonNotificationsNotificationPage implements OnInit, OnDestroy { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         if ('subject' in notification) { | ||||
|             this.subject = notification.subject; | ||||
|             this.content = notification.mobiletext || notification.fullmessagehtml; | ||||
|             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); | ||||
|         } | ||||
|         this.notification = 'subject' in notification ? | ||||
|             notification : | ||||
|             await AddonNotifications.convertPushToMessage(notification); | ||||
| 
 | ||||
|         await this.loadActions(notification); | ||||
|         await this.loadActions(this.notification); | ||||
|         AddonNotificationsHelper.markNotificationAsRead(notification); | ||||
| 
 | ||||
|         this.loaded = true; | ||||
| @ -153,7 +127,7 @@ export class AddonNotificationsNotificationPage implements OnInit, OnDestroy { | ||||
|      * @param notification Notification. | ||||
|      * @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)) { | ||||
|             // No URL, nothing to do.
 | ||||
|             return; | ||||
| @ -161,7 +135,7 @@ export class AddonNotificationsNotificationPage implements OnInit, OnDestroy { | ||||
| 
 | ||||
|         let actions: CoreContentLinksAction[] = []; | ||||
|         this.actionsData = notification.customdata; | ||||
|         this.contextUrl = notification.contexturl; | ||||
|         this.contextUrl = notification.contexturl || undefined; | ||||
|         this.courseId = 'courseid' in notification ? notification.courseid : undefined; | ||||
| 
 | ||||
|         // Treat appurl first if any.
 | ||||
| @ -231,4 +205,4 @@ class AddonNotificationSwipeItemsManager extends CoreSwipeNavigationItemsManager | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| type AddonNotificationsNotification = AddonNotificationsNotificationToRender | AddonNotificationsNotificationData; | ||||
| type AddonNotificationsNotification = AddonNotificationsNotificationMessageFormatted | AddonNotificationsPushNotification; | ||||
|  | ||||
| @ -41,7 +41,7 @@ export class AddonNotificationsPushClickHandlerService implements CorePushNotifi | ||||
|      * @param notification The notification to check. | ||||
|      * @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) { | ||||
|             // The notification doesn't come from Moodle. Handle it.
 | ||||
|             return true; | ||||
| @ -63,7 +63,7 @@ export class AddonNotificationsPushClickHandlerService implements CorePushNotifi | ||||
|      * @param notification Notification to mark. | ||||
|      * @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)); | ||||
|     } | ||||
| 
 | ||||
| @ -73,7 +73,7 @@ export class AddonNotificationsPushClickHandlerService implements CorePushNotifi | ||||
|      * @param notification The notification to check. | ||||
|      * @returns Promise resolved when done. | ||||
|      */ | ||||
|     async handleClick(notification: AddonNotificationsNotificationData): Promise<void> { | ||||
|     async handleClick(notification: AddonNotificationsPushNotification): Promise<void> { | ||||
| 
 | ||||
|         if (notification.customdata?.extendedtext) { | ||||
|             // Display the text in a modal.
 | ||||
| @ -137,7 +137,7 @@ export class AddonNotificationsPushClickHandlerService implements CorePushNotifi | ||||
| 
 | ||||
| export const AddonNotificationsPushClickHandler = makeSingleton(AddonNotificationsPushClickHandlerService); | ||||
| 
 | ||||
| export type AddonNotificationsNotificationData = CorePushNotificationsNotificationBasicData & { | ||||
| export type AddonNotificationsPushNotification = CorePushNotificationsNotificationBasicData & { | ||||
|     contexturl?: string; // URL related to the notification.
 | ||||
|     savedmessageid?: number; // Notification ID (optional).
 | ||||
|     id?: number; // Notification ID (optional).
 | ||||
|  | ||||
| @ -28,7 +28,7 @@ import { | ||||
|     AddonNotificationsProvider, | ||||
| } from './notifications'; | ||||
| import { CoreEvents } from '@singletons/events'; | ||||
| import { AddonNotificationsNotificationData } from './handlers/push-click'; | ||||
| import { AddonNotificationsPushNotification } from './handlers/push-click'; | ||||
| import { CoreTimeUtils } from '@services/utils/time'; | ||||
| 
 | ||||
| /** | ||||
| @ -42,23 +42,12 @@ export class AddonNotificationsHelperProvider { | ||||
|      * | ||||
|      * @param notification The notification object. | ||||
|      * @returns The notification formatted to render. | ||||
|      * @deprecated since 4.2. This function isn't needed anymore. | ||||
|      */ | ||||
|     formatNotificationText( | ||||
|         notification: AddonNotificationsNotificationMessageFormatted, | ||||
|     ): AddonNotificationsNotificationToRender { | ||||
|         const formattedNotification: AddonNotificationsNotificationToRender = 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; | ||||
|     ): AddonNotificationsNotificationMessageFormatted { | ||||
|         return notification; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -128,7 +117,7 @@ export class AddonNotificationsHelperProvider { | ||||
|      * @returns Promise resolved when done. | ||||
|      */ | ||||
|     async markNotificationAsRead( | ||||
|         notification: AddonNotificationsNotificationMessageFormatted | AddonNotificationsNotificationData, | ||||
|         notification: AddonNotificationsNotificationMessageFormatted | AddonNotificationsPushNotification, | ||||
|         siteId?: string, | ||||
|     ): Promise<boolean> { | ||||
|         if ('read' in notification && (notification.read || notification.timeread > 0)) { | ||||
| @ -197,11 +186,3 @@ type AddonNotificationsPreferencesNotificationProcessorFormatted = AddonNotifica | ||||
| export type AddonNotificationsPreferencesProcessorFormatted = AddonNotificationsPreferencesProcessor & { | ||||
|     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; | ||||
| }; | ||||
|  | ||||
| @ -18,11 +18,12 @@ import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites'; | ||||
| import { CoreWSExternalWarning } from '@services/ws'; | ||||
| import { CoreTextUtils } from '@services/utils/text'; | ||||
| 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 { CoreLogger } from '@singletons/logger'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { Translate, makeSingleton } from '@singletons'; | ||||
| import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; | ||||
| import { AddonNotificationsPushNotification } from './handlers/push-click'; | ||||
| 
 | ||||
| declare module '@singletons/events' { | ||||
| 
 | ||||
| @ -56,6 +57,48 @@ export class 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. | ||||
|      * | ||||
| @ -76,7 +119,7 @@ export class AddonNotificationsProvider { | ||||
|             notification.read = notification.timeread > 0; | ||||
| 
 | ||||
|             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.
 | ||||
| @ -91,13 +134,16 @@ export class AddonNotificationsProvider { | ||||
| 
 | ||||
|             if (!notification.iconurl) { | ||||
|                 // 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.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) { | ||||
|                 // Try to get the profile picture of the user.
 | ||||
|                 try { | ||||
| @ -502,8 +548,8 @@ export type AddonNotificationsNotificationMessage = { | ||||
|     fullmessagehtml: string; // The message in html.
 | ||||
|     smallmessage: string; // The shorten message.
 | ||||
|     notification: number; // Is a notification?.
 | ||||
|     contexturl: string; // Context URL.
 | ||||
|     contexturlname: string; // Context URL link name.
 | ||||
|     contexturl: string | null; // Context URL.
 | ||||
|     contexturlname: string | null; // Context URL link name.
 | ||||
|     timecreated: number; // Time created.
 | ||||
|     timeread: number; // Time read.
 | ||||
|     usertofullname: string; // User to full name.
 | ||||
| @ -532,15 +578,16 @@ export type AddonNotificationsGetUserNotificationPreferencesResult = { | ||||
|  * Calculated data for messages returned by core_message_get_messages. | ||||
|  */ | ||||
| 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.
 | ||||
|     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.
 | ||||
|     read?: boolean; // Calculated in the app. Whether the notifications is read.
 | ||||
|     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.
 | ||||
|     read: boolean; // Calculated in the app. Whether the notifications is read.
 | ||||
|     courseid?: number; // Calculated in the app. Course the notification belongs to.
 | ||||
|     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.
 | ||||
|     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.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  | ||||
| @ -280,7 +280,7 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncDirec | ||||
|             button.setAttribute('aria-label', label); | ||||
|             // 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" \ | ||||
|                 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>'; | ||||
| 
 | ||||
|             button.addEventListener('click', (e: Event) => { | ||||
|  | ||||
| @ -187,11 +187,7 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy { | ||||
| 
 | ||||
|             this.comments = comments.concat(this.comments); | ||||
| 
 | ||||
|             this.comments.forEach((comment, index) => { | ||||
|                 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.comments.forEach((comment, index) => this.calculateCommentData(comment, this.comments[index - 1])); | ||||
| 
 | ||||
|             this.canDeleteComments = this.addDeleteCommentsAvailable && | ||||
|                 (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. | ||||
|      * | ||||
| @ -245,17 +253,15 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy { | ||||
|         this.refreshIcon = CoreConstants.ICON_LOADING; | ||||
|         this.syncIcon = CoreConstants.ICON_LOADING; | ||||
| 
 | ||||
|         try { | ||||
|             await this.invalidateComments(); | ||||
|         } finally { | ||||
|             this.page = 0; | ||||
|             this.comments = []; | ||||
|         await CoreUtils.ignoreErrors(this.invalidateComments()); | ||||
| 
 | ||||
|             try { | ||||
|                 await this.fetchComments(true, showErrors); | ||||
|             } finally { | ||||
|                 refresher?.complete(); | ||||
|             } | ||||
|         this.page = 0; | ||||
|         this.comments = []; | ||||
| 
 | ||||
|         try { | ||||
|             await this.fetchComments(true, showErrors); | ||||
|         } finally { | ||||
|             refresher?.complete(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @ -325,13 +331,11 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy { | ||||
|             if (commentsResponse) { | ||||
|                 this.invalidateComments(); | ||||
| 
 | ||||
|                 const addedComments = await this.loadCommentProfile(commentsResponse); | ||||
|                 addedComments.showDate = this.showDate(addedComments, 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]); | ||||
|                 const addedComment = await this.loadCommentProfile(commentsResponse); | ||||
|                 this.calculateCommentData(addedComment, this.comments[this.comments.length - 1]); | ||||
| 
 | ||||
|                 // Add the comment to the top.
 | ||||
|                 this.comments = this.comments.concat([addedComments]); | ||||
|                 this.comments = this.comments.concat([addedComment]); | ||||
|                 this.canDeleteComments = this.addDeleteCommentsAvailable; | ||||
| 
 | ||||
|                 CoreEvents.trigger(CoreCommentsProvider.COMMENTS_COUNT_CHANGED_EVENT, { | ||||
| @ -343,6 +347,8 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy { | ||||
|                     countChange: 1, | ||||
|                 }, CoreSites.getCurrentSiteId()); | ||||
| 
 | ||||
|                 this.refreshInBackground(); | ||||
| 
 | ||||
|             } else if (commentsResponse === false) { | ||||
|                 // Comments added in offline mode.
 | ||||
|                 await this.loadOfflineData(); | ||||
| @ -410,6 +416,8 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy { | ||||
|                         area: this.area, | ||||
|                         countChange: -1, | ||||
|                     }, CoreSites.getCurrentSiteId()); | ||||
| 
 | ||||
|                     this.refreshInBackground(); | ||||
|                 } | ||||
|             } else { | ||||
|                 this.loadOfflineData(); | ||||
| @ -602,6 +610,28 @@ export class CoreCommentsViewerPage implements OnInit, OnDestroy { | ||||
|         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 | ||||
|      */ | ||||
|  | ||||
| @ -460,7 +460,7 @@ export class CorePushNotificationsProvider { | ||||
|             title: notification.title, | ||||
|             message: notification.message, | ||||
|             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; | ||||
| @ -964,7 +964,7 @@ export type CorePushNotificationsNotificationBasicRawData = { | ||||
| export type CorePushNotificationsNotificationBasicData = Omit<CorePushNotificationsNotificationBasicRawData, 'customdata'> & { | ||||
|     title?: string; // Notification title.
 | ||||
|     message?: string; // Notification message.
 | ||||
|     customdata?: Record<string, unknown>; // Parsed custom data.
 | ||||
|     customdata?: Record<string, string|number>; // Parsed custom data.
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  | ||||
| @ -806,7 +806,7 @@ export class CoreQuestionHelperProvider { | ||||
|                 newIcon.className = 'core-correct-icon ion-color ion-color-success questioncorrectnessicon'; | ||||
|             } else { | ||||
|                 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'; | ||||
|             } | ||||
| 
 | ||||
|  | ||||
| @ -45,7 +45,7 @@ function buildRoutes(injector: Injector): Routes { | ||||
|         }, | ||||
|         { | ||||
|             ...indexAreaRoute, | ||||
|             path: `${indexAreaRoute.path}/index`, | ||||
|             path: `index/${indexAreaRoute.path}`, | ||||
|         }, | ||||
|     ]; | ||||
| 
 | ||||
|  | ||||
| @ -4,7 +4,7 @@ | ||||
|             <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> | ||||
|         </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()"> | ||||
|         </core-search-box> | ||||
| 
 | ||||
|  | ||||
| @ -59,6 +59,11 @@ export const USER_PROFILE_PICTURE_UPDATED = 'CoreUserProfilePictureUpdated'; | ||||
|  */ | ||||
| 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. | ||||
|  */ | ||||
|  | ||||
| @ -216,6 +216,7 @@ | ||||
|     "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}}).", | ||||
|     "nopermissiontoaccesspage": "You don't have permission to access this page.", | ||||
|     "noreplyname": "Do not reply to this email", | ||||
|     "noresults": "No results", | ||||
|     "noselection": "No selection", | ||||
|     "notapplicable": "n/a", | ||||
|  | ||||
| @ -451,8 +451,8 @@ div.core-iframe-network-error { | ||||
|         width: 50%; | ||||
|         height: 50%; | ||||
|         background-color: var(--danger); | ||||
|         -webkit-mask: url("/assets/fonts/font-awesome/solid/exclamation-triangle.svg") no-repeat 50% 50%; | ||||
|         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/triangle-exclamation.svg") no-repeat 50% 50%; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user