From 604e866943fa1124469edc0154f65a16c8966c43 Mon Sep 17 00:00:00 2001
From: Dani Palou <dani@moodle.com>
Date: Thu, 24 Mar 2022 15:11:13 +0100
Subject: [PATCH 1/8] MOBILE-3833 data: Fix unable to change group for new
 entries

---
 src/addons/mod/bigbluebuttonbn/components/index/index.html  | 4 ++--
 src/addons/mod/data/pages/edit/edit.html                    | 4 ++--
 src/addons/mod/data/pages/edit/edit.ts                      | 6 ++++--
 .../feedback/components/index/addon-mod-feedback-index.html | 4 ++--
 4 files changed, 10 insertions(+), 8 deletions(-)

diff --git a/src/addons/mod/bigbluebuttonbn/components/index/index.html b/src/addons/mod/bigbluebuttonbn/components/index/index.html
index 8003d0166..98308626b 100644
--- a/src/addons/mod/bigbluebuttonbn/components/index/index.html
+++ b/src/addons/mod/bigbluebuttonbn/components/index/index.html
@@ -23,8 +23,8 @@
 
         <ion-item class="ion-text-wrap core-group-selector">
             <ion-label id="addon-bigbluebuttonbn-groupslabel">
-                <ng-container *ngIf="groupInfo.separateGroups">{{'core.groupsseparate' | translate }}</ng-container>
-                <ng-container *ngIf="groupInfo.visibleGroups">{{'core.groupsvisible' | translate }}</ng-container>
+                <ng-container *ngIf="groupInfo.separateGroups">{{ 'core.groupsseparate' | translate }}</ng-container>
+                <ng-container *ngIf="groupInfo.visibleGroups">{{ 'core.groupsvisible' | translate }}</ng-container>
             </ion-label>
             <ion-select [(ngModel)]="groupId" (ionChange)="groupChanged()" aria-labelledby="addon-bigbluebuttonbn-groupslabel"
                 interface="action-sheet" [interfaceOptions]="{header: 'core.group' | translate}">
diff --git a/src/addons/mod/data/pages/edit/edit.html b/src/addons/mod/data/pages/edit/edit.html
index 4b04f9b68..875d160f0 100644
--- a/src/addons/mod/data/pages/edit/edit.html
+++ b/src/addons/mod/data/pages/edit/edit.html
@@ -20,8 +20,8 @@
     <core-loading [hideUntil]="loaded">
         <ion-item class="ion-text-wrap core-group-selector" *ngIf="groupInfo && (groupInfo.separateGroups || groupInfo.visibleGroups)">
             <ion-label id="addon-data-groupslabel">
-                <ng-container *ngIf="groupInfo.separateGroups">{{ 'core.groupsvisible' | translate }}</ng-container>
-                <ng-container *ngIf="groupInfo.visibleGroups">{{ 'core.groupsseparate' | translate }}</ng-container>
+                <ng-container *ngIf="groupInfo.separateGroups">{{ 'core.groupsseparate' | translate }}</ng-container>
+                <ng-container *ngIf="groupInfo.visibleGroups">{{ 'core.groupsvisible' | translate }}</ng-container>
             </ion-label>
             <ion-select [(ngModel)]="selectedGroup" (ionChange)="setGroup(selectedGroup)" aria-labelledby="addon-data-groupslabel"
                 interface="action-sheet" [interfaceOptions]="{header: 'core.group' | translate}">
diff --git a/src/addons/mod/data/pages/edit/edit.ts b/src/addons/mod/data/pages/edit/edit.ts
index 53a73c691..208d9de72 100644
--- a/src/addons/mod/data/pages/edit/edit.ts
+++ b/src/addons/mod/data/pages/edit/edit.ts
@@ -163,8 +163,10 @@ export class AddonModDataEditPage implements OnInit {
             const entry = await AddonModDataHelper.fetchEntry(this.database, this.fieldsArray, this.entryId || 0);
             this.entry = entry.entry;
 
-            // Load correct group.
-            this.selectedGroup = this.entry.groupid;
+            if (this.entryId) {
+                // Load correct group.
+                this.selectedGroup = this.entry.groupid;
+            }
 
             // Check permissions when adding a new entry or offline entry.
             if (!this.isEditing) {
diff --git a/src/addons/mod/feedback/components/index/addon-mod-feedback-index.html b/src/addons/mod/feedback/components/index/addon-mod-feedback-index.html
index 8a72abe19..eb378ee03 100644
--- a/src/addons/mod/feedback/components/index/addon-mod-feedback-index.html
+++ b/src/addons/mod/feedback/components/index/addon-mod-feedback-index.html
@@ -61,8 +61,8 @@
     <ion-list *ngIf="access && access.canviewanalysis && !access.isempty">
         <ion-item class="ion-text-wrap core-group-selector" *ngIf="groupInfo && (groupInfo.separateGroups || groupInfo.visibleGroups)">
             <ion-label id="addon-feedback-groupslabel">
-                <ng-container *ngIf="groupInfo.separateGroups">{{'core.groupsseparate' | translate }}</ng-container>
-                <ng-container *ngIf="groupInfo.visibleGroups">{{'core.groupsvisible' | translate }}</ng-container>
+                <ng-container *ngIf="groupInfo.separateGroups">{{ 'core.groupsseparate' | translate }}</ng-container>
+                <ng-container *ngIf="groupInfo.visibleGroups">{{ 'core.groupsvisible' | translate }}</ng-container>
             </ion-label>
             <ion-select [(ngModel)]="group" (ionChange)="setGroup(group)" aria-labelledby="addon-feedback-groupslabel"
                 interface="action-sheet" [interfaceOptions]="{header: 'core.group' | translate}">

From 9c2116c33b2d9a5f0b004482ce32c9437f9f37dd Mon Sep 17 00:00:00 2001
From: Dani Palou <dani@moodle.com>
Date: Thu, 24 Mar 2022 16:08:16 +0100
Subject: [PATCH 2/8] MOBILE-3833 data: Hide All participants with visible
 groups

---
 src/addons/mod/data/components/index/index.ts | 6 ++++++
 src/addons/mod/data/pages/edit/edit.ts        | 6 ++++++
 src/addons/mod/data/pages/entry/entry.ts      | 6 ++++++
 3 files changed, 18 insertions(+)

diff --git a/src/addons/mod/data/components/index/index.ts b/src/addons/mod/data/components/index/index.ts
index 4708f24c7..bb4b1b8dc 100644
--- a/src/addons/mod/data/components/index/index.ts
+++ b/src/addons/mod/data/components/index/index.ts
@@ -218,6 +218,12 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp
         }
 
         this.groupInfo = await CoreGroups.getActivityGroupInfo(this.database.coursemodule);
+        if (this.groupInfo.visibleGroups && this.groupInfo.groups?.length) {
+            // There is a bug in Moodle with All participants and visible groups (MOBILE-3597). Remove it.
+            this.groupInfo.groups = this.groupInfo.groups.filter(group => group.id !== 0);
+            this.groupInfo.defaultGroupId = this.groupInfo.groups[0].id;
+        }
+
         this.selectedGroup = CoreGroups.validateGroupId(this.selectedGroup, this.groupInfo);
 
         this.access = await AddonModData.getDatabaseAccessInformation(this.database.id, {
diff --git a/src/addons/mod/data/pages/edit/edit.ts b/src/addons/mod/data/pages/edit/edit.ts
index 208d9de72..52abfab1e 100644
--- a/src/addons/mod/data/pages/edit/edit.ts
+++ b/src/addons/mod/data/pages/edit/edit.ts
@@ -174,6 +174,12 @@ export class AddonModDataEditPage implements OnInit {
 
                 if (refresh) {
                     this.groupInfo = await CoreGroups.getActivityGroupInfo(this.database.coursemodule);
+                    if (this.groupInfo.visibleGroups && this.groupInfo.groups?.length) {
+                        // There is a bug in Moodle with All participants and visible groups (MOBILE-3597). Remove it.
+                        this.groupInfo.groups = this.groupInfo.groups.filter(group => group.id !== 0);
+                        this.groupInfo.defaultGroupId = this.groupInfo.groups[0].id;
+                    }
+
                     this.selectedGroup = CoreGroups.validateGroupId(this.selectedGroup, this.groupInfo);
                     this.initialSelectedGroup = this.selectedGroup;
                 }
diff --git a/src/addons/mod/data/pages/entry/entry.ts b/src/addons/mod/data/pages/entry/entry.ts
index 648726a62..6bc442857 100644
--- a/src/addons/mod/data/pages/entry/entry.ts
+++ b/src/addons/mod/data/pages/entry/entry.ts
@@ -174,6 +174,12 @@ export class AddonModDataEntryPage implements OnInit, OnDestroy {
             this.access = await AddonModData.getDatabaseAccessInformation(this.database.id, { cmId: this.moduleId });
 
             this.groupInfo = await CoreGroups.getActivityGroupInfo(this.database.coursemodule);
+            if (this.groupInfo.visibleGroups && this.groupInfo.groups?.length) {
+                // There is a bug in Moodle with All participants and visible groups (MOBILE-3597). Remove it.
+                this.groupInfo.groups = this.groupInfo.groups.filter(group => group.id !== 0);
+                this.groupInfo.defaultGroupId = this.groupInfo.groups[0].id;
+            }
+
             this.selectedGroup = CoreGroups.validateGroupId(this.selectedGroup, this.groupInfo);
 
             const actions = AddonModDataHelper.getActions(this.database, this.access, this.entry!);

From 1a0733139679f16421a1f64ae9b3898077db52ed Mon Sep 17 00:00:00 2001
From: Dani Palou <dani@moodle.com>
Date: Thu, 24 Mar 2022 17:29:58 +0100
Subject: [PATCH 3/8] MOBILE-3833 course: Display data ASAP in course downloads

---
 scripts/langindex.json                        |  3 +-
 .../pages/course-storage/course-storage.html  | 31 ++++++++-----
 .../pages/course-storage/course-storage.ts    | 43 +++++++++++--------
 src/core/lang.json                            |  1 +
 4 files changed, 49 insertions(+), 29 deletions(-)

diff --git a/scripts/langindex.json b/scripts/langindex.json
index ec7f4e8eb..8221281fa 100644
--- a/scripts/langindex.json
+++ b/scripts/langindex.json
@@ -1453,6 +1453,7 @@
   "core.block.tour_navigation_dashboard_content": "tool_usertours",
   "core.block.tour_navigation_dashboard_title": "tool_usertours",
   "core.browser": "local_moodlemobileapp",
+  "core.calculating": "local_moodlemobileapp",
   "core.cancel": "moodle",
   "core.cannotconnect": "local_moodlemobileapp",
   "core.cannotconnecttrouble": "local_moodlemobileapp",
@@ -1673,6 +1674,7 @@
   "core.editor.underline": "atto_underline/pluginname",
   "core.editor.unorderedlist": "atto_unorderedlist/pluginname",
   "core.emptysplit": "local_moodlemobileapp",
+  "core.endonesteptour": "tool_usertours",
   "core.error": "moodle",
   "core.errorchangecompletion": "local_moodlemobileapp",
   "core.errordeletefile": "local_moodlemobileapp",
@@ -2342,7 +2344,6 @@
   "core.usernotfullysetup": "error",
   "core.users": "moodle",
   "core.usersuspended": "tool_reportbuilder",
-  "core.endonesteptour": "tool_usertours",
   "core.view": "moodle",
   "core.viewcode": "local_moodlemobileapp",
   "core.vieweditor": "local_moodlemobileapp",
diff --git a/src/addons/storagemanager/pages/course-storage/course-storage.html b/src/addons/storagemanager/pages/course-storage/course-storage.html
index b6353dd6c..b36d1e85d 100644
--- a/src/addons/storagemanager/pages/course-storage/course-storage.html
+++ b/src/addons/storagemanager/pages/course-storage/course-storage.html
@@ -24,15 +24,18 @@
                     <ion-label>
                         <p class="item-heading ion-text-wrap">{{ 'addon.storagemanager.totaldownloads' | translate }}</p>
                     </ion-label>
-                    <ion-badge color="light" slot="end">{{ totalSize | coreBytesToSize }}
+                    <ion-badge color="light" slot="end">
+                        <ng-container *ngIf="sizeLoaded">{{ totalSize | coreBytesToSize }}</ng-container>
+                        <ng-container *ngIf="!sizeLoaded">{{ 'core.calculating' | translate }}</ng-container>
                     </ion-badge>
                 </ion-item>
-                <ion-button *ngIf="downloadCourseEnabled" (click)="prefetchCourse()" expand="block" fill="outline" class="ion-no-margin">
+                <ion-button *ngIf="downloadCourseEnabled" (click)="prefetchCourse()" expand="block" fill="outline" class="ion-no-margin"
+                    [disabled]="prefetchCourseData.loading">
                     <ion-icon *ngIf="!prefetchCourseData.loading" [name]="prefetchCourseData.icon" slot="start"></ion-icon>
                     <ion-spinner *ngIf="prefetchCourseData.loading" slot="start"></ion-spinner>
                     {{ prefetchCourseData.statusTranslatable | translate }}
                 </ion-button>
-                <ion-button *ngIf="totalSize > 0" (click)="deleteForCourse()" expand="block" color="danger"
+                <ion-button *ngIf="sizeLoaded && totalSize > 0" (click)="deleteForCourse()" expand="block" color="danger"
                     class="ion-no-margin ion-margin-top">
                     <ion-icon name="fas-trash" slot="start" [attr.aria-label]="'addon.storagemanager.deletedatafrom' | translate:
                         { name: title }">
@@ -52,18 +55,21 @@
                                 </core-format-text>
                             </p>
                             <ion-badge [color]="section.downloadStatus == statusDownloaded ? 'success' : 'light'"
-                                *ngIf="section.totalSize > 0">
+                                *ngIf="section.sizeLoaded && section.totalSize > 0">
                                 <ion-icon name="fam-cloud-done" *ngIf="section.downloadStatus == statusDownloaded"
                                     [attr.aria-label]="'core.downloaded' | translate">
                                 </ion-icon>{{ section.totalSize | coreBytesToSize }}
                             </ion-badge>
+                            <ion-badge color="light" *ngIf="!section.sizeLoaded">
+                                {{ 'core.calculating' | translate }}
+                            </ion-badge>
                             <!-- Download progress. -->
                             <p *ngIf="downloadEnabled && section.isDownloading">
                                 <core-progress-bar [progress]="section.total == 0 ? -1 : section.count / section.total">
                                 </core-progress-bar>
                             </p>
                         </ion-label>
-                        <div class="storage-buttons" slot="end" *ngIf="section.totalSize > 0 || downloadEnabled">
+                        <div class="storage-buttons" slot="end" *ngIf="(section.sizeLoaded && section.totalSize > 0) || downloadEnabled">
                             <div *ngIf="downloadEnabled" slot="end" class="core-button-spinner">
                                 <core-download-refresh *ngIf="!section.isDownloading && section.downloadStatus != statusDownloaded"
                                     [status]="section.downloadStatus" [enabled]="true" (action)="prefecthSection(section)"
@@ -77,7 +83,8 @@
                                     {{section.count}} / {{section.total}}
                                 </ion-badge>
                             </div>
-                            <ion-button (click)="deleteForSection(section)" *ngIf="section.totalSize > 0" color="danger" fill="clear">
+                            <ion-button (click)="deleteForSection(section)" *ngIf="section.sizeLoaded && section.totalSize > 0"
+                                color="danger" fill="clear">
                                 <ion-icon name="fas-trash" slot="icon-only"
                                     [attr.aria-label]="'addon.storagemanager.deletedatafrom' | translate: { name: section.name }">
                                 </ion-icon>
@@ -87,7 +94,8 @@
                 </ion-card-header>
                 <ion-card-content>
                     <ng-container *ngFor="let module of section.modules">
-                        <ion-item class="ion-no-padding core-course-storage-activity" *ngIf="downloadEnabled || module.totalSize > 0">
+                        <ion-item class="ion-no-padding core-course-storage-activity"
+                            *ngIf="downloadEnabled || (module.sizeLoaded && module.totalSize > 0)">
                             <core-mod-icon slot="start" *ngIf="module.handlerData.icon" [modicon]="module.handlerData.icon"
                                 [modname]="module.modname" [componentId]="module.instance">
                             </core-mod-icon>
@@ -98,11 +106,14 @@
                                     </core-format-text>
                                 </h3>
                                 <ion-badge [color]="module.downloadStatus == statusDownloaded ? 'success' : 'light'"
-                                    *ngIf="module.totalSize > 0">
+                                    *ngIf="module.sizeLoaded && module.totalSize > 0">
                                     <ion-icon name="fam-cloud-done" *ngIf="module.downloadStatus == statusDownloaded"
                                         [attr.aria-label]="'core.downloaded' | translate">
                                     </ion-icon>{{ module.totalSize | coreBytesToSize }}
                                 </ion-badge>
+                                <ion-badge color="light" *ngIf="!module.sizeLoaded">
+                                    {{ 'core.calculating' | translate }}
+                                </ion-badge>
                             </ion-label>
 
                             <div class="storage-buttons" slot="end">
@@ -111,8 +122,8 @@
                                     [canTrustDownload]="true" [loading]="module.spinner || module.handlerData.spinner"
                                     (action)="prefetchModule(module, section)">
                                 </core-download-refresh>
-                                <ion-button fill="clear" (click)="deleteForModule(module, section)" *ngIf="module.totalSize > 0"
-                                    color="danger">
+                                <ion-button fill="clear" (click)="deleteForModule(module, section)"
+                                    *ngIf="module.sizeLoaded && module.totalSize > 0" color="danger">
                                     <ion-icon name="fas-trash" slot="icon-only"
                                         [attr.aria-label]="'addon.storagemanager.deletedatafrom' | translate: { name: module.name }">
                                     </ion-icon>
diff --git a/src/addons/storagemanager/pages/course-storage/course-storage.ts b/src/addons/storagemanager/pages/course-storage/course-storage.ts
index aa14b5269..5c3f371cb 100644
--- a/src/addons/storagemanager/pages/course-storage/course-storage.ts
+++ b/src/addons/storagemanager/pages/course-storage/course-storage.ts
@@ -48,6 +48,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
     loaded = false;
     sections: AddonStorageManagerCourseSection[] = [];
     totalSize = 0;
+    sizeLoaded = false;
 
     downloadEnabled = false;
     downloadCourseEnabled = false;
@@ -107,13 +108,13 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
         this.sections = (await CoreCourseHelper.addHandlerDataForModules(sections, this.courseId)).sections
             .map((section) => ({ ...section, totalSize: 0 }));
 
+        this.loaded = true;
+
         await Promise.all([
             this.loadSizes(),
             this.initCoursePrefetch(),
             this.initModulePrefetch(),
         ]);
-
-        this.loaded = true;
     }
 
     /**
@@ -241,12 +242,15 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
      */
     protected async loadSizes(): Promise<void> {
         this.totalSize = 0;
+        this.sizeLoaded = false;
 
-        const promises: Promise<void>[] = [];
-        this.sections.forEach((section) => {
+        await Promise.all(this.sections.map(async (section) => {
             section.totalSize = 0;
-            section.modules.forEach((module) => {
+            section.sizeLoaded = false;
+
+            await Promise.all(section.modules.map(async (module) => {
                 module.totalSize = 0;
+                module.sizeLoaded = false;
 
                 // Note: This function only gets the size for modules which are downloadable.
                 // For other modules it always returns 0, even if they have downloaded some files.
@@ -255,21 +259,22 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
                 // But these aren't necessarily consistent, for example mod_frog vs mmaModFrog.
                 // There is nothing enforcing correct values.
                 // Most modules which have large files are downloadable, so I think this is sufficient.
-                const promise = CoreCourseModulePrefetchDelegate.getModuleStoredSize(module, this.courseId).then((size) => {
-                    // There are some cases where the return from this is not a valid number.
-                    if (!isNaN(size)) {
-                        module.totalSize = Number(size);
-                        section.totalSize += size;
-                        this.totalSize += size;
-                    }
+                const size = await CoreCourseModulePrefetchDelegate.getModuleStoredSize(module, this.courseId);
 
-                    return;
-                });
-                promises.push(promise);
-            });
-        });
+                // There are some cases where the return from this is not a valid number.
+                if (!isNaN(size)) {
+                    module.totalSize = Number(size);
+                    section.totalSize += size;
+                    this.totalSize += size;
+                }
 
-        await Promise.all(promises);
+                module.sizeLoaded = true;
+            }));
+
+            section.sizeLoaded = true;
+        }));
+
+        this.sizeLoaded = true;
 
         // Mark course as not downloaded if course size is 0.
         if (this.totalSize == 0) {
@@ -608,11 +613,13 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
 
 type AddonStorageManagerCourseSection = Omit<CoreCourseSectionWithStatus, 'modules'> & {
     totalSize: number;
+    sizeLoaded?: boolean;
     modules: AddonStorageManagerModule[];
 };
 
 type AddonStorageManagerModule = CoreCourseModuleData & {
     totalSize?: number;
+    sizeLoaded?: boolean;
     prefetchHandler?: CoreCourseModulePrefetchHandler;
     spinner?: boolean;
     downloadStatus?: string;
diff --git a/src/core/lang.json b/src/core/lang.json
index 8dd623b4e..a66c1a6a3 100644
--- a/src/core/lang.json
+++ b/src/core/lang.json
@@ -13,6 +13,7 @@
     "areyousure": "Are you sure?",
     "back": "Back",
     "browser": "Browser",
+    "calculating": "Calculating",
     "cancel": "Cancel",
     "cannotconnect": "Cannot connect",
     "cannotconnecttrouble": "We're having trouble connecting to your site.",

From 37af8e3c69b4338a874fe39fe8bb5fd85c5e39a2 Mon Sep 17 00:00:00 2001
From: Dani Palou <dani@moodle.com>
Date: Mon, 28 Mar 2022 12:54:14 +0200
Subject: [PATCH 4/8] MOBILE-3833 course: Improve performance after download or
 delete

---
 .../pages/course-storage/course-storage.html  | 23 ++---
 .../pages/course-storage/course-storage.ts    | 91 ++++++++++++++-----
 2 files changed, 82 insertions(+), 32 deletions(-)

diff --git a/src/addons/storagemanager/pages/course-storage/course-storage.html b/src/addons/storagemanager/pages/course-storage/course-storage.html
index b36d1e85d..57b45f33c 100644
--- a/src/addons/storagemanager/pages/course-storage/course-storage.html
+++ b/src/addons/storagemanager/pages/course-storage/course-storage.html
@@ -25,8 +25,8 @@
                         <p class="item-heading ion-text-wrap">{{ 'addon.storagemanager.totaldownloads' | translate }}</p>
                     </ion-label>
                     <ion-badge color="light" slot="end">
-                        <ng-container *ngIf="sizeLoaded">{{ totalSize | coreBytesToSize }}</ng-container>
-                        <ng-container *ngIf="!sizeLoaded">{{ 'core.calculating' | translate }}</ng-container>
+                        <ng-container *ngIf="!calculatingSize">{{ totalSize | coreBytesToSize }}</ng-container>
+                        <ng-container *ngIf="calculatingSize">{{ 'core.calculating' | translate }}</ng-container>
                     </ion-badge>
                 </ion-item>
                 <ion-button *ngIf="downloadCourseEnabled" (click)="prefetchCourse()" expand="block" fill="outline" class="ion-no-margin"
@@ -35,7 +35,7 @@
                     <ion-spinner *ngIf="prefetchCourseData.loading" slot="start"></ion-spinner>
                     {{ prefetchCourseData.statusTranslatable | translate }}
                 </ion-button>
-                <ion-button *ngIf="sizeLoaded && totalSize > 0" (click)="deleteForCourse()" expand="block" color="danger"
+                <ion-button [disabled]="calculatingSize || totalSize <= 0" (click)="deleteForCourse()" expand="block" color="danger"
                     class="ion-no-margin ion-margin-top">
                     <ion-icon name="fas-trash" slot="start" [attr.aria-label]="'addon.storagemanager.deletedatafrom' | translate:
                         { name: title }">
@@ -55,12 +55,12 @@
                                 </core-format-text>
                             </p>
                             <ion-badge [color]="section.downloadStatus == statusDownloaded ? 'success' : 'light'"
-                                *ngIf="section.sizeLoaded && section.totalSize > 0">
+                                *ngIf="!section.calculatingSize && section.totalSize > 0">
                                 <ion-icon name="fam-cloud-done" *ngIf="section.downloadStatus == statusDownloaded"
                                     [attr.aria-label]="'core.downloaded' | translate">
                                 </ion-icon>{{ section.totalSize | coreBytesToSize }}
                             </ion-badge>
-                            <ion-badge color="light" *ngIf="!section.sizeLoaded">
+                            <ion-badge color="light" *ngIf="section.calculatingSize">
                                 {{ 'core.calculating' | translate }}
                             </ion-badge>
                             <!-- Download progress. -->
@@ -69,7 +69,8 @@
                                 </core-progress-bar>
                             </p>
                         </ion-label>
-                        <div class="storage-buttons" slot="end" *ngIf="(section.sizeLoaded && section.totalSize > 0) || downloadEnabled">
+                        <div class="storage-buttons" slot="end"
+                            *ngIf="(!section.calculatingSize && section.totalSize > 0) || downloadEnabled">
                             <div *ngIf="downloadEnabled" slot="end" class="core-button-spinner">
                                 <core-download-refresh *ngIf="!section.isDownloading && section.downloadStatus != statusDownloaded"
                                     [status]="section.downloadStatus" [enabled]="true" (action)="prefecthSection(section)"
@@ -83,7 +84,7 @@
                                     {{section.count}} / {{section.total}}
                                 </ion-badge>
                             </div>
-                            <ion-button (click)="deleteForSection(section)" *ngIf="section.sizeLoaded && section.totalSize > 0"
+                            <ion-button (click)="deleteForSection(section)" *ngIf="!section.calculatingSize && section.totalSize > 0"
                                 color="danger" fill="clear">
                                 <ion-icon name="fas-trash" slot="icon-only"
                                     [attr.aria-label]="'addon.storagemanager.deletedatafrom' | translate: { name: section.name }">
@@ -95,7 +96,7 @@
                 <ion-card-content>
                     <ng-container *ngFor="let module of section.modules">
                         <ion-item class="ion-no-padding core-course-storage-activity"
-                            *ngIf="downloadEnabled || (module.sizeLoaded && module.totalSize > 0)">
+                            *ngIf="downloadEnabled || (!module.calculatingSize && module.totalSize > 0)">
                             <core-mod-icon slot="start" *ngIf="module.handlerData.icon" [modicon]="module.handlerData.icon"
                                 [modname]="module.modname" [componentId]="module.instance">
                             </core-mod-icon>
@@ -106,12 +107,12 @@
                                     </core-format-text>
                                 </h3>
                                 <ion-badge [color]="module.downloadStatus == statusDownloaded ? 'success' : 'light'"
-                                    *ngIf="module.sizeLoaded && module.totalSize > 0">
+                                    *ngIf="!module.calculatingSize && module.totalSize > 0">
                                     <ion-icon name="fam-cloud-done" *ngIf="module.downloadStatus == statusDownloaded"
                                         [attr.aria-label]="'core.downloaded' | translate">
                                     </ion-icon>{{ module.totalSize | coreBytesToSize }}
                                 </ion-badge>
-                                <ion-badge color="light" *ngIf="!module.sizeLoaded">
+                                <ion-badge color="light" *ngIf="module.calculatingSize">
                                     {{ 'core.calculating' | translate }}
                                 </ion-badge>
                             </ion-label>
@@ -123,7 +124,7 @@
                                     (action)="prefetchModule(module, section)">
                                 </core-download-refresh>
                                 <ion-button fill="clear" (click)="deleteForModule(module, section)"
-                                    *ngIf="module.sizeLoaded && module.totalSize > 0" color="danger">
+                                    *ngIf="!module.calculatingSize && module.totalSize > 0" color="danger">
                                     <ion-icon name="fas-trash" slot="icon-only"
                                         [attr.aria-label]="'addon.storagemanager.deletedatafrom' | translate: { name: module.name }">
                                     </ion-icon>
diff --git a/src/addons/storagemanager/pages/course-storage/course-storage.ts b/src/addons/storagemanager/pages/course-storage/course-storage.ts
index 5c3f371cb..39c3d42d0 100644
--- a/src/addons/storagemanager/pages/course-storage/course-storage.ts
+++ b/src/addons/storagemanager/pages/course-storage/course-storage.ts
@@ -48,7 +48,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
     loaded = false;
     sections: AddonStorageManagerCourseSection[] = [];
     totalSize = 0;
-    sizeLoaded = false;
+    calculatingSize = true;
 
     downloadEnabled = false;
     downloadCourseEnabled = false;
@@ -106,12 +106,20 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
 
         const sections = await CoreCourse.getSections(this.courseId, false, true);
         this.sections = (await CoreCourseHelper.addHandlerDataForModules(sections, this.courseId)).sections
-            .map((section) => ({ ...section, totalSize: 0 }));
+            .map(section => ({
+                ...section,
+                totalSize: 0,
+                calculatingSize: true,
+                modules: section.modules.map(module => ({
+                    ...module,
+                    calculatingSize: true,
+                })),
+            }));
 
         this.loaded = true;
 
         await Promise.all([
-            this.loadSizes(),
+            this.initSizes(),
             this.initCoursePrefetch(),
             this.initModulePrefetch(),
         ]);
@@ -240,18 +248,9 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
     /**
      * Init section, course and modules sizes.
      */
-    protected async loadSizes(): Promise<void> {
-        this.totalSize = 0;
-        this.sizeLoaded = false;
-
+    protected async initSizes(): Promise<void> {
         await Promise.all(this.sections.map(async (section) => {
-            section.totalSize = 0;
-            section.sizeLoaded = false;
-
             await Promise.all(section.modules.map(async (module) => {
-                module.totalSize = 0;
-                module.sizeLoaded = false;
-
                 // Note: This function only gets the size for modules which are downloadable.
                 // For other modules it always returns 0, even if they have downloaded some files.
                 // However there is no 100% reliable way to actually track the files in this case.
@@ -268,13 +267,13 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
                     this.totalSize += size;
                 }
 
-                module.sizeLoaded = true;
+                module.calculatingSize = false;
             }));
 
-            section.sizeLoaded = true;
+            section.calculatingSize = false;
         }));
 
-        this.sizeLoaded = true;
+        this.calculatingSize = false;
 
         // Mark course as not downloaded if course size is 0.
         if (this.totalSize == 0) {
@@ -282,6 +281,56 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
         }
     }
 
+    /**
+     * Update the sizes of some modules.
+     *
+     * @param modules Modules.
+     * @param section Section the modules belong to.
+     * @return Promise resolved when done.
+     */
+    protected async updateModulesSizes(
+        modules: AddonStorageManagerModule[],
+        section?: AddonStorageManagerCourseSection,
+    ): Promise<void> {
+        this.calculatingSize = true;
+
+        await Promise.all(modules.map(async (module) => {
+            if (module.calculatingSize) {
+                return;
+            }
+
+            module.calculatingSize = true;
+
+            if (!section) {
+                section = this.sections.find((section) => section.modules.some((mod) => mod.id === module.id));
+                if (section) {
+                    section.calculatingSize = true;
+                }
+            }
+
+            try {
+                const size = await CoreCourseModulePrefetchDelegate.getModuleStoredSize(module, this.courseId);
+
+                const diff = (isNaN(size) ? 0 : size) - (module.totalSize ?? 0);
+
+                module.totalSize = Number(size);
+                this.totalSize += diff;
+                if (section) {
+                    section.totalSize += diff;
+                }
+            } catch {
+                // Ignore errors, it shouldn't happen.
+            } finally {
+                module.calculatingSize = false;
+            }
+        }));
+
+        this.calculatingSize = false;
+        if (section) {
+            section.calculatingSize = false;
+        }
+    }
+
     /**
      * The user has requested a delete for the whole course data.
      *
@@ -406,7 +455,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
         } finally {
             modal.dismiss();
 
-            await this.loadSizes();
+            await this.updateModulesSizes(modules, section);
             CoreCourseHelper.calculateSectionsStatus(this.sections, this.courseId, false, false);
         }
     }
@@ -455,7 +504,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
                     CoreDomUtils.showErrorModalDefault(error, 'core.course.errordownloadingsection', true);
                 }
             } finally {
-                await this.loadSizes();
+                await this.updateModulesSizes(section.modules, section);
             }
         } catch (error) {
             // User cancelled or there was an error calculating the size.
@@ -501,7 +550,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
         } finally {
             module.spinner = false;
 
-            await this.loadSizes();
+            await this.updateModulesSizes([module]);
         }
     }
 
@@ -613,13 +662,13 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
 
 type AddonStorageManagerCourseSection = Omit<CoreCourseSectionWithStatus, 'modules'> & {
     totalSize: number;
-    sizeLoaded?: boolean;
+    calculatingSize: boolean;
     modules: AddonStorageManagerModule[];
 };
 
 type AddonStorageManagerModule = CoreCourseModuleData & {
     totalSize?: number;
-    sizeLoaded?: boolean;
+    calculatingSize: boolean;
     prefetchHandler?: CoreCourseModulePrefetchHandler;
     spinner?: boolean;
     downloadStatus?: string;

From 0298273fc4c62b88b8473790d64cbaf976d6d246 Mon Sep 17 00:00:00 2001
From: Dani Palou <dani@moodle.com>
Date: Tue, 29 Mar 2022 12:39:18 +0200
Subject: [PATCH 5/8] MOBILE-3833 course: Collapse sections in downloads page

---
 .../pages/course-storage/course-storage.html  | 84 ++++++++++---------
 .../pages/course-storage/course-storage.ts    | 30 ++++++-
 .../course-format/course-format.html          |  5 ++
 .../components/course-format/course-format.ts | 64 +++++++++-----
 .../components/course-index/course-index.html | 56 +++++++------
 .../course/pages/contents/contents.html       |  5 --
 .../course/pages/contents/contents.ts         |  8 --
 7 files changed, 152 insertions(+), 100 deletions(-)

diff --git a/src/addons/storagemanager/pages/course-storage/course-storage.html b/src/addons/storagemanager/pages/course-storage/course-storage.html
index 57b45f33c..2df7b3282 100644
--- a/src/addons/storagemanager/pages/course-storage/course-storage.html
+++ b/src/addons/storagemanager/pages/course-storage/course-storage.html
@@ -47,7 +47,13 @@
         <ng-container *ngFor="let section of sections">
             <ion-card class="section" *ngIf="section.modules.length > 0">
                 <ion-card-header>
-                    <ion-item class="ion-no-padding" lines="full">
+                    <ion-item class="ion-no-padding" [lines]="section.expanded ? 'full' : 'none'" button detail="false"
+                        (click)="toggleExpand($event, section)" [class.core-course-storage-section-expanded]="section.expanded"
+                        [attr.aria-label]="(section.expanded ? 'core.collapse' : 'core.expand') | translate"
+                        [attr.aria-expanded]="section.expanded" [attr.aria-controls]="'core-course-storage-section-' + section.id">
+                        <ion-icon name="fas-chevron-right" flip-rtl slot="start" class="expandable-status-icon"
+                            [class.expandable-status-icon-expanded]="section.expanded">
+                        </ion-icon>
                         <ion-label>
                             <p class="item-heading ion-text-wrap">
                                 <core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="section.course"
@@ -93,44 +99,46 @@
                         </div>
                     </ion-item>
                 </ion-card-header>
-                <ion-card-content>
-                    <ng-container *ngFor="let module of section.modules">
-                        <ion-item class="ion-no-padding core-course-storage-activity"
-                            *ngIf="downloadEnabled || (!module.calculatingSize && module.totalSize > 0)">
-                            <core-mod-icon slot="start" *ngIf="module.handlerData.icon" [modicon]="module.handlerData.icon"
-                                [modname]="module.modname" [componentId]="module.instance">
-                            </core-mod-icon>
-                            <ion-label class="ion-text-wrap">
-                                <h3 class="{{module.handlerData!.class}} addon-storagemanager-module-size">
-                                    <core-format-text [text]="module.handlerData.title" [courseId]="module.course" contextLevel="module"
-                                        [contextInstanceId]="module.id" [adaptImg]="false">
-                                    </core-format-text>
-                                </h3>
-                                <ion-badge [color]="module.downloadStatus == statusDownloaded ? 'success' : 'light'"
-                                    *ngIf="!module.calculatingSize && module.totalSize > 0">
-                                    <ion-icon name="fam-cloud-done" *ngIf="module.downloadStatus == statusDownloaded"
-                                        [attr.aria-label]="'core.downloaded' | translate">
-                                    </ion-icon>{{ module.totalSize | coreBytesToSize }}
-                                </ion-badge>
-                                <ion-badge color="light" *ngIf="module.calculatingSize">
-                                    {{ 'core.calculating' | translate }}
-                                </ion-badge>
-                            </ion-label>
+                <ion-card-content id="core-course-storage-section-{{section.id}}">
+                    <ng-container *ngIf="section.expanded">
+                        <ng-container *ngFor="let module of section.modules">
+                            <ion-item class="ion-no-padding core-course-storage-activity"
+                                *ngIf="downloadEnabled || (!module.calculatingSize && module.totalSize > 0)">
+                                <core-mod-icon slot="start" *ngIf="module.handlerData.icon" [modicon]="module.handlerData.icon"
+                                    [modname]="module.modname" [componentId]="module.instance">
+                                </core-mod-icon>
+                                <ion-label class="ion-text-wrap">
+                                    <h3 class="{{module.handlerData!.class}} addon-storagemanager-module-size">
+                                        <core-format-text [text]="module.handlerData.title" [courseId]="module.course" contextLevel="module"
+                                            [contextInstanceId]="module.id" [adaptImg]="false">
+                                        </core-format-text>
+                                    </h3>
+                                    <ion-badge [color]="module.downloadStatus == statusDownloaded ? 'success' : 'light'"
+                                        *ngIf="!module.calculatingSize && module.totalSize > 0">
+                                        <ion-icon name="fam-cloud-done" *ngIf="module.downloadStatus == statusDownloaded"
+                                            [attr.aria-label]="'core.downloaded' | translate">
+                                        </ion-icon>{{ module.totalSize | coreBytesToSize }}
+                                    </ion-badge>
+                                    <ion-badge color="light" *ngIf="module.calculatingSize">
+                                        {{ 'core.calculating' | translate }}
+                                    </ion-badge>
+                                </ion-label>
 
-                            <div class="storage-buttons" slot="end">
-                                <core-download-refresh *ngIf="downloadEnabled && module.handlerData?.showDownloadButton &&
-                                    module.downloadStatus != statusDownloaded" [status]="module.downloadStatus" [enabled]="true"
-                                    [canTrustDownload]="true" [loading]="module.spinner || module.handlerData.spinner"
-                                    (action)="prefetchModule(module, section)">
-                                </core-download-refresh>
-                                <ion-button fill="clear" (click)="deleteForModule(module, section)"
-                                    *ngIf="!module.calculatingSize && module.totalSize > 0" color="danger">
-                                    <ion-icon name="fas-trash" slot="icon-only"
-                                        [attr.aria-label]="'addon.storagemanager.deletedatafrom' | translate: { name: module.name }">
-                                    </ion-icon>
-                                </ion-button>
-                            </div>
-                        </ion-item>
+                                <div class="storage-buttons" slot="end">
+                                    <core-download-refresh *ngIf="downloadEnabled && module.handlerData?.showDownloadButton &&
+                                        module.downloadStatus != statusDownloaded" [status]="module.downloadStatus" [enabled]="true"
+                                        [canTrustDownload]="true" [loading]="module.spinner || module.handlerData.spinner"
+                                        (action)="prefetchModule(module, section)">
+                                    </core-download-refresh>
+                                    <ion-button fill="clear" (click)="deleteForModule(module, section)"
+                                        *ngIf="!module.calculatingSize && module.totalSize > 0" color="danger">
+                                        <ion-icon name="fas-trash" slot="icon-only"
+                                            [attr.aria-label]="'addon.storagemanager.deletedatafrom' | translate: { name: module.name }">
+                                        </ion-icon>
+                                    </ion-button>
+                                </div>
+                            </ion-item>
+                        </ng-container>
                     </ng-container>
                 </ion-card-content>
             </ion-card>
diff --git a/src/addons/storagemanager/pages/course-storage/course-storage.ts b/src/addons/storagemanager/pages/course-storage/course-storage.ts
index 39c3d42d0..6aa817913 100644
--- a/src/addons/storagemanager/pages/course-storage/course-storage.ts
+++ b/src/addons/storagemanager/pages/course-storage/course-storage.ts
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 import { CoreConstants } from '@/core/constants';
-import { Component, OnDestroy, OnInit } from '@angular/core';
+import { Component, ElementRef, OnDestroy, OnInit } from '@angular/core';
 import { CoreCourse, CoreCourseProvider } from '@features/course/services/course';
 import {
     CoreCourseHelper,
@@ -30,6 +30,7 @@ import { CoreSites } from '@services/sites';
 import { CoreDomUtils } from '@services/utils/dom';
 import { CoreUtils } from '@services/utils/utils';
 import { Translate } from '@singletons';
+import { CoreDom } from '@singletons/dom';
 import { CoreEventObserver, CoreEvents } from '@singletons/events';
 
 /**
@@ -62,6 +63,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
 
     statusDownloaded = CoreConstants.DOWNLOADED;
 
+    protected initialSectionId?: number;
     protected siteUpdatedObserver?: CoreEventObserver;
     protected courseStatusObserver?: CoreEventObserver;
     protected sectionStatusObserver?: CoreEventObserver;
@@ -69,7 +71,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
     protected isDestroyed = false;
     protected isGuest = false;
 
-    constructor() {
+    constructor(protected elementRef: ElementRef) {
         // Refresh the enabled flags if site is updated.
         this.siteUpdatedObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
             this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
@@ -100,16 +102,19 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
         }
 
         this.isGuest = !!CoreNavigator.getRouteBooleanParam('isGuest');
+        this.initialSectionId = CoreNavigator.getRouteNumberParam('sectionId');
 
         this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
         this.downloadEnabled = !CoreSites.getRequiredCurrentSite().isOfflineDisabled();
 
-        const sections = await CoreCourse.getSections(this.courseId, false, true);
+        const sections = (await CoreCourse.getSections(this.courseId, false, true))
+            .filter((section) => !CoreCourseHelper.isSectionStealth(section));
         this.sections = (await CoreCourseHelper.addHandlerDataForModules(sections, this.courseId)).sections
             .map(section => ({
                 ...section,
                 totalSize: 0,
                 calculatingSize: true,
+                expanded: section.id === this.initialSectionId,
                 modules: section.modules.map(module => ({
                     ...module,
                     calculatingSize: true,
@@ -118,6 +123,12 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
 
         this.loaded = true;
 
+        CoreDom.scrollToElement(
+            this.elementRef.nativeElement,
+            '.core-course-storage-section-expanded',
+            { addYAxis: -10 },
+        );
+
         await Promise.all([
             this.initSizes(),
             this.initCoursePrefetch(),
@@ -641,6 +652,18 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
         }
     }
 
+    /**
+     * Toggle expand status.
+     *
+     * @param event Event object.
+     * @param section Section to expand / collapse.
+     */
+    toggleExpand(event: Event, section: AddonStorageManagerCourseSection): void {
+        section.expanded = !section.expanded;
+        event.stopPropagation();
+        event.preventDefault();
+    }
+
     /**
      * @inheritdoc
      */
@@ -663,6 +686,7 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
 type AddonStorageManagerCourseSection = Omit<CoreCourseSectionWithStatus, 'modules'> & {
     totalSize: number;
     calculatingSize: boolean;
+    expanded: boolean;
     modules: AddonStorageManagerModule[];
 };
 
diff --git a/src/core/features/course/components/course-format/course-format.html b/src/core/features/course/components/course-format/course-format.html
index 2d8cd3391..4031d1654 100644
--- a/src/core/features/course/components/course-format/course-format.html
+++ b/src/core/features/course/components/course-format/course-format.html
@@ -1,3 +1,8 @@
+<core-navbar-buttons slot="end" prepend>
+    <ion-button fill="clear" (click)="gotoCourseDownloads()" [attr.aria-label]="'addon.storagemanager.coursedownloads' | translate">
+        <ion-icon name="fas-cloud-download-alt" slot="icon-only" aria-hidden="true"></ion-icon>
+    </ion-button>
+</core-navbar-buttons>
 <core-dynamic-component [component]="courseFormatComponent" [data]="data">
     <!-- Default course format. -->
     <core-loading [hideUntil]="loaded">
diff --git a/src/core/features/course/components/course-format/course-format.ts b/src/core/features/course/components/course-format/course-format.ts
index dca3d5443..c1bee68f6 100644
--- a/src/core/features/course/components/course-format/course-format.ts
+++ b/src/core/features/course/components/course-format/course-format.ts
@@ -391,29 +391,38 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
         );
     }
 
+    /**
+     * Get selected section ID. If viewing all sections, use current scrolled section.
+     *
+     * @return Section ID, undefined if not found.
+     */
+    protected async getSelectedSectionId(): Promise<number | undefined> {
+        if (this.selectedSection?.id !== this.allSectionsId) {
+            return this.selectedSection?.id;
+        }
+
+        // Check current scrolled section.
+        const allSectionElements: NodeListOf<HTMLElement> =
+            this.elementRef.nativeElement.querySelectorAll('section.core-course-module-list-wrapper');
+
+        const scroll = await this.content.getScrollElement();
+        const containerTop = scroll.getBoundingClientRect().top;
+
+        const element = Array.from(allSectionElements).find((element) => {
+            const position = element.getBoundingClientRect();
+
+            // The bottom is inside the container or lower.
+            return position.bottom >= containerTop;
+        });
+
+        return Number(element?.getAttribute('id')) || undefined;
+    }
+
     /**
      * Display the course index modal.
      */
     async openCourseIndex(): Promise<void> {
-        let selectedId = this.selectedSection?.id;
-
-        if (selectedId == this.allSectionsId) {
-            // Check current scrolled section.
-            const allSectionElements: NodeListOf<HTMLElement> =
-                this.elementRef.nativeElement.querySelectorAll('section.section-wrapper');
-
-            const scroll = await this.content.getScrollElement();
-            const containerTop = scroll.getBoundingClientRect().top;
-
-            const element = Array.from(allSectionElements).find((element) => {
-                const position = element.getBoundingClientRect();
-
-                // The bottom is inside the container or lower.
-                return position.bottom >= containerTop;
-            });
-
-            selectedId = Number(element?.getAttribute('id')) || undefined;
-        }
+        const selectedId = await this.getSelectedSectionId();
 
         const data = await CoreDomUtils.openModal<CoreCourseIndexSectionWithModule>({
             component: CoreCourseCourseIndexComponent,
@@ -453,6 +462,23 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
         this.moduleId = data.moduleId;
     }
 
+    /**
+     * Open course downloads page.
+     */
+    async gotoCourseDownloads(): Promise<void> {
+        const selectedId = await this.getSelectedSectionId();
+
+        CoreNavigator.navigateToSitePath(
+            `storage/${this.course.id}`,
+            {
+                params: {
+                    title: this.course.fullname,
+                    sectionId: selectedId,
+                },
+            },
+        );
+    }
+
     /**
      * Function called when selected section changes.
      *
diff --git a/src/core/features/course/components/course-index/course-index.html b/src/core/features/course/components/course-index/course-index.html
index 892a63767..8de92a89b 100644
--- a/src/core/features/course/components/course-index/course-index.html
+++ b/src/core/features/course/components/course-index/course-index.html
@@ -48,34 +48,36 @@
                     <ion-icon name="fas-eye-slash" *ngIf="!section.visible && section.uservisible" slot="end" class="restricted"
                         [attr.aria-label]="'core.course.hiddenfromstudents' | translate"></ion-icon>
                 </ion-item>
-                <ng-container *ngIf="section.expanded">
-                    <ng-container *ngFor="let module of section.modules">
-                        <ion-item class="module" [class.item-dimmed]="!module.visible" [class.item-hightlighted]="section.highlighted"
-                            (click)="selectSectionOrModule($event, section.id, module.id)" button>
-                            <ion-icon class="completioninfo completion_none" name="" *ngIf="module.completionStatus === undefined"
-                                slot="start" aria-hidden="true"></ion-icon>
-                            <ion-icon class="completioninfo completion_incomplete" name="far-circle" *ngIf="module.completionStatus === 0"
-                                slot="start" [attr.aria-label]="'core.course.todo' | translate">
-                            </ion-icon>
-                            <ion-icon class="completioninfo completion_complete" name="fas-circle"
-                                *ngIf="module.completionStatus === 1 || module.completionStatus === 2" color="success" slot="start"
-                                [attr.aria-label]="'core.course.done' | translate">
-                            </ion-icon>
-                            <ion-icon class="completioninfo completion_fail" name="fas-circle" *ngIf="module.completionStatus === 3"
-                                color="danger" slot="start" [attr.aria-label]="'core.course.failed' | translate">
-                            </ion-icon>
-                            <ion-label>
-                                <p class="item-heading">
-                                    <core-format-text [text]="module.name" contextLevel="module" [contextInstanceId]="module.id"
-                                        [courseId]="module.course">
-                                    </core-format-text>
-                                </p>
-                            </ion-label>
-                            <ion-icon name="fas-lock" *ngIf="!module.uservisible" slot="end" class="restricted"
-                                [attr.aria-label]="'core.restricted' | translate"></ion-icon>
-                        </ion-item>
+                <div id="core-course-index-section-{{section.id}}">
+                    <ng-container *ngIf="section.expanded">
+                        <ng-container *ngFor="let module of section.modules">
+                            <ion-item class="module" [class.item-dimmed]="!module.visible" [class.item-hightlighted]="section.highlighted"
+                                (click)="selectSectionOrModule($event, section.id, module.id)" button>
+                                <ion-icon class="completioninfo completion_none" name="" *ngIf="module.completionStatus === undefined"
+                                    slot="start" aria-hidden="true"></ion-icon>
+                                <ion-icon class="completioninfo completion_incomplete" name="far-circle"
+                                    *ngIf="module.completionStatus === 0" slot="start" [attr.aria-label]="'core.course.todo' | translate">
+                                </ion-icon>
+                                <ion-icon class="completioninfo completion_complete" name="fas-circle"
+                                    *ngIf="module.completionStatus === 1 || module.completionStatus === 2" color="success" slot="start"
+                                    [attr.aria-label]="'core.course.done' | translate">
+                                </ion-icon>
+                                <ion-icon class="completioninfo completion_fail" name="fas-circle" *ngIf="module.completionStatus === 3"
+                                    color="danger" slot="start" [attr.aria-label]="'core.course.failed' | translate">
+                                </ion-icon>
+                                <ion-label>
+                                    <p class="item-heading">
+                                        <core-format-text [text]="module.name" contextLevel="module" [contextInstanceId]="module.id"
+                                            [courseId]="module.course">
+                                        </core-format-text>
+                                    </p>
+                                </ion-label>
+                                <ion-icon name="fas-lock" *ngIf="!module.uservisible" slot="end" class="restricted"
+                                    [attr.aria-label]="'core.restricted' | translate"></ion-icon>
+                            </ion-item>
+                        </ng-container>
                     </ng-container>
-                </ng-container>
+                </div>
             </ng-container>
         </ng-container>
     </ion-list>
diff --git a/src/core/features/course/pages/contents/contents.html b/src/core/features/course/pages/contents/contents.html
index bc3f404f8..2d0eeb1db 100644
--- a/src/core/features/course/pages/contents/contents.html
+++ b/src/core/features/course/pages/contents/contents.html
@@ -1,8 +1,3 @@
-<core-navbar-buttons slot="end" prepend>
-    <ion-button fill="clear" (click)="gotoCourseDownloads()" [attr.aria-label]="'addon.storagemanager.coursedownloads' | translate">
-        <ion-icon name="fas-cloud-download-alt" slot="icon-only" aria-hidden="true"></ion-icon>
-    </ion-button>
-</core-navbar-buttons>
 <ion-content>
     <ion-refresher slot="fixed" [disabled]="!dataLoaded || !displayRefresher" (ionRefresh)="doRefresh($event.target)">
         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
diff --git a/src/core/features/course/pages/contents/contents.ts b/src/core/features/course/pages/contents/contents.ts
index 81953a209..7413a0444 100644
--- a/src/core/features/course/pages/contents/contents.ts
+++ b/src/core/features/course/pages/contents/contents.ts
@@ -366,14 +366,6 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
         }
     }
 
-    gotoCourseDownloads(): void {
-        CoreNavigator.navigateToSitePath(
-            `storage/${this.course.id}`,
-            { params: { title: this.course.fullname } },
-        );
-
-    }
-
     /**
      * @inheritdoc
      */

From ca25ad0420e8bd1281f7ac9b9d727f1b8369578b Mon Sep 17 00:00:00 2001
From: Dani Palou <dani@moodle.com>
Date: Tue, 29 Mar 2022 13:22:52 +0200
Subject: [PATCH 6/8] MOBILE-3833 config: Remove unused globalization plugin

---
 config.xml        |  5 -----
 package-lock.json | 18 ------------------
 package.json      |  4 +---
 3 files changed, 1 insertion(+), 26 deletions(-)

diff --git a/config.xml b/config.xml
index bfcacc07b..da7166bf5 100644
--- a/config.xml
+++ b/config.xml
@@ -130,11 +130,6 @@
                 <param name="android-package" value="org.apache.cordova.geolocation.Geolocation" />
             </feature>
         </config-file>
-        <config-file parent="/*" target="res/xml/config.xml">
-            <feature name="Globalization">
-                <param name="android-package" value="org.apache.cordova.globalization.Globalization" />
-            </feature>
-        </config-file>
         <config-file parent="/*" target="res/xml/config.xml">
             <feature name="InAppBrowser">
                 <param name="android-package" value="org.apache.cordova.inappbrowser.InAppBrowser" />
diff --git a/package-lock.json b/package-lock.json
index 40f640b46..a73545b60 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -71,7 +71,6 @@
         "cordova-plugin-file": "6.0.2",
         "cordova-plugin-file-opener2": "3.0.5",
         "cordova-plugin-geolocation": "4.1.0",
-        "cordova-plugin-globalization": "1.11.0",
         "cordova-plugin-ionic-keyboard": "2.2.0",
         "cordova-plugin-media": "5.0.4",
         "cordova-plugin-media-capture": "3.0.3",
@@ -11389,18 +11388,6 @@
         }
       }
     },
-    "node_modules/cordova-plugin-globalization": {
-      "version": "1.11.0",
-      "resolved": "https://registry.npmjs.org/cordova-plugin-globalization/-/cordova-plugin-globalization-1.11.0.tgz",
-      "integrity": "sha1-6sMVgQAphJOvowvolA5pj2HvvP4=",
-      "engines": {
-        "cordovaDependencies": {
-          "2.0.0": {
-            "cordova": ">100"
-          }
-        }
-      }
-    },
     "node_modules/cordova-plugin-ionic-keyboard": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/cordova-plugin-ionic-keyboard/-/cordova-plugin-ionic-keyboard-2.2.0.tgz",
@@ -40148,11 +40135,6 @@
       "resolved": "https://registry.npmjs.org/cordova-plugin-geolocation/-/cordova-plugin-geolocation-4.1.0.tgz",
       "integrity": "sha512-y5io/P10xGMxSn2KEqfv/fExK47eA1pmSonJdmDqDsaSADV9JpgdPx0mUSA08+5pzma/OS9R0LoODeDPx7Jvjg=="
     },
-    "cordova-plugin-globalization": {
-      "version": "1.11.0",
-      "resolved": "https://registry.npmjs.org/cordova-plugin-globalization/-/cordova-plugin-globalization-1.11.0.tgz",
-      "integrity": "sha1-6sMVgQAphJOvowvolA5pj2HvvP4="
-    },
     "cordova-plugin-ionic-keyboard": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/cordova-plugin-ionic-keyboard/-/cordova-plugin-ionic-keyboard-2.2.0.tgz",
diff --git a/package.json b/package.json
index 295ce081c..767ed5d13 100644
--- a/package.json
+++ b/package.json
@@ -100,7 +100,6 @@
     "cordova-plugin-file": "6.0.2",
     "cordova-plugin-file-opener2": "3.0.5",
     "cordova-plugin-geolocation": "4.1.0",
-    "cordova-plugin-globalization": "1.11.0",
     "cordova-plugin-ionic-keyboard": "2.2.0",
     "cordova-plugin-media": "5.0.4",
     "cordova-plugin-media-capture": "3.0.3",
@@ -236,7 +235,6 @@
         "ANDROIDX_VERSION": "1.0.0",
         "ANDROIDX_APPCOMPAT_VERSION": "1.3.1"
       },
-      "cordova-plugin-globalization": {},
       "@moodlehq/cordova-plugin-file-transfer": {},
       "cordova-plugin-prevent-override": {},
       "cordova-plugin-androidx-adapter": {}
@@ -245,4 +243,4 @@
   "optionalDependencies": {
     "keytar": "7.2.0"
   }
-}
+}
\ No newline at end of file

From 6dadd7b9a64697ad7e63841a520164facd477317 Mon Sep 17 00:00:00 2001
From: Dani Palou <dani@moodle.com>
Date: Wed, 30 Mar 2022 09:38:33 +0200
Subject: [PATCH 7/8] MOBILE-3833 core: Fix handle links with URL scheme

---
 .../features/contentlinks/services/contentlinks-helper.ts  | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/src/core/features/contentlinks/services/contentlinks-helper.ts b/src/core/features/contentlinks/services/contentlinks-helper.ts
index 8c5ccd06c..e4d6379dd 100644
--- a/src/core/features/contentlinks/services/contentlinks-helper.ts
+++ b/src/core/features/contentlinks/services/contentlinks-helper.ts
@@ -21,6 +21,7 @@ import { makeSingleton, Translate } from '@singletons';
 import { CoreNavigator } from '@services/navigator';
 import { Params } from '@angular/router';
 import { CoreContentLinksChooseSiteModalComponent } from '../components/choose-site-modal/choose-site-modal';
+import { CoreCustomURLSchemes } from '@services/urlschemes';
 
 /**
  * Service that provides some features regarding content links.
@@ -138,6 +139,12 @@ export class CoreContentLinksHelperProvider {
         openBrowserRoot?: boolean,
     ): Promise<boolean> {
         try {
+            if (CoreCustomURLSchemes.isCustomURL(url)) {
+                await CoreCustomURLSchemes.handleCustomURL(url);
+
+                return true;
+            }
+
             if (checkRoot) {
                 const data = await CoreSites.isStoredRootURL(url, username);
 

From 9884ceb6b34164c770ba5d3e59332b5cfda8522f Mon Sep 17 00:00:00 2001
From: Dani Palou <dani@moodle.com>
Date: Wed, 30 Mar 2022 09:38:55 +0200
Subject: [PATCH 8/8] MOBILE-3833 ios: Fix handle iframe links in iOS

---
 src/core/initializers/inject-ios-scripts.ts |  3 ++-
 src/core/services/utils/iframe.ts           | 19 +++----------------
 2 files changed, 5 insertions(+), 17 deletions(-)

diff --git a/src/core/initializers/inject-ios-scripts.ts b/src/core/initializers/inject-ios-scripts.ts
index 64b26993d..cfd717666 100644
--- a/src/core/initializers/inject-ios-scripts.ts
+++ b/src/core/initializers/inject-ios-scripts.ts
@@ -17,10 +17,11 @@ import { CoreIframeUtils } from '@services/utils/iframe';
 import { Platform } from '@singletons';
 
 export default async function(): Promise<void> {
+    await Platform.ready();
+
     if (!CoreApp.isIOS() || !('WKUserScript' in window)) {
         return;
     }
 
-    await Platform.ready();
     CoreIframeUtils.injectiOSScripts(window);
 }
diff --git a/src/core/services/utils/iframe.ts b/src/core/services/utils/iframe.ts
index 1faf9308e..08745cb44 100644
--- a/src/core/services/utils/iframe.ts
+++ b/src/core/services/utils/iframe.ts
@@ -446,26 +446,13 @@ export class CoreIframeUtilsProvider {
             } else {
                 element.setAttribute('src', url);
             }
-        } else if (CoreUrlUtils.isLocalFileUrl(url)) {
-            // It's a local file.
-            const filename = url.substring(url.lastIndexOf('/') + 1);
-
-            if (!CoreFileHelper.isOpenableInApp({ filename })) {
-                try {
-                    await CoreFileHelper.showConfirmOpenUnsupportedFile();
-                } catch (error) {
-                    return; // Cancelled, stop.
-                }
-            }
-
+        } else {
             try {
-                await CoreUtils.openFile(url);
+                // It's an external link or a local file, check if it can be opened in the app.
+                await CoreWindow.open(url, name);
             } catch (error) {
                 CoreDomUtils.showErrorModal(error);
             }
-        } else {
-            // It's an external link, check if it can be opened in the app.
-            await CoreWindow.open(url, name);
         }
     }