From 007835b6b800818725d5a8063dc3a9ce8eb15d9e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= <crazyserver@gmail.com>
Date: Wed, 1 Dec 2021 12:26:46 +0100
Subject: [PATCH] MOBILE-3099 module: Add preview page to show restricted
 activities

---
 .../mod/url/services/handlers/module.ts       |   9 +-
 .../course/classes/module-base-handler.ts     |  29 ++++-
 .../module-info/core-course-module-info.html  |  13 +-
 .../components/module-info/module-info.ts     |   2 +
 .../core-course-module-manual-completion.html |   8 +-
 .../module-navigation/module-navigation.ts    |  41 +++---
 .../components/module/core-course-module.html |   4 +-
 .../core-course-unsupported-module.html       |   3 -
 .../features/course/course-lazy.module.ts     |   6 +-
 .../pages/module-preview/module-preview.html  |  65 ++++++++++
 .../module-preview.module.ts}                 |   8 +-
 .../module-preview/module-preview.page.ts     | 118 ++++++++++++++++++
 .../unsupported-module.html                   |  27 ----
 .../unsupported-module.page.ts                |  54 --------
 .../services/handlers/default-module.ts       |  18 ++-
 .../course/services/module-delegate.ts        |  31 +++++
 .../classes/handlers/module-handler.ts        |  35 ++++--
 17 files changed, 323 insertions(+), 148 deletions(-)
 create mode 100644 src/core/features/course/pages/module-preview/module-preview.html
 rename src/core/features/course/pages/{unsupported-module/unsupported-module.module.ts => module-preview/module-preview.module.ts} (83%)
 create mode 100644 src/core/features/course/pages/module-preview/module-preview.page.ts
 delete mode 100644 src/core/features/course/pages/unsupported-module/unsupported-module.html
 delete mode 100644 src/core/features/course/pages/unsupported-module/unsupported-module.page.ts

diff --git a/src/addons/mod/url/services/handlers/module.ts b/src/addons/mod/url/services/handlers/module.ts
index e6257f845..69c7d2c4b 100644
--- a/src/addons/mod/url/services/handlers/module.ts
+++ b/src/addons/mod/url/services/handlers/module.ts
@@ -19,7 +19,7 @@ import { CoreModuleHandlerBase } from '@features/course/classes/module-base-hand
 import { CoreCourse } from '@features/course/services/course';
 import { CoreCourseModule } from '@features/course/services/course-helper';
 import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
-import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
+import { CoreNavigationOptions } from '@services/navigator';
 import { CoreDomUtils } from '@services/utils/dom';
 import { CoreUtils } from '@services/utils/utils';
 import { makeSingleton } from '@singletons';
@@ -90,12 +90,7 @@ export class AddonModUrlModuleHandlerService extends CoreModuleHandlerBase imple
                     if (shouldOpen) {
                         openUrl(module, courseId);
                     } else {
-                        options = options || {};
-                        options.params = options.params || {};
-                        Object.assign(options.params, { module });
-                        const routeParams = '/' + courseId + '/' + module.id;
-
-                        CoreNavigator.navigateToSitePath(AddonModUrlModuleHandlerService.PAGE_NAME + routeParams, options);
+                        this.openActivityPage(module, courseId, options);
                     }
                 } finally {
                     modal.dismiss();
diff --git a/src/core/features/course/classes/module-base-handler.ts b/src/core/features/course/classes/module-base-handler.ts
index 474fb231c..3d59a9d00 100644
--- a/src/core/features/course/classes/module-base-handler.ts
+++ b/src/core/features/course/classes/module-base-handler.ts
@@ -51,14 +51,31 @@ export class CoreModuleHandlerBase implements Partial<CoreCourseModuleHandler> {
                 courseId: number,
                 options?: CoreNavigationOptions,
             ): Promise<void> => {
-                options = options || {};
-                options.params = options.params || {};
-                Object.assign(options.params, { module });
-                const routeParams = '/' + courseId + '/' + module.id;
-
-                await CoreNavigator.navigateToSitePath(this.pageName + routeParams, options);
+                await this.openActivityPage(module, courseId, options);
             },
         };
     }
 
+    /**
+     * Opens the activity page.
+     *
+     * @param module The module object.
+     * @param courseId The course ID.
+     * @param options Options for the navigation.
+     * @return Promise resolved when done.
+     */
+    async openActivityPage(module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): Promise<void> {
+        if (!CoreCourse.moduleHasView(module)) {
+            return;
+        }
+
+        options = options || {};
+        options.params = options.params || {};
+        Object.assign(options.params, { module });
+
+        const routeParams = '/' + courseId + '/' + module.id;
+
+        await CoreNavigator.navigateToSitePath(this.pageName + routeParams, options);
+    }
+
 }
diff --git a/src/core/features/course/components/module-info/core-course-module-info.html b/src/core/features/course/components/module-info/core-course-module-info.html
index 7afb15368..702b6ff54 100644
--- a/src/core/features/course/components/module-info/core-course-module-info.html
+++ b/src/core/features/course/components/module-info/core-course-module-info.html
@@ -13,24 +13,25 @@
 <ion-item class="ion-text-wrap" *ngIf="description" lines="none">
     <ion-label>
         <core-format-text [text]="description" [component]="component" [componentId]="componentId" contextLevel="module"
-            [contextInstanceId]="module.id" [courseId]="courseId" [maxHeight]="120">
+            [contextInstanceId]="module.id" [courseId]="courseId" [maxHeight]="expandDescription ? null : 120">
         </core-format-text>
     </ion-label>
 </ion-item>
 <ng-content select="[description]"></ng-content>
-<ion-item class="ion-text-wrap" lines="none" *ngIf="showCompletion && (module.dates?.length || module.completiondata)">
+<ion-item class="ion-text-wrap" lines="none" *ngIf="showCompletion && (module.dates?.length ||
+        (module.completiondata && (module.completiondata.isautomatic || showManualCompletion) && module.uservisible))">
     <ion-label>
         <!-- Activity dates. -->
-        <div *ngIf="module.dates && module.dates.length" class="core-module-dates">
+        <div *ngIf="module.dates?.length" class="core-module-dates">
             <p *ngFor="let date of module.dates">
                 <strong>{{ date.label }}</strong> {{ date.timestamp * 1000 | coreFormatDate:'strftimedatetime' }}
             </p>
         </div>
 
         <!-- Module completion. -->
-        <core-course-module-completion *ngIf="module.completiondata" [completion]="module.completiondata" [moduleName]="module.name"
-            [moduleId]="module.id" [showCompletionConditions]="true" [showManualCompletion]="true"
-            (completionChanged)="completionChanged.emit($event)">
+        <core-course-module-completion *ngIf="module.completiondata && module.uservisible" [completion]="module.completiondata"
+            [moduleName]="module.name" [moduleId]="module.id" [showCompletionConditions]="true"
+            [showManualCompletion]="showManualCompletion" (completionChanged)="completionChanged.emit($event)">
         </core-course-module-completion>
     </ion-label>
 </ion-item>
diff --git a/src/core/features/course/components/module-info/module-info.ts b/src/core/features/course/components/module-info/module-info.ts
index 063ebc88f..2c958c99f 100644
--- a/src/core/features/course/components/module-info/module-info.ts
+++ b/src/core/features/course/components/module-info/module-info.ts
@@ -36,12 +36,14 @@ import { CoreSites } from '@services/sites';
 export class CoreCourseModuleInfoComponent implements OnInit {
 
     @Input() module!: CoreCourseModule; // The module to render.
+    @Input() showManualCompletion = true; // Whether to show manual completion, true by default.
     @Input() courseId!: number; // The courseId the module belongs to.
 
     @Input() component!: string; // Component for format text directive.
     @Input() componentId!: string | number; // Component ID to use in conjunction with the component.
 
     @Input() description?: string | false; // The description to display. If false, no description will be shown.
+    @Input() expandDescription = false; // If the description should be expanded by default.
 
     @Input() hasDataToSync = false; // If the activity has any data to be synced.
 
diff --git a/src/core/features/course/components/module-manual-completion/core-course-module-manual-completion.html b/src/core/features/course/components/module-manual-completion/core-course-module-manual-completion.html
index ab4055ff0..d205a5a65 100644
--- a/src/core/features/course/components/module-manual-completion/core-course-module-manual-completion.html
+++ b/src/core/features/course/components/module-manual-completion/core-course-module-manual-completion.html
@@ -2,20 +2,22 @@
 
     <ng-container *ngIf="completion.istrackeduser">
         <ng-container *ngIf="completion.state">
-            <ion-button color="success" fill="outline" [attr.aria-label]="accessibleDescription" (click)="completionClicked($event)">
+            <ion-button color="success" expand="block" fill="outline" [attr.aria-label]="accessibleDescription"
+                (click)="completionClicked($event)">
                 <ion-icon name="fas-check" slot="start" aria-hidden="true"></ion-icon>
                 {{ 'core.course.completion_manual:done' | translate }}
             </ion-button>
         </ng-container>
         <ng-container *ngIf="!completion.state">
-            <ion-button color="light" [attr.aria-label]="accessibleDescription" (click)="completionClicked($event)">
+            <ion-button color="dark" expand="block" fill="outline" [attr.aria-label]="accessibleDescription"
+                (click)="completionClicked($event)">
                 {{ 'core.course.completion_manual:markdone' | translate }}
             </ion-button>
         </ng-container>
     </ng-container>
 
     <ng-container *ngIf="!completion.istrackeduser">
-        <ion-button disabled="true" color="light">
+        <ion-button disabled="true" color="dark" expand="block" fill="outline">
             {{ 'core.course.completion_manual:markdone' | translate }}
         </ion-button>
     </ng-container>
diff --git a/src/core/features/course/components/module-navigation/module-navigation.ts b/src/core/features/course/components/module-navigation/module-navigation.ts
index 627f0bbd5..dfb9602a4 100644
--- a/src/core/features/course/components/module-navigation/module-navigation.ts
+++ b/src/core/features/course/components/module-navigation/module-navigation.ts
@@ -14,10 +14,11 @@
 
 import { Component, ElementRef, Input, OnDestroy, OnInit } from '@angular/core';
 import { CoreCourse, CoreCourseProvider, CoreCourseWSSection } from '@features/course/services/course';
-import { CoreCourseModule } from '@features/course/services/course-helper';
+import { CoreCourseModule, CoreCourseSection } from '@features/course/services/course-helper';
 import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
 import { IonContent } from '@ionic/angular';
 import { ScrollDetail } from '@ionic/core';
+import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
 import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
 import { CoreDomUtils } from '@services/utils/dom';
 import { CoreUtils } from '@services/utils/utils';
@@ -41,6 +42,8 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
 
     nextModule?: CoreCourseModule;
     previousModule?: CoreCourseModule;
+    nextModuleSection?: CoreCourseSection;
+    previousModuleSection?: CoreCourseSection;
     loaded = false;
 
     protected element: HTMLElement;
@@ -201,9 +204,10 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
                 for (let j = startModule; j < section.modules.length && this.nextModule == undefined; j++) {
                     const module = section.modules[j];
 
-                    const found = await this.isModuleAvailable(module, section.id);
+                    const found = await this.isModuleAvailable(module);
                     if (found) {
                         this.nextModule = module;
+                        this.nextModuleSection = section;
                     }
                 }
             }
@@ -224,9 +228,10 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
                 for (let j = startModule; j >= 0 && this.previousModule == undefined; j--) {
                     const module = section.modules[j];
 
-                    const found = await this.isModuleAvailable(module, section.id);
+                    const found = await this.isModuleAvailable(module);
                     if (found) {
                         this.previousModule = module;
+                        this.previousModuleSection = section;
                     }
                 }
             }
@@ -237,20 +242,10 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
      * Module is visible by the user and it has a specific view (e.g. not a label).
      *
      * @param module Module to check.
-     * @param sectionId Section ID the module belongs to.
      * @return Wether the module is available to the user or not.
      */
-    protected async isModuleAvailable(module: CoreCourseModule, sectionId: number): Promise<boolean> {
-        if (module.uservisible === false || !CoreCourse.instance.moduleHasView(module)) {
-            return false;
-        }
-
-        if (!module.handlerData) {
-            module.handlerData =
-                await CoreCourseModuleDelegate.getModuleDataFor(module.modname, module, this.courseId, sectionId);
-        }
-
-        return !!module.handlerData?.action;
+    protected async isModuleAvailable(module: CoreCourseModule): Promise<boolean> {
+        return CoreCourse.instance.moduleHasView(module);
     }
 
     /**
@@ -291,11 +286,19 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
             return;
         }
 
-        if (!module.handlerData?.action) {
-            return;
+        if (module.uservisible === false) {
+            const section = next ? this.nextModuleSection : this.previousModuleSection;
+            const options: CoreNavigationOptions = {
+                replace: true,
+                params: {
+                    module,
+                    section,
+                },
+            };
+            CoreNavigator.navigateToSitePath('course/' + this.courseId + '/' + module.id +'/module-preview', options);
+        } else {
+            CoreCourseModuleDelegate.openActivityPage(module.modname, module, this.courseId, { replace: true });
         }
-
-        module.handlerData.action(new Event('click'), module, this.courseId, { replace: true });
     }
 
     /**
diff --git a/src/core/features/course/components/module/core-course-module.html b/src/core/features/course/components/module/core-course-module.html
index ea1bec5df..65637562c 100644
--- a/src/core/features/course/components/module/core-course-module.html
+++ b/src/core/features/course/components/module/core-course-module.html
@@ -75,8 +75,8 @@
                 </div>
 
                 <!-- Module completion. -->
-                <core-course-module-completion *ngIf="module.completiondata" [completion]="module.completiondata" [moduleName]="module.name"
-                    [moduleId]="module.id" [showCompletionConditions]="showCompletionConditions"
+                <core-course-module-completion *ngIf="module.completiondata && module.uservisible" [completion]="module.completiondata"
+                    [moduleName]="module.name" [moduleId]="module.id" [showCompletionConditions]="showCompletionConditions"
                     [showManualCompletion]="showManualCompletion" (completionChanged)="completionChanged.emit($event)">
                 </core-course-module-completion>
 
diff --git a/src/core/features/course/components/unsupported-module/core-course-unsupported-module.html b/src/core/features/course/components/unsupported-module/core-course-unsupported-module.html
index 6b7fe1848..a55256ca3 100644
--- a/src/core/features/course/components/unsupported-module/core-course-unsupported-module.html
+++ b/src/core/features/course/components/unsupported-module/core-course-unsupported-module.html
@@ -1,6 +1,3 @@
-<core-course-module-info [description]="module?.description" [courseId]="courseId" [module]="module">
-</core-course-module-info>
-
 <div class="ion-padding">
     <h2 *ngIf="!isDisabledInSite && isSupportedByTheApp">{{ 'core.whoops' | translate }}</h2>
     <h2 *ngIf="isDisabledInSite || !isSupportedByTheApp">{{ 'core.uhoh' | translate }}</h2>
diff --git a/src/core/features/course/course-lazy.module.ts b/src/core/features/course/course-lazy.module.ts
index 80f28334e..5233864dd 100644
--- a/src/core/features/course/course-lazy.module.ts
+++ b/src/core/features/course/course-lazy.module.ts
@@ -23,9 +23,9 @@ const routes: Routes = [
         loadChildren: () => import('./pages/index/index.module').then( m => m.CoreCourseIndexPageModule),
     },
     {
-        path: ':courseId/unsupported-module',
-        loadChildren: () => import('./pages/unsupported-module/unsupported-module.module')
-            .then( m => m.CoreCourseUnsupportedModulePageModule),
+        path: ':courseId/:cmId/module-preview',
+        loadChildren: () => import('./pages/module-preview/module-preview.module')
+            .then( m => m.CoreCourseModulePreviewPageModule),
     },
     {
         path: ':courseId/list-mod-type',
diff --git a/src/core/features/course/pages/module-preview/module-preview.html b/src/core/features/course/pages/module-preview/module-preview.html
new file mode 100644
index 000000000..8f352f3b5
--- /dev/null
+++ b/src/core/features/course/pages/module-preview/module-preview.html
@@ -0,0 +1,65 @@
+<ion-header collapsible>
+    <ion-toolbar>
+        <ion-buttons slot="start">
+            <ion-back-button [text]="'core.back' | translate"></ion-back-button>
+        </ion-buttons>
+        <ion-title>
+            <h1>
+                <core-format-text [text]="title" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
+                </core-format-text>
+            </h1>
+        </ion-title>
+
+        <ion-buttons slot="end">
+            <core-context-menu>
+                <core-context-menu-item [priority]="900" *ngIf="module.url" [href]="module!.url"
+                    [content]="'core.openinbrowser' | translate" iconAction="fas-external-link-alt">
+                </core-context-menu-item>
+            </core-context-menu>
+        </ion-buttons>
+    </ion-toolbar>
+</ion-header>
+<ion-content>
+    <ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event.target)">
+        <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
+    </ion-refresher>
+    <core-loading [hideUntil]="loaded">
+        <core-course-module-info [module]="module" [courseId]="courseId" [description]="module.description" [component]="module.modname"
+            [componentId]="module.id" (completionChanged)="onCompletionChange()" [expandDescription]="true"
+            [showManualCompletion]="showManualCompletion">
+
+            <div class="safe-area-padding-horizontal ion-padding" *ngIf="module.handlerData?.extraBadge">
+                <ion-badge class="ion-text-wrap ion-text-start" [color]="module.handlerData?.extraBadgeColor">
+                    <span [innerHTML]="module.handlerData?.extraBadge"></span>
+                </ion-badge>
+            </div>
+            <div class="safe-area-padding-horizontal ion-padding" *ngIf="module.visible === 0 && (!section || section.visible)">
+                <ion-badge class="ion-text-wrap">
+                    {{ 'core.course.hiddenfromstudents' | translate }}
+                </ion-badge>
+            </div>
+            <div class="safe-area-padding-horizontal ion-padding" *ngIf="module.visible !== 0 && module.isStealth">
+                <ion-badge class="ion-text-wrap">
+                    {{ 'core.course.hiddenoncoursepage' | translate }}
+                </ion-badge>
+            </div>
+            <div class="safe-area-padding-horizontal ion-padding core-module-availabilityinfo" *ngIf="module.availabilityinfo">
+                <ion-badge class="ion-text-wrap">{{ 'core.restricted' | translate }}</ion-badge>
+                <div>
+                    <core-format-text [text]="module.availabilityinfo" contextLevel="module" [contextInstanceId]="module.id"
+                        [courseId]="courseId" class="ion-text-wrap">
+                    </core-format-text>
+                </div>
+            </div>
+            <div class="safe-area-padding-horizontal ion-padding" *ngIf="module.completiondata?.offline">
+                <ion-badge color="warning" class="ion-text-wrap">
+                    {{ 'core.course.manualcompletionnotsynced' | translate }}
+                </ion-badge>
+            </div>
+
+            <core-course-unsupported-module *ngIf="unsupported" [module]="module" [courseId]="courseId"></core-course-unsupported-module>
+        </core-course-module-info>
+    </core-loading>
+
+    <core-course-module-navigation [hidden]="!loaded" [courseId]="courseId" [currentModuleId]="module.id"></core-course-module-navigation>
+</ion-content>
diff --git a/src/core/features/course/pages/unsupported-module/unsupported-module.module.ts b/src/core/features/course/pages/module-preview/module-preview.module.ts
similarity index 83%
rename from src/core/features/course/pages/unsupported-module/unsupported-module.module.ts
rename to src/core/features/course/pages/module-preview/module-preview.module.ts
index 596d96650..ab346fcb0 100644
--- a/src/core/features/course/pages/unsupported-module/unsupported-module.module.ts
+++ b/src/core/features/course/pages/module-preview/module-preview.module.ts
@@ -16,13 +16,13 @@ import { NgModule } from '@angular/core';
 import { RouterModule, Routes } from '@angular/router';
 
 import { CoreSharedModule } from '@/core/shared.module';
-import { CoreCourseUnsupportedModulePage } from './unsupported-module.page';
+import { CoreCourseModulePreviewPage } from './module-preview.page';
 import { CoreCourseComponentsModule } from '@features/course/components/components.module';
 
 const routes: Routes = [
     {
         path: '',
-        component: CoreCourseUnsupportedModulePage,
+        component: CoreCourseModulePreviewPage,
     },
 ];
 
@@ -33,8 +33,8 @@ const routes: Routes = [
         CoreCourseComponentsModule,
     ],
     declarations: [
-        CoreCourseUnsupportedModulePage,
+        CoreCourseModulePreviewPage,
     ],
     exports: [RouterModule],
 })
-export class CoreCourseUnsupportedModulePageModule {}
+export class CoreCourseModulePreviewPageModule { }
diff --git a/src/core/features/course/pages/module-preview/module-preview.page.ts b/src/core/features/course/pages/module-preview/module-preview.page.ts
new file mode 100644
index 000000000..51dc29b23
--- /dev/null
+++ b/src/core/features/course/pages/module-preview/module-preview.page.ts
@@ -0,0 +1,118 @@
+// (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, OnInit } from '@angular/core';
+import { CoreCourse } from '@features/course/services/course';
+import { CoreCourseHelper, CoreCourseModule, CoreCourseSection } from '@features/course/services/course-helper';
+import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
+import { IonRefresher } from '@ionic/angular';
+import { CoreNavigator } from '@services/navigator';
+import { CoreDomUtils } from '@services/utils/dom';
+import { CoreUtils } from '@services/utils/utils';
+
+/**
+ * Page that displays a module preview.
+ */
+@Component({
+    selector: 'page-core-course-module-preview',
+    templateUrl: 'module-preview.html',
+})
+export class CoreCourseModulePreviewPage implements OnInit {
+
+    title!: string;
+    module!: CoreCourseModule;
+    section?: CoreCourseSection; // The section the module belongs to.
+    courseId!: number;
+    loaded = false;
+    unsupported = false;
+    showManualCompletion = false;
+
+    protected debouncedUpdateModule?: () => void; // Update the module after a certain time.
+
+    /**
+     * @inheritdoc
+     */
+    async ngOnInit(): Promise<void> {
+        try {
+            this.module = CoreNavigator.getRequiredRouteParam<CoreCourseModule>('module');
+            this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
+            this.section = CoreNavigator.getRouteParam<CoreCourseSection>('section');
+        } catch (error) {
+            CoreDomUtils.showErrorModal(error);
+
+            CoreNavigator.back();
+
+            return;
+        }
+
+        this.debouncedUpdateModule = CoreUtils.debounce(() => {
+            this.doRefresh();
+        }, 10000);
+
+        await this.fetchModule();
+    }
+
+    /**
+     * Fetch module.
+     *
+     * @return Promise resolved when done.
+     */
+    protected async fetchModule(refresh = false): Promise<void> {
+        if (refresh) {
+            this.module = await CoreCourse.getModule(this.module.id, this.courseId);
+        }
+
+        CoreCourseHelper.calculateModuleCompletionData(this.module, this.courseId);
+
+        await CoreCourseHelper.loadModuleOfflineCompletion(this.courseId, this.module);
+
+        this.unsupported = !CoreCourseModuleDelegate.getHandlerName(this.module.modname);
+        if (!this.unsupported) {
+            this.module.handlerData =
+                await CoreCourseModuleDelegate.getModuleDataFor(this.module.modname, this.module, this.courseId);
+        }
+
+        this.title = this.module.name;
+
+        this.showManualCompletion = await CoreCourseModuleDelegate.manualCompletionAlwaysShown(this.module);
+
+        this.loaded = true;
+    }
+
+    /**
+     * Refresh the data.
+     *
+     * @param refresher Refresher.
+     * @return Promise resolved when done.
+     */
+    async doRefresh(refresher?: IonRefresher): Promise<void> {
+
+        await CoreCourse.invalidateModule(this.module.id);
+
+        this.fetchModule(true);
+
+        refresher?.complete();
+    }
+
+    /**
+     * The completion of the modules has changed.
+     *
+     * @return Promise resolved when done.
+     */
+    async onCompletionChange(): Promise<void> {
+        // Update the module data after a while.
+        this.debouncedUpdateModule?.();
+    }
+
+}
diff --git a/src/core/features/course/pages/unsupported-module/unsupported-module.html b/src/core/features/course/pages/unsupported-module/unsupported-module.html
deleted file mode 100644
index 6760fda91..000000000
--- a/src/core/features/course/pages/unsupported-module/unsupported-module.html
+++ /dev/null
@@ -1,27 +0,0 @@
-<ion-header collapsible>
-    <ion-toolbar>
-        <ion-buttons slot="start">
-            <ion-back-button [text]="'core.back' | translate"></ion-back-button>
-        </ion-buttons>
-        <ion-title>
-            <h1>
-                <core-format-text [text]="module?.name" contextLevel="module" [contextInstanceId]="module?.id" [courseId]="courseId">
-                </core-format-text>
-            </h1>
-        </ion-title>
-
-        <ion-buttons slot="end">
-            <core-context-menu>
-                <core-context-menu-item [priority]="900" *ngIf="module?.url" [href]="module!.url"
-                    [content]="'core.openinbrowser' | translate" iconAction="fas-external-link-alt">
-                </core-context-menu-item>
-                <core-context-menu-item [priority]="800" *ngIf="module?.description" [content]="'core.moduleintro' | translate"
-                    (action)="expandDescription()" iconAction="fas-arrow-right">
-                </core-context-menu-item>
-            </core-context-menu>
-        </ion-buttons>
-    </ion-toolbar>
-</ion-header>
-<ion-content>
-    <core-course-unsupported-module [module]="module" [courseId]="courseId"></core-course-unsupported-module>
-</ion-content>
diff --git a/src/core/features/course/pages/unsupported-module/unsupported-module.page.ts b/src/core/features/course/pages/unsupported-module/unsupported-module.page.ts
deleted file mode 100644
index 78bb28a44..000000000
--- a/src/core/features/course/pages/unsupported-module/unsupported-module.page.ts
+++ /dev/null
@@ -1,54 +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, OnInit } from '@angular/core';
-
-import { CoreCourseWSModule } from '@features/course/services/course';
-import { CoreNavigator } from '@services/navigator';
-import { CoreTextUtils } from '@services/utils/text';
-import { Translate } from '@singletons';
-
-/**
- * Page that displays info about an unsupported module.
- */
-@Component({
-    selector: 'page-core-course-unsupported-module',
-    templateUrl: 'unsupported-module.html',
-})
-export class CoreCourseUnsupportedModulePage implements OnInit {
-
-    module?: CoreCourseWSModule;
-    courseId?: number;
-
-    /**
-     * @inheritDoc
-     */
-    ngOnInit(): void {
-        this.module = CoreNavigator.getRouteParam('module');
-        this.courseId = CoreNavigator.getRouteNumberParam('courseId');
-    }
-
-    /**
-     * Expand the description.
-     */
-    expandDescription(): void {
-        CoreTextUtils.viewText(Translate.instant('core.description'), this.module!.description!, {
-            filter: true,
-            contextLevel: 'module',
-            instanceId: this.module!.id,
-            courseId: this.courseId,
-        });
-    }
-
-}
diff --git a/src/core/features/course/services/handlers/default-module.ts b/src/core/features/course/services/handlers/default-module.ts
index 25bf41663..6e8cb25d6 100644
--- a/src/core/features/course/services/handlers/default-module.ts
+++ b/src/core/features/course/services/handlers/default-module.ts
@@ -49,14 +49,11 @@ export class CoreCourseModuleDefaultHandler implements CoreCourseModuleHandler {
             icon: await CoreCourse.getModuleIconSrc(module.modname, module.modicon),
             title: module.name,
             class: 'core-course-default-handler core-course-module-' + module.modname + '-handler',
-            action: (event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions) => {
+            action: async (event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions) => {
                 event.preventDefault();
                 event.stopPropagation();
 
-                options = options || {};
-                options.params = { module };
-
-                CoreNavigator.navigateToSitePath('course/' + courseId + '/unsupported-module', options);
+                await this.openActivityPage(module, courseId, options);
             },
         };
 
@@ -92,4 +89,15 @@ export class CoreCourseModuleDefaultHandler implements CoreCourseModuleHandler {
         return true;
     }
 
+    /**
+     * @inheritdoc
+     */
+    async openActivityPage(module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): Promise<void> {
+        options = options || {};
+        options.params = options.params || {};
+        Object.assign(options.params, { module });
+
+        await CoreNavigator.navigateToSitePath('course/' + courseId + '/' + module.id +'/module-preview', options);
+    }
+
 }
diff --git a/src/core/features/course/services/module-delegate.ts b/src/core/features/course/services/module-delegate.ts
index 66dec4184..2776043ff 100644
--- a/src/core/features/course/services/module-delegate.ts
+++ b/src/core/features/course/services/module-delegate.ts
@@ -102,6 +102,16 @@ export interface CoreCourseModuleHandler extends CoreDelegateHandler {
      * @return Promise resolved with boolean: whether the manual completion should always be displayed.
      */
     manualCompletionAlwaysShown?(module: CoreCourseModule): Promise<boolean>;
+
+    /**
+     * Opens the activity page.
+     *
+     * @param module The module object.
+     * @param courseId The course ID.
+     * @param options Options for the navigation.
+     * @return Promise resolved when done.
+     */
+    openActivityPage(module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): Promise<void>;
 }
 
 /**
@@ -295,6 +305,27 @@ export class CoreCourseModuleDelegateService extends CoreDelegate<CoreCourseModu
         );
     }
 
+    /**
+     * Opens the activity page.
+     *
+     * @param module The module object.
+     * @param courseId The course ID.
+     * @param options Options for the navigation.
+     * @return Promise resolved when done.
+     */
+    async openActivityPage(
+        modname: string,
+        module: CoreCourseModule,
+        courseId: number,
+        options?: CoreNavigationOptions,
+    ): Promise<void> {
+        return await this.executeFunctionOnEnabled<void>(
+            modname,
+            'openActivityPage',
+            [module, courseId, options],
+        );
+    }
+
     /**
      * Check if a certain module type is disabled in a site.
      *
diff --git a/src/core/features/siteplugins/classes/handlers/module-handler.ts b/src/core/features/siteplugins/classes/handlers/module-handler.ts
index 1276a3a87..847d2894b 100644
--- a/src/core/features/siteplugins/classes/handlers/module-handler.ts
+++ b/src/core/features/siteplugins/classes/handlers/module-handler.ts
@@ -15,7 +15,7 @@
 import { Type } from '@angular/core';
 
 import { CoreConstants } from '@/core/constants';
-import { CoreCourseAnyModuleData, CoreCourseWSModule } from '@features/course/services/course';
+import { CoreCourse, CoreCourseAnyModuleData, CoreCourseWSModule } from '@features/course/services/course';
 import { CoreCourseModule } from '@features/course/services/course-helper';
 import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
 import { CoreSitePluginsModuleIndexComponent } from '@features/siteplugins/components/module-index/module-index';
@@ -92,17 +92,16 @@ export class CoreSitePluginsModuleHandler extends CoreSitePluginsBaseHandler imp
 
         if (this.handlerSchema.method) {
             // There is a method, add an action.
-            handlerData.action = (event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions) => {
+            handlerData.action = async (
+                event: Event,
+                module: CoreCourseModule,
+                courseId: number,
+                options?: CoreNavigationOptions,
+            ) => {
                 event.preventDefault();
                 event.stopPropagation();
 
-                options = options || {};
-                options.params = {
-                    title: module.name,
-                    module,
-                };
-
-                CoreNavigator.navigateToSitePath(`siteplugins/module/${courseId}/${module.id}`, options);
+                await this.openActivityPage(module, courseId, options);
             };
         }
 
@@ -229,4 +228,22 @@ export class CoreSitePluginsModuleHandler extends CoreSitePluginsBaseHandler imp
         return false;
     }
 
+    /**
+     * @inheritdoc
+     */
+    async openActivityPage(module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): Promise<void> {
+        if (!CoreCourse.moduleHasView(module)) {
+            return;
+        }
+
+        options = options || {};
+        options.params = options.params || {};
+        Object.assign(options.params, {
+            title: module.name,
+            module,
+        });
+
+        CoreNavigator.navigateToSitePath(`siteplugins/module/${courseId}/${module.id}`, options);
+    }
+
 }