Skip to content

Commit

Permalink
Merge pull request #4253 from dpalou/MOBILE-4670
Browse files Browse the repository at this point in the history
Mobile 4670
  • Loading branch information
crazyserver authored Dec 10, 2024
2 parents beb98e9 + 4cbb3fe commit 99e1223
Show file tree
Hide file tree
Showing 25 changed files with 653 additions and 132 deletions.
5 changes: 5 additions & 0 deletions scripts/langindex.json
Original file line number Diff line number Diff line change
Expand Up @@ -1571,6 +1571,7 @@
"core.confirmopeninbrowser": "local_moodlemobileapp",
"core.confirmremoveselectedfile": "local_moodlemobileapp",
"core.confirmremoveselectedfiles": "local_moodlemobileapp",
"core.connectandtryagain": "local_moodlemobileapp",
"core.connectionlost": "local_moodlemobileapp",
"core.considereddigitalminor": "moodle",
"core.contactsupport": "local_moodlemobileapp",
Expand All @@ -1588,11 +1589,14 @@
"core.copytoclipboard": "local_moodlemobileapp",
"core.course": "moodle",
"core.course.activitydisabled": "local_moodlemobileapp",
"core.course.activitynotavailableoffline": "local_moodlemobileapp",
"core.course.activitynotyetviewableremoteaddon": "local_moodlemobileapp",
"core.course.activityrequiresconnection": "local_moodlemobileapp",
"core.course.allsections": "local_moodlemobileapp",
"core.course.aria:sectionprogress": "local_moodlemobileapp",
"core.course.availablespace": "local_moodlemobileapp",
"core.course.cannotdeletewhiledownloading": "local_moodlemobileapp",
"core.course.changesofflinemaybelost": "local_moodlemobileapp",
"core.course.communicationroomlink": "course",
"core.course.completion_automatic:done": "course",
"core.course.completion_automatic:failed": "course",
Expand Down Expand Up @@ -2269,6 +2273,7 @@
"core.mygroups": "group",
"core.name": "moodle",
"core.needhelp": "local_moodlemobileapp",
"core.needinternettoaccessit": "local_moodlemobileapp",
"core.networkerroriframemsg": "local_moodlemobileapp",
"core.networkerrormsg": "local_moodlemobileapp",
"core.never": "moodle",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,21 @@
<core-course-module-info [module]="module" [description]="description" [component]="component" [componentId]="componentId"
[courseId]="courseId" [hasDataToSync]="hasOffline" (completionChanged)="onCompletionChange()" />

<!-- Offline disabled. -->
<ion-card class="core-warning-card" *ngIf="!siteCanDownload && playing">
<!-- User tried to play in offline a package that must be played in online. -->
@if (triedToPlay && !isOnline && (!siteCanDownload || hasMissingDependencies)) {
<ion-card class="core-warning-card">
<ion-item>
<ion-icon name="fas-triangle-exclamation" slot="start" aria-hidden="true" />
<ion-label>
@if (!siteCanDownload) {
{{ 'core.h5p.offlinedisabled' | translate }} {{ 'addon.mod_h5pactivity.offlinedisabledwarning' | translate }}
} @else {
{{ 'core.course.activitynotavailableoffline' | translate }} {{ 'core.needinternettoaccessit' | translate }}
}
</ion-label>
</ion-item>
</ion-card>
}

<!-- Preview mode. -->
<ion-card class="core-warning-card" *ngIf="accessInfo && !trackComponent">
Expand Down Expand Up @@ -69,7 +75,7 @@

<core-h5p-iframe *ngIf="playing" [fileUrl]="fileUrl" [displayOptions]="displayOptions" [onlinePlayerUrl]="onlinePlayerUrl"
[trackComponent]="trackComponent" [contextId]="h5pActivity?.context" [enableInAppFullscreen]="true" [saveFreq]="saveFreq"
[state]="contentState" />
[state]="contentState" [component]="component" [componentId]="componentId" [fileTimemodified]="deployedFile?.timemodified" />
</core-loading>

<core-course-module-navigation collapsible-footer [hidden]="showLoading" [courseId]="courseId" [currentModuleId]="module.id" />
89 changes: 86 additions & 3 deletions src/addons/mod/h5pactivity/components/index/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ import {
ADDON_MOD_H5PACTIVITY_STATE_ID,
ADDON_MOD_H5PACTIVITY_TRACK_COMPONENT,
} from '../../constants';
import { CoreH5PMissingDependenciesError } from '@features/h5p/classes/errors/missing-dependencies-error';
import { CoreToasts, ToastDuration } from '@services/toasts';
import { Subscription } from 'rxjs';
import { NgZone, Translate } from '@singletons';
import { CoreError } from '@classes/errors/error';

/**
* Component that displays an H5P activity entry page.
Expand Down Expand Up @@ -89,15 +94,19 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
isOpeningPage = false;
canViewAllAttempts = false;
saveStateEnabled = false;
hasMissingDependencies = false;
saveFreq?: number;
contentState?: string;
isOnline: boolean;
triedToPlay = false;

protected fetchContentDefaultError = 'addon.mod_h5pactivity.errorgetactivity';
protected syncEventName = ADDON_MOD_H5PACTIVITY_AUTO_SYNCED;
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.
protected onlineObserver: Subscription;

constructor(
protected content?: IonContent,
Expand All @@ -111,6 +120,39 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
// Listen for messages from the iframe.
this.messageListenerFunction = (event) => this.onIframeMessage(event);
window.addEventListener('message', this.messageListenerFunction);

this.isOnline = CoreNetwork.isOnline();
this.onlineObserver = CoreNetwork.onChange().subscribe(() => {
// Execute the callback in the Angular zone, so change detection doesn't stop working.
NgZone.run(() => {
this.networkChanged();
});
});
}

/**
* React to a network status change.
*/
protected networkChanged(): void {
const wasOnline = this.isOnline;
this.isOnline = CoreNetwork.isOnline();

if (this.playing && !this.fileUrl && !this.isOnline && wasOnline && this.trackComponent) {
// User lost connection while playing an online package with tracking. Show an error.
CoreDomUtils.showErrorModal(new CoreError(Translate.instant('core.course.changesofflinemaybelost'), {
title: Translate.instant('core.youreoffline'),
}));

return;
}

if (this.isOnline && this.triedToPlay) {
// User couldn't play the package because he was offline, but he reconnected. Try again.
this.triedToPlay = false;
this.play();

return;
}
}

/**
Expand Down Expand Up @@ -164,7 +206,7 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
);
}

if (!this.siteCanDownload || this.state === DownloadStatus.DOWNLOADED) {
if (!this.siteCanDownload || this.state === DownloadStatus.DOWNLOADED || this.hasMissingDependencies) {
// Cannot download the file or already downloaded, play the package directly.
this.play();

Expand Down Expand Up @@ -219,12 +261,18 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
return;
}

this.deployedFile = await AddonModH5PActivity.getDeployedFile(this.h5pActivity, {
const deployedFile = await AddonModH5PActivity.getDeployedFile(this.h5pActivity, {
displayOptions: this.displayOptions,
siteId: this.siteId,
});

this.fileUrl = CoreFileHelper.getFileUrl(this.deployedFile);
this.hasMissingDependencies = await AddonModH5PActivity.hasMissingDependencies(this.module.id, deployedFile);
if (this.hasMissingDependencies) {
return;
}

this.deployedFile = deployedFile;
this.fileUrl = CoreFileHelper.getFileUrl(deployedFile);

// Listen for changes in the state.
const eventName = await CoreFilepool.getFileEventNameByUrl(this.site.getId(), this.fileUrl);
Expand Down Expand Up @@ -362,6 +410,20 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
return;
}

if (error instanceof CoreH5PMissingDependenciesError) {
// Cannot be played offline, use online player.
this.hasMissingDependencies = true;
this.fileUrl = undefined;
this.play();

CoreToasts.show({
message: Translate.instant('core.course.activityrequiresconnection'),
duration: ToastDuration.LONG,
});

return;
}

CoreDomUtils.showErrorModalDefault(error, 'core.errordownloading', true);
}
}
Expand Down Expand Up @@ -448,6 +510,16 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
return;
}

if (!this.fileUrl && !this.isOnline) {
this.triedToPlay = true;

CoreDomUtils.showErrorModal(new CoreError(Translate.instant('core.connectandtryagain'), {
title: Translate.instant('core.course.activitynotavailableoffline'),
}));

return;
}

this.playing = true;

// Mark the activity as viewed.
Expand All @@ -456,6 +528,11 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
this.checkCompletion();

this.analyticsLogEvent('mod_h5pactivity_view_h5pactivity');

if (!this.fileUrl && this.trackComponent) {
// User is playing the package in online, invalidate attempts to fetch latest data.
AddonModH5PActivity.invalidateUserAttempts(this.h5pActivity.id, CoreSites.getCurrentSiteUserId());
}
}

/**
Expand All @@ -466,6 +543,11 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
const userId = CoreSites.getCurrentSiteUserId();

try {
if (!this.fileUrl && this.trackComponent && this.h5pActivity) {
// User is playing the package in online, invalidate attempts to fetch latest data.
await AddonModH5PActivity.invalidateUserAttempts(this.h5pActivity.id, CoreSites.getCurrentSiteUserId());
}

await CoreNavigator.navigateToSitePath(
`${ADDON_MOD_H5PACTIVITY_PAGE_NAME}/${this.courseId}/${this.module.id}/userattempts/${userId}`,
);
Expand Down Expand Up @@ -732,6 +814,7 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
super.ngOnDestroy();

this.observer?.off();
this.onlineObserver.unsubscribe();

// Wait a bit to make sure all messages have been received.
setTimeout(() => {
Expand Down
56 changes: 56 additions & 0 deletions src/addons/mod/h5pactivity/services/h5pactivity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ import {
AddonModH5PActivityGradeMethod,
} from '../constants';
import { CoreCacheUpdateFrequency } from '@/core/constants';
import { CoreFileHelper } from '@services/file-helper';
import { CorePromiseUtils } from '@singletons/promise-utils';
import { CoreH5PMissingDependencyDBRecord } from '@features/h5p/services/database/h5p';

/**
* Service that provides some features for H5P activity.
Expand Down Expand Up @@ -571,6 +574,45 @@ export class AddonModH5PActivityProvider {
return this.getH5PActivityByField(courseId, 'id', id, options);
}

/**
* Get missing dependencies for a certain H5P activity.
*
* @param componentId Component ID.
* @param deployedFile File to check.
* @param siteId Site ID. If not defined, current site.
* @returns Missing dependencies, empty if no missing dependencies.
*/
async getMissingDependencies(
componentId: number,
deployedFile: CoreWSFile,
siteId?: string,
): Promise<CoreH5PMissingDependencyDBRecord[]> {
const fileUrl = CoreFileHelper.getFileUrl(deployedFile);

const missingDependencies =
await CoreH5P.h5pFramework.getMissingDependenciesForComponent(ADDON_MOD_H5PACTIVITY_COMPONENT, componentId, siteId);
if (!missingDependencies.length) {
return [];
}

// The activity had missing dependencies, but the package could have changed (e.g. the teacher fixed it).
// Check which of the dependencies apply to the current package.
const fileId = await CoreH5P.h5pFramework.getFileIdForMissingDependencies(fileUrl, siteId);

const filteredMissingDependencies = missingDependencies.filter(dependency =>
dependency.fileid === fileId && dependency.filetimemodified === deployedFile.timemodified);
if (filteredMissingDependencies.length > 0) {
return filteredMissingDependencies;
}

// Package has changed, delete previous missing dependencies.
await CorePromiseUtils.ignoreErrors(
CoreH5P.h5pFramework.deleteMissingDependenciesForComponent(ADDON_MOD_H5PACTIVITY_COMPONENT, componentId, siteId),
);

return [];
}

/**
* Get cache key for attemps WS calls.
*
Expand Down Expand Up @@ -658,6 +700,20 @@ export class AddonModH5PActivityProvider {

}

/**
* Check if a package has missing dependencies.
*
* @param componentId Component ID.
* @param deployedFile File to check.
* @param siteId Site ID. If not defined, current site.
* @returns Whether the package has missing dependencies.
*/
async hasMissingDependencies(componentId: number, deployedFile: CoreWSFile, siteId?: string): Promise<boolean> {
const missingDependencies = await this.getMissingDependencies(componentId, deployedFile, siteId);

return missingDependencies.length > 0;
}

/**
* Invalidates access information.
*
Expand Down
19 changes: 19 additions & 0 deletions src/addons/mod/h5pactivity/services/handlers/prefetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,12 @@ export class AddonModH5PActivityPrefetchHandlerService extends CoreCourseActivit
siteId: siteId,
});

// If we already detected that the file has missing dependencies there's no need to download it again.
const missingDependencies = await AddonModH5PActivity.getMissingDependencies(module.id, deployedFile, siteId);
if (missingDependencies.length > 0) {
throw CoreH5P.h5pFramework.buildMissingDependenciesErrorFromDBRecords(missingDependencies);
}

if (AddonModH5PActivity.isSaveStateEnabled(h5pActivity)) {
// If the file needs to be downloaded, delete the states because it means the package has changed or user deleted it.
const fileState = await CoreFilepool.getFileStateByUrl(siteId, CoreFileHelper.getFileUrl(deployedFile));
Expand Down Expand Up @@ -254,6 +260,19 @@ export class AddonModH5PActivityPrefetchHandlerService extends CoreCourseActivit
);
}

/**
* @inheritdoc
*/
async removeFiles(module: CoreCourseAnyModuleData, courseId: number): Promise<void> {
// Remove files and delete any missing dependency stored to force recalculating them.
await Promise.all([
super.removeFiles(module, courseId),
CorePromiseUtils.ignoreErrors(
CoreH5P.h5pFramework.deleteMissingDependenciesForComponent(ADDON_MOD_H5PACTIVITY_COMPONENT, module.id),
),
]);
}

}

export const AddonModH5PActivityPrefetchHandler = makeSingleton(AddonModH5PActivityPrefetchHandlerService);
13 changes: 11 additions & 2 deletions src/core/classes/errors/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,29 @@ import { CoreErrorObject } from '@services/error-helper';
*/
export class CoreError extends Error {

title?: string;
debug?: CoreErrorDebug;

constructor(message?: string, debug?: CoreErrorDebug) {
constructor(message?: string, options: CoreErrorOptions = {}) {
super(message);

// Fix prototype chain: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget
this.name = new.target.name;
Object.setPrototypeOf(this, new.target.prototype);

this.debug = debug;
this.title = options.title;
this.debug = options.debug;
}

}

export type CoreErrorOptions = {
// Error title. By default, 'Error'.
title?: string;
// Debugging information.
debug?: CoreErrorDebug;
};

/**
* Debug information of the error.
*/
Expand Down
Loading

0 comments on commit 99e1223

Please sign in to comment.