From 8cd89ead85db55fd7c4fe5d472bee15ba15ecbb0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= <crazyserver@gmail.com>
Date: Fri, 12 Nov 2021 16:32:54 +0100
Subject: [PATCH 1/3] MOBILE-3914 course: Add side blocks component and replace
 it

---
 scripts/langindex.json                        |   2 +
 src/core/components/empty-box/empty-box.scss  |   4 -
 .../block/components/components.module.ts     |   9 +-
 .../core-block-course-blocks.html             |  15 --
 .../course-blocks/course-blocks.scss          |  61 ------
 .../side-blocks-button.html                   |   3 +
 .../side-blocks-button.scss                   |  29 +++
 .../side-blocks-button/side-blocks-button.ts  |  45 ++++
 .../components/side-blocks/side-blocks.html   |  26 +++
 .../side-blocks.ts}                           |  62 ++----
 src/core/features/block/lang.json             |   6 +-
 .../features/block/services/block-helper.ts   |  16 ++
 .../components/format/core-course-format.html | 203 +++++++++---------
 .../course/components/format/format.ts        |  23 +-
 .../features/sitehome/pages/index/index.html  |  95 ++++----
 .../features/sitehome/pages/index/index.ts    |  35 ++-
 src/theme/theme.light.scss                    |   7 -
 17 files changed, 328 insertions(+), 313 deletions(-)
 delete mode 100644 src/core/features/block/components/course-blocks/core-block-course-blocks.html
 delete mode 100644 src/core/features/block/components/course-blocks/course-blocks.scss
 create mode 100644 src/core/features/block/components/side-blocks-button/side-blocks-button.html
 create mode 100644 src/core/features/block/components/side-blocks-button/side-blocks-button.scss
 create mode 100644 src/core/features/block/components/side-blocks-button/side-blocks-button.ts
 create mode 100644 src/core/features/block/components/side-blocks/side-blocks.html
 rename src/core/features/block/components/{course-blocks/course-blocks.ts => side-blocks/side-blocks.ts} (61%)

diff --git a/scripts/langindex.json b/scripts/langindex.json
index 1df9a198e..94f7fe43e 100644
--- a/scripts/langindex.json
+++ b/scripts/langindex.json
@@ -1399,6 +1399,8 @@
   "core.areyousure": "moodle",
   "core.back": "moodle",
   "core.block.blocks": "moodle",
+  "core.block.noblocks": "error",
+  "core.block.opendrawerblocks": "moodle",
   "core.browser": "local_moodlemobileapp",
   "core.cancel": "moodle",
   "core.cannotconnect": "local_moodlemobileapp",
diff --git a/src/core/components/empty-box/empty-box.scss b/src/core/components/empty-box/empty-box.scss
index 99ffdc978..b0b3c2254 100644
--- a/src/core/components/empty-box/empty-box.scss
+++ b/src/core/components/empty-box/empty-box.scss
@@ -72,7 +72,3 @@
         height: auto;
     }
 }
-
-:host-context(core-block-course-blocks) .core-empty-box {
-    position: relative;
-}
diff --git a/src/core/features/block/components/components.module.ts b/src/core/features/block/components/components.module.ts
index 7d64e292d..ab8bf883c 100644
--- a/src/core/features/block/components/components.module.ts
+++ b/src/core/features/block/components/components.module.ts
@@ -16,15 +16,17 @@ import { NgModule } from '@angular/core';
 import { CoreBlockComponent } from './block/block';
 import { CoreBlockOnlyTitleComponent } from './only-title-block/only-title-block';
 import { CoreBlockPreRenderedComponent } from './pre-rendered-block/pre-rendered-block';
-import { CoreBlockCourseBlocksComponent } from './course-blocks/course-blocks';
 import { CoreSharedModule } from '@/core/shared.module';
+import { CoreBlockSideBlocksComponent } from './side-blocks/side-blocks';
+import { CoreBlockSideBlocksButtonComponent } from './side-blocks-button/side-blocks-button';
 
 @NgModule({
     declarations: [
         CoreBlockComponent,
         CoreBlockOnlyTitleComponent,
         CoreBlockPreRenderedComponent,
-        CoreBlockCourseBlocksComponent,
+        CoreBlockSideBlocksComponent,
+        CoreBlockSideBlocksButtonComponent,
     ],
     imports: [
         CoreSharedModule,
@@ -33,7 +35,8 @@ import { CoreSharedModule } from '@/core/shared.module';
         CoreBlockComponent,
         CoreBlockOnlyTitleComponent,
         CoreBlockPreRenderedComponent,
-        CoreBlockCourseBlocksComponent,
+        CoreBlockSideBlocksComponent,
+        CoreBlockSideBlocksButtonComponent,
     ],
 })
 export class CoreBlockComponentsModule {}
diff --git a/src/core/features/block/components/course-blocks/core-block-course-blocks.html b/src/core/features/block/components/course-blocks/core-block-course-blocks.html
deleted file mode 100644
index 120e60749..000000000
--- a/src/core/features/block/components/course-blocks/core-block-course-blocks.html
+++ /dev/null
@@ -1,15 +0,0 @@
-<div class="core-course-blocks-content">
-    <ng-content></ng-content>
-</div>
-
-<div *ngIf="blocks && blocks.length > 0 && !hideBlocks" [class.core-hide-blocks]="hideBottomBlocks" class="core-course-blocks-side">
-    <core-loading [hideUntil]="dataLoaded" [fullscreen]="false">
-        <ion-list>
-            <!-- Course expand="block"s. -->
-            <ng-container *ngFor="let block of blocks">
-                <core-block *ngIf="block.visible" [block]="block" contextLevel="course" [instanceId]="courseId"
-                    [extraData]="{'downloadEnabled': downloadEnabled}"></core-block>
-            </ng-container>
-        </ion-list>
-    </core-loading>
-</div>
diff --git a/src/core/features/block/components/course-blocks/course-blocks.scss b/src/core/features/block/components/course-blocks/course-blocks.scss
deleted file mode 100644
index c7ba5b65e..000000000
--- a/src/core/features/block/components/course-blocks/course-blocks.scss
+++ /dev/null
@@ -1,61 +0,0 @@
-:host {
-    --side-blocks-box-shadow: var(--core-menu-box-shadow-start);
-
-    &.core-no-blocks .core-course-blocks-content {
-        height: auto;
-    }
-
-    &.core-has-blocks {
-        @media (min-width: 768px) {
-            display: flex;
-
-            flex-direction: row;
-            flex-wrap: nowrap;
-
-            .core-course-blocks-content {
-                box-shadow: none !important;
-                flex-grow: 1;
-                max-width: 100%;
-
-                --ion-safe-area-right: 0px;
-            }
-
-            div.core-course-blocks-side {
-                max-width: var(--side-blocks-max-width);
-                min-width: var(--side-blocks-min-width);
-                box-shadow: var(--side-blocks-box-shadow);
-                z-index: 2;
-            }
-
-            .core-course-blocks-content,
-            div.core-course-blocks-side {
-                position: relative;
-                height: 100%;
-
-                .core-loading-center,
-                core-loading.core-loading-loaded {
-                    position: initial;
-                }
-            }
-        }
-
-        @media (max-width: 767.98px) {
-            // Disable scroll on individual columns.
-            div.core-course-blocks-side {
-                height: auto;
-
-                &.core-hide-blocks {
-                    display: none;
-                }
-            }
-        }
-    }
-}
-
-:host-context([dir="rtl"]).core-has-blocks {
-    @media (min-width: 768px) {
-        div.core-course-blocks-side {
-            box-shadow: var(--side-blocks-box-shadow);
-        }
-    }
-}
diff --git a/src/core/features/block/components/side-blocks-button/side-blocks-button.html b/src/core/features/block/components/side-blocks-button/side-blocks-button.html
new file mode 100644
index 000000000..b4d634937
--- /dev/null
+++ b/src/core/features/block/components/side-blocks-button/side-blocks-button.html
@@ -0,0 +1,3 @@
+<ion-button (click)="openBlocks()" [attr.aria-label]="'core.block.opendrawerblocks' | translate">
+    <ion-icon name="fas-cubes" slot="icon-only" aria-hidden="true"></ion-icon>
+</ion-button>
diff --git a/src/core/features/block/components/side-blocks-button/side-blocks-button.scss b/src/core/features/block/components/side-blocks-button/side-blocks-button.scss
new file mode 100644
index 000000000..3e9277bfc
--- /dev/null
+++ b/src/core/features/block/components/side-blocks-button/side-blocks-button.scss
@@ -0,0 +1,29 @@
+@import "~theme/globals";
+
+:host {
+    @include position(50%, 0px, null, null);
+    position: fixed;
+    z-index: 10;
+
+    ion-button {
+        margin: 0;
+        --padding-start: 0.5em;
+        --padding-end: 0;
+        --border-radius: 2em 0 0 2em;
+
+        &::part(native) {
+            @include core-transition(padding, 200ms);
+        }
+        
+        &:hover {
+            --padding-end: 1.2em;
+            --padding-start: 1em;
+        }
+    }
+}
+
+:host-context([dir=rtl]) {
+    ion-button {
+        --border-radius: 0 2em 2em 0;
+    }
+}
diff --git a/src/core/features/block/components/side-blocks-button/side-blocks-button.ts b/src/core/features/block/components/side-blocks-button/side-blocks-button.ts
new file mode 100644
index 000000000..71f3e4898
--- /dev/null
+++ b/src/core/features/block/components/side-blocks-button/side-blocks-button.ts
@@ -0,0 +1,45 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Component, Input } from '@angular/core';
+import { CoreDomUtils } from '@services/utils/dom';
+import { CoreBlockSideBlocksComponent } from '../side-blocks/side-blocks';
+
+/**
+ * Component that displays a button to open blocks.
+ */
+@Component({
+    selector: 'core-block-side-blocks-button',
+    templateUrl: 'side-blocks-button.html',
+    styleUrls: ['side-blocks-button.scss'],
+})
+export class CoreBlockSideBlocksButtonComponent {
+
+    @Input() courseId!: number;
+    @Input() downloadEnabled = false;
+
+    /**
+     * Open side blocks.
+     */
+    openBlocks(): void {
+        CoreDomUtils.openSideModal({
+            component: CoreBlockSideBlocksComponent,
+            componentProps: {
+                courseId: this.courseId,
+                downloadEnabled: this.downloadEnabled,
+            },
+        });
+    }
+
+}
diff --git a/src/core/features/block/components/side-blocks/side-blocks.html b/src/core/features/block/components/side-blocks/side-blocks.html
new file mode 100644
index 000000000..627139e3c
--- /dev/null
+++ b/src/core/features/block/components/side-blocks/side-blocks.html
@@ -0,0 +1,26 @@
+<ion-header>
+    <ion-toolbar>
+        <h1>{{ 'core.block.blocks' | translate }}</h1>
+        <ion-buttons slot="end">
+            <ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
+                <ion-icon name="fas-times" slot="icon-only" aria-hidden=true></ion-icon>
+            </ion-button>
+        </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">
+        <ion-list *ngIf="blocks.length > 0">
+            <ng-container *ngFor="let block of blocks">
+                <core-block *ngIf="block.visible" [block]="block" contextLevel="course" [instanceId]="courseId"
+                    [extraData]="{'downloadEnabled': downloadEnabled}"></core-block>
+            </ng-container>
+        </ion-list>
+
+        <core-empty-box *ngIf="blocks.length == 0" icon="fas-cubes" [message]="'core.block.noblocks' | translate">
+        </core-empty-box>
+    </core-loading>
+</ion-content>
diff --git a/src/core/features/block/components/course-blocks/course-blocks.ts b/src/core/features/block/components/side-blocks/side-blocks.ts
similarity index 61%
rename from src/core/features/block/components/course-blocks/course-blocks.ts
rename to src/core/features/block/components/side-blocks/side-blocks.ts
index ad8b01d5f..18dc3c9ca 100644
--- a/src/core/features/block/components/course-blocks/course-blocks.ts
+++ b/src/core/features/block/components/side-blocks/side-blocks.ts
@@ -12,50 +12,38 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { Component, ViewChildren, Input, OnInit, QueryList, ElementRef } from '@angular/core';
-import { IonContent } from '@ionic/angular';
+import { Component, ViewChildren, Input, OnInit, QueryList } from '@angular/core';
+import { ModalController } from '@singletons';
 import { CoreDomUtils } from '@services/utils/dom';
 import { CoreCourse, CoreCourseBlock } from '@features/course/services/course';
 import { CoreBlockHelper } from '../../services/block-helper';
 import { CoreBlockComponent } from '../block/block';
 import { CoreUtils } from '@services/utils/utils';
+import { IonRefresher } from '@ionic/angular';
 
 /**
- * Component that displays the list of course blocks.
+ * Component that displays the list of side blocks.
  */
 @Component({
-    selector: 'core-block-course-blocks',
-    templateUrl: 'core-block-course-blocks.html',
-    styleUrls: ['course-blocks.scss'],
+    selector: 'core-block-side-blocks',
+    templateUrl: 'side-blocks.html',
 })
-export class CoreBlockCourseBlocksComponent implements OnInit {
+export class CoreBlockSideBlocksComponent implements OnInit {
 
     @Input() courseId!: number;
-    @Input() hideBlocks = false;
-    @Input() hideBottomBlocks = false;
     @Input() downloadEnabled = false;
 
     @ViewChildren(CoreBlockComponent) blocksComponents?: QueryList<CoreBlockComponent>;
 
-    dataLoaded = false;
+    loaded = false;
     blocks: CoreCourseBlock[] = [];
 
-    protected element: HTMLElement;
-
-    constructor(
-        element: ElementRef,
-        protected content: IonContent,
-    ) {
-        this.element = element.nativeElement;
-    }
-
     /**
-     * Component being initialized.
+     * @inheritdoc
      */
     async ngOnInit(): Promise<void> {
-        this.element.classList.add('core-no-blocks');
         this.loadContent().finally(() => {
-            this.dataLoaded = true;
+            this.loaded = true;
         });
     }
 
@@ -87,7 +75,6 @@ export class CoreBlockCourseBlocksComponent implements OnInit {
      * @return Promise resolved when done.
      */
     async loadContent(): Promise<void> {
-
         try {
             this.blocks = await CoreBlockHelper.getCourseBlocks(this.courseId);
         } catch (error) {
@@ -95,29 +82,26 @@ export class CoreBlockCourseBlocksComponent implements OnInit {
 
             this.blocks = [];
         }
-
-        const scrollElement = await this.content.getScrollElement();
-        if (!this.hideBlocks && this.blocks.length > 0) {
-            this.element.classList.add('core-has-blocks');
-            this.element.classList.remove('core-no-blocks');
-
-            scrollElement.classList.add('core-course-block-with-blocks');
-        } else {
-            this.element.classList.remove('core-has-blocks');
-            this.element.classList.add('core-no-blocks');
-            scrollElement.classList.remove('core-course-block-with-blocks');
-        }
     }
 
     /**
-     * Refresh data.
+     * Refresh the data.
      *
-     * @return Promise resolved when done.
+     * @param refresher Refresher.
      */
-    async doRefresh(): Promise<void> {
+    async doRefresh(refresher?: IonRefresher): Promise<void> {
         await CoreUtils.ignoreErrors(this.invalidateBlocks());
 
-        await this.loadContent();
+        await this.loadContent().finally(() => {
+            refresher?.complete();
+        });
+    }
+
+    /**
+     * Close modal.
+     */
+    closeModal(): void {
+        ModalController.dismiss();
     }
 
 }
diff --git a/src/core/features/block/lang.json b/src/core/features/block/lang.json
index 9b136b8ee..cc3f3c95a 100644
--- a/src/core/features/block/lang.json
+++ b/src/core/features/block/lang.json
@@ -1,3 +1,5 @@
 {
-    "blocks": "Blocks"
-}
\ No newline at end of file
+    "blocks": "Blocks",
+    "noblocks": "No blocks found!",
+    "opendrawerblocks": "Open block drawer"
+}
diff --git a/src/core/features/block/services/block-helper.ts b/src/core/features/block/services/block-helper.ts
index c946bd75b..00b33cd23 100644
--- a/src/core/features/block/services/block-helper.ts
+++ b/src/core/features/block/services/block-helper.ts
@@ -54,6 +54,22 @@ export class CoreBlockHelperProvider {
         return blocks;
     }
 
+    /**
+     * Returns if the course has any block.
+     *
+     * @param courseId Course ID.
+     * @return Wether course has blocks.
+     */
+    async hasCourseBlocks(courseId: number): Promise<boolean> {
+        try {
+            const blocks = await this.getCourseBlocks(courseId);
+
+            return blocks.length > 0;
+        } catch {
+            return false;
+        }
+    }
+
 }
 
 export const CoreBlockHelper = makeSingleton(CoreBlockHelperProvider);
diff --git a/src/core/features/course/components/format/core-course-format.html b/src/core/features/course/components/format/core-course-format.html
index def9a3009..e4b211c67 100644
--- a/src/core/features/course/components/format/core-course-format.html
+++ b/src/core/features/course/components/format/core-course-format.html
@@ -6,126 +6,115 @@
         </core-context-menu-item>
     </core-context-menu>
 </core-navbar-buttons>
+<core-dynamic-component [component]="courseFormatComponent" [data]="data">
+    <!-- Default course format. -->
+    <core-loading [hideUntil]="loaded">
+        <!-- Section selector. -->
+        <core-dynamic-component [component]="sectionSelectorComponent" [data]="data">
 
-<core-block-course-blocks *ngIf="loaded" [courseId]="course!.id" [hideBlocks]="!displayBlocks" [downloadEnabled]="downloadEnabled"
-    [hideBottomBlocks]="selectedSection && selectedSection.id == allSectionsId && canLoadMore">
+            <div *ngIf="displaySectionSelector && sections && hasSeveralSections"
+                class="ion-text-wrap ion-justify-content-between ion-align-items-center core-button-selector-row"
+                [class.core-section-download]="downloadEnabled">
+                <core-combobox [modalOptions]="sectionSelectorModalOptions" interface="modal" listboxId="core-course-section-button"
+                    icon="fas-folder" [label]="'core.course.section' | translate"
+                    [selection]="selectedSection ? selectedSection.name : 'core.course.sections' | translate"
+                    (onChange)="sectionChanged($event)">
+                    <span slot="text">
+                        <core-format-text *ngIf="selectedSection" [text]="selectedSection.name" contextLevel="course"
+                            [contextInstanceId]="course?.id" [clean]="true" [singleLine]="true">
+                        </core-format-text>
+                        <ng-container *ngIf="!selectedSection">{{ 'core.course.sections' | translate }}</ng-container>
+                    </span>
+                </core-combobox>
+                <!-- Section download. -->
+                <ng-container *ngTemplateOutlet="sectionDownloadTemplate; context: {section: selectedSection}"></ng-container>
+            </div>
+        </core-dynamic-component>
 
-    <core-dynamic-component [component]="courseFormatComponent" [data]="data">
-        <!-- Default course format. -->
-        <core-loading [hideUntil]="loaded">
-            <!-- Section selector. -->
-            <core-dynamic-component [component]="sectionSelectorComponent" [data]="data">
-
-                <div *ngIf="displaySectionSelector && sections && hasSeveralSections"
-                    class="ion-text-wrap ion-justify-content-between ion-align-items-center core-button-selector-row"
-                    [class.core-section-download]="downloadEnabled">
-                    <core-combobox
-                        [modalOptions]="sectionSelectorModalOptions"
-                        interface="modal"
-                        listboxId="core-course-section-button"
-                        icon="fas-folder"
-                        [label]="'core.course.section' | translate"
-                        [selection]="selectedSection ? selectedSection.name : 'core.course.sections' | translate"
-                        (onChange)="sectionChanged($event)"
-                    >
-                        <span slot="text">
-                            <core-format-text *ngIf="selectedSection" [text]="selectedSection.name" contextLevel="course"
-                                [contextInstanceId]="course?.id" [clean]="true" [singleLine]="true">
-                            </core-format-text>
-                            <ng-container *ngIf="!selectedSection">{{ 'core.course.sections' | translate }}</ng-container>
-                        </span>
-                    </core-combobox>
-                    <!-- Section download. -->
-                    <ng-container *ngTemplateOutlet="sectionDownloadTemplate; context: {section: selectedSection}"></ng-container>
+        <!-- Course summary. By default we only display the course progress. -->
+        <core-dynamic-component [component]="courseSummaryComponent" [data]="data">
+            <ion-list lines="none" class="core-format-progress-list" *ngIf="imageThumb || (selectedSection?.id == allSectionsId && progress !== undefined) ||
+                    (selectedSection && selectedSection.id != allSectionsId &&
+                    (selectedSection.availabilityinfo || selectedSection.visible === 0))">
+                <div *ngIf="imageThumb" class="core-course-thumb">
+                    <img [src]="imageThumb" core-external-content alt="" />
                 </div>
+                <ng-container *ngIf="selectedSection">
+                    <ion-item class="core-course-progress" *ngIf="selectedSection?.id == allSectionsId && progress !== undefined">
+                        <core-progress-bar [progress]="progress" a11yText="core.course.aria:sectionprogress">
+                        </core-progress-bar>
+                    </ion-item>
+                    <ion-item *ngIf="selectedSection && selectedSection.id != allSectionsId &&
+                        (selectedSection.availabilityinfo || selectedSection.visible === 0)">
+                        <ion-badge color="info" class="ion-text-wrap"
+                            *ngIf="selectedSection.visible === 0 && selectedSection.uservisible !== false">
+                            {{ 'core.course.hiddenfromstudents' | translate }}
+                        </ion-badge>
+                        <ion-badge color="info" class="ion-text-wrap"
+                            *ngIf="selectedSection.visible === 0 && selectedSection.uservisible === false">
+                            {{ 'core.notavailable' | translate }}
+                        </ion-badge>
+                        <ion-badge color="info" class="ion-text-wrap" *ngIf="selectedSection.availabilityinfo">
+                            <core-format-text [text]="selectedSection.availabilityinfo" contextLevel="course"
+                                [contextInstanceId]="course?.id">
+                            </core-format-text>
+                        </ion-badge>
+                    </ion-item>
+                </ng-container>
+            </ion-list>
+        </core-dynamic-component>
+
+        <!-- Single section. -->
+        <div *ngIf="selectedSection && selectedSection.id != allSectionsId">
+            <core-dynamic-component [component]="singleSectionComponent" [data]="data">
+                <ng-container *ngTemplateOutlet="sectionTemplate; context: {section: selectedSection}"></ng-container>
+                <core-empty-box *ngIf="!selectedSection.hasContent" icon="fas-th-large"
+                    [message]="'core.course.nocontentavailable' | translate">
+                </core-empty-box>
+            </core-dynamic-component>
+        </div>
+
+        <!-- Multiple sections. -->
+        <div *ngIf="selectedSection && selectedSection.id == allSectionsId">
+            <core-dynamic-component [component]="allSectionsComponent" [data]="data">
+                <ng-container *ngFor="let section of sections; index as i">
+                    <ng-container *ngIf="i <= showSectionId">
+                        <ng-container *ngTemplateOutlet="sectionTemplate; context: {section: section}"></ng-container>
+                    </ng-container>
+                </ng-container>
             </core-dynamic-component>
 
-            <!-- Course summary. By default we only display the course progress. -->
-            <core-dynamic-component [component]="courseSummaryComponent" [data]="data">
-                <ion-list lines="none" class="core-format-progress-list"
-                    *ngIf="imageThumb || (selectedSection?.id == allSectionsId && progress !== undefined) ||
-                        (selectedSection && selectedSection.id != allSectionsId &&
-                        (selectedSection.availabilityinfo || selectedSection.visible === 0))">
-                    <div *ngIf="imageThumb" class="core-course-thumb">
-                        <img [src]="imageThumb" core-external-content alt=""/>
-                    </div>
-                    <ng-container *ngIf="selectedSection">
-                        <ion-item class="core-course-progress"
-                            *ngIf="selectedSection?.id == allSectionsId && progress !== undefined">
-                            <core-progress-bar [progress]="progress" a11yText="core.course.aria:sectionprogress">
-                            </core-progress-bar>
-                        </ion-item>
-                        <ion-item *ngIf="selectedSection && selectedSection.id != allSectionsId &&
-                            (selectedSection.availabilityinfo || selectedSection.visible === 0)">
-                            <ion-badge color="info" class="ion-text-wrap"
-                                *ngIf="selectedSection.visible === 0 && selectedSection.uservisible !== false">
-                                {{ 'core.course.hiddenfromstudents' | translate }}
-                            </ion-badge>
-                            <ion-badge color="info" class="ion-text-wrap"
-                                *ngIf="selectedSection.visible === 0 && selectedSection.uservisible === false">
-                                {{ 'core.notavailable' | translate }}
-                            </ion-badge>
-                            <ion-badge color="info" class="ion-text-wrap" *ngIf="selectedSection.availabilityinfo">
-                                <core-format-text [text]="selectedSection.availabilityinfo" contextLevel="course"
-                                    [contextInstanceId]="course?.id">
-                                </core-format-text>
-                            </ion-badge>
-                        </ion-item>
-                    </ng-container>
-                </ion-list>
-            </core-dynamic-component>
+            <core-infinite-loading [enabled]="canLoadMore" (action)="showMoreActivities($event)"></core-infinite-loading>
+        </div>
 
-            <!-- Single section. -->
-            <div *ngIf="selectedSection && selectedSection.id != allSectionsId">
-                <core-dynamic-component [component]="singleSectionComponent" [data]="data">
-                    <ng-container *ngTemplateOutlet="sectionTemplate; context: {section: selectedSection}"></ng-container>
-                    <core-empty-box *ngIf="!selectedSection.hasContent" icon="fas-th-large"
-                        [message]="'core.course.nocontentavailable' | translate">
-                    </core-empty-box>
-                </core-dynamic-component>
-            </div>
+        <ion-buttons class="ion-padding core-course-section-nav-buttons safe-area-padding-horizontal"
+            *ngIf="displaySectionSelector && sections?.length">
+            <ion-button *ngIf="previousSection" (click)="sectionChanged(previousSection)" fill="outline" color="primary"
+                [attr.aria-label]="('core.previous' | translate) + ': ' + previousSection.name">
+                <ion-icon name="fas-chevron-left" slot="icon-only" aria-hidden="true"></ion-icon>
+                <core-format-text class="sr-only" [text]="previousSection.name" contextLevel="course" [contextInstanceId]="course?.id">
+                </core-format-text>
+            </ion-button>
+            <ion-button *ngIf="nextSection" (click)="sectionChanged(nextSection)" fill="solid" color="primary"
+                [attr.aria-label]="('core.next' | translate) + ': ' + nextSection.name">
+                <core-format-text class="sr-only" [text]="nextSection.name" contextLevel="course" [contextInstanceId]="course?.id">
+                </core-format-text>
+                <ion-icon name="fas-chevron-right" slot="icon-only" aria-hidden="true"></ion-icon>
+            </ion-button>
+        </ion-buttons>
 
-            <!-- Multiple sections. -->
-            <div *ngIf="selectedSection && selectedSection.id == allSectionsId">
-                <core-dynamic-component [component]="allSectionsComponent" [data]="data">
-                    <ng-container *ngFor="let section of sections; index as i">
-                        <ng-container *ngIf="i <= showSectionId">
-                            <ng-container *ngTemplateOutlet="sectionTemplate; context: {section: section}"></ng-container>
-                        </ng-container>
-                    </ng-container>
-                </core-dynamic-component>
-
-                <core-infinite-loading [enabled]="canLoadMore" (action)="showMoreActivities($event)"></core-infinite-loading>
-            </div>
-
-            <ion-buttons class="ion-padding core-course-section-nav-buttons safe-area-padding-horizontal"
-                *ngIf="displaySectionSelector && sections?.length">
-                <ion-button *ngIf="previousSection" (click)="sectionChanged(previousSection)" fill="outline" color="primary"
-                    [attr.aria-label]="('core.previous' | translate) + ': ' + previousSection.name">
-                    <ion-icon name="fas-chevron-left" slot="icon-only" aria-hidden="true"></ion-icon>
-                    <core-format-text class="sr-only" [text]="previousSection.name" contextLevel="course"
-                        [contextInstanceId]="course?.id">
-                    </core-format-text>
-                </ion-button>
-                <ion-button *ngIf="nextSection" (click)="sectionChanged(nextSection)" fill="solid" color="primary"
-                    [attr.aria-label]="('core.next' | translate) + ': ' + nextSection.name">
-                    <core-format-text class="sr-only" [text]="nextSection.name" contextLevel="course"
-                        [contextInstanceId]="course?.id">
-                    </core-format-text>
-                    <ion-icon name="fas-chevron-right" slot="icon-only" aria-hidden="true"></ion-icon>
-                </ion-button>
-            </ion-buttons>
-        </core-loading>
-    </core-dynamic-component>
-</core-block-course-blocks>
+        <core-block-side-blocks-button *ngIf="course && displayBlocks && hasBlocks" [courseId]="course.id"
+            [downloadEnabled]="downloadEnabled">
+        </core-block-side-blocks-button>
+    </core-loading>
+</core-dynamic-component>
 
 <!-- Template to render a section. -->
 <ng-template #sectionTemplate let-section="section">
     <section *ngIf="!section.hiddenbynumsections && section.id != allSectionsId && section.id != stealthModulesSectionId">
         <!-- Title is only displayed when viewing all sections. -->
         <ion-item-divider *ngIf="selectedSection?.id == allSectionsId && section.name" class="ion-text-wrap" color="light"
-            [class.core-section-download]="downloadEnabled"
-            [class.item-dimmed]="section.visible === 0 || section.uservisible === false">
+            [class.core-section-download]="downloadEnabled" [class.item-dimmed]="section.visible === 0 || section.uservisible === false">
             <ion-label>
                 <h2>
                     <core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="course?.id">
diff --git a/src/core/features/course/components/format/format.ts b/src/core/features/course/components/format/format.ts
index a76e10358..6e8ae36bc 100644
--- a/src/core/features/course/components/format/format.ts
+++ b/src/core/features/course/components/format/format.ts
@@ -24,7 +24,6 @@ import {
     ViewChildren,
     QueryList,
     Type,
-    ViewChild,
     ElementRef,
 } from '@angular/core';
 import { ModalOptions } from '@ionic/core';
@@ -48,8 +47,8 @@ import { CoreEventObserver, CoreEvents } from '@singletons/events';
 import { IonContent, IonRefresher } from '@ionic/angular';
 import { CoreUtils } from '@services/utils/utils';
 import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
-import { CoreBlockCourseBlocksComponent } from '@features/block/components/course-blocks/course-blocks';
 import { CoreCourseSectionSelectorComponent } from '../section-selector/section-selector';
+import { CoreBlockHelper } from '@features/block/services/block-helper';
 
 /**
  * Component to display course contents using a certain format. If the format isn't found, use default one.
@@ -79,7 +78,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
     @Output() completionChanged = new EventEmitter<CoreCourseModuleCompletionData>(); // Notify when any module completion changes.
 
     @ViewChildren(CoreDynamicComponent) dynamicComponents?: QueryList<CoreDynamicComponent>;
-    @ViewChild(CoreBlockCourseBlocksComponent) courseBlocksComponent?: CoreBlockCourseBlocksComponent;
 
     // All the possible component classes.
     courseFormatComponent?: Type<unknown>;
@@ -92,8 +90,9 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
     showSectionId = 0;
     data: Record<string, unknown> = {}; // Data to pass to the components.
 
-    displaySectionSelector?: boolean;
-    displayBlocks?: boolean;
+    displaySectionSelector = false;
+    displayBlocks = false;
+    hasBlocks = false;
     selectedSection?: CoreCourseSection;
     previousSection?: CoreCourseSection;
     nextSection?: CoreCourseSection;
@@ -180,7 +179,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
     /**
      * Detect changes on input properties.
      */
-    ngOnChanges(changes: { [name: string]: SimpleChange }): void {
+    async ngOnChanges(changes: { [name: string]: SimpleChange }): Promise<void> {
         this.setInputData();
         this.sectionSelectorModalOptions.componentProps!.course = this.course;
         this.sectionSelectorModalOptions.componentProps!.sections = this.sections;
@@ -191,6 +190,9 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 
             this.displaySectionSelector = CoreCourseFormatDelegate.displaySectionSelector(this.course);
             this.displayBlocks = CoreCourseFormatDelegate.displayBlocks(this.course);
+
+            this.hasBlocks = await CoreBlockHelper.hasCourseBlocks(this.course.id);
+
             this.updateProgress();
 
             if ('overviewfiles' in this.course) {
@@ -498,8 +500,13 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
             await component.callComponentFunction('doRefresh', [refresher, done, afterCompletionChange]);
         }) || [];
 
-        if (this.courseBlocksComponent) {
-            promises.push(this.courseBlocksComponent.doRefresh());
+        if (this.course) {
+            const courseId = this.course.id;
+            promises.push(CoreCourse.invalidateCourseBlocks(courseId).then(async () => {
+                this.hasBlocks = await CoreBlockHelper.hasCourseBlocks(courseId);
+
+                return;
+            }));
         }
 
         await Promise.all(promises);
diff --git a/src/core/features/sitehome/pages/index/index.html b/src/core/features/sitehome/pages/index/index.html
index f96e2ea06..9220391ae 100644
--- a/src/core/features/sitehome/pages/index/index.html
+++ b/src/core/features/sitehome/pages/index/index.html
@@ -3,11 +3,9 @@
         <ion-icon name="fas-search" slot="icon-only" aria-hidden="true"></ion-icon>
     </ion-button>
     <core-context-menu>
-        <core-context-menu-item [priority]="1000" *ngIf="displayEnableDownload"
-            [content]="'core.settings.showdownloadoptions' | translate" (action)="switchDownload()"
-            iconAction="toggle" [(toggle)]="downloadEnabled"></core-context-menu-item>
-        <core-context-menu-item [priority]="500"
-            [content]="'addon.storagemanager.managestorage' | translate"
+        <core-context-menu-item [priority]="1000" *ngIf="displayEnableDownload" [content]="'core.settings.showdownloadoptions' | translate"
+            (action)="switchDownload()" iconAction="toggle" [(toggle)]="downloadEnabled"></core-context-menu-item>
+        <core-context-menu-item [priority]="500" [content]="'addon.storagemanager.managestorage' | translate"
             (action)="manageCoursesStorage()" iconAction="fas-archive"></core-context-menu-item>
     </core-context-menu>
 </core-navbar-buttons>
@@ -15,49 +13,52 @@
     <ion-refresher slot="fixed" [disabled]="!dataLoaded" (ionRefresh)="doRefresh($event.target)">
         <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
     </ion-refresher>
-    <core-block-course-blocks [courseId]="siteHomeId" [downloadEnabled]="downloadEnabled">
-        <core-loading [hideUntil]="dataLoaded">
-                <ion-list>
-                    <!-- Site home main contents. -->
-                    <ng-container *ngIf="section && section.hasContent">
-                        <ion-item class="ion-text-wrap" *ngIf="section.summary">
-                            <ion-label><core-format-text [text]="section.summary" contextLevel="course" [contextInstanceId]="siteHomeId">
-                            </core-format-text></ion-label>
-                        </ion-item>
+    <core-loading [hideUntil]="dataLoaded">
+        <ion-list>
+            <!-- Site home main contents. -->
+            <ng-container *ngIf="section && section.hasContent">
+                <ion-item class="ion-text-wrap" *ngIf="section.summary">
+                    <ion-label>
+                        <core-format-text [text]="section.summary" contextLevel="course" [contextInstanceId]="siteHomeId">
+                        </core-format-text>
+                    </ion-label>
+                </ion-item>
 
-                        <core-course-module *ngFor="let module of section.modules" [module]="module" [courseId]="siteHomeId"
-                            [downloadEnabled]="downloadEnabled" [section]="section"></core-course-module>
-                    </ng-container>
+                <core-course-module *ngFor="let module of section.modules" [module]="module" [courseId]="siteHomeId"
+                    [downloadEnabled]="downloadEnabled" [section]="section"></core-course-module>
+            </ng-container>
 
-                    <!-- Site home items: news, categories, courses, etc. -->
-                    <ng-container *ngIf="items.length > 0">
-                        <core-spacer *ngIf="section && section!.hasContent"></core-spacer>
-                        <ng-container *ngFor="let item of items">
-                            <ng-container [ngSwitch]="item">
-                                <ng-container *ngSwitchCase="'LIST_OF_COURSE'">
-                                    <ng-template *ngTemplateOutlet="allCourseList"></ng-template>
-                                </ng-container>
-                                <ng-container *ngSwitchCase="'LIST_OF_CATEGORIES'">
-                                    <ng-template *ngTemplateOutlet="categories"></ng-template>
-                                </ng-container>
-                                <ng-container *ngSwitchCase="'COURSE_SEARCH_BOX'">
-                                    <ng-template *ngTemplateOutlet="courseSearch"></ng-template>
-                                </ng-container>
-                                <ng-container *ngSwitchCase="'ENROLLED_COURSES'">
-                                    <ng-template *ngTemplateOutlet="enrolledCourseList"></ng-template>
-                                </ng-container>
-                                <ng-container *ngSwitchCase="'NEWS_ITEMS'">
-                                    <ng-template *ngTemplateOutlet="news"></ng-template>
-                                </ng-container>
-                            </ng-container>
+            <!-- Site home items: news, categories, courses, etc. -->
+            <ng-container *ngIf="items.length > 0">
+                <core-spacer *ngIf="section && section!.hasContent"></core-spacer>
+                <ng-container *ngFor="let item of items">
+                    <ng-container [ngSwitch]="item">
+                        <ng-container *ngSwitchCase="'LIST_OF_COURSE'">
+                            <ng-template *ngTemplateOutlet="allCourseList"></ng-template>
+                        </ng-container>
+                        <ng-container *ngSwitchCase="'LIST_OF_CATEGORIES'">
+                            <ng-template *ngTemplateOutlet="categories"></ng-template>
+                        </ng-container>
+                        <ng-container *ngSwitchCase="'COURSE_SEARCH_BOX'">
+                            <ng-template *ngTemplateOutlet="courseSearch"></ng-template>
+                        </ng-container>
+                        <ng-container *ngSwitchCase="'ENROLLED_COURSES'">
+                            <ng-template *ngTemplateOutlet="enrolledCourseList"></ng-template>
+                        </ng-container>
+                        <ng-container *ngSwitchCase="'NEWS_ITEMS'">
+                            <ng-template *ngTemplateOutlet="news"></ng-template>
                         </ng-container>
                     </ng-container>
-                </ion-list>
-            <core-empty-box *ngIf="!hasContent" icon="fas-box-open" [message]="'core.course.nocontentavailable' | translate">
+                </ng-container>
+            </ng-container>
+        </ion-list>
+        <core-block-side-blocks-button *ngIf="hasBlocks" [courseId]="siteHomeId" [downloadEnabled]="downloadEnabled">
+        </core-block-side-blocks-button>
 
-            </core-empty-box>
-        </core-loading>
-    </core-block-course-blocks>
+        <core-empty-box *ngIf="!hasContent" icon="fas-box-open" [message]="'core.course.nocontentavailable' | translate">
+
+        </core-empty-box>
+    </core-loading>
 </ion-content>
 
 <ng-template #allCourseList>
@@ -88,13 +89,17 @@
     <ion-item button class="ion-text-wrap" (click)="openMyCourses()" detail="true">
         <ion-icon name="fas-graduation-cap" fixed-width slot="start" aria-hidden="true">
         </ion-icon>
-        <ion-label><h2>{{ 'core.courses.mycourses' | translate}}</h2></ion-label>
+        <ion-label>
+            <h2>{{ 'core.courses.mycourses' | translate}}</h2>
+        </ion-label>
     </ion-item>
 </ng-template>
 
 <ng-template #courseSearch>
     <ion-item button class="ion-text-wrap" (click)="openSearch()" detail="true">
         <ion-icon name="fas-search" slot="start" aria-hidden="true"></ion-icon>
-        <ion-label><h2>{{ 'core.courses.searchcourses' | translate}}</h2></ion-label>
+        <ion-label>
+            <h2>{{ 'core.courses.searchcourses' | translate}}</h2>
+        </ion-label>
     </ion-item>
 </ng-template>
diff --git a/src/core/features/sitehome/pages/index/index.ts b/src/core/features/sitehome/pages/index/index.ts
index 777d6ce9c..eae6406f8 100644
--- a/src/core/features/sitehome/pages/index/index.ts
+++ b/src/core/features/sitehome/pages/index/index.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
+import { Component, OnDestroy, OnInit } from '@angular/core';
 import { IonRefresher } from '@ionic/angular';
 import { Params } from '@angular/router';
 
@@ -24,10 +24,11 @@ import { CoreSiteHome } from '@features/sitehome/services/sitehome';
 import { CoreCourses, CoreCoursesProvider } from '@features//courses/services/courses';
 import { CoreEventObserver, CoreEvents } from '@singletons/events';
 import { CoreCourseHelper, CoreCourseModule } from '@features/course/services/course-helper';
-import { CoreBlockCourseBlocksComponent } from '@features/block/components/course-blocks/course-blocks';
 import { CoreCourseModuleDelegate, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
 import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
 import { CoreNavigator } from '@services/navigator';
+import { CoreBlockHelper } from '@features/block/services/block-helper';
+import { CoreUtils } from '@services/utils/utils';
 
 /**
  * Page that displays site home index.
@@ -38,14 +39,13 @@ import { CoreNavigator } from '@services/navigator';
 })
 export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
 
-    @ViewChild(CoreBlockCourseBlocksComponent) courseBlocksComponent?: CoreBlockCourseBlocksComponent;
-
     dataLoaded = false;
     section?: CoreCourseWSSection & {
         hasContent?: boolean;
     };
 
     hasContent = false;
+    hasBlocks = false;
     items: string[] = [];
     siteHomeId = 1;
     currentSite!: CoreSite;
@@ -106,8 +106,8 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
         this.items = await CoreSiteHome.getFrontPageItems(config.frontpageloggedin);
         this.hasContent = this.items.length > 0;
 
-        if (this.items.some((item) => item == 'NEWS_ITEMS')) {
-            // Get the news forum.
+        // Get the news forum.
+        if (this.items.includes('NEWS_ITEMS')) {
             try {
                 const forum = await CoreSiteHome.getNewsForum(this.siteHomeId);
                 this.newsForumModule = await CoreCourse.getModule(forum.cmid, forum.course);
@@ -140,17 +140,17 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
             }
 
             // Add log in Moodle.
-            CoreCourse.logView(
+            CoreUtils.ignoreErrors(CoreCourse.logView(
                 this.siteHomeId,
                 undefined,
                 undefined,
                 this.currentSite.getInfo()?.sitename,
-            ).catch(() => {
-                // Ignore errors.
-            });
+            ));
         } catch (error) {
             CoreDomUtils.showErrorModalDefault(error, 'core.course.couldnotloadsectioncontent', true);
         }
+
+        this.hasBlocks = await CoreBlockHelper.hasCourseBlocks(this.siteHomeId);
     }
 
     /**
@@ -170,24 +170,15 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
             return;
         }));
 
+        promises.push(CoreCourse.invalidateCourseBlocks(this.siteHomeId));
+
         if (this.section && this.section.modules) {
             // Invalidate modules prefetch data.
             promises.push(CoreCourseModulePrefetchDelegate.invalidateModules(this.section.modules, this.siteHomeId));
         }
 
-        if (this.courseBlocksComponent) {
-            promises.push(this.courseBlocksComponent.invalidateBlocks());
-        }
-
         Promise.all(promises).finally(async () => {
-            const p2: Promise<unknown>[] = [];
-
-            p2.push(this.loadContent());
-            if (this.courseBlocksComponent) {
-                p2.push(this.courseBlocksComponent.loadContent());
-            }
-
-            await Promise.all(p2).finally(() => {
+            await this.loadContent().finally(() => {
                 refresher?.complete();
             });
         });
diff --git a/src/theme/theme.light.scss b/src/theme/theme.light.scss
index 56998e1eb..fa001efb9 100644
--- a/src/theme/theme.light.scss
+++ b/src/theme/theme.light.scss
@@ -197,13 +197,6 @@
         --background: var(--core-progressbar-background);
     }
 
-    --core-side-blocks-max-width: 30%;
-    --core-side-blocks-min-width: 280px;
-    core-block-course-blocks {
-        --side-blocks-max-width: var(--core-side-blocks-max-width);
-        --side-blocks-min-width: var(--core-side-blocks-min-width);
-    }
-
     --ion-item-background:        #{$ion-item-background};
     --ion-item-detail-icon-color: var(--gray-darker);
     --ion-item-detail-icon-font-size: 20px;

From 0bfe870af1dffed5f9b1447c65e28bea8e36861e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= <crazyserver@gmail.com>
Date: Fri, 12 Nov 2021 16:32:54 +0100
Subject: [PATCH 2/3] MOBILE-3914 dashboard: Add side blocks on dashboard

---
 .../components/side-blocks/side-blocks.ts     | 15 ++++--
 .../courses/pages/dashboard/dashboard.html    | 13 ++---
 .../courses/pages/dashboard/dashboard.ts      |  8 +++-
 .../features/courses/services/dashboard.ts    | 47 ++++++++++++++++++-
 .../services/handlers/dashboard-home.ts       |  2 +-
 5 files changed, 72 insertions(+), 13 deletions(-)

diff --git a/src/core/features/block/components/side-blocks/side-blocks.ts b/src/core/features/block/components/side-blocks/side-blocks.ts
index 18dc3c9ca..eefc8f527 100644
--- a/src/core/features/block/components/side-blocks/side-blocks.ts
+++ b/src/core/features/block/components/side-blocks/side-blocks.ts
@@ -20,6 +20,7 @@ import { CoreBlockHelper } from '../../services/block-helper';
 import { CoreBlockComponent } from '../block/block';
 import { CoreUtils } from '@services/utils/utils';
 import { IonRefresher } from '@ionic/angular';
+import { CoreCoursesDashboard } from '@features/courses/services/dashboard';
 
 /**
  * Component that displays the list of side blocks.
@@ -30,7 +31,7 @@ import { IonRefresher } from '@ionic/angular';
 })
 export class CoreBlockSideBlocksComponent implements OnInit {
 
-    @Input() courseId!: number;
+    @Input() courseId?: number;
     @Input() downloadEnabled = false;
 
     @ViewChildren(CoreBlockComponent) blocksComponents?: QueryList<CoreBlockComponent>;
@@ -55,8 +56,10 @@ export class CoreBlockSideBlocksComponent implements OnInit {
     async invalidateBlocks(): Promise<void> {
         const promises: Promise<void>[] = [];
 
-        if (CoreBlockHelper.canGetCourseBlocks()) {
+        if (this.courseId) {
             promises.push(CoreCourse.invalidateCourseBlocks(this.courseId));
+        } else {
+            promises.push(CoreCoursesDashboard.invalidateDashboardBlocks());
         }
 
         // Invalidate the blocks.
@@ -76,7 +79,13 @@ export class CoreBlockSideBlocksComponent implements OnInit {
      */
     async loadContent(): Promise<void> {
         try {
-            this.blocks = await CoreBlockHelper.getCourseBlocks(this.courseId);
+            if (this.courseId) {
+                this.blocks = await CoreBlockHelper.getCourseBlocks(this.courseId);
+            } else {
+                const blocks = await CoreCoursesDashboard.getDashboardBlocks();
+
+                this.blocks = blocks.sideBlocks;
+            }
         } catch (error) {
             CoreDomUtils.showErrorModal(error);
 
diff --git a/src/core/features/courses/pages/dashboard/dashboard.html b/src/core/features/courses/pages/dashboard/dashboard.html
index c79854f7a..ee5fb7421 100644
--- a/src/core/features/courses/pages/dashboard/dashboard.html
+++ b/src/core/features/courses/pages/dashboard/dashboard.html
@@ -4,10 +4,9 @@
     </ion-button>
     <core-context-menu>
         <core-context-menu-item *ngIf="(downloadCourseEnabled || downloadCoursesEnabled)" [priority]="1000"
-            [content]="'core.settings.showdownloadoptions' | translate" (action)="switchDownload()"
-            iconAction="toggle" [(toggle)]="downloadEnabled"></core-context-menu-item>
-        <core-context-menu-item [priority]="500"
-            [content]="'addon.storagemanager.managestorage' | translate"
+            [content]="'core.settings.showdownloadoptions' | translate" (action)="switchDownload()" iconAction="toggle"
+            [(toggle)]="downloadEnabled"></core-context-menu-item>
+        <core-context-menu-item [priority]="500" [content]="'addon.storagemanager.managestorage' | translate"
             (action)="manageCoursesStorage()" iconAction="fas-archive"></core-context-menu-item>
     </core-context-menu>
 </core-navbar-buttons>
@@ -20,11 +19,13 @@
         <ion-list>
             <ng-container *ngFor="let block of blocks">
                 <core-block *ngIf="block.visible" [block]="block" contextLevel="user" [instanceId]="userId"
-                [extraData]="{'downloadEnabled': downloadEnabled}"></core-block>
+                    [extraData]="{'downloadEnabled': downloadEnabled}"></core-block>
             </ng-container>
         </ion-list>
 
-        <core-empty-box *ngIf="blocks.length == 0" icon="fas-th-large" [message]="'core.course.nocontentavailable' | translate">
+        <core-block-side-blocks-button *ngIf="hasSideBlocks" [downloadEnabled]="downloadEnabled"></core-block-side-blocks-button>
+
+        <core-empty-box *ngIf="blocks.length == 0" icon="fas-cubes" [message]="'core.course.nocontentavailable' | translate">
         </core-empty-box>
     </core-loading>
 </ion-content>
diff --git a/src/core/features/courses/pages/dashboard/dashboard.ts b/src/core/features/courses/pages/dashboard/dashboard.ts
index 5102614cc..2b7448a0b 100644
--- a/src/core/features/courses/pages/dashboard/dashboard.ts
+++ b/src/core/features/courses/pages/dashboard/dashboard.ts
@@ -23,6 +23,7 @@ import { CoreDomUtils } from '@services/utils/dom';
 import { CoreCourseBlock } from '@features/course/services/course';
 import { CoreBlockComponent } from '@features/block/components/block/block';
 import { CoreNavigator } from '@services/navigator';
+import { CoreBlockDelegate } from '@features/block/services/block-delegate';
 
 /**
  * Page that displays the dashboard page.
@@ -36,6 +37,7 @@ export class CoreCoursesDashboardPage implements OnInit, OnDestroy {
 
     @ViewChildren(CoreBlockComponent) blocksComponents?: QueryList<CoreBlockComponent>;
 
+    hasSideBlocks = false;
     searchEnabled = false;
     downloadEnabled = false;
     downloadCourseEnabled = false;
@@ -88,7 +90,11 @@ export class CoreCoursesDashboardPage implements OnInit, OnDestroy {
             this.userId = CoreSites.getCurrentSiteUserId();
 
             try {
-                this.blocks = await CoreCoursesDashboard.getDashboardBlocks();
+                const blocks = await CoreCoursesDashboard.getDashboardBlocks();
+
+                this.blocks = blocks.mainBlocks;
+
+                this.hasSideBlocks = CoreBlockDelegate.hasSupportedBlock(blocks.sideBlocks);
             } catch (error) {
                 CoreDomUtils.showErrorModal(error);
 
diff --git a/src/core/features/courses/services/dashboard.ts b/src/core/features/courses/services/dashboard.ts
index d592f2e2d..76823a0b2 100644
--- a/src/core/features/courses/services/dashboard.ts
+++ b/src/core/features/courses/services/dashboard.ts
@@ -38,14 +38,14 @@ export class CoreCoursesDashboardProvider {
     }
 
     /**
-     * Get dashboard blocks.
+     * Get dashboard blocks from WS.
      *
      * @param userId User ID. Default, current user.
      * @param siteId Site ID. If not defined, current site.
      * @return Promise resolved with the list of blocks.
      * @since 3.6
      */
-    async getDashboardBlocks(userId?: number, siteId?: string): Promise<CoreCourseBlock[]> {
+    protected async getDashboardBlocksFromWS(userId?: number, siteId?: string): Promise<CoreCourseBlock[]> {
         const site = await CoreSites.getSite(siteId);
 
         const params: CoreBlockGetDashboardBlocksWSParams = {
@@ -63,6 +63,44 @@ export class CoreCoursesDashboardProvider {
         return result.blocks || [];
     }
 
+    /**
+     * Get dashboard blocks.
+     *
+     * @param userId User ID. Default, current user.
+     * @param siteId Site ID. If not defined, current site.
+     * @return Promise resolved with the list of blocks.
+     */
+    async getDashboardBlocks(userId?: number, siteId?: string): Promise<CoreCoursesDashboardBlocks> {
+        const blocks = await CoreCoursesDashboard.getDashboardBlocksFromWS(userId, siteId);
+
+        let mainBlocks: CoreCourseBlock[] = [];
+        let sideBlocks: CoreCourseBlock[] = [];
+
+        blocks.forEach((block) => {
+            if (block.region == 'content' || block.region == 'main') {
+                mainBlocks.push(block);
+            } else {
+                sideBlocks.push(block);
+            }
+        });
+
+        if (mainBlocks.length == 0) {
+            mainBlocks = [];
+            sideBlocks = [];
+
+            blocks.forEach((block) => {
+                if (block.region.match('side')) {
+                    sideBlocks.push(block);
+                } else {
+                    mainBlocks.push(block);
+                }
+            });
+        }
+
+        return { mainBlocks, sideBlocks };
+
+    }
+
     /**
      * Invalidates dashboard blocks WS call.
      *
@@ -122,6 +160,11 @@ export class CoreCoursesDashboardProvider {
 
 export const CoreCoursesDashboard = makeSingleton(CoreCoursesDashboardProvider);
 
+export type CoreCoursesDashboardBlocks = {
+    mainBlocks: CoreCourseBlock[];
+    sideBlocks: CoreCourseBlock[];
+};
+
 /**
  * Params of core_block_get_dashboard_blocks WS.
  */
diff --git a/src/core/features/courses/services/handlers/dashboard-home.ts b/src/core/features/courses/services/handlers/dashboard-home.ts
index 998a6fac7..0076031a8 100644
--- a/src/core/features/courses/services/handlers/dashboard-home.ts
+++ b/src/core/features/courses/services/handlers/dashboard-home.ts
@@ -68,7 +68,7 @@ export class CoreDashboardHomeHandlerService implements CoreMainMenuHomeHandler
         if (dashboardAvailable && blocksEnabled) {
             const blocks = await CoreCoursesDashboard.getDashboardBlocks(undefined, siteId);
 
-            return CoreBlockDelegate.hasSupportedBlock(blocks);
+            return CoreBlockDelegate.hasSupportedBlock(blocks.mainBlocks) || CoreBlockDelegate.hasSupportedBlock(blocks.sideBlocks);
         }
 
         // Check if my overview is enabled. If it's enabled we will fake enabled blocks.

From d990fa44f97993b3260b0e6d8eb6aa1f2a7a1e3a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= <crazyserver@gmail.com>
Date: Tue, 16 Nov 2021 13:12:36 +0100
Subject: [PATCH 3/3] MOBILE-3914 utils: Close modal before navigating

---
 src/core/services/utils/dom.ts | 53 +++++++++++++++++++++++++++-------
 1 file changed, 43 insertions(+), 10 deletions(-)

diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts
index e4b6deb5a..610b1753b 100644
--- a/src/core/services/utils/dom.ts
+++ b/src/core/services/utils/dom.ts
@@ -29,7 +29,15 @@ import { CoreIonLoadingElement } from '@classes/ion-loading';
 import { CoreCanceledError } from '@classes/errors/cancelederror';
 import { CoreAnyError, CoreError } from '@classes/errors/error';
 import { CoreSilentError } from '@classes/errors/silenterror';
-import { makeSingleton, Translate, AlertController, ToastController, PopoverController, ModalController } from '@singletons';
+import {
+    makeSingleton,
+    Translate,
+    AlertController,
+    ToastController,
+    PopoverController,
+    ModalController,
+    Router,
+} from '@singletons';
 import { CoreLogger } from '@singletons/logger';
 import { CoreFileSizeSum } from '@services/plugin-file-delegate';
 import { CoreNetworkError } from '@classes/errors/network-error';
@@ -41,6 +49,9 @@ import { CoreZoomLevel } from '@features/settings/services/settings-helper';
 import { CoreErrorWithTitle } from '@classes/errors/errorwithtitle';
 import { AddonFilterMultilangHandler } from '@addons/filter/multilang/services/handlers/multilang';
 import { CoreSites } from '@services/sites';
+import { NavigationStart } from '@angular/router';
+import { filter } from 'rxjs/operators';
+import { Subscription } from 'rxjs';
 
 /*
  * "Utils" service with helper functions for UI, DOM elements and HTML code.
@@ -1651,18 +1662,32 @@ export class CoreDomUtilsProvider {
     /**
      * Opens a Modal.
      *
-     * @param modalOptions Modal Options.
+     * @param options Modal Options.
      */
     async openModal<T = unknown>(
-        modalOptions: ModalOptions,
+        options: OpenModalOptions,
     ): Promise<T | undefined> {
 
+        const { waitForDismissCompleted, closeOnNavigate, ...modalOptions } = options;
+        const listenCloseEvents = closeOnNavigate ?? true; // Default to true.
+
         const modal = await ModalController.create(modalOptions);
 
+        let navSubscription: Subscription | undefined;
+        if (listenCloseEvents) {
+            // Listen navigation events to close modals.
+            navSubscription = Router.events
+                .pipe(filter(event => event instanceof NavigationStart))
+                .subscribe(async () => {
+                    modal.dismiss();
+                });
+        }
+
         await modal.present();
 
-        // If onDidDismiss is nedded we can add a new param to the function to wait one function or the other.
-        const result = await modal.onWillDismiss<T>();
+        const result = waitForDismissCompleted ? await modal.onDidDismiss<T>() : await modal.onWillDismiss<T>();
+        navSubscription?.unsubscribe();
+
         if (result?.data) {
             return result?.data;
         }
@@ -1671,21 +1696,21 @@ export class CoreDomUtilsProvider {
     /**
      * Opens a side Modal.
      *
-     * @param modalOptions Modal Options.
+     * @param options Modal Options.
      */
     async openSideModal<T = unknown>(
-        modalOptions: ModalOptions,
+        options: OpenModalOptions,
     ): Promise<T | undefined> {
 
-        modalOptions = Object.assign({
+        options = Object.assign({
             cssClass: 'core-modal-lateral',
             showBackdrop: true,
             backdropDismiss: true,
             enterAnimation: CoreModalLateralTransitionEnter,
             leaveAnimation: CoreModalLateralTransitionLeave,
-        }, modalOptions);
+        }, options);
 
-        return await this.openModal<T>(modalOptions);
+        return await this.openModal<T>(options);
     }
 
     /**
@@ -2012,3 +2037,11 @@ export const CoreDomUtils = makeSingleton(CoreDomUtilsProvider);
 export type OpenPopoverOptions = PopoverOptions & {
     waitForDismissCompleted?: boolean;
 };
+
+/**
+ * Options for the openModal function.
+ */
+export type OpenModalOptions = ModalOptions & {
+    waitForDismissCompleted?: boolean;
+    closeOnNavigate?: boolean; // Default true.
+};