forked from CIT/Vmeda.Online
		
	
						commit
						09b962cf04
					
				@ -1557,6 +1557,7 @@
 | 
			
		||||
  "core.course.highlighted": "moodle",
 | 
			
		||||
  "core.course.insufficientavailablequota": "local_moodlemobileapp",
 | 
			
		||||
  "core.course.insufficientavailablespace": "local_moodlemobileapp",
 | 
			
		||||
  "core.course.lastaccessedactivity": "local_moodlemobileapp",
 | 
			
		||||
  "core.course.manualcompletionnotsynced": "local_moodlemobileapp",
 | 
			
		||||
  "core.course.modulenotfound": "local_moodlemobileapp",
 | 
			
		||||
  "core.course.nocontentavailable": "local_moodlemobileapp",
 | 
			
		||||
 | 
			
		||||
@ -49,17 +49,41 @@ export class AddonBlockRecentlyAccessedItemsProvider {
 | 
			
		||||
            cacheKey: this.getRecentItemsCacheKey(),
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const items: AddonBlockRecentlyAccessedItemsItem[] =
 | 
			
		||||
        let items: AddonBlockRecentlyAccessedItemsItem[] =
 | 
			
		||||
            await site.read('block_recentlyaccesseditems_get_recent_items', undefined, preSets);
 | 
			
		||||
 | 
			
		||||
        return await Promise.all(items.map(async (item) => {
 | 
			
		||||
        const cmIds: number[] = [];
 | 
			
		||||
 | 
			
		||||
        items = await Promise.all(items.map(async (item) => {
 | 
			
		||||
            const modicon = item.icon && CoreDomUtils.getHTMLElementAttribute(item.icon, 'src');
 | 
			
		||||
 | 
			
		||||
            item.iconUrl = await CoreCourse.getModuleIconSrc(item.modname, modicon || undefined);
 | 
			
		||||
            item.iconTitle = item.icon && CoreDomUtils.getHTMLElementAttribute(item.icon, 'title');
 | 
			
		||||
            cmIds.push(item.cmid);
 | 
			
		||||
 | 
			
		||||
            return item;
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        // Check if the viewed module should be updated for each activity.
 | 
			
		||||
        const lastViewedMap = await CoreCourse.getCertainModulesViewed(cmIds, site.getId());
 | 
			
		||||
 | 
			
		||||
        items.forEach((recentItem) => {
 | 
			
		||||
            const timeAccess = recentItem.timeaccess * 1000;
 | 
			
		||||
            const lastViewed = lastViewedMap[recentItem.cmid];
 | 
			
		||||
 | 
			
		||||
            if (lastViewed && lastViewed.timeaccess >= timeAccess) {
 | 
			
		||||
                return; // No need to update.
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Update access.
 | 
			
		||||
            CoreCourse.storeModuleViewed(recentItem.courseid, recentItem.cmid, {
 | 
			
		||||
                timeaccess: recentItem.timeaccess * 1000,
 | 
			
		||||
                sectionId: lastViewed && lastViewed.sectionId,
 | 
			
		||||
                siteId: site.getId(),
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return items;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -43,6 +43,7 @@ export class AddonBlogEntriesPage implements OnInit {
 | 
			
		||||
    protected canLoadMoreEntries = false;
 | 
			
		||||
    protected canLoadMoreUserEntries = true;
 | 
			
		||||
    protected siteHomeId: number;
 | 
			
		||||
    protected fetchSuccess = false;
 | 
			
		||||
 | 
			
		||||
    loaded = false;
 | 
			
		||||
    canLoadMore = false;
 | 
			
		||||
@ -123,8 +124,6 @@ export class AddonBlogEntriesPage implements OnInit {
 | 
			
		||||
        deepLinkManager.treatLink();
 | 
			
		||||
 | 
			
		||||
        await this.fetchEntries();
 | 
			
		||||
 | 
			
		||||
        CoreUtils.ignoreErrors(AddonBlog.logView(this.filter));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -176,13 +175,7 @@ export class AddonBlogEntriesPage implements OnInit {
 | 
			
		||||
 | 
			
		||||
                entry.summary = CoreTextUtils.replacePluginfileUrls(entry.summary, entry.summaryfiles || []);
 | 
			
		||||
 | 
			
		||||
                return CoreUser.getProfile(entry.userid, entry.courseid, true).then((user) => {
 | 
			
		||||
                    entry.user = user;
 | 
			
		||||
 | 
			
		||||
                    return;
 | 
			
		||||
                }).catch(() => {
 | 
			
		||||
                    // Ignore errors.
 | 
			
		||||
                });
 | 
			
		||||
                entry.user = await CoreUtils.ignoreErrors(CoreUser.getProfile(entry.userid, entry.courseid, true));
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (refresh) {
 | 
			
		||||
@ -205,6 +198,11 @@ export class AddonBlogEntriesPage implements OnInit {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await Promise.all(promises);
 | 
			
		||||
 | 
			
		||||
            if (!this.fetchSuccess) {
 | 
			
		||||
                this.fetchSuccess = true;
 | 
			
		||||
                CoreUtils.ignoreErrors(AddonBlog.logView(this.filter));
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModalDefault(error, 'addon.blog.errorloadentries', true);
 | 
			
		||||
            this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading.
 | 
			
		||||
 | 
			
		||||
@ -58,6 +58,8 @@ export class AddonCompetencyCompetencyPage implements OnInit, OnDestroy {
 | 
			
		||||
    contextLevel?: string;
 | 
			
		||||
    contextInstanceId?: number;
 | 
			
		||||
 | 
			
		||||
    protected fetchSuccess = false;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        try {
 | 
			
		||||
            const planId = CoreNavigator.getRouteNumberParam('planId');
 | 
			
		||||
@ -117,33 +119,6 @@ export class AddonCompetencyCompetencyPage implements OnInit, OnDestroy {
 | 
			
		||||
            await source.reload();
 | 
			
		||||
            await this.competencies.start();
 | 
			
		||||
            await this.fetchCompetency();
 | 
			
		||||
 | 
			
		||||
            if (!this.competency) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const name = this.competency.competency.competency.shortname;
 | 
			
		||||
 | 
			
		||||
            if (source instanceof AddonCompetencyPlanCompetenciesSource) {
 | 
			
		||||
                this.planStatus && await CoreUtils.ignoreErrors(
 | 
			
		||||
                    AddonCompetency.logCompetencyInPlanView(
 | 
			
		||||
                        source.PLAN_ID,
 | 
			
		||||
                        this.requireCompetencyId(),
 | 
			
		||||
                        this.planStatus,
 | 
			
		||||
                        name,
 | 
			
		||||
                        source.user?.id,
 | 
			
		||||
                    ),
 | 
			
		||||
                );
 | 
			
		||||
            } else {
 | 
			
		||||
                await CoreUtils.ignoreErrors(
 | 
			
		||||
                    AddonCompetency.logCompetencyInCourseView(
 | 
			
		||||
                        source.COURSE_ID,
 | 
			
		||||
                        this.requireCompetencyId(),
 | 
			
		||||
                        name,
 | 
			
		||||
                        source.USER_ID,
 | 
			
		||||
                    ),
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.competencyLoaded = true;
 | 
			
		||||
        }
 | 
			
		||||
@ -180,6 +155,32 @@ export class AddonCompetencyCompetencyPage implements OnInit, OnDestroy {
 | 
			
		||||
                    evidence.description = Translate.instant(key, { $a: evidence.desca });
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (!this.fetchSuccess) {
 | 
			
		||||
                this.fetchSuccess = true;
 | 
			
		||||
                const name = this.competency.competency.competency.shortname;
 | 
			
		||||
 | 
			
		||||
                if (source instanceof AddonCompetencyPlanCompetenciesSource) {
 | 
			
		||||
                    this.planStatus && await CoreUtils.ignoreErrors(
 | 
			
		||||
                        AddonCompetency.logCompetencyInPlanView(
 | 
			
		||||
                            source.PLAN_ID,
 | 
			
		||||
                            this.requireCompetencyId(),
 | 
			
		||||
                            this.planStatus,
 | 
			
		||||
                            name,
 | 
			
		||||
                            source.user?.id,
 | 
			
		||||
                        ),
 | 
			
		||||
                    );
 | 
			
		||||
                } else {
 | 
			
		||||
                    await CoreUtils.ignoreErrors(
 | 
			
		||||
                        AddonCompetency.logCompetencyInCourseView(
 | 
			
		||||
                            source.COURSE_ID,
 | 
			
		||||
                            this.requireCompetencyId(),
 | 
			
		||||
                            name,
 | 
			
		||||
                            source.USER_ID,
 | 
			
		||||
                        ),
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModalDefault(error, 'Error getting competency data.');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -36,6 +36,8 @@ export class AddonCompetencyCompetencySummaryPage implements OnInit {
 | 
			
		||||
    contextLevel?: ContextLevel;
 | 
			
		||||
    contextInstanceId?: number;
 | 
			
		||||
 | 
			
		||||
    protected fetchSuccess = false; // Whether a fetch was finished successfully.
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
@ -54,9 +56,6 @@ export class AddonCompetencyCompetencySummaryPage implements OnInit {
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await this.fetchCompetency();
 | 
			
		||||
            const name = this.competency!.competency && this.competency!.competency.shortname;
 | 
			
		||||
 | 
			
		||||
            CoreUtils.ignoreErrors(AddonCompetency.logCompetencyView(this.competencyId, name));
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.competencyLoaded = true;
 | 
			
		||||
        }
 | 
			
		||||
@ -73,10 +72,15 @@ export class AddonCompetencyCompetencySummaryPage implements OnInit {
 | 
			
		||||
            if (!this.contextLevel || this.contextInstanceId === undefined) {
 | 
			
		||||
                // Context not specified, use user context.
 | 
			
		||||
                this.contextLevel = ContextLevel.USER;
 | 
			
		||||
                this.contextInstanceId = result.usercompetency!.userid;
 | 
			
		||||
                this.contextInstanceId = result.usercompetency?.userid;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.competency = result.competency;
 | 
			
		||||
 | 
			
		||||
            if (!this.fetchSuccess) {
 | 
			
		||||
                this.fetchSuccess = true;
 | 
			
		||||
                CoreUtils.ignoreErrors(AddonCompetency.logCompetencyView(this.competencyId, this.competency.competency.shortname));
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModalDefault(error, 'Error getting competency summary data.');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -17,7 +17,6 @@ import { Params } from '@angular/router';
 | 
			
		||||
import { CoreSite } from '@classes/site';
 | 
			
		||||
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
 | 
			
		||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { IonContent } from '@ionic/angular';
 | 
			
		||||
import { CoreGroupInfo, CoreGroups } from '@services/groups';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
@ -121,7 +120,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
 | 
			
		||||
            (data) => {
 | 
			
		||||
                if (this.assign && data.assignmentId == this.assign.id && data.userId == this.currentUserId) {
 | 
			
		||||
                    // Assignment submitted, check completion.
 | 
			
		||||
                    CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
 | 
			
		||||
                    this.checkCompletion();
 | 
			
		||||
 | 
			
		||||
                    // Reload data since it can have offline data now.
 | 
			
		||||
                    this.showLoadingAndRefresh(true, false);
 | 
			
		||||
@ -138,25 +137,6 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
 | 
			
		||||
        }, this.siteId);
 | 
			
		||||
 | 
			
		||||
        await this.loadContent(false, true);
 | 
			
		||||
 | 
			
		||||
        if (!this.assign) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await AddonModAssign.logView(this.assign.id, this.assign.name);
 | 
			
		||||
            CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
 | 
			
		||||
        } catch {
 | 
			
		||||
            // Ignore errors. Just don't check Module completion.
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.canViewAllSubmissions) {
 | 
			
		||||
            // User can see all submissions, log grading view.
 | 
			
		||||
            CoreUtils.ignoreErrors(AddonModAssign.logGradingView(this.assign.id, this.assign.name));
 | 
			
		||||
        } else if (this.canViewOwnSubmission) {
 | 
			
		||||
            // User can only see their own submission, log view the user submission.
 | 
			
		||||
            CoreUtils.ignoreErrors(AddonModAssign.logSubmissionView(this.assign.id, this.assign.name));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -232,6 +212,25 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected async logActivity(): Promise<void> {
 | 
			
		||||
        if (!this.assign) {
 | 
			
		||||
            return; // Shouldn't happen.
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await AddonModAssign.logView(this.assign.id, this.assign.name);
 | 
			
		||||
 | 
			
		||||
        if (this.canViewAllSubmissions) {
 | 
			
		||||
            // User can see all submissions, log grading view.
 | 
			
		||||
            CoreUtils.ignoreErrors(AddonModAssign.logGradingView(this.assign.id, this.assign.name));
 | 
			
		||||
        } else if (this.canViewOwnSubmission) {
 | 
			
		||||
            // User can only see their own submission, log view the user submission.
 | 
			
		||||
            CoreUtils.ignoreErrors(AddonModAssign.logSubmissionView(this.assign.id, this.assign.name));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set group to see the summary.
 | 
			
		||||
     *
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,6 @@
 | 
			
		||||
import { Component, OnInit, Optional } from '@angular/core';
 | 
			
		||||
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
 | 
			
		||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { IonContent } from '@ionic/angular';
 | 
			
		||||
import { CoreGroupInfo, CoreGroups } from '@services/groups';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
@ -54,18 +53,6 @@ export class AddonModBBBIndexComponent extends CoreCourseModuleMainActivityCompo
 | 
			
		||||
        super.ngOnInit();
 | 
			
		||||
 | 
			
		||||
        await this.loadContent();
 | 
			
		||||
 | 
			
		||||
        if (!this.bbb) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await AddonModBBB.logView(this.bbb.id, this.bbb.name);
 | 
			
		||||
 | 
			
		||||
            CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
 | 
			
		||||
        } catch {
 | 
			
		||||
            // Ignore errors.
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -82,7 +69,6 @@ export class AddonModBBBIndexComponent extends CoreCourseModuleMainActivityCompo
 | 
			
		||||
        this.groupId = CoreGroups.validateGroupId(this.groupId, this.groupInfo);
 | 
			
		||||
 | 
			
		||||
        await this.fetchMeetingInfo();
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -107,6 +93,17 @@ export class AddonModBBBIndexComponent extends CoreCourseModuleMainActivityCompo
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected async logActivity(): Promise<void> {
 | 
			
		||||
        if (!this.bbb) {
 | 
			
		||||
            return; // Shouldn't happen.
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await AddonModBBB.logView(this.bbb.id, this.bbb.name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update meeting info.
 | 
			
		||||
     *
 | 
			
		||||
 | 
			
		||||
@ -18,7 +18,6 @@ import { AddonModBook, AddonModBookBookWSData, AddonModBookNumbering, AddonModBo
 | 
			
		||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Component that displays a book entry page.
 | 
			
		||||
@ -36,6 +35,7 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
 | 
			
		||||
    hasStartedBook = false;
 | 
			
		||||
 | 
			
		||||
    protected book?: AddonModBookBookWSData;
 | 
			
		||||
    protected checkCompletionAfterLog = false;
 | 
			
		||||
 | 
			
		||||
    constructor( @Optional() courseContentsPage?: CoreCourseContentsPage) {
 | 
			
		||||
        super('AddonModBookIndexComponent', courseContentsPage);
 | 
			
		||||
@ -48,9 +48,6 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
 | 
			
		||||
        super.ngOnInit();
 | 
			
		||||
 | 
			
		||||
        this.loadContent();
 | 
			
		||||
 | 
			
		||||
        // Log book viewed.
 | 
			
		||||
        await CoreUtils.ignoreErrors(AddonModBook.logView(this.module.instance, undefined, this.module.name));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -93,6 +90,13 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
 | 
			
		||||
        this.chapters = AddonModBook.getTocList(contents);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected async logActivity(): Promise<void> {
 | 
			
		||||
        AddonModBook.logView(this.module.instance, undefined, this.module.name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Open the book in a certain chapter.
 | 
			
		||||
     *
 | 
			
		||||
 | 
			
		||||
@ -391,7 +391,7 @@ export class AddonModBookProvider {
 | 
			
		||||
    async storeLastChapterViewed(id: number, chapterId: number, courseId: number, siteId?: string): Promise<void> {
 | 
			
		||||
        const site = await CoreSites.getSite(siteId);
 | 
			
		||||
 | 
			
		||||
        await site.storeLastViewed(AddonModBookProvider.COMPONENT, id, chapterId, String(courseId));
 | 
			
		||||
        await site.storeLastViewed(AddonModBookProvider.COMPONENT, id, chapterId, { data: String(courseId) });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,6 @@
 | 
			
		||||
import { Component, OnInit, Optional } from '@angular/core';
 | 
			
		||||
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
 | 
			
		||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { IonContent } from '@ionic/angular';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { CoreTimeUtils } from '@services/utils/time';
 | 
			
		||||
@ -53,18 +52,6 @@ export class AddonModChatIndexComponent extends CoreCourseModuleMainActivityComp
 | 
			
		||||
        super.ngOnInit();
 | 
			
		||||
 | 
			
		||||
        await this.loadContent();
 | 
			
		||||
 | 
			
		||||
        if (!this.chat) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await AddonModChat.logView(this.chat.id, this.chat.name);
 | 
			
		||||
 | 
			
		||||
            CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
 | 
			
		||||
        } catch {
 | 
			
		||||
            // Ignore errors.
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -89,6 +76,17 @@ export class AddonModChatIndexComponent extends CoreCourseModuleMainActivityComp
 | 
			
		||||
        this.dataRetrieved.emit(this.chat);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected async logActivity(): Promise<void> {
 | 
			
		||||
        if (!this.chat) {
 | 
			
		||||
            return; // Shouldn't happen.
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await AddonModChat.logView(this.chat.id, this.chat.name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Enter the chat.
 | 
			
		||||
     */
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,6 @@
 | 
			
		||||
import { Component, Optional, OnInit } from '@angular/core';
 | 
			
		||||
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
 | 
			
		||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { IonContent } from '@ionic/angular';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
@ -86,18 +85,6 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo
 | 
			
		||||
        this.userId = CoreSites.getCurrentSiteUserId();
 | 
			
		||||
 | 
			
		||||
        await this.loadContent(false, true);
 | 
			
		||||
 | 
			
		||||
        if (!this.choice) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await AddonModChoice.logView(this.choice.id, this.choice.name);
 | 
			
		||||
 | 
			
		||||
            await CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
 | 
			
		||||
        } catch {
 | 
			
		||||
            // Ignore errors.
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -325,6 +312,17 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo
 | 
			
		||||
        this.canSeeResults = hasVotes || AddonModChoice.canStudentSeeResults(choice, this.hasAnsweredOnline);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected async logActivity(): Promise<void> {
 | 
			
		||||
        if (!this.choice) {
 | 
			
		||||
            return; // Shouldn't happen.
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await AddonModChoice.logView(this.choice.id, this.choice.name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if a choice is open.
 | 
			
		||||
     *
 | 
			
		||||
@ -384,7 +382,7 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo
 | 
			
		||||
            if (online) {
 | 
			
		||||
                CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: this.moduleName });
 | 
			
		||||
                // Check completion since it could be configured to complete once the user answers the choice.
 | 
			
		||||
                CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
 | 
			
		||||
                this.checkCompletion();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await this.dataUpdated(online);
 | 
			
		||||
 | 
			
		||||
@ -18,7 +18,6 @@ import { Params } from '@angular/router';
 | 
			
		||||
import { CoreCommentsProvider } from '@features/comments/services/comments';
 | 
			
		||||
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
 | 
			
		||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { CoreRatingProvider } from '@features/rating/services/rating';
 | 
			
		||||
import { CoreRatingSyncProvider } from '@features/rating/services/rating-sync';
 | 
			
		||||
import { IonContent } from '@ionic/angular';
 | 
			
		||||
@ -154,7 +153,6 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        await this.loadContent(false, true);
 | 
			
		||||
        await this.logView(true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -410,7 +408,7 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp
 | 
			
		||||
        try {
 | 
			
		||||
            await this.fetchEntriesData();
 | 
			
		||||
            // Log activity view for coherence with Moodle web.
 | 
			
		||||
            await this.logView();
 | 
			
		||||
            await this.logActivity();
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true);
 | 
			
		||||
        } finally {
 | 
			
		||||
@ -456,7 +454,7 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp
 | 
			
		||||
            await this.fetchEntriesData();
 | 
			
		||||
 | 
			
		||||
            // Log activity view for coherence with Moodle web.
 | 
			
		||||
            return this.logView();
 | 
			
		||||
            return this.logActivity();
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true);
 | 
			
		||||
        }
 | 
			
		||||
@ -522,24 +520,14 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Log viewing the activity.
 | 
			
		||||
     *
 | 
			
		||||
     * @param checkCompletion Whether to check completion.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected async logView(checkCompletion = false): Promise<void> {
 | 
			
		||||
    protected async logActivity(): Promise<void> {
 | 
			
		||||
        if (!this.database || !this.database.id) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await AddonModData.logView(this.database.id, this.database.name);
 | 
			
		||||
            if (checkCompletion) {
 | 
			
		||||
                CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
 | 
			
		||||
            }
 | 
			
		||||
        } catch {
 | 
			
		||||
            // Ignore errors, the user could be offline.
 | 
			
		||||
        }
 | 
			
		||||
        await AddonModData.logView(this.database.id, this.database.name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -55,6 +55,7 @@ export class AddonModDataEntryPage implements OnInit, OnDestroy {
 | 
			
		||||
    protected entryChangedObserver: CoreEventObserver; // It will observe the changed entry event.
 | 
			
		||||
    protected fields: Record<number, AddonModDataField> = {};
 | 
			
		||||
    protected fieldsArray: AddonModDataField[] = [];
 | 
			
		||||
    protected logAfterFetch = true;
 | 
			
		||||
 | 
			
		||||
    moduleId = 0;
 | 
			
		||||
    courseId!: number;
 | 
			
		||||
@ -149,7 +150,6 @@ export class AddonModDataEntryPage implements OnInit, OnDestroy {
 | 
			
		||||
        this.commentsEnabled = !CoreComments.areCommentsDisabledInSite();
 | 
			
		||||
 | 
			
		||||
        await this.fetchEntryData();
 | 
			
		||||
        this.logView();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -201,6 +201,14 @@ export class AddonModDataEntryPage implements OnInit, OnDestroy {
 | 
			
		||||
                title: this.title,
 | 
			
		||||
                group: this.selectedGroup,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            if (this.logAfterFetch) {
 | 
			
		||||
                this.logAfterFetch = false;
 | 
			
		||||
                await CoreUtils.ignoreErrors(AddonModData.logView(this.database.id, this.database.name));
 | 
			
		||||
 | 
			
		||||
                // Store module viewed. It's done in this page because it can be reached using a link.
 | 
			
		||||
                CoreCourse.storeModuleViewed(this.courseId, this.moduleId);
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            if (!refresh) {
 | 
			
		||||
                // Some call failed, retry without using cache since it might be a new activity.
 | 
			
		||||
@ -225,9 +233,9 @@ export class AddonModDataEntryPage implements OnInit, OnDestroy {
 | 
			
		||||
        this.entryId = undefined;
 | 
			
		||||
        this.entry = undefined;
 | 
			
		||||
        this.entryLoaded = false;
 | 
			
		||||
        this.logAfterFetch = true;
 | 
			
		||||
 | 
			
		||||
        await this.fetchEntryData();
 | 
			
		||||
        this.logView();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -286,9 +294,9 @@ export class AddonModDataEntryPage implements OnInit, OnDestroy {
 | 
			
		||||
        this.entry = undefined;
 | 
			
		||||
        this.entryId = undefined;
 | 
			
		||||
        this.entryLoaded = false;
 | 
			
		||||
        this.logAfterFetch = true;
 | 
			
		||||
 | 
			
		||||
        await this.fetchEntryData();
 | 
			
		||||
        this.logView();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -397,19 +405,6 @@ export class AddonModDataEntryPage implements OnInit, OnDestroy {
 | 
			
		||||
        AddonModData.invalidateEntryData(this.database!.id, this.entryId!);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Log viewing the activity.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async logView(): Promise<void> {
 | 
			
		||||
        if (!this.database || !this.database.id) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await CoreUtils.ignoreErrors(AddonModData.logView(this.database.id, this.database.name));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Component being destroyed.
 | 
			
		||||
     */
 | 
			
		||||
 | 
			
		||||
@ -82,6 +82,7 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity
 | 
			
		||||
 | 
			
		||||
    protected submitObserver: CoreEventObserver;
 | 
			
		||||
    protected syncEventName = AddonModFeedbackSyncProvider.AUTO_SYNCED;
 | 
			
		||||
    protected checkCompletionAfterLog = false;
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        protected content?: IonContent,
 | 
			
		||||
@ -125,15 +126,22 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await this.loadContent(false, true);
 | 
			
		||||
 | 
			
		||||
            if (this.feedback) {
 | 
			
		||||
                CoreUtils.ignoreErrors(AddonModFeedback.logView(this.feedback.id, this.feedback.name));
 | 
			
		||||
            }
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.tabsReady = true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected async logActivity(): Promise<void> {
 | 
			
		||||
        if (!this.feedback) {
 | 
			
		||||
            return; // Shouldn't happen.
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await AddonModFeedback.logView(this.feedback.id, this.feedback.name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
 | 
			
		||||
@ -16,6 +16,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
 | 
			
		||||
import { ActivatedRouteSnapshot } from '@angular/router';
 | 
			
		||||
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
 | 
			
		||||
import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { CoreTextUtils } from '@services/utils/text';
 | 
			
		||||
@ -49,6 +50,7 @@ export class AddonModFeedbackAttemptPage implements OnInit, OnDestroy {
 | 
			
		||||
    loaded = false;
 | 
			
		||||
 | 
			
		||||
    protected attemptId: number;
 | 
			
		||||
    protected fetchSuccess = false;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
 | 
			
		||||
@ -131,6 +133,11 @@ export class AddonModFeedbackAttemptPage implements OnInit, OnDestroy {
 | 
			
		||||
                return attemptItem;
 | 
			
		||||
            }).filter((itemData) => itemData); // Filter items with errors.
 | 
			
		||||
 | 
			
		||||
            if (!this.fetchSuccess) {
 | 
			
		||||
                this.fetchSuccess = true;
 | 
			
		||||
                // Store module viewed. It's done in this page because it can be reached using a link.
 | 
			
		||||
                CoreCourse.storeModuleViewed(this.courseId, this.cmId);
 | 
			
		||||
            }
 | 
			
		||||
        } catch (message) {
 | 
			
		||||
            // Some call failed on fetch, go back.
 | 
			
		||||
            CoreDomUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true);
 | 
			
		||||
 | 
			
		||||
@ -25,6 +25,7 @@ import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { AddonModFeedbackAttemptItem, AddonModFeedbackAttemptsSource } from '../../classes/feedback-attempts-source';
 | 
			
		||||
import { AddonModFeedbackWSAnonAttempt, AddonModFeedbackWSAttempt } from '../../services/feedback';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Page that displays feedback attempts.
 | 
			
		||||
@ -37,14 +38,14 @@ export class AddonModFeedbackAttemptsPage implements AfterViewInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
    @ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
 | 
			
		||||
 | 
			
		||||
    promisedAttempts: CorePromisedValue<CoreListItemsManager<AddonModFeedbackAttemptItem, AddonModFeedbackAttemptsSource>>;
 | 
			
		||||
    promisedAttempts: CorePromisedValue<AddonModFeedbackAttemptsManager>;
 | 
			
		||||
    fetchFailed = false;
 | 
			
		||||
 | 
			
		||||
    constructor(protected route: ActivatedRoute) {
 | 
			
		||||
        this.promisedAttempts = new CorePromisedValue();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get attempts(): CoreListItemsManager<AddonModFeedbackAttemptItem, AddonModFeedbackAttemptsSource> | null {
 | 
			
		||||
    get attempts(): AddonModFeedbackAttemptsManager | null {
 | 
			
		||||
        return this.promisedAttempts.value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -95,7 +96,7 @@ export class AddonModFeedbackAttemptsPage implements AfterViewInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
            source.selectedGroup = CoreNavigator.getRouteNumberParam('group') || 0;
 | 
			
		||||
 | 
			
		||||
            this.promisedAttempts.resolve(new CoreListItemsManager(source, this.route.component));
 | 
			
		||||
            this.promisedAttempts.resolve(new AddonModFeedbackAttemptsManager(source, this.route.component));
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModal(error);
 | 
			
		||||
 | 
			
		||||
@ -181,3 +182,18 @@ export class AddonModFeedbackAttemptsPage implements AfterViewInit, OnDestroy {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Attempts manager.
 | 
			
		||||
 */
 | 
			
		||||
class AddonModFeedbackAttemptsManager extends CoreListItemsManager<AddonModFeedbackAttemptItem, AddonModFeedbackAttemptsSource> {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected async logActivity(): Promise<void> {
 | 
			
		||||
        // Store module viewed. It's done in this page because it can be reached using a link.
 | 
			
		||||
        CoreCourse.storeModuleViewed(this.getSource().COURSE_ID, this.getSource().CM_ID);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -13,6 +13,7 @@
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, OnInit } from '@angular/core';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { IonRefresher } from '@ionic/angular';
 | 
			
		||||
import { CoreGroupInfo, CoreGroups } from '@services/groups';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
@ -34,6 +35,7 @@ export class AddonModFeedbackNonRespondentsPage implements OnInit {
 | 
			
		||||
    protected courseId!: number;
 | 
			
		||||
    protected feedback?: AddonModFeedbackWSFeedback;
 | 
			
		||||
    protected page = 0;
 | 
			
		||||
    protected fetchSuccess = false;
 | 
			
		||||
 | 
			
		||||
    selectedGroup!: number;
 | 
			
		||||
    groupInfo?: CoreGroupInfo;
 | 
			
		||||
@ -81,6 +83,12 @@ export class AddonModFeedbackNonRespondentsPage implements OnInit {
 | 
			
		||||
            this.selectedGroup = CoreGroups.validateGroupId(this.selectedGroup, this.groupInfo);
 | 
			
		||||
 | 
			
		||||
            await this.loadGroupUsers(this.selectedGroup);
 | 
			
		||||
 | 
			
		||||
            if (!this.fetchSuccess) {
 | 
			
		||||
                this.fetchSuccess = true;
 | 
			
		||||
                // Store module viewed. It's done in this page because it can be reached using a link.
 | 
			
		||||
                CoreCourse.storeModuleViewed(this.courseId, this.cmId);
 | 
			
		||||
            }
 | 
			
		||||
        } catch (message) {
 | 
			
		||||
            CoreDomUtils.showErrorModalDefault(message, 'core.course.errorgetmodule', true);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -62,13 +62,6 @@ export class AddonModFolderIndexComponent extends CoreCourseModuleMainResourceCo
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await this.loadContent();
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                await AddonModFolder.logView(this.module.instance, this.module.name);
 | 
			
		||||
                CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
 | 
			
		||||
            } catch {
 | 
			
		||||
                // Ignore errors.
 | 
			
		||||
            }
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.showLoading = false;
 | 
			
		||||
        }
 | 
			
		||||
@ -97,6 +90,13 @@ export class AddonModFolderIndexComponent extends CoreCourseModuleMainResourceCo
 | 
			
		||||
        this.contents = AddonModFolderHelper.formatContents(contents);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected async logActivity(): Promise<void> {
 | 
			
		||||
        await AddonModFolder.logView(this.module.instance, this.module.name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Navigate to a subfolder.
 | 
			
		||||
     *
 | 
			
		||||
 | 
			
		||||
@ -99,6 +99,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
 | 
			
		||||
    protected ratingOfflineObserver?: CoreEventObserver;
 | 
			
		||||
    protected ratingSyncObserver?: CoreEventObserver;
 | 
			
		||||
    protected sourceUnsubscribe?: () => void;
 | 
			
		||||
    protected checkCompletionAfterLog = false; // Use CoreListItemsManager log system instead.
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        public route: ActivatedRoute,
 | 
			
		||||
@ -570,7 +571,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // Check completion since it could be configured to complete once the user adds a new discussion or replies.
 | 
			
		||||
            CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
 | 
			
		||||
            this.checkCompletion();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -692,15 +693,13 @@ class AddonModForumDiscussionsManager extends CoreListItemsManager<AddonModForum
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        CoreUtils.ignoreErrors(
 | 
			
		||||
            AddonModForum.instance
 | 
			
		||||
                .logView(forum.id, forum.name)
 | 
			
		||||
                .then(async () => {
 | 
			
		||||
                    CoreCourse.checkModuleCompletion(this.page.courseId, this.page.module.completiondata);
 | 
			
		||||
        try {
 | 
			
		||||
            await AddonModForum.instance.logView(forum.id, forum.name);
 | 
			
		||||
 | 
			
		||||
                    return;
 | 
			
		||||
                }),
 | 
			
		||||
        );
 | 
			
		||||
            CoreCourse.checkModuleCompletion(this.page.courseId, this.page.module.completiondata);
 | 
			
		||||
        } catch {
 | 
			
		||||
            // Ignore errors.
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -17,6 +17,7 @@ import { Component, OnDestroy, ViewChild, OnInit, AfterViewInit, ElementRef, Opt
 | 
			
		||||
import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
 | 
			
		||||
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
 | 
			
		||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { CoreFileUploader } from '@features/fileuploader/services/fileuploader';
 | 
			
		||||
import { CoreRatingInfo, CoreRatingProvider } from '@features/rating/services/rating';
 | 
			
		||||
import { CoreRatingOffline } from '@features/rating/services/rating-offline';
 | 
			
		||||
@ -118,6 +119,7 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
 | 
			
		||||
    protected ratingOfflineObserver?: CoreEventObserver;
 | 
			
		||||
    protected ratingSyncObserver?: CoreEventObserver;
 | 
			
		||||
    protected changeDiscObserver?: CoreEventObserver;
 | 
			
		||||
    protected fetchSuccess = false;
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        @Optional() protected splitView: CoreSplitViewComponent,
 | 
			
		||||
@ -547,6 +549,12 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
 | 
			
		||||
 | 
			
		||||
            this.hasOfflineRatings =
 | 
			
		||||
                await CoreRatingOffline.hasRatings('mod_forum', 'post', ContextLevel.MODULE, this.cmId, this.discussionId);
 | 
			
		||||
 | 
			
		||||
            if (!this.fetchSuccess) {
 | 
			
		||||
                this.fetchSuccess = true;
 | 
			
		||||
                // Store module viewed. It's done in this page because it can be reached using a link.
 | 
			
		||||
                this.courseId && this.cmId && CoreCourse.storeModuleViewed(this.courseId, this.cmId);
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModal(error);
 | 
			
		||||
        } finally {
 | 
			
		||||
 | 
			
		||||
@ -81,12 +81,13 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
 | 
			
		||||
    protected sourceUnsubscribe?: () => void;
 | 
			
		||||
    protected ratingOfflineObserver?: CoreEventObserver;
 | 
			
		||||
    protected ratingSyncObserver?: CoreEventObserver;
 | 
			
		||||
    protected checkCompletionAfterLog = false; // Use CoreListItemsManager log system instead.
 | 
			
		||||
 | 
			
		||||
    getDivider?: (entry: AddonModGlossaryEntry) => string;
 | 
			
		||||
    showDivider: (entry: AddonModGlossaryEntry, previous?: AddonModGlossaryEntry) => boolean = () => false;
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        protected route: ActivatedRoute,
 | 
			
		||||
        public route: ActivatedRoute,
 | 
			
		||||
        protected content?: IonContent,
 | 
			
		||||
        @Optional() protected courseContentsPage?: CoreCourseContentsPage,
 | 
			
		||||
    ) {
 | 
			
		||||
@ -124,10 +125,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
 | 
			
		||||
            [this.courseId, this.module.id, this.courseContentsPage ? `${AddonModGlossaryModuleHandlerService.PAGE_NAME}/` : ''],
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        this.promisedEntries.resolve(new AddonModGlossaryEntriesManager(
 | 
			
		||||
            source,
 | 
			
		||||
            this.route.component,
 | 
			
		||||
        ));
 | 
			
		||||
        this.promisedEntries.resolve(new AddonModGlossaryEntriesManager(source, this));
 | 
			
		||||
 | 
			
		||||
        this.sourceUnsubscribe = source.addListener({
 | 
			
		||||
            onItemsUpdated: items => this.hasOffline = !!items.find(item => source.isOfflineEntry(item)),
 | 
			
		||||
@ -139,7 +137,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
 | 
			
		||||
                this.showLoadingAndRefresh(false);
 | 
			
		||||
 | 
			
		||||
                // Check completion since it could be configured to complete once the user adds a new entry.
 | 
			
		||||
                CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
 | 
			
		||||
                this.checkCompletion();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
@ -166,12 +164,6 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
 | 
			
		||||
 | 
			
		||||
        await this.loadContent(false, true);
 | 
			
		||||
        await entries.start(this.splitView);
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            // Ignore errors.
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -437,6 +429,14 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
 | 
			
		||||
 */
 | 
			
		||||
class AddonModGlossaryEntriesManager extends CoreListItemsManager<AddonModGlossaryEntryItem, AddonModGlossaryEntriesSource> {
 | 
			
		||||
 | 
			
		||||
    page: AddonModGlossaryIndexComponent;
 | 
			
		||||
 | 
			
		||||
    constructor(source: AddonModGlossaryEntriesSource, page: AddonModGlossaryIndexComponent) {
 | 
			
		||||
        super(source, page.route.component);
 | 
			
		||||
 | 
			
		||||
        this.page = page;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get offlineEntries(): AddonModGlossaryOfflineEntry[] {
 | 
			
		||||
        return this.getSource().offlineEntries;
 | 
			
		||||
    }
 | 
			
		||||
@ -463,7 +463,13 @@ class AddonModGlossaryEntriesManager extends CoreListItemsManager<AddonModGlossa
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await AddonModGlossary.logView(glossary.id, viewMode, glossary.name);
 | 
			
		||||
        try {
 | 
			
		||||
            await AddonModGlossary.logView(glossary.id, viewMode, glossary.name);
 | 
			
		||||
 | 
			
		||||
            CoreCourse.checkModuleCompletion(this.page.courseId, this.page.module.completiondata);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            // Ignore errors.
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -17,6 +17,7 @@ import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router';
 | 
			
		||||
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
 | 
			
		||||
import { CoreCommentsCommentsComponent } from '@features/comments/components/comments/comments';
 | 
			
		||||
import { CoreComments } from '@features/comments/services/comments';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { CoreRatingInfo } from '@features/rating/services/rating';
 | 
			
		||||
import { CoreTag } from '@features/tag/services/tag';
 | 
			
		||||
import { IonRefresher } from '@ionic/angular';
 | 
			
		||||
@ -55,8 +56,10 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy {
 | 
			
		||||
    tagsEnabled = false;
 | 
			
		||||
    commentsEnabled = false;
 | 
			
		||||
    courseId!: number;
 | 
			
		||||
    cmId?: number;
 | 
			
		||||
 | 
			
		||||
    protected entryId!: number;
 | 
			
		||||
    protected fetchSuccess = false;
 | 
			
		||||
 | 
			
		||||
    constructor(protected route: ActivatedRoute) {}
 | 
			
		||||
 | 
			
		||||
@ -72,15 +75,17 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy {
 | 
			
		||||
            this.commentsEnabled = !CoreComments.areCommentsDisabledInSite();
 | 
			
		||||
 | 
			
		||||
            if (routeData.swipeEnabled ?? true) {
 | 
			
		||||
                const cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
 | 
			
		||||
                this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
 | 
			
		||||
                const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
 | 
			
		||||
                    AddonModGlossaryEntriesSource,
 | 
			
		||||
                    [this.courseId, cmId, routeData.glossaryPathPrefix ?? ''],
 | 
			
		||||
                    [this.courseId, this.cmId, routeData.glossaryPathPrefix ?? ''],
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                this.entries = new AddonModGlossaryEntryEntriesSwipeManager(source);
 | 
			
		||||
 | 
			
		||||
                await this.entries.start();
 | 
			
		||||
            } else {
 | 
			
		||||
                this.cmId = CoreNavigator.getRouteNumberParam('cmId');
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModal(error);
 | 
			
		||||
@ -143,6 +148,12 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy {
 | 
			
		||||
            this.entry = result.entry;
 | 
			
		||||
            this.ratingInfo = result.ratinginfo;
 | 
			
		||||
 | 
			
		||||
            if (!this.fetchSuccess) {
 | 
			
		||||
                this.fetchSuccess = true;
 | 
			
		||||
                // Store module viewed. It's done in this page because it can be reached using a link.
 | 
			
		||||
                this.cmId && CoreCourse.storeModuleViewed(this.courseId, this.cmId);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (this.glossary) {
 | 
			
		||||
                // Glossary already loaded, nothing else to load.
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
@ -19,7 +19,6 @@ import { CoreConstants } from '@/core/constants';
 | 
			
		||||
import { CoreSite } from '@classes/site';
 | 
			
		||||
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
 | 
			
		||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { CoreH5PDisplayOptions } from '@features/h5p/classes/core';
 | 
			
		||||
import { CoreH5PHelper } from '@features/h5p/classes/helper';
 | 
			
		||||
import { CoreH5P } from '@features/h5p/services/h5p';
 | 
			
		||||
@ -85,6 +84,7 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
 | 
			
		||||
    protected site: CoreSite;
 | 
			
		||||
    protected observer?: CoreEventObserver;
 | 
			
		||||
    protected messageListenerFunction: (event: MessageEvent) => Promise<void>;
 | 
			
		||||
    protected checkCompletionAfterLog = false; // It's called later, when the user plays the package.
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        protected content?: IonContent,
 | 
			
		||||
@ -390,7 +390,7 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
 | 
			
		||||
        // Mark the activity as viewed.
 | 
			
		||||
        await AddonModH5PActivity.logView(this.h5pActivity.id, this.h5pActivity.name, this.siteId);
 | 
			
		||||
 | 
			
		||||
        CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
 | 
			
		||||
        this.checkCompletion();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -464,7 +464,7 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
 | 
			
		||||
                // Check if the H5P has ended. Final statements don't include a subContentId.
 | 
			
		||||
                const hasEnded = data.statements.some(statement => !statement.object.id.includes('subContentId='));
 | 
			
		||||
                if (hasEnded) {
 | 
			
		||||
                    CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
 | 
			
		||||
                    this.checkCompletion();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
 | 
			
		||||
@ -25,6 +25,7 @@ import {
 | 
			
		||||
    AddonModH5PActivityData,
 | 
			
		||||
    AddonModH5PActivityAttemptResults,
 | 
			
		||||
} from '../../services/h5pactivity';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Page that displays results of an attempt.
 | 
			
		||||
@ -45,6 +46,7 @@ export class AddonModH5PActivityAttemptResultsPage implements OnInit {
 | 
			
		||||
    cmId!: number;
 | 
			
		||||
 | 
			
		||||
    protected attemptId!: number;
 | 
			
		||||
    protected fetchSuccess = false;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
@ -62,17 +64,7 @@ export class AddonModH5PActivityAttemptResultsPage implements OnInit {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await this.fetchData();
 | 
			
		||||
 | 
			
		||||
            if (this.h5pActivity) {
 | 
			
		||||
                await AddonModH5PActivity.logViewReport(this.h5pActivity.id, this.h5pActivity.name, { attemptId: this.attemptId });
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModalDefault(error, 'Error loading attempt.');
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.loaded = true;
 | 
			
		||||
        }
 | 
			
		||||
        await this.fetchData();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -92,13 +84,31 @@ export class AddonModH5PActivityAttemptResultsPage implements OnInit {
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async fetchData(): Promise<void> {
 | 
			
		||||
        this.h5pActivity = await AddonModH5PActivity.getH5PActivity(this.courseId, this.cmId);
 | 
			
		||||
        try {
 | 
			
		||||
            this.h5pActivity = await AddonModH5PActivity.getH5PActivity(this.courseId, this.cmId);
 | 
			
		||||
 | 
			
		||||
        this.attempt = await AddonModH5PActivity.getAttemptResults(this.h5pActivity.id, this.attemptId, {
 | 
			
		||||
            cmId: this.cmId,
 | 
			
		||||
        });
 | 
			
		||||
            this.attempt = await AddonModH5PActivity.getAttemptResults(this.h5pActivity.id, this.attemptId, {
 | 
			
		||||
                cmId: this.cmId,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        await this.fetchUserProfile();
 | 
			
		||||
            await this.fetchUserProfile();
 | 
			
		||||
 | 
			
		||||
            if (!this.fetchSuccess) {
 | 
			
		||||
                this.fetchSuccess = true;
 | 
			
		||||
                CoreUtils.ignoreErrors(AddonModH5PActivity.logViewReport(
 | 
			
		||||
                    this.h5pActivity.id,
 | 
			
		||||
                    this.h5pActivity.name,
 | 
			
		||||
                    { attemptId: this.attemptId },
 | 
			
		||||
                ));
 | 
			
		||||
 | 
			
		||||
                // Store module viewed. It's done in this page because it can be reached using a link.
 | 
			
		||||
                CoreCourse.storeModuleViewed(this.courseId, this.cmId);
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModalDefault(error, 'Error loading attempt.');
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.loaded = true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -26,6 +26,7 @@ import {
 | 
			
		||||
    AddonModH5PActivityData,
 | 
			
		||||
    AddonModH5PActivityUserAttempts,
 | 
			
		||||
} from '../../services/h5pactivity';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Page that displays user attempts of a certain user.
 | 
			
		||||
@ -46,6 +47,7 @@ export class AddonModH5PActivityUserAttemptsPage implements OnInit {
 | 
			
		||||
    isCurrentUser = false;
 | 
			
		||||
 | 
			
		||||
    protected userId!: number;
 | 
			
		||||
    protected fetchSuccess = false;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
@ -65,17 +67,7 @@ export class AddonModH5PActivityUserAttemptsPage implements OnInit {
 | 
			
		||||
 | 
			
		||||
        this.isCurrentUser = this.userId == CoreSites.getCurrentSiteUserId();
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await this.fetchData();
 | 
			
		||||
 | 
			
		||||
            if (this.h5pActivity) {
 | 
			
		||||
                await AddonModH5PActivity.logViewReport(this.h5pActivity.id, this.h5pActivity.name, { userId: this.userId });
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModalDefault(error, 'Error loading attempts.');
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.loaded = true;
 | 
			
		||||
        }
 | 
			
		||||
        await this.fetchData();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -95,12 +87,30 @@ export class AddonModH5PActivityUserAttemptsPage implements OnInit {
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async fetchData(): Promise<void> {
 | 
			
		||||
        this.h5pActivity = await AddonModH5PActivity.getH5PActivity(this.courseId, this.cmId);
 | 
			
		||||
        try {
 | 
			
		||||
            this.h5pActivity = await AddonModH5PActivity.getH5PActivity(this.courseId, this.cmId);
 | 
			
		||||
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            this.fetchAttempts(),
 | 
			
		||||
            this.fetchUserProfile(),
 | 
			
		||||
        ]);
 | 
			
		||||
            await Promise.all([
 | 
			
		||||
                this.fetchAttempts(),
 | 
			
		||||
                this.fetchUserProfile(),
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            if (!this.fetchSuccess) {
 | 
			
		||||
                this.fetchSuccess = true;
 | 
			
		||||
                CoreUtils.ignoreErrors(AddonModH5PActivity.logViewReport(
 | 
			
		||||
                    this.h5pActivity.id,
 | 
			
		||||
                    this.h5pActivity.name,
 | 
			
		||||
                    { userId: this.userId },
 | 
			
		||||
                ));
 | 
			
		||||
 | 
			
		||||
                // Store module viewed. It's done in this page because it can be reached using a link.
 | 
			
		||||
                CoreCourse.storeModuleViewed(this.courseId, this.cmId);
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModalDefault(error, 'Error loading attempts.');
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.loaded = true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -13,6 +13,7 @@
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, OnInit } from '@angular/core';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { CoreUser, CoreUserProfile } from '@features/user/services/user';
 | 
			
		||||
import { IonRefresher } from '@ionic/angular';
 | 
			
		||||
 | 
			
		||||
@ -44,6 +45,7 @@ export class AddonModH5PActivityUsersAttemptsPage implements OnInit {
 | 
			
		||||
    canLoadMore = false;
 | 
			
		||||
 | 
			
		||||
    protected page = 0;
 | 
			
		||||
    protected fetchSuccess = false;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
@ -60,17 +62,7 @@ export class AddonModH5PActivityUsersAttemptsPage implements OnInit {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await this.fetchData();
 | 
			
		||||
 | 
			
		||||
            if (this.h5pActivity) {
 | 
			
		||||
                await AddonModH5PActivity.logViewReport(this.h5pActivity.id, this.h5pActivity.name);
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModalDefault(error, 'Error loading attempts.');
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.loaded = true;
 | 
			
		||||
        }
 | 
			
		||||
        this.fetchData();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -91,11 +83,25 @@ export class AddonModH5PActivityUsersAttemptsPage implements OnInit {
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async fetchData(refresh?: boolean): Promise<void> {
 | 
			
		||||
        this.h5pActivity = await AddonModH5PActivity.getH5PActivity(this.courseId, this.cmId);
 | 
			
		||||
        try {
 | 
			
		||||
            this.h5pActivity = await AddonModH5PActivity.getH5PActivity(this.courseId, this.cmId);
 | 
			
		||||
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            this.fetchUsers(refresh),
 | 
			
		||||
        ]);
 | 
			
		||||
            await Promise.all([
 | 
			
		||||
                this.fetchUsers(refresh),
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            if (!this.fetchSuccess) {
 | 
			
		||||
                this.fetchSuccess = true;
 | 
			
		||||
                CoreUtils.ignoreErrors(AddonModH5PActivity.logViewReport(this.h5pActivity.id, this.h5pActivity.name));
 | 
			
		||||
 | 
			
		||||
                // Store module viewed. It's done in this page because it can be reached using a link.
 | 
			
		||||
                CoreCourse.storeModuleViewed(this.courseId, this.cmId);
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModalDefault(error, 'Error loading attempts.');
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.loaded = true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -20,6 +20,7 @@ import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { makeSingleton } from '@singletons';
 | 
			
		||||
import { AddonModH5PActivity } from '../h5pactivity';
 | 
			
		||||
import { AddonModH5PActivityModuleHandlerService } from './module';
 | 
			
		||||
@ -60,7 +61,7 @@ export class AddonModH5PActivityReportLinkHandlerService extends CoreContentLink
 | 
			
		||||
                    } else {
 | 
			
		||||
                        const userId = params.userid ? Number(params.userid) : undefined;
 | 
			
		||||
 | 
			
		||||
                        this.openUserAttempts(module.id, module.course, siteId, userId);
 | 
			
		||||
                        await this.openUserAttempts(module.id, module.course, instanceId, siteId, userId);
 | 
			
		||||
                    }
 | 
			
		||||
                } catch (error) {
 | 
			
		||||
                    CoreDomUtils.showErrorModalDefault(error, 'Error processing link.');
 | 
			
		||||
@ -100,13 +101,35 @@ export class AddonModH5PActivityReportLinkHandlerService extends CoreContentLink
 | 
			
		||||
     *
 | 
			
		||||
     * @param cmId Module ID.
 | 
			
		||||
     * @param courseId Course ID.
 | 
			
		||||
     * @param id Instance ID.
 | 
			
		||||
     * @param siteId Site ID.
 | 
			
		||||
     * @param userId User ID. If not defined, current user in site.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected openUserAttempts(cmId: number, courseId: number, siteId: string, userId?: number): void {
 | 
			
		||||
        userId = userId || CoreSites.getCurrentSiteUserId();
 | 
			
		||||
        const path = AddonModH5PActivityModuleHandlerService.PAGE_NAME + `/${courseId}/${cmId}/userattempts/${userId}`;
 | 
			
		||||
    protected async openUserAttempts(cmId: number, courseId: number, id: number, siteId: string, userId?: number): Promise<void> {
 | 
			
		||||
        let canViewAllAttempts = false;
 | 
			
		||||
 | 
			
		||||
        if (!userId) {
 | 
			
		||||
            // No user ID specified. Check if current user can view all attempts.
 | 
			
		||||
            userId = CoreSites.getCurrentSiteUserId();
 | 
			
		||||
            canViewAllAttempts = await AddonModH5PActivity.canGetUsersAttempts(siteId);
 | 
			
		||||
 | 
			
		||||
            if (canViewAllAttempts) {
 | 
			
		||||
                const accessInfo = await CoreUtils.ignoreErrors(AddonModH5PActivity.getAccessInformation(id, {
 | 
			
		||||
                    cmId,
 | 
			
		||||
                    siteId,
 | 
			
		||||
                }));
 | 
			
		||||
 | 
			
		||||
                canViewAllAttempts = !!accessInfo?.canreviewattempts;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let path: string;
 | 
			
		||||
        if (canViewAllAttempts) {
 | 
			
		||||
            path = `${AddonModH5PActivityModuleHandlerService.PAGE_NAME}/${courseId}/${cmId}/users`;
 | 
			
		||||
        } else {
 | 
			
		||||
            path = `${AddonModH5PActivityModuleHandlerService.PAGE_NAME}/${courseId}/${cmId}/userattempts/${userId}`;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        CoreNavigator.navigateToSitePath(path, {
 | 
			
		||||
            siteId,
 | 
			
		||||
 | 
			
		||||
@ -45,13 +45,6 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom
 | 
			
		||||
        super.ngOnInit();
 | 
			
		||||
 | 
			
		||||
        await this.loadContent();
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await AddonModImscp.logView(this.module.instance, this.module.name);
 | 
			
		||||
            CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
 | 
			
		||||
        } catch {
 | 
			
		||||
            // Ignore errors.
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -103,6 +96,13 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom
 | 
			
		||||
        this.items = AddonModImscp.createItemList(contents);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected async logActivity(): Promise<void> {
 | 
			
		||||
        await AddonModImscp.logView(this.module.instance, this.module.name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Open IMSCP book with a certain item.
 | 
			
		||||
     *
 | 
			
		||||
 | 
			
		||||
@ -303,7 +303,7 @@ export class AddonModImscpProvider {
 | 
			
		||||
    async storeLastItemViewed(id: number, href: string, courseId: number, siteId?: string): Promise<void> {
 | 
			
		||||
        const site = await CoreSites.getSite(siteId);
 | 
			
		||||
 | 
			
		||||
        await site.storeLastViewed(AddonModImscpProvider.COMPONENT, id, href, String(courseId));
 | 
			
		||||
        await site.storeLastViewed(AddonModImscpProvider.COMPONENT, id, href, { data: String(courseId) });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -18,7 +18,6 @@ import { Component, Input, ViewChild, ElementRef, OnInit, OnDestroy, Optional }
 | 
			
		||||
import { CoreTabsComponent } from '@components/tabs/tabs';
 | 
			
		||||
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
 | 
			
		||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { CoreUser } from '@features/user/services/user';
 | 
			
		||||
import { IonContent, IonInput } from '@ionic/angular';
 | 
			
		||||
import { CoreGroupInfo, CoreGroups } from '@services/groups';
 | 
			
		||||
@ -108,12 +107,6 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo
 | 
			
		||||
        this.selectedTab = this.action == 'report' ? 1 : 0;
 | 
			
		||||
 | 
			
		||||
        await this.loadContent(false, true);
 | 
			
		||||
 | 
			
		||||
        if (!this.lesson || this.preventReasons.length) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.logView();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -285,7 +278,7 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo
 | 
			
		||||
    protected hasSyncSucceed(result: AddonModLessonSyncResult): boolean {
 | 
			
		||||
        if (result.updated || this.dataSent) {
 | 
			
		||||
            // Check completion status if something was sent.
 | 
			
		||||
            CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
 | 
			
		||||
            this.checkCompletion();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.dataSent = false;
 | 
			
		||||
@ -373,20 +366,14 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Log viewing the lesson.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected async logView(): Promise<void> {
 | 
			
		||||
        if (!this.lesson) {
 | 
			
		||||
    protected async logActivity(): Promise<void> {
 | 
			
		||||
        if (!this.lesson || this.preventReasons.length) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await CoreUtils.ignoreErrors(
 | 
			
		||||
            AddonModLesson.logViewLesson(this.lesson.id, this.password, this.lesson.name),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
 | 
			
		||||
        await AddonModLesson.logViewLesson(this.lesson.id, this.password, this.lesson.name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -631,7 +618,7 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo
 | 
			
		||||
            this.preventReasons = preventReason ? [preventReason] : [];
 | 
			
		||||
 | 
			
		||||
            // Log view now that we have the password.
 | 
			
		||||
            this.logView();
 | 
			
		||||
            this.logActivity();
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModal(error);
 | 
			
		||||
        } finally {
 | 
			
		||||
 | 
			
		||||
@ -35,6 +35,7 @@ import {
 | 
			
		||||
} from '../../services/lesson';
 | 
			
		||||
import { AddonModLessonAnswerData, AddonModLessonHelper } from '../../services/lesson-helper';
 | 
			
		||||
import { CoreTimeUtils } from '@services/utils/time';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Page that displays a retake made by a certain user.
 | 
			
		||||
@ -59,6 +60,7 @@ export class AddonModLessonUserRetakePage implements OnInit {
 | 
			
		||||
    protected userId?: number; // User ID to see the retakes.
 | 
			
		||||
    protected retakeNumber?: number; // Number of the initial retake to see.
 | 
			
		||||
    protected previousSelectedRetake?: number; // To be able to detect the previous selected retake when it has changed.
 | 
			
		||||
    protected fetchSuccess = false;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Component being initialized.
 | 
			
		||||
@ -160,6 +162,12 @@ export class AddonModLessonUserRetakePage implements OnInit {
 | 
			
		||||
            this.student.profileimageurl = user?.profileimageurl;
 | 
			
		||||
 | 
			
		||||
            await this.setRetake(this.selectedRetake);
 | 
			
		||||
 | 
			
		||||
            if (!this.fetchSuccess) {
 | 
			
		||||
                this.fetchSuccess = true;
 | 
			
		||||
                // Store module viewed. It's done in this page because it can be reached using a link.
 | 
			
		||||
                CoreCourse.storeModuleViewed(this.courseId, this.cmId);
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModalDefault(error, 'Error getting data.', true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -21,6 +21,7 @@ import { makeSingleton } from '@singletons';
 | 
			
		||||
import { AddonModLtiHelper } from '../lti-helper';
 | 
			
		||||
import { AddonModLtiIndexComponent } from '../../components/index';
 | 
			
		||||
import { CoreModuleHandlerBase } from '@features/course/classes/module-base-handler';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Handler to support LTI modules.
 | 
			
		||||
@ -67,6 +68,8 @@ export class AddonModLtiModuleHandlerService extends CoreModuleHandlerBase imple
 | 
			
		||||
            action: (event: Event, module: CoreCourseModuleData, courseId: number): void => {
 | 
			
		||||
                // Launch the LTI.
 | 
			
		||||
                AddonModLtiHelper.getDataAndLaunch(courseId, module);
 | 
			
		||||
 | 
			
		||||
                CoreCourse.storeModuleViewed(courseId, module.id);
 | 
			
		||||
            },
 | 
			
		||||
        }];
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -52,13 +52,6 @@ export class AddonModPageIndexComponent extends CoreCourseModuleMainResourceComp
 | 
			
		||||
        super.ngOnInit();
 | 
			
		||||
 | 
			
		||||
        await this.loadContent();
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await AddonModPage.logView(this.module.instance, this.module.name);
 | 
			
		||||
            CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
 | 
			
		||||
        } catch {
 | 
			
		||||
            // Ignore errors.
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -118,4 +111,11 @@ export class AddonModPageIndexComponent extends CoreCourseModuleMainResourceComp
 | 
			
		||||
        this.timemodified = 'timemodified' in this.page ? this.page.timemodified : undefined;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected async logActivity(): Promise<void> {
 | 
			
		||||
        await AddonModPage.logView(this.module.instance, this.module.name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -17,7 +17,6 @@ import { Component, OnDestroy, OnInit, Optional } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
 | 
			
		||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { CoreQuestionBehaviourDelegate } from '@features/question/services/behaviour-delegate';
 | 
			
		||||
import { IonContent } from '@ionic/angular';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
@ -121,18 +120,6 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        await this.loadContent(false, true);
 | 
			
		||||
 | 
			
		||||
        if (!this.quiz) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await AddonModQuiz.logViewQuiz(this.quiz.id, this.quiz.name);
 | 
			
		||||
 | 
			
		||||
            CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
 | 
			
		||||
        } catch {
 | 
			
		||||
            // Ignore errors.
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -387,6 +374,17 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected async logActivity(): Promise<void> {
 | 
			
		||||
        if (!this.quiz) {
 | 
			
		||||
            return; // Shouldn't happen.
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await AddonModQuiz.logViewQuiz(this.quiz.id, this.quiz.name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Go to review an attempt that has just been finished.
 | 
			
		||||
     *
 | 
			
		||||
@ -398,7 +396,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // If we go to auto review it means an attempt was finished. Check completion status.
 | 
			
		||||
        CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
 | 
			
		||||
        this.checkCompletion();
 | 
			
		||||
 | 
			
		||||
        // Verify that user can see the review.
 | 
			
		||||
        const attemptId = this.autoReview.attemptId;
 | 
			
		||||
@ -425,7 +423,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
 | 
			
		||||
    protected hasSyncSucceed(result: AddonModQuizSyncResult): boolean {
 | 
			
		||||
        if (result.attemptFinished) {
 | 
			
		||||
            // An attempt was finished, check completion status.
 | 
			
		||||
            CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
 | 
			
		||||
            this.checkCompletion();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // If the sync call isn't rejected it means the sync was successful.
 | 
			
		||||
@ -508,7 +506,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
 | 
			
		||||
 | 
			
		||||
        if (syncEventData.attemptFinished) {
 | 
			
		||||
            // An attempt was finished, check completion status.
 | 
			
		||||
            CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
 | 
			
		||||
            this.checkCompletion();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.quiz && syncEventData.quizId == this.quiz.id) {
 | 
			
		||||
 | 
			
		||||
@ -13,6 +13,7 @@
 | 
			
		||||
// limitations under the License.
 | 
			
		||||
 | 
			
		||||
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { CoreQuestionQuestionParsed } from '@features/question/services/question';
 | 
			
		||||
import { CoreQuestionHelper } from '@features/question/services/question-helper';
 | 
			
		||||
import { IonContent, IonRefresher } from '@ionic/angular';
 | 
			
		||||
@ -73,6 +74,7 @@ export class AddonModQuizReviewPage implements OnInit {
 | 
			
		||||
    protected attemptId!: number; // The attempt being reviewed.
 | 
			
		||||
    protected currentPage!: number; // The current page being reviewed.
 | 
			
		||||
    protected options?: AddonModQuizCombinedReviewOptions; // Review options.
 | 
			
		||||
    protected fetchSuccess = false;
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        protected elementRef: ElementRef,
 | 
			
		||||
@ -99,10 +101,6 @@ export class AddonModQuizReviewPage implements OnInit {
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await this.fetchData();
 | 
			
		||||
 | 
			
		||||
            CoreUtils.ignoreErrors(
 | 
			
		||||
                AddonModQuiz.logViewAttemptReview(this.attemptId, this.quiz!.id, this.quiz!.name),
 | 
			
		||||
            );
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.loaded = true;
 | 
			
		||||
        }
 | 
			
		||||
@ -160,6 +158,16 @@ export class AddonModQuizReviewPage implements OnInit {
 | 
			
		||||
 | 
			
		||||
            // Load questions.
 | 
			
		||||
            await this.loadPage(this.currentPage);
 | 
			
		||||
 | 
			
		||||
            if (!this.fetchSuccess) {
 | 
			
		||||
                this.fetchSuccess = true;
 | 
			
		||||
                CoreUtils.ignoreErrors(
 | 
			
		||||
                    AddonModQuiz.logViewAttemptReview(this.attemptId, this.quiz.id, this.quiz.name),
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                // Store module viewed. It's done in this page because it can be reached using a link.
 | 
			
		||||
                CoreCourse.storeModuleViewed(this.courseId, this.cmId);
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModalDefault(error, 'addon.mod_quiz.errorgetquiz', true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -90,12 +90,6 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        await this.loadContent();
 | 
			
		||||
        try {
 | 
			
		||||
            await AddonModResource.logView(this.module.instance, this.module.name);
 | 
			
		||||
            CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
 | 
			
		||||
        } catch {
 | 
			
		||||
            // Ignore errors.
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -189,6 +183,13 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected async logActivity(): Promise<void> {
 | 
			
		||||
        await AddonModResource.logView(this.module.instance, this.module.name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Opens a file.
 | 
			
		||||
     *
 | 
			
		||||
 | 
			
		||||
@ -90,6 +90,8 @@ export class AddonModResourceModuleHandlerService extends CoreModuleHandlerBase
 | 
			
		||||
                const hide = await this.hideOpenButton(module);
 | 
			
		||||
                if (!hide) {
 | 
			
		||||
                    AddonModResourceHelper.openModuleFile(module, courseId);
 | 
			
		||||
 | 
			
		||||
                    CoreCourse.storeModuleViewed(courseId, module.id);
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
        }];
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,6 @@ import { CoreConstants } from '@/core/constants';
 | 
			
		||||
import { Component, OnInit, Optional } from '@angular/core';
 | 
			
		||||
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
 | 
			
		||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { IonContent } from '@ionic/angular';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { CoreSync } from '@services/sync';
 | 
			
		||||
@ -114,21 +113,6 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
 | 
			
		||||
        if (this.skip) {
 | 
			
		||||
            this.open();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await AddonModScorm.logView(this.scorm.id, this.scorm.name);
 | 
			
		||||
 | 
			
		||||
            this.checkCompletion();
 | 
			
		||||
        } catch {
 | 
			
		||||
            // Ignore errors.
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check the completion.
 | 
			
		||||
     */
 | 
			
		||||
    protected checkCompletion(): void {
 | 
			
		||||
        CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -357,6 +341,17 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom
 | 
			
		||||
        this.gradeFormatted = AddonModScorm.formatGrade(scorm, this.grade);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected async logActivity(): Promise<void> {
 | 
			
		||||
        if (!this.scorm) {
 | 
			
		||||
            return; // Shouldn't happen.
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await AddonModScorm.logView(this.scorm.id, this.scorm.name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Checks if sync has succeed from result sync data.
 | 
			
		||||
     *
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,6 @@ import { Component, OnInit, Optional } from '@angular/core';
 | 
			
		||||
import { CoreIonLoadingElement } from '@classes/ion-loading';
 | 
			
		||||
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
 | 
			
		||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { IonContent } from '@ionic/angular';
 | 
			
		||||
import { CoreSites } from '@services/sites';
 | 
			
		||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
			
		||||
@ -75,13 +74,6 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo
 | 
			
		||||
        this.currentUserId = CoreSites.getCurrentSiteUserId();
 | 
			
		||||
 | 
			
		||||
        await this.loadContent(false, true);
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await AddonModSurvey.logView(this.survey!.id, this.survey!.name);
 | 
			
		||||
            CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
 | 
			
		||||
        } catch {
 | 
			
		||||
            // Ignore errors. Just don't check Module completion.
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -166,6 +158,17 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected async logActivity(): Promise<void> {
 | 
			
		||||
        if (!this.survey) {
 | 
			
		||||
            return; // Shouldn't happen.
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await AddonModSurvey.logView(this.survey.id, this.survey.name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if answers are valid to be submitted.
 | 
			
		||||
     *
 | 
			
		||||
 | 
			
		||||
@ -47,6 +47,8 @@ export class AddonModUrlIndexComponent extends CoreCourseModuleMainResourceCompo
 | 
			
		||||
    mimetype?: string;
 | 
			
		||||
    displayDescription = true;
 | 
			
		||||
 | 
			
		||||
    protected checkCompletionAfterLog = false;
 | 
			
		||||
 | 
			
		||||
    constructor(@Optional() courseContentsPage?: CoreCourseContentsPage) {
 | 
			
		||||
        super('AddonModUrlIndexComponent', courseContentsPage);
 | 
			
		||||
    }
 | 
			
		||||
@ -58,12 +60,6 @@ export class AddonModUrlIndexComponent extends CoreCourseModuleMainResourceCompo
 | 
			
		||||
        super.ngOnInit();
 | 
			
		||||
 | 
			
		||||
        await this.loadContent();
 | 
			
		||||
 | 
			
		||||
        if ((this.shouldIframe ||
 | 
			
		||||
            (this.shouldEmbed && this.isOther)) ||
 | 
			
		||||
            (!this.shouldIframe && (!this.shouldEmbed || !this.isOther))) {
 | 
			
		||||
            this.logView();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -170,12 +166,24 @@ export class AddonModUrlIndexComponent extends CoreCourseModuleMainResourceCompo
 | 
			
		||||
    protected async logView(): Promise<void> {
 | 
			
		||||
        try {
 | 
			
		||||
            await AddonModUrl.logView(this.module.instance, this.module.name);
 | 
			
		||||
            CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
 | 
			
		||||
 | 
			
		||||
            this.checkCompletion();
 | 
			
		||||
        } catch {
 | 
			
		||||
            // Ignore errors.
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected async logActivity(): Promise<void> {
 | 
			
		||||
        if ((this.shouldIframe ||
 | 
			
		||||
            (this.shouldEmbed && this.isOther)) ||
 | 
			
		||||
            (!this.shouldIframe && (!this.shouldEmbed || !this.isOther))) {
 | 
			
		||||
            this.logView();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Opens a file.
 | 
			
		||||
     */
 | 
			
		||||
 | 
			
		||||
@ -63,7 +63,7 @@ export class AddonModUrlModuleHandlerService extends CoreModuleHandlerBase imple
 | 
			
		||||
         * @param module The module object.
 | 
			
		||||
         * @param courseId The course ID.
 | 
			
		||||
         */
 | 
			
		||||
        const openUrl = async (module: CoreCourseModuleData): Promise<void> => {
 | 
			
		||||
        const openUrl = async (module: CoreCourseModuleData, courseId: number): Promise<void> => {
 | 
			
		||||
            try {
 | 
			
		||||
                if (module.instance) {
 | 
			
		||||
                    await AddonModUrl.logView(module.instance, module.name);
 | 
			
		||||
@ -73,6 +73,8 @@ export class AddonModUrlModuleHandlerService extends CoreModuleHandlerBase imple
 | 
			
		||||
                // Ignore errors.
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            CoreCourse.storeModuleViewed(courseId, module.id);
 | 
			
		||||
 | 
			
		||||
            const contents = await CoreCourse.getModuleContents(module);
 | 
			
		||||
            AddonModUrlHelper.open(contents[0].fileurl);
 | 
			
		||||
        };
 | 
			
		||||
@ -89,7 +91,7 @@ export class AddonModUrlModuleHandlerService extends CoreModuleHandlerBase imple
 | 
			
		||||
                    const shouldOpen = await this.shouldOpenLink(module);
 | 
			
		||||
 | 
			
		||||
                    if (shouldOpen) {
 | 
			
		||||
                        openUrl(module);
 | 
			
		||||
                        openUrl(module, courseId);
 | 
			
		||||
                    } else {
 | 
			
		||||
                        this.openActivityPage(module, module.course, options);
 | 
			
		||||
                    }
 | 
			
		||||
@ -101,8 +103,8 @@ export class AddonModUrlModuleHandlerService extends CoreModuleHandlerBase imple
 | 
			
		||||
                hidden: true, // Hide it until we calculate if it should be displayed or not.
 | 
			
		||||
                icon: 'fas-link',
 | 
			
		||||
                label: 'core.openmodinbrowser',
 | 
			
		||||
                action: (event: Event, module: CoreCourseModuleData): void => {
 | 
			
		||||
                    openUrl(module);
 | 
			
		||||
                action: (event: Event, module: CoreCourseModuleData, courseId: number): void => {
 | 
			
		||||
                    openUrl(module, courseId);
 | 
			
		||||
                },
 | 
			
		||||
            }],
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
@ -148,24 +148,6 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp
 | 
			
		||||
                this.openMap();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!this.wiki) {
 | 
			
		||||
            CoreNavigator.back();
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!this.pageId) {
 | 
			
		||||
            try {
 | 
			
		||||
                await AddonModWiki.logView(this.wiki.id, this.wiki.name);
 | 
			
		||||
 | 
			
		||||
                CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
 | 
			
		||||
            } catch {
 | 
			
		||||
                // Ignore errors.
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            CoreUtils.ignoreErrors(AddonModWiki.logPageView(this.pageId, this.wiki.id, this.wiki.name));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -449,6 +431,22 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected async logActivity(): Promise<void> {
 | 
			
		||||
        if (!this.wiki) {
 | 
			
		||||
            return; // Shouldn't happen.
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!this.pageId) {
 | 
			
		||||
            await AddonModWiki.logView(this.wiki.id, this.wiki.name);
 | 
			
		||||
        } else {
 | 
			
		||||
            this.checkCompletionAfterLog = false;
 | 
			
		||||
            CoreUtils.ignoreErrors(AddonModWiki.logPageView(this.pageId, this.wiki.id, this.wiki.name));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get path to the wiki home view. If cannot determine or it's current view, return undefined.
 | 
			
		||||
     *
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,6 @@ import { Component, Input, OnDestroy, OnInit, Optional } from '@angular/core';
 | 
			
		||||
import { Params } from '@angular/router';
 | 
			
		||||
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
 | 
			
		||||
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
 | 
			
		||||
import { CoreCourse } from '@features/course/services/course';
 | 
			
		||||
import { IonContent } from '@ionic/angular';
 | 
			
		||||
import { CoreGroupInfo, CoreGroups } from '@services/groups';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
@ -138,16 +137,6 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
 | 
			
		||||
        super.ngOnInit();
 | 
			
		||||
 | 
			
		||||
        await this.loadContent(false, true);
 | 
			
		||||
        if (!this.workshop) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await AddonModWorkshop.logView(this.workshop.id, this.workshop.name);
 | 
			
		||||
            CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            // Ignore errors.
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -164,7 +153,7 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
 | 
			
		||||
            this.showLoadingAndRefresh(true);
 | 
			
		||||
 | 
			
		||||
            // Check completion since it could be configured to complete once the user adds a new discussion or replies.
 | 
			
		||||
            CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
 | 
			
		||||
            this.checkCompletion();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -257,6 +246,17 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
 | 
			
		||||
        await this.setPhaseInfo();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    protected async logActivity(): Promise<void> {
 | 
			
		||||
        if (!this.workshop) {
 | 
			
		||||
            return; // Shouldn't happen.
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await AddonModWorkshop.logView(this.workshop.id, this.workshop.name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Retrieves and shows submissions grade page.
 | 
			
		||||
     *
 | 
			
		||||
 | 
			
		||||
@ -102,6 +102,7 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy, CanLea
 | 
			
		||||
    protected obsAssessmentSaved: CoreEventObserver;
 | 
			
		||||
    protected syncObserver: CoreEventObserver;
 | 
			
		||||
    protected isDestroyed = false;
 | 
			
		||||
    protected fetchSuccess = false;
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        protected fb: FormBuilder,
 | 
			
		||||
@ -157,12 +158,7 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy, CanLea
 | 
			
		||||
 | 
			
		||||
        await this.fetchSubmissionData();
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await AddonModWorkshop.logViewSubmission(this.submissionId, this.workshopId, this.workshop.name);
 | 
			
		||||
            CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
 | 
			
		||||
        } catch {
 | 
			
		||||
            // Ignore errors.
 | 
			
		||||
        }
 | 
			
		||||
        this.logView();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -447,6 +443,8 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy, CanLea
 | 
			
		||||
            CoreEvents.trigger(AddonModWorkshopProvider.ASSESSMENT_INVALIDATED, null, this.siteId);
 | 
			
		||||
 | 
			
		||||
            await this.fetchSubmissionData();
 | 
			
		||||
 | 
			
		||||
            this.logView();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -596,6 +594,24 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy, CanLea
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Log submission viewed.
 | 
			
		||||
     */
 | 
			
		||||
    protected async logView(): Promise<void> {
 | 
			
		||||
        if (this.fetchSuccess) {
 | 
			
		||||
            return; // Already done.
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.fetchSuccess = true;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await AddonModWorkshop.logViewSubmission(this.submissionId, this.workshopId, this.workshop.name);
 | 
			
		||||
            CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
 | 
			
		||||
        } catch {
 | 
			
		||||
            // Ignore errors.
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Component being destroyed.
 | 
			
		||||
     */
 | 
			
		||||
 | 
			
		||||
@ -54,6 +54,7 @@ export class AddonNotesListPage implements OnInit, OnDestroy {
 | 
			
		||||
    currentUserId!: number;
 | 
			
		||||
 | 
			
		||||
    protected syncObserver!: CoreEventObserver;
 | 
			
		||||
    protected logAfterFetch = true;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        try {
 | 
			
		||||
@ -91,8 +92,6 @@ export class AddonNotesListPage implements OnInit, OnDestroy {
 | 
			
		||||
     */
 | 
			
		||||
    async ngOnInit(): Promise<void> {
 | 
			
		||||
        await this.fetchNotes(true);
 | 
			
		||||
 | 
			
		||||
        CoreUtils.ignoreErrors(AddonNotes.logView(this.courseId, this.userId));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -128,6 +127,11 @@ export class AddonNotesListPage implements OnInit, OnDestroy {
 | 
			
		||||
            } else {
 | 
			
		||||
                this.notes = await AddonNotes.getNotesUserData(notesList);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (this.logAfterFetch) {
 | 
			
		||||
                this.logAfterFetch = false;
 | 
			
		||||
                CoreUtils.ignoreErrors(AddonNotes.logView(this.courseId, this.userId));
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModal(error);
 | 
			
		||||
        } finally {
 | 
			
		||||
@ -172,9 +176,9 @@ export class AddonNotesListPage implements OnInit, OnDestroy {
 | 
			
		||||
        this.notesLoaded = false;
 | 
			
		||||
        this.refreshIcon = CoreConstants.ICON_LOADING;
 | 
			
		||||
        this.syncIcon = CoreConstants.ICON_LOADING;
 | 
			
		||||
        this.logAfterFetch = true;
 | 
			
		||||
 | 
			
		||||
        await this.fetchNotes(true);
 | 
			
		||||
        CoreUtils.ignoreErrors(AddonNotes.logView(this.courseId, this.userId));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -34,6 +34,7 @@ export class CoreListItemsManager<
 | 
			
		||||
    protected pageRouteLocator?: unknown | ActivatedRoute;
 | 
			
		||||
    protected splitView?: CoreSplitViewComponent;
 | 
			
		||||
    protected splitViewOutletSubscription?: Subscription;
 | 
			
		||||
    protected fetchSuccess = false; // Whether a fetch was finished successfully.
 | 
			
		||||
 | 
			
		||||
    constructor(source: Source, pageRouteLocator: unknown | ActivatedRoute) {
 | 
			
		||||
        super(source);
 | 
			
		||||
@ -71,9 +72,6 @@ export class CoreListItemsManager<
 | 
			
		||||
 | 
			
		||||
        // Calculate current selected item.
 | 
			
		||||
        this.updateSelectedItem();
 | 
			
		||||
 | 
			
		||||
        // Log activity.
 | 
			
		||||
        await CoreUtils.ignoreErrors(this.logActivity());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -145,6 +143,8 @@ export class CoreListItemsManager<
 | 
			
		||||
     */
 | 
			
		||||
    async reload(): Promise<void> {
 | 
			
		||||
        await this.getSource().reload();
 | 
			
		||||
 | 
			
		||||
        this.finishSuccessfulFetch();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -152,6 +152,21 @@ export class CoreListItemsManager<
 | 
			
		||||
     */
 | 
			
		||||
    async load(): Promise<void> {
 | 
			
		||||
        await this.getSource().load();
 | 
			
		||||
 | 
			
		||||
        this.finishSuccessfulFetch();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Finish a successful fetch.
 | 
			
		||||
     */
 | 
			
		||||
    protected async finishSuccessfulFetch(): Promise<void> {
 | 
			
		||||
        if (this.fetchSuccess) {
 | 
			
		||||
            return; // Already treated.
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Log activity.
 | 
			
		||||
        this.fetchSuccess = true;
 | 
			
		||||
        await CoreUtils.ignoreErrors(this.logActivity());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -160,7 +160,7 @@ export class CoreSite {
 | 
			
		||||
        this.lastViewedTable = asyncInstance(() => CoreSites.getSiteTable(CoreSite.LAST_VIEWED_TABLE, {
 | 
			
		||||
            siteId: this.getId(),
 | 
			
		||||
            database: this.getDb(),
 | 
			
		||||
            config: { cachingStrategy: CoreDatabaseCachingStrategy.None },
 | 
			
		||||
            config: { cachingStrategy: CoreDatabaseCachingStrategy.Eager },
 | 
			
		||||
            primaryKeyColumns: ['component', 'id'],
 | 
			
		||||
        }));
 | 
			
		||||
        this.setInfo(infos);
 | 
			
		||||
@ -2000,21 +2000,55 @@ export class CoreSite {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get several last viewed for a certain component.
 | 
			
		||||
     *
 | 
			
		||||
     * @param component The component.
 | 
			
		||||
     * @param ids IDs. If not provided or empty, return all last viewed for a component.
 | 
			
		||||
     * @return Resolves with last viewed records, undefined if error.
 | 
			
		||||
     */
 | 
			
		||||
    async getComponentLastViewed(component: string, ids: number[] = []): Promise<CoreSiteLastViewedDBRecord[] | undefined> {
 | 
			
		||||
        try {
 | 
			
		||||
            if (!ids.length) {
 | 
			
		||||
                return await this.lastViewedTable.getMany({ component });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const whereAndParams = SQLiteDB.getInOrEqual(ids);
 | 
			
		||||
 | 
			
		||||
            whereAndParams.sql = 'id ' + whereAndParams.sql + ' AND component = ?';
 | 
			
		||||
            whereAndParams.params.push(component);
 | 
			
		||||
 | 
			
		||||
            return await this.lastViewedTable.getManyWhere({
 | 
			
		||||
                sql: whereAndParams.sql,
 | 
			
		||||
                sqlParams: whereAndParams.params,
 | 
			
		||||
                js: (record) => record.component === component && ids.includes(record.id),
 | 
			
		||||
            });
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            // Not found.
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Store a last viewed record.
 | 
			
		||||
     *
 | 
			
		||||
     * @param component The component.
 | 
			
		||||
     * @param id ID.
 | 
			
		||||
     * @param value Last viewed item value.
 | 
			
		||||
     * @param data Other data.
 | 
			
		||||
     * @param options Options.
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    async storeLastViewed(component: string, id: number, value: string | number, data?: string): Promise<void> {
 | 
			
		||||
    async storeLastViewed(
 | 
			
		||||
        component: string,
 | 
			
		||||
        id: number,
 | 
			
		||||
        value: string | number,
 | 
			
		||||
        options: CoreSiteStoreLastViewedOptions = {},
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        await this.lastViewedTable.insert({
 | 
			
		||||
            component,
 | 
			
		||||
            id,
 | 
			
		||||
            value: String(value),
 | 
			
		||||
            data,
 | 
			
		||||
            data: options.data,
 | 
			
		||||
            timeaccess: options.timeaccess ?? Date.now(),
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -2349,5 +2383,14 @@ export type CoreSiteLastViewedDBRecord = {
 | 
			
		||||
    component: string;
 | 
			
		||||
    id: number;
 | 
			
		||||
    value: string;
 | 
			
		||||
    timeaccess: number;
 | 
			
		||||
    data?: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Options for storeLastViewed.
 | 
			
		||||
 */
 | 
			
		||||
export type CoreSiteStoreLastViewedOptions = {
 | 
			
		||||
    data?: string; // Other data.
 | 
			
		||||
    timeaccess?: number; // Accessed time. If not set, current time.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -169,6 +169,8 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await this.fetchContent(refresh, sync, showErrors);
 | 
			
		||||
 | 
			
		||||
            this.finishSuccessfulFetch();
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            if (!refresh && !CoreSites.getCurrentSite()?.isOfflineDisabled() && this.isNotFoundError(error)) {
 | 
			
		||||
                // Module not found, retry without using cache.
 | 
			
		||||
 | 
			
		||||
@ -72,6 +72,8 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
 | 
			
		||||
    protected showCompletion = false; // Whether to show completion inside the activity.
 | 
			
		||||
    protected displayDescription = true; // Wether to show Module description on module page, and not on summary or the contrary.
 | 
			
		||||
    protected isDestroyed = false; // Whether the component is destroyed.
 | 
			
		||||
    protected fetchSuccess = false; // Whether a fetch was finished successfully.
 | 
			
		||||
    protected checkCompletionAfterLog = true; // Whether to check if completion has changed after calling logActivity.
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        @Optional() @Inject('') loggerName: string = 'CoreCourseModuleMainResourceComponent',
 | 
			
		||||
@ -191,6 +193,8 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await this.fetchContent(refresh);
 | 
			
		||||
 | 
			
		||||
            this.finishSuccessfulFetch();
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            if (!refresh && !CoreSites.getCurrentSite()?.isOfflineDisabled() && this.isNotFoundError(error)) {
 | 
			
		||||
                // Module not found, retry without using cache.
 | 
			
		||||
@ -427,6 +431,47 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Finish a successful fetch.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async finishSuccessfulFetch(): Promise<void> {
 | 
			
		||||
        if (this.fetchSuccess) {
 | 
			
		||||
            return; // Already treated.
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.fetchSuccess = true;
 | 
			
		||||
        CoreCourse.storeModuleViewed(this.courseId, this.module.id, { sectionId: this.module.section });
 | 
			
		||||
 | 
			
		||||
        // Log activity now.
 | 
			
		||||
        try {
 | 
			
		||||
            await this.logActivity();
 | 
			
		||||
 | 
			
		||||
            if (this.checkCompletionAfterLog) {
 | 
			
		||||
                this.checkCompletion();
 | 
			
		||||
            }
 | 
			
		||||
        } catch {
 | 
			
		||||
            // Ignore errors.
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Log activity.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async logActivity(): Promise<void> {
 | 
			
		||||
        // To be overridden.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check the module completion.
 | 
			
		||||
     */
 | 
			
		||||
    protected checkCompletion(): void {
 | 
			
		||||
        CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Component being destroyed.
 | 
			
		||||
     */
 | 
			
		||||
 | 
			
		||||
@ -97,7 +97,9 @@
 | 
			
		||||
 | 
			
		||||
        <ng-container *ngFor="let module of section.modules">
 | 
			
		||||
            <core-course-module *ngIf="module.visibleoncoursepage !== 0" [module]="module" [section]="section"
 | 
			
		||||
                [showActivityDates]="course.showactivitydates" [showCompletionConditions]="course.showcompletionconditions">
 | 
			
		||||
                [showActivityDates]="course.showactivitydates" [showCompletionConditions]="course.showcompletionconditions"
 | 
			
		||||
                [isLastViewed]="lastModuleViewed && lastModuleViewed.cmId === module.id" [class.core-course-module-not-viewed]="
 | 
			
		||||
                    !viewedModules[module.id] && (!module.completiondata || module.completiondata.state === completionStatusIncomplete)">
 | 
			
		||||
            </core-course-module>
 | 
			
		||||
        </ng-container>
 | 
			
		||||
    </section>
 | 
			
		||||
 | 
			
		||||
@ -29,6 +29,7 @@ import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-comp
 | 
			
		||||
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
 | 
			
		||||
import {
 | 
			
		||||
    CoreCourse,
 | 
			
		||||
    CoreCourseModuleCompletionStatus,
 | 
			
		||||
    CoreCourseProvider,
 | 
			
		||||
} from '@features/course/services/course';
 | 
			
		||||
import {
 | 
			
		||||
@ -42,6 +43,7 @@ import { CoreCourseCourseIndexComponent, CoreCourseIndexSectionWithModule } from
 | 
			
		||||
import { CoreBlockHelper } from '@features/block/services/block-helper';
 | 
			
		||||
import { CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
 | 
			
		||||
import { CoreCourseViewedModulesDBRecord } from '@features/course/services/database/course';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Component to display course contents using a certain format. If the format isn't found, use default one.
 | 
			
		||||
@ -90,9 +92,14 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
    stealthModulesSectionId: number = CoreCourseProvider.STEALTH_MODULES_SECTION_ID;
 | 
			
		||||
    loaded = false;
 | 
			
		||||
    highlighted?: string;
 | 
			
		||||
    lastModuleViewed?: CoreCourseViewedModulesDBRecord;
 | 
			
		||||
    viewedModules: Record<number, boolean> = {};
 | 
			
		||||
    completionStatusIncomplete = CoreCourseModuleCompletionStatus.COMPLETION_INCOMPLETE;
 | 
			
		||||
 | 
			
		||||
    protected selectTabObserver?: CoreEventObserver;
 | 
			
		||||
    protected modViewedObserver?: CoreEventObserver;
 | 
			
		||||
    protected lastCourseFormat?: string;
 | 
			
		||||
    protected viewedModulesInitialized = false;
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        protected content: IonContent,
 | 
			
		||||
@ -133,6 +140,24 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.modViewedObserver = CoreEvents.on(CoreEvents.COURSE_MODULE_VIEWED, (data) => {
 | 
			
		||||
            if (data.courseId !== this.course.id) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.viewedModules[data.cmId] = true;
 | 
			
		||||
            if (!this.lastModuleViewed || data.timeaccess > this.lastModuleViewed.timeaccess) {
 | 
			
		||||
                this.lastModuleViewed = data;
 | 
			
		||||
 | 
			
		||||
                if (this.selectedSection) {
 | 
			
		||||
                    // Change section to display the one with the last viewed module
 | 
			
		||||
                    const lastViewedSection = this.getViewedModuleSection(this.sections, data);
 | 
			
		||||
                    if (lastViewedSection && lastViewedSection.id !== this.selectedSection?.id) {
 | 
			
		||||
                        this.sectionChanged(lastViewedSection, data.cmId);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -179,8 +204,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
        this.lastCourseFormat = this.course.format;
 | 
			
		||||
 | 
			
		||||
        this.highlighted = CoreCourseFormatDelegate.getSectionHightlightedName(this.course);
 | 
			
		||||
        const currentSection = await CoreCourseFormatDelegate.getCurrentSection(this.course, this.sections);
 | 
			
		||||
        currentSection.highlighted = true;
 | 
			
		||||
        const currentSectionData = await CoreCourseFormatDelegate.getCurrentSection(this.course, this.sections);
 | 
			
		||||
        currentSectionData.section.highlighted = true;
 | 
			
		||||
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            this.loadCourseFormatComponent(),
 | 
			
		||||
@ -236,6 +261,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
        const hasAllSections = sections[0].id == CoreCourseProvider.ALL_SECTIONS_ID;
 | 
			
		||||
        const hasSeveralSections = sections.length > 2 || (sections.length == 2 && !hasAllSections);
 | 
			
		||||
 | 
			
		||||
        await this.initializeViewedModules();
 | 
			
		||||
 | 
			
		||||
        if (this.selectedSection) {
 | 
			
		||||
            const selectedSection = this.selectedSection;
 | 
			
		||||
            // We have a selected section, but the list has changed. Search the section in the list.
 | 
			
		||||
@ -243,7 +270,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
 | 
			
		||||
            if (!newSection) {
 | 
			
		||||
                // Section not found, calculate which one to use.
 | 
			
		||||
                newSection = await CoreCourseFormatDelegate.getCurrentSection(this.course, sections);
 | 
			
		||||
                const currentSectionData = await CoreCourseFormatDelegate.getCurrentSection(this.course, sections);
 | 
			
		||||
                newSection = currentSectionData.section;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.sectionChanged(newSection);
 | 
			
		||||
@ -269,16 +297,75 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!this.loaded) {
 | 
			
		||||
            // No section specified, not found or not visible, get current section.
 | 
			
		||||
            const section = await CoreCourseFormatDelegate.getCurrentSection(this.course, sections);
 | 
			
		||||
            // No section specified, not found or not visible, load current section or the section with last module viewed.
 | 
			
		||||
            const currentSectionData = await CoreCourseFormatDelegate.getCurrentSection(this.course, sections);
 | 
			
		||||
 | 
			
		||||
            const lastModuleViewed = this.lastModuleViewed;
 | 
			
		||||
            let section = currentSectionData.section;
 | 
			
		||||
            let moduleId: number | undefined;
 | 
			
		||||
 | 
			
		||||
            if (!currentSectionData.forceSelected && lastModuleViewed) {
 | 
			
		||||
                // Search the section with the last module viewed.
 | 
			
		||||
                const lastModuleSection = this.getViewedModuleSection(sections, lastModuleViewed);
 | 
			
		||||
 | 
			
		||||
                section = lastModuleSection || section;
 | 
			
		||||
                moduleId = lastModuleSection ? lastModuleViewed?.cmId : undefined;
 | 
			
		||||
            } else if (lastModuleViewed && currentSectionData.section.modules.some(module => module.id === lastModuleViewed.cmId)) {
 | 
			
		||||
                // Last module viewed is inside the highlighted section.
 | 
			
		||||
                moduleId = lastModuleViewed.cmId;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.loaded = true;
 | 
			
		||||
            this.sectionChanged(section);
 | 
			
		||||
            this.sectionChanged(section, moduleId);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Initialize viewed modules.
 | 
			
		||||
     *
 | 
			
		||||
     * @return Promise resolved when done.
 | 
			
		||||
     */
 | 
			
		||||
    protected async initializeViewedModules(): Promise<void> {
 | 
			
		||||
        if (this.viewedModulesInitialized) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const viewedModules = await CoreCourse.getViewedModules(this.course.id);
 | 
			
		||||
 | 
			
		||||
        this.viewedModulesInitialized = true;
 | 
			
		||||
        this.lastModuleViewed = viewedModules[0];
 | 
			
		||||
        viewedModules.forEach(entry => {
 | 
			
		||||
            this.viewedModules[entry.cmId] = true;
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the section of a viewed module.
 | 
			
		||||
     *
 | 
			
		||||
     * @param sections List of sections.
 | 
			
		||||
     * @param viewedModule Viewed module.
 | 
			
		||||
     * @return Section, undefined if not found.
 | 
			
		||||
     */
 | 
			
		||||
    protected getViewedModuleSection(
 | 
			
		||||
        sections: CoreCourseSection[],
 | 
			
		||||
        viewedModule: CoreCourseViewedModulesDBRecord,
 | 
			
		||||
    ): CoreCourseSection | undefined {
 | 
			
		||||
        if (viewedModule.sectionId) {
 | 
			
		||||
            const lastModuleSection = sections.find(section => section.id === viewedModule.sectionId);
 | 
			
		||||
 | 
			
		||||
            if (lastModuleSection) {
 | 
			
		||||
                return lastModuleSection;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // No sectionId or section not found. Search the module.
 | 
			
		||||
        return sections.find(
 | 
			
		||||
            section => section.modules.some(module => module.id === viewedModule.cmId),
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Display the course index modal.
 | 
			
		||||
     */
 | 
			
		||||
@ -345,8 +432,9 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
     * Function called when selected section changes.
 | 
			
		||||
     *
 | 
			
		||||
     * @param newSection The new selected section.
 | 
			
		||||
     * @param moduleId The module to scroll to.
 | 
			
		||||
     */
 | 
			
		||||
    sectionChanged(newSection: CoreCourseSection): void {
 | 
			
		||||
    sectionChanged(newSection: CoreCourseSection, moduleId?: number): void {
 | 
			
		||||
        const previousValue = this.selectedSection;
 | 
			
		||||
        this.selectedSection = newSection;
 | 
			
		||||
        this.data.section = this.selectedSection;
 | 
			
		||||
@ -377,13 +465,11 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
            this.showMoreActivities();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.moduleId && previousValue === undefined) {
 | 
			
		||||
        // Scroll to module if needed. Give more priority to the input.
 | 
			
		||||
        const moduleIdToScroll = this.moduleId && previousValue === undefined ? this.moduleId : moduleId;
 | 
			
		||||
        if (moduleIdToScroll) {
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
                CoreDomUtils.scrollToElementBySelector(
 | 
			
		||||
                    this.elementRef.nativeElement,
 | 
			
		||||
                    this.content,
 | 
			
		||||
                    '#core-course-module-' + this.moduleId,
 | 
			
		||||
                );
 | 
			
		||||
                this.scrollToModule(moduleIdToScroll);
 | 
			
		||||
            }, 200);
 | 
			
		||||
        } else {
 | 
			
		||||
            this.content.scrollToTop(0);
 | 
			
		||||
@ -399,6 +485,19 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
        this.invalidateSectionButtons();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Scroll to a certain module.
 | 
			
		||||
     *
 | 
			
		||||
     * @param moduleId Module ID.
 | 
			
		||||
     */
 | 
			
		||||
    protected scrollToModule(moduleId: number): void {
 | 
			
		||||
        CoreDomUtils.scrollToElementBySelector(
 | 
			
		||||
            this.elementRef.nativeElement,
 | 
			
		||||
            this.content,
 | 
			
		||||
            '#core-course-module-' + moduleId,
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Compare if two sections are equal.
 | 
			
		||||
     *
 | 
			
		||||
@ -502,7 +601,8 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    ngOnDestroy(): void {
 | 
			
		||||
        this.selectTabObserver && this.selectTabObserver.off();
 | 
			
		||||
        this.selectTabObserver?.off();
 | 
			
		||||
        this.modViewedObserver?.off();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -68,11 +68,11 @@ export class CoreCourseCourseIndexComponent implements OnInit {
 | 
			
		||||
            completionEnabled = this.course.showcompletionconditions;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const currentSection = await CoreCourseFormatDelegate.getCurrentSection(this.course, this.sections);
 | 
			
		||||
        const currentSectionData = await CoreCourseFormatDelegate.getCurrentSection(this.course, this.sections);
 | 
			
		||||
 | 
			
		||||
        if (this.selectedId === undefined) {
 | 
			
		||||
            // Highlight current section if none is selected.
 | 
			
		||||
            this.selectedId = currentSection.id;
 | 
			
		||||
            this.selectedId = currentSectionData.section.id;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Clone sections to add information.
 | 
			
		||||
@ -104,7 +104,7 @@ export class CoreCourseCourseIndexComponent implements OnInit {
 | 
			
		||||
                    name: section.name,
 | 
			
		||||
                    availabilityinfo: !!section.availabilityinfo,
 | 
			
		||||
                    expanded: section.id === this.selectedId,
 | 
			
		||||
                    highlighted: currentSection?.id === section.id,
 | 
			
		||||
                    highlighted: currentSectionData.section.id === section.id,
 | 
			
		||||
                    hasVisibleModules: modules.length > 0,
 | 
			
		||||
                    modules: modules,
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
@ -271,6 +271,10 @@ export class CoreCourseModuleSummaryComponent implements OnInit, OnDestroy {
 | 
			
		||||
            {
 | 
			
		||||
                replace: true,
 | 
			
		||||
                animationDirection: 'back',
 | 
			
		||||
                params: {
 | 
			
		||||
                    module: this.module,
 | 
			
		||||
                    openModule: false,
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -99,6 +99,11 @@
 | 
			
		||||
 | 
			
		||||
            </ion-label>
 | 
			
		||||
        </ion-item>
 | 
			
		||||
 | 
			
		||||
        <div class="core-course-last-module-viewed" *ngIf="isLastViewed">
 | 
			
		||||
            <ion-icon name="fas-eye" aria-hidden="true"></ion-icon>
 | 
			
		||||
            {{ 'core.course.lastaccessedactivity' | translate }}
 | 
			
		||||
        </div>
 | 
			
		||||
    </ng-container>
 | 
			
		||||
 | 
			
		||||
    <!-- Loading. -->
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@
 | 
			
		||||
:host {
 | 
			
		||||
    --horizontal-margin: 10px;
 | 
			
		||||
    --vertical-margin: 10px;
 | 
			
		||||
    --core-course-module-not-viewed-border-color: var(--gray-500);
 | 
			
		||||
 | 
			
		||||
    ion-card {
 | 
			
		||||
        margin: var(--vertical-margin) var(--horizontal-margin);
 | 
			
		||||
@ -91,4 +92,18 @@
 | 
			
		||||
    .core-course-module-info ::ng-deep core-course-module-completion .core-module-automatic-completion-conditions .completioninfo.completion_complete {
 | 
			
		||||
        display: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.core-course-module-not-viewed {
 | 
			
		||||
        --ion-card-border-color: var(--core-course-module-not-viewed-border-color);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .core-course-last-module-viewed {
 | 
			
		||||
        padding: 8px 12px;
 | 
			
		||||
        color: var(--subdued-text-color);
 | 
			
		||||
        border-top: 1px solid var(--stroke);
 | 
			
		||||
 | 
			
		||||
        ion-icon {
 | 
			
		||||
            margin-right: 4px;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -48,6 +48,7 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy {
 | 
			
		||||
    @Input() showActivityDates = false; // Whether to show activity dates.
 | 
			
		||||
    @Input() showCompletionConditions = false; // Whether to show activity completion conditions.
 | 
			
		||||
    @Input() showLegacyCompletion?: boolean; // Whether to show module completion in the old format.
 | 
			
		||||
    @Input() isLastViewed = false; // Whether it's the last module viewed in a course.
 | 
			
		||||
    @Output() completionChanged = new EventEmitter<CoreCourseModuleCompletionData>(); // Notify when module completion changes.
 | 
			
		||||
 | 
			
		||||
    modNameTranslated = '';
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,7 @@
 | 
			
		||||
import { Injectable } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
import { CoreTimeUtils } from '@services/utils/time';
 | 
			
		||||
import { CoreCourseFormatHandler } from '@features/course/services/format-delegate';
 | 
			
		||||
import { CoreCourseFormatCurrentSectionData, CoreCourseFormatHandler } from '@features/course/services/format-delegate';
 | 
			
		||||
import { makeSingleton, Translate } from '@singletons';
 | 
			
		||||
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
 | 
			
		||||
import { CoreCourseWSSection } from '@features/course/services/course';
 | 
			
		||||
@ -41,12 +41,18 @@ export class CoreCourseFormatWeeksHandlerService implements CoreCourseFormatHand
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async getCurrentSection(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise<CoreCourseSection> {
 | 
			
		||||
    async getCurrentSection(
 | 
			
		||||
        course: CoreCourseAnyCourseData,
 | 
			
		||||
        sections: CoreCourseSection[],
 | 
			
		||||
    ): Promise<CoreCourseFormatCurrentSectionData<CoreCourseSection>> {
 | 
			
		||||
        const now = CoreTimeUtils.timestamp();
 | 
			
		||||
 | 
			
		||||
        if ((course.startdate && now < course.startdate) || (course.enddate && now > course.enddate)) {
 | 
			
		||||
            // Course hasn't started yet or it has ended already. Return all sections.
 | 
			
		||||
            return sections[0];
 | 
			
		||||
            return {
 | 
			
		||||
                section: sections[0],
 | 
			
		||||
                forceSelected: false,
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (let i = 0; i < sections.length; i++) {
 | 
			
		||||
@ -57,12 +63,18 @@ export class CoreCourseFormatWeeksHandlerService implements CoreCourseFormatHand
 | 
			
		||||
 | 
			
		||||
            const dates = this.getSectionDates(section, course.startdate || 0);
 | 
			
		||||
            if (now >= dates.start && now < dates.end) {
 | 
			
		||||
                return section;
 | 
			
		||||
                return {
 | 
			
		||||
                    section,
 | 
			
		||||
                    forceSelected: false,
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // The section wasn't found, return all sections.
 | 
			
		||||
        return sections[0];
 | 
			
		||||
        return {
 | 
			
		||||
            section: sections[0],
 | 
			
		||||
            forceSelected: false,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -44,6 +44,7 @@
 | 
			
		||||
    "highlighted": "Highlighted",
 | 
			
		||||
    "insufficientavailablespace": "You are trying to download {{size}}. This will leave your device with insufficient space to operate normally. Please clear some storage space first.",
 | 
			
		||||
    "insufficientavailablequota": "Your device could not allocate space to save this download. It may be reserving space for app and system updates. Please clear some storage space first.",
 | 
			
		||||
    "lastaccessedactivity": "Last accessed activity",
 | 
			
		||||
    "manualcompletionnotsynced": "Manual completion not synchronised.",
 | 
			
		||||
    "modulenotfound": "Resource or activity not found, please make sure you're online and it's still available.",
 | 
			
		||||
    "nocontentavailable": "No content available at the moment.",
 | 
			
		||||
 | 
			
		||||
@ -59,6 +59,7 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
 | 
			
		||||
    protected module?: CoreCourseModuleData;
 | 
			
		||||
    protected modNavOptions?: CoreNavigationOptions;
 | 
			
		||||
    protected isGuest = false;
 | 
			
		||||
    protected openModule = true;
 | 
			
		||||
    protected contentsTab: CoreTabsOutletTab & { pageParams: Params } = {
 | 
			
		||||
        page: CONTENTS_PAGE_NAME,
 | 
			
		||||
        title: 'core.course',
 | 
			
		||||
@ -138,6 +139,7 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
 | 
			
		||||
        this.module = CoreNavigator.getRouteParam<CoreCourseModuleData>('module');
 | 
			
		||||
        this.isGuest = !!CoreNavigator.getRouteBooleanParam('isGuest');
 | 
			
		||||
        this.modNavOptions = CoreNavigator.getRouteParam<CoreNavigationOptions>('modNavOptions');
 | 
			
		||||
        this.openModule = CoreNavigator.getRouteBooleanParam('openModule') ?? true; // If false, just scroll to module.
 | 
			
		||||
        if (!this.modNavOptions) {
 | 
			
		||||
            // Fallback to old way of passing params. @deprecated since 4.0.
 | 
			
		||||
            const modParams = CoreNavigator.getRouteParam<Params>('modParams');
 | 
			
		||||
@ -157,6 +159,10 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
        if (this.module) {
 | 
			
		||||
            this.contentsTab.pageParams.moduleId = this.module.id;
 | 
			
		||||
            if (!this.contentsTab.pageParams.sectionId && !this.contentsTab.pageParams.sectionNumber) {
 | 
			
		||||
                // No section specified, use module section.
 | 
			
		||||
                this.contentsTab.pageParams.sectionId = this.module.section;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.tabs.push(this.contentsTab);
 | 
			
		||||
@ -172,7 +178,7 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
 | 
			
		||||
     * A tab was selected.
 | 
			
		||||
     */
 | 
			
		||||
    tabSelected(): void {
 | 
			
		||||
        if (!this.module || !this.course) {
 | 
			
		||||
        if (!this.module || !this.course || !this.openModule) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        // Now that the first tab has been selected we can load the module.
 | 
			
		||||
 | 
			
		||||
@ -2022,6 +2022,7 @@ export type CoreCourseModuleData = Omit<CoreCourseGetContentsWSModule, 'completi
 | 
			
		||||
    isStealth?: boolean;
 | 
			
		||||
    handlerData?: CoreCourseModuleHandlerData;
 | 
			
		||||
    completiondata?: CoreCourseModuleCompletionData;
 | 
			
		||||
    section: number;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 | 
			
		||||
@ -26,7 +26,9 @@ import { CoreConstants } from '@/core/constants';
 | 
			
		||||
import { makeSingleton, Platform, Translate } from '@singletons';
 | 
			
		||||
import { CoreStatusWithWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws';
 | 
			
		||||
 | 
			
		||||
import { CoreCourseStatusDBRecord, COURSE_STATUS_TABLE } from './database/course';
 | 
			
		||||
import {
 | 
			
		||||
    CoreCourseStatusDBRecord, CoreCourseViewedModulesDBRecord, COURSE_STATUS_TABLE, COURSE_VIEWED_MODULES_TABLE ,
 | 
			
		||||
} from './database/course';
 | 
			
		||||
import { CoreCourseOffline } from './course-offline';
 | 
			
		||||
import { CoreError } from '@classes/errors/error';
 | 
			
		||||
import {
 | 
			
		||||
@ -49,6 +51,7 @@ import { lazyMap, LazyMap } from '@/core/utils/lazy-map';
 | 
			
		||||
import { asyncInstance, AsyncInstance } from '@/core/utils/async-instance';
 | 
			
		||||
import { CoreDatabaseTable } from '@classes/database/database-table';
 | 
			
		||||
import { CoreDatabaseCachingStrategy } from '@classes/database/database-table-proxy';
 | 
			
		||||
import { SQLiteDB } from '@classes/sqlitedb';
 | 
			
		||||
 | 
			
		||||
const ROOT_CACHE_KEY = 'mmCourse:';
 | 
			
		||||
 | 
			
		||||
@ -140,6 +143,7 @@ export class CoreCourseProvider {
 | 
			
		||||
 | 
			
		||||
    protected logger: CoreLogger;
 | 
			
		||||
    protected statusTables: LazyMap<AsyncInstance<CoreDatabaseTable<CoreCourseStatusDBRecord>>>;
 | 
			
		||||
    protected viewedModulesTables: LazyMap<AsyncInstance<CoreDatabaseTable<CoreCourseViewedModulesDBRecord, 'courseId' | 'cmId'>>>;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        this.logger = CoreLogger.getInstance('CoreCourseProvider');
 | 
			
		||||
@ -152,6 +156,17 @@ export class CoreCourseProvider {
 | 
			
		||||
                }),
 | 
			
		||||
            ),
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        this.viewedModulesTables = lazyMap(
 | 
			
		||||
            siteId => asyncInstance(
 | 
			
		||||
                () => CoreSites.getSiteTable<CoreCourseViewedModulesDBRecord, 'courseId' | 'cmId'>(COURSE_VIEWED_MODULES_TABLE, {
 | 
			
		||||
                    siteId,
 | 
			
		||||
                    config: { cachingStrategy: CoreDatabaseCachingStrategy.None },
 | 
			
		||||
                    primaryKeyColumns: ['courseId', 'cmId'],
 | 
			
		||||
                    onDestroy: () => delete this.viewedModulesTables[siteId],
 | 
			
		||||
                }),
 | 
			
		||||
            ),
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -340,6 +355,31 @@ export class CoreCourseProvider {
 | 
			
		||||
        return ROOT_CACHE_KEY + 'activitiescompletion:' + courseId + ':' + userId;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get certain module viewed records in the app.
 | 
			
		||||
     *
 | 
			
		||||
     * @param ids Module IDs.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with map of last module viewed data.
 | 
			
		||||
     */
 | 
			
		||||
    async getCertainModulesViewed(ids: number[] = [], siteId?: string): Promise<Record<number, CoreCourseViewedModulesDBRecord>> {
 | 
			
		||||
        if (!ids.length) {
 | 
			
		||||
            return {};
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const site = await CoreSites.getSite(siteId);
 | 
			
		||||
 | 
			
		||||
        const whereAndParams = SQLiteDB.getInOrEqual(ids);
 | 
			
		||||
 | 
			
		||||
        const entries = await this.viewedModulesTables[site.getId()].getManyWhere({
 | 
			
		||||
            sql: 'cmId ' + whereAndParams.sql,
 | 
			
		||||
            sqlParams: whereAndParams.params,
 | 
			
		||||
            js: (record) => ids.includes(record.cmId),
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return CoreUtils.arrayToObject(entries, 'cmId');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get course blocks.
 | 
			
		||||
     *
 | 
			
		||||
@ -425,6 +465,19 @@ export class CoreCourseProvider {
 | 
			
		||||
        return entries.map((entry) => entry.id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get last module viewed in the app for a course.
 | 
			
		||||
     *
 | 
			
		||||
     * @param id Course ID.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with last module viewed data, undefined if none.
 | 
			
		||||
     */
 | 
			
		||||
    async getLastModuleViewed(id: number, siteId?: string): Promise<CoreCourseViewedModulesDBRecord | undefined> {
 | 
			
		||||
        const viewedModules = await this.getViewedModules(id, siteId);
 | 
			
		||||
 | 
			
		||||
        return viewedModules[0];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a module from Moodle.
 | 
			
		||||
     *
 | 
			
		||||
@ -548,7 +601,7 @@ export class CoreCourseProvider {
 | 
			
		||||
 | 
			
		||||
        let foundModule: CoreCourseGetContentsWSModule | undefined;
 | 
			
		||||
 | 
			
		||||
        const foundSection = sections.some((section) => {
 | 
			
		||||
        const foundSection = sections.find((section) => {
 | 
			
		||||
            if (section.id != CoreCourseProvider.STEALTH_MODULES_SECTION_ID &&
 | 
			
		||||
                sectionId !== undefined &&
 | 
			
		||||
                sectionId != section.id
 | 
			
		||||
@ -562,7 +615,7 @@ export class CoreCourseProvider {
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (foundSection && foundModule) {
 | 
			
		||||
            return this.addAdditionalModuleData(foundModule, courseId);
 | 
			
		||||
            return this.addAdditionalModuleData(foundModule, courseId, foundSection.id);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        throw new CoreError(Translate.instant('core.course.modulenotfound'));
 | 
			
		||||
@ -573,11 +626,13 @@ export class CoreCourseProvider {
 | 
			
		||||
     *
 | 
			
		||||
     * @param module Module.
 | 
			
		||||
     * @param courseId Course ID of the module.
 | 
			
		||||
     * @param sectionId Section ID of the module.
 | 
			
		||||
     * @return Module with additional info.
 | 
			
		||||
     */
 | 
			
		||||
    protected  addAdditionalModuleData(
 | 
			
		||||
    protected addAdditionalModuleData(
 | 
			
		||||
        module: CoreCourseGetContentsWSModule,
 | 
			
		||||
        courseId: number,
 | 
			
		||||
        sectionId: number,
 | 
			
		||||
    ): CoreCourseModuleData {
 | 
			
		||||
        let completionData: CoreCourseModuleCompletionData | undefined = undefined;
 | 
			
		||||
 | 
			
		||||
@ -590,13 +645,12 @@ export class CoreCourseProvider {
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const moduleWithCourse: CoreCourseModuleData = {
 | 
			
		||||
        return  {
 | 
			
		||||
            ...module,
 | 
			
		||||
            course: courseId,
 | 
			
		||||
            section: sectionId,
 | 
			
		||||
            completiondata: completionData,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return moduleWithCourse;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -873,7 +927,7 @@ export class CoreCourseProvider {
 | 
			
		||||
        // Add course to all modules.
 | 
			
		||||
        return sections.map((section) => ({
 | 
			
		||||
            ...section,
 | 
			
		||||
            modules: section.modules.map((module) => this.addAdditionalModuleData(module, courseId)),
 | 
			
		||||
            modules: section.modules.map((module) => this.addAdditionalModuleData(module, courseId, section.id)),
 | 
			
		||||
        }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -901,6 +955,23 @@ export class CoreCourseProvider {
 | 
			
		||||
        return sections.reduce((previous: CoreCourseModuleData[], section) => previous.concat(section.modules || []), []);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get all viewed modules in a course, ordered by timeaccess in descending order.
 | 
			
		||||
     *
 | 
			
		||||
     * @param courseId Course ID.
 | 
			
		||||
     * @param siteId Site ID. If not defined, current site.
 | 
			
		||||
     * @return Promise resolved with the list of viewed modules.
 | 
			
		||||
     */
 | 
			
		||||
    async getViewedModules(courseId: number, siteId?: string): Promise<CoreCourseViewedModulesDBRecord[]> {
 | 
			
		||||
        const site = await CoreSites.getSite(siteId);
 | 
			
		||||
 | 
			
		||||
        return await this.viewedModulesTables[site.getId()].getMany({ courseId }, {
 | 
			
		||||
            sorting: [
 | 
			
		||||
                { timeaccess: 'desc' },
 | 
			
		||||
            ],
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Invalidates course blocks WS call.
 | 
			
		||||
     *
 | 
			
		||||
@ -1348,6 +1419,34 @@ export class CoreCourseProvider {
 | 
			
		||||
        this.triggerCourseStatusChanged(courseId, status, siteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Store activity as viewed.
 | 
			
		||||
     *
 | 
			
		||||
     * @param courseId Chapter ID.
 | 
			
		||||
     * @param cmId Module ID.
 | 
			
		||||
     * @param options Other options.
 | 
			
		||||
     * @return Promise resolved with last chapter viewed, undefined if none.
 | 
			
		||||
     */
 | 
			
		||||
    async storeModuleViewed(courseId: number, cmId: number, options: CoreCourseStoreModuleViewedOptions = {}): Promise<void> {
 | 
			
		||||
        const site = await CoreSites.getSite(options.siteId);
 | 
			
		||||
 | 
			
		||||
        const timeaccess = options.timeaccess ?? Date.now();
 | 
			
		||||
 | 
			
		||||
        await this.viewedModulesTables[site.getId()].insert({
 | 
			
		||||
            courseId,
 | 
			
		||||
            cmId,
 | 
			
		||||
            sectionId: options.sectionId,
 | 
			
		||||
            timeaccess,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        CoreEvents.trigger(CoreEvents.COURSE_MODULE_VIEWED, {
 | 
			
		||||
            courseId,
 | 
			
		||||
            cmId,
 | 
			
		||||
            timeaccess,
 | 
			
		||||
            sectionId: options.sectionId,
 | 
			
		||||
        }, site.getId());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Translate a module name to current language.
 | 
			
		||||
     *
 | 
			
		||||
@ -1770,3 +1869,12 @@ type CoreCompletionUpdateActivityCompletionStatusManuallyWSParams = {
 | 
			
		||||
export type CoreCourseAnyModuleData = CoreCourseModuleData | CoreCourseModuleBasicInfo & {
 | 
			
		||||
    contents?: CoreCourseModuleContentFile[]; // If needed, calculated in the app in loadModuleContents.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Options for storeModuleViewed.
 | 
			
		||||
 */
 | 
			
		||||
export type CoreCourseStoreModuleViewedOptions = {
 | 
			
		||||
    sectionId?: number;
 | 
			
		||||
    timeaccess?: number;
 | 
			
		||||
    siteId?: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -18,9 +18,10 @@ import { CoreSiteSchema } from '@services/sites';
 | 
			
		||||
 * Database variables for CoreCourse service.
 | 
			
		||||
 */
 | 
			
		||||
export const COURSE_STATUS_TABLE = 'course_status';
 | 
			
		||||
export const COURSE_VIEWED_MODULES_TABLE = 'course_viewed_modules';
 | 
			
		||||
export const SITE_SCHEMA: CoreSiteSchema = {
 | 
			
		||||
    name: 'CoreCourseProvider',
 | 
			
		||||
    version: 1,
 | 
			
		||||
    version: 2,
 | 
			
		||||
    tables: [
 | 
			
		||||
        {
 | 
			
		||||
            name: COURSE_STATUS_TABLE,
 | 
			
		||||
@ -53,6 +54,29 @@ export const SITE_SCHEMA: CoreSiteSchema = {
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            name: COURSE_VIEWED_MODULES_TABLE,
 | 
			
		||||
            columns: [
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'courseId',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'cmId',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'timeaccess',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                    notNull: true,
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'sectionId',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
            primaryKeys: ['courseId', 'cmId'],
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -85,7 +109,7 @@ export const OFFLINE_SITE_SCHEMA: CoreSiteSchema = {
 | 
			
		||||
                    type: 'TEXT',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'timecompleted',
 | 
			
		||||
                    name: 'timeaccess',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
@ -102,6 +126,13 @@ export type CoreCourseStatusDBRecord = {
 | 
			
		||||
    previousDownloadTime: number;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type CoreCourseViewedModulesDBRecord = {
 | 
			
		||||
    courseId: number;
 | 
			
		||||
    cmId: number;
 | 
			
		||||
    timeaccess: number;
 | 
			
		||||
    sectionId?: number;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type CoreCourseManualCompletionDBRecord = {
 | 
			
		||||
    cmid: number;
 | 
			
		||||
    completed: number;
 | 
			
		||||
 | 
			
		||||
@ -98,9 +98,13 @@ export interface CoreCourseFormatHandler extends CoreDelegateHandler {
 | 
			
		||||
     *
 | 
			
		||||
     * @param course The course to get the title.
 | 
			
		||||
     * @param sections List of sections.
 | 
			
		||||
     * @return Promise resolved with current section.
 | 
			
		||||
     * @return Promise resolved with current section and whether the section should be selected. If only the section is returned,
 | 
			
		||||
     *         forceSelected will default to false.
 | 
			
		||||
     */
 | 
			
		||||
    getCurrentSection?(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise<CoreCourseSection>;
 | 
			
		||||
    getCurrentSection?(
 | 
			
		||||
        course: CoreCourseAnyCourseData,
 | 
			
		||||
        sections: CoreCourseSection[],
 | 
			
		||||
    ): Promise<CoreCourseFormatCurrentSectionData<CoreCourseSection> | CoreCourseSection>;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the name for the highlighted section.
 | 
			
		||||
@ -299,21 +303,37 @@ export class CoreCourseFormatDelegateService extends CoreDelegate<CoreCourseForm
 | 
			
		||||
     *
 | 
			
		||||
     * @param course The course to get the title.
 | 
			
		||||
     * @param sections List of sections.
 | 
			
		||||
     * @return Promise resolved with current section.
 | 
			
		||||
     * @return Promise.
 | 
			
		||||
     */
 | 
			
		||||
    async getCurrentSection<T = CoreCourseSection>(course: CoreCourseAnyCourseData, sections: T[]): Promise<T> {
 | 
			
		||||
    async getCurrentSection<T = CoreCourseSection>(
 | 
			
		||||
        course: CoreCourseAnyCourseData,
 | 
			
		||||
        sections: T[],
 | 
			
		||||
    ): Promise<CoreCourseFormatCurrentSectionData<T>> {
 | 
			
		||||
        try {
 | 
			
		||||
            const section = await this.executeFunctionOnEnabled<T>(
 | 
			
		||||
            const sectionData = await this.executeFunctionOnEnabled<CoreCourseFormatCurrentSectionData<T> | T>(
 | 
			
		||||
                course.format || '',
 | 
			
		||||
                'getCurrentSection',
 | 
			
		||||
                [course, sections],
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            return section || sections[0];
 | 
			
		||||
            if (sectionData && 'forceSelected' in sectionData) {
 | 
			
		||||
                return sectionData;
 | 
			
		||||
            } else if (sectionData) {
 | 
			
		||||
                // Function just returned the section, don't force selecting it.
 | 
			
		||||
                return {
 | 
			
		||||
                    section: sectionData,
 | 
			
		||||
                    forceSelected: false,
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
        } catch {
 | 
			
		||||
            // This function should never fail. Just return the first section (usually, "All sections").
 | 
			
		||||
            return sections[0];
 | 
			
		||||
            // This function should never fail.
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Return the first section (usually, "All sections").
 | 
			
		||||
        return {
 | 
			
		||||
            section: sections[0],
 | 
			
		||||
            forceSelected: false,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -380,3 +400,8 @@ export class CoreCourseFormatDelegateService extends CoreDelegate<CoreCourseForm
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const CoreCourseFormatDelegate = makeSingleton(CoreCourseFormatDelegateService);
 | 
			
		||||
 | 
			
		||||
export type CoreCourseFormatCurrentSectionData<T = CoreCourseSection> = {
 | 
			
		||||
    section: T; // Current section.
 | 
			
		||||
    forceSelected: boolean; // If true, the app will force selecting the section when opening the course.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -18,7 +18,7 @@ import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
 | 
			
		||||
import { CoreUtils } from '@services/utils/utils';
 | 
			
		||||
import { Translate } from '@singletons';
 | 
			
		||||
import { CoreCourseSection } from '../course-helper';
 | 
			
		||||
import { CoreCourseFormatHandler } from '../format-delegate';
 | 
			
		||||
import { CoreCourseFormatCurrentSectionData, CoreCourseFormatHandler } from '../format-delegate';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Default handler used when the course format doesn't have a specific implementation.
 | 
			
		||||
@ -74,7 +74,10 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler {
 | 
			
		||||
    /**
 | 
			
		||||
     * @inheritdoc
 | 
			
		||||
     */
 | 
			
		||||
    async getCurrentSection(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise<CoreCourseSection> {
 | 
			
		||||
    async getCurrentSection(
 | 
			
		||||
        course: CoreCourseAnyCourseData,
 | 
			
		||||
        sections: CoreCourseSection[],
 | 
			
		||||
    ): Promise<CoreCourseFormatCurrentSectionData<CoreCourseSection>> {
 | 
			
		||||
        let marker: number | undefined;
 | 
			
		||||
 | 
			
		||||
        // We need the "marker" to determine the current section.
 | 
			
		||||
@ -93,12 +96,18 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler {
 | 
			
		||||
            const section = sections.find((sect) => sect.section == marker);
 | 
			
		||||
 | 
			
		||||
            if (section) {
 | 
			
		||||
                return section;
 | 
			
		||||
                return {
 | 
			
		||||
                    section,
 | 
			
		||||
                    forceSelected: true,
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Marked section not found or we couldn't retrieve the marker. Return all sections.
 | 
			
		||||
        return sections[0];
 | 
			
		||||
        return {
 | 
			
		||||
            section: sections[0],
 | 
			
		||||
            forceSelected: false,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 | 
			
		||||
@ -54,6 +54,8 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy {
 | 
			
		||||
    totalColumnsSpan?: number;
 | 
			
		||||
    withinSplitView?: boolean;
 | 
			
		||||
 | 
			
		||||
    protected fetchSuccess = false;
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        protected route: ActivatedRoute,
 | 
			
		||||
        protected element: ElementRef<HTMLElement>,
 | 
			
		||||
@ -93,7 +95,6 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
        await this.courses?.start();
 | 
			
		||||
        await this.fetchInitialGrades();
 | 
			
		||||
        await CoreGrades.logCourseGradesView(this.courseId, this.userId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@ -198,6 +199,11 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy {
 | 
			
		||||
        this.columns = formattedTable.columns;
 | 
			
		||||
        this.rows = formattedTable.rows;
 | 
			
		||||
        this.totalColumnsSpan = formattedTable.columns.reduce((total, column) => total + column.colspan, 0);
 | 
			
		||||
 | 
			
		||||
        if (!this.fetchSuccess) {
 | 
			
		||||
            this.fetchSuccess = true;
 | 
			
		||||
            await CoreGrades.logCourseGradesView(this.courseId, this.userId);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -54,6 +54,7 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
 | 
			
		||||
    newsForumModule?: CoreCourseModuleData;
 | 
			
		||||
 | 
			
		||||
    protected updateSiteObserver: CoreEventObserver;
 | 
			
		||||
    protected fetchSuccess = false;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        // Refresh the enabled flags if site is updated.
 | 
			
		||||
@ -137,13 +138,15 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
 | 
			
		||||
                this.hasContent = result.hasContent || this.hasContent;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Add log in Moodle.
 | 
			
		||||
            CoreUtils.ignoreErrors(CoreCourse.logView(
 | 
			
		||||
                this.siteHomeId,
 | 
			
		||||
                undefined,
 | 
			
		||||
                undefined,
 | 
			
		||||
                this.currentSite.getInfo()?.sitename,
 | 
			
		||||
            ));
 | 
			
		||||
            if (!this.fetchSuccess) {
 | 
			
		||||
                this.fetchSuccess = true;
 | 
			
		||||
                CoreUtils.ignoreErrors(CoreCourse.logView(
 | 
			
		||||
                    this.siteHomeId,
 | 
			
		||||
                    undefined,
 | 
			
		||||
                    undefined,
 | 
			
		||||
                    this.currentSite.getInfo()?.sitename,
 | 
			
		||||
                ));
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            CoreDomUtils.showErrorModalDefault(error, 'core.course.couldnotloadsectioncontent', true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -48,6 +48,7 @@ export class CoreUserProfilePage implements OnInit, OnDestroy {
 | 
			
		||||
    protected site!: CoreSite;
 | 
			
		||||
    protected obsProfileRefreshed: CoreEventObserver;
 | 
			
		||||
    protected subscription?: Subscription;
 | 
			
		||||
    protected fetchSuccess = false;
 | 
			
		||||
 | 
			
		||||
    userLoaded = false;
 | 
			
		||||
    isLoadingHandlers = false;
 | 
			
		||||
@ -106,18 +107,6 @@ export class CoreUserProfilePage implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            await this.fetchUser();
 | 
			
		||||
 | 
			
		||||
            if (!this.user) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                await CoreUser.logView(this.userId, this.courseId, this.user.fullname);
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                this.isDeleted = error?.errorcode === 'userdeleted' || error?.errorcode === 'wsaccessuserdeleted';
 | 
			
		||||
                this.isSuspended = error?.errorcode === 'wsaccessusersuspended';
 | 
			
		||||
                this.isEnrolled = error?.errorcode !== 'notenrolledprofile';
 | 
			
		||||
            }
 | 
			
		||||
        } finally {
 | 
			
		||||
            this.userLoaded = true;
 | 
			
		||||
        }
 | 
			
		||||
@ -162,6 +151,18 @@ export class CoreUserProfilePage implements OnInit, OnDestroy {
 | 
			
		||||
                this.isLoadingHandlers = !CoreUserDelegate.areHandlersLoaded(user.id, context, this.courseId);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (!this.fetchSuccess) {
 | 
			
		||||
                this.fetchSuccess = true;
 | 
			
		||||
 | 
			
		||||
                try {
 | 
			
		||||
                    await CoreUser.logView(this.userId, this.courseId, this.user.fullname);
 | 
			
		||||
                } catch (error) {
 | 
			
		||||
                    this.isDeleted = error?.errorcode === 'userdeleted' || error?.errorcode === 'wsaccessuserdeleted';
 | 
			
		||||
                    this.isSuspended = error?.errorcode === 'wsaccessusersuspended';
 | 
			
		||||
                    this.isEnrolled = error?.errorcode !== 'notenrolledprofile';
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            // Error is null for deleted users, do not show the modal.
 | 
			
		||||
            CoreDomUtils.showErrorModal(error);
 | 
			
		||||
 | 
			
		||||
@ -145,6 +145,10 @@ export const SITE_SCHEMA: CoreSiteSchema = {
 | 
			
		||||
                    name: 'data',
 | 
			
		||||
                    type: 'TEXT',
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'timeaccess',
 | 
			
		||||
                    type: 'INTEGER',
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
            primaryKeys: ['component', 'id'],
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
@ -60,6 +60,7 @@ export interface CoreEventsData {
 | 
			
		||||
    [CoreEvents.FILE_SHARED]: CoreEventFileSharedData;
 | 
			
		||||
    [CoreEvents.APP_LAUNCHED_URL]: CoreEventAppLaunchedData;
 | 
			
		||||
    [CoreEvents.ORIENTATION_CHANGE]: CoreEventOrientationData;
 | 
			
		||||
    [CoreEvents.COURSE_MODULE_VIEWED]: CoreEventCourseModuleViewed;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
@ -110,6 +111,7 @@ export class CoreEvents {
 | 
			
		||||
    static readonly FORM_ACTION = 'form_action';
 | 
			
		||||
    static readonly ACTIVITY_DATA_SENT = 'activity_data_sent';
 | 
			
		||||
    static readonly DEVICE_REGISTERED_IN_MOODLE = 'device_registered_in_moodle';
 | 
			
		||||
    static readonly COURSE_MODULE_VIEWED = 'course_module_viewed';
 | 
			
		||||
 | 
			
		||||
    protected static logger = CoreLogger.getInstance('CoreEvents');
 | 
			
		||||
    protected static observables: { [eventName: string]: Subject<unknown> } = {};
 | 
			
		||||
@ -427,3 +429,13 @@ export type CoreEventAppLaunchedData = {
 | 
			
		||||
export type CoreEventOrientationData = {
 | 
			
		||||
    orientation: CoreScreenOrientation;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Data passed to COURSE_MODULE_VIEWED event.
 | 
			
		||||
 */
 | 
			
		||||
export type CoreEventCourseModuleViewed = {
 | 
			
		||||
    courseId: number;
 | 
			
		||||
    cmId: number;
 | 
			
		||||
    timeaccess: number;
 | 
			
		||||
    sectionId?: number;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -22,7 +22,8 @@ information provided here is intended especially for developers.
 | 
			
		||||
- The parameters of the following functions in CoreCourseHelper have changed: navigateToModuleByInstance, navigateToModule, openModule.
 | 
			
		||||
- fillContextMenu, expandDescription, gotoBlog, prefetch and removeFiles functions have been removed from CoreCourseModuleMainResourceComponent.
 | 
			
		||||
- contextMenuPrefetch and fillContextMenu have been removed from CoreCourseHelper.
 | 
			
		||||
-The variable "loaded" in CoreCourseModuleMainResourceComponent has been changed to "showLoading" to reflect its purpose better.
 | 
			
		||||
- The variable "loaded" in CoreCourseModuleMainResourceComponent has been changed to "showLoading" to reflect its purpose better.
 | 
			
		||||
- The function getCurrentSection of course formats can now return a forceSelected boolean along with the section (defaults to false if not returned).
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user