Skip to content

Commit badb71c

Browse files
authored
Merge pull request #2404 from dpalou/MOBILE-3412
Mobile 3412
2 parents f233d16 + ef3a952 commit badb71c

File tree

29 files changed

+1050
-61
lines changed

29 files changed

+1050
-61
lines changed

scripts/langindex.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,7 @@
684684
"addon.mod_h5pactivity.no_compatible_track": "h5pactivity",
685685
"addon.mod_h5pactivity.offlinedisabledwarning": "local_moodlemobileapp",
686686
"addon.mod_h5pactivity.outcome": "h5pactivity",
687+
"addon.mod_h5pactivity.previewmode": "h5pactivity",
687688
"addon.mod_h5pactivity.result_fill-in": "h5pactivity",
688689
"addon.mod_h5pactivity.result_other": "h5pactivity",
689690
"addon.mod_h5pactivity.review_my_attempts": "h5pactivity",
@@ -1405,6 +1406,7 @@
14051406
"core.confirmdeletefile": "repository",
14061407
"core.confirmgotabroot": "local_moodlemobileapp",
14071408
"core.confirmgotabrootdefault": "local_moodlemobileapp",
1409+
"core.confirmleaveunknownchanges": "local_moodlemobileapp",
14081410
"core.confirmloss": "local_moodlemobileapp",
14091411
"core.confirmopeninbrowser": "local_moodlemobileapp",
14101412
"core.considereddigitalminor": "moodle",
@@ -1858,6 +1860,7 @@
18581860
"core.mod_folder": "folder/pluginname",
18591861
"core.mod_forum": "forum/pluginname",
18601862
"core.mod_glossary": "glossary/pluginname",
1863+
"core.mod_h5pactivity": "h5pactivity/pluginname",
18611864
"core.mod_ims": "imscp/pluginname",
18621865
"core.mod_imscp": "imscp/pluginname",
18631866
"core.mod_label": "label/pluginname",

src/addon/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
<core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" [iconAction]="'open'"></core-context-menu-item>
66
<core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" (action)="expandDescription()" [iconAction]="'arrow-forward'"></core-context-menu-item>
77
<core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}" [iconAction]="'fa-newspaper-o'" (action)="gotoBlog($event)"></core-context-menu-item>
8-
<core-context-menu-item *ngIf="loaded && isOnline" [priority]="700" [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"></core-context-menu-item>
8+
<core-context-menu-item *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false"></core-context-menu-item>
9+
<core-context-menu-item *ngIf="loaded && hasOffline && isOnline" [priority]="600" [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon" [closeOnClick]="false"></core-context-menu-item>
910
<core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch($event)" [iconAction]="prefetchStatusIcon" [closeOnClick]="false"></core-context-menu-item>
1011
<core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.removefiles' | translate:{$a: size}" [iconDescription]="'cube'" (action)="removeFiles($event)" [iconAction]="'trash'" [closeOnClick]="false"></core-context-menu-item>
1112
</core-context-menu>
@@ -16,11 +17,21 @@
1617

1718
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId"></core-course-module-description>
1819

20+
<!-- Offline data stored. -->
21+
<ion-card class="core-warning-card" icon-start *ngIf="hasOffline">
22+
<ion-icon name="warning"></ion-icon> {{ 'core.hasdatatosync' | translate:{$a: moduleName} }}
23+
</ion-card>
24+
1925
<!-- Offline disabled. -->
2026
<ion-card class="core-warning-card" icon-start *ngIf="!siteCanDownload && playing">
2127
<ion-icon name="warning"></ion-icon> {{ 'core.h5p.offlinedisabled' | translate }} {{ 'addon.mod_h5pactivity.offlinedisabledwarning' | translate }}
2228
</ion-card>
2329

30+
<!-- Preview mode. -->
31+
<ion-card class="core-warning-card" icon-start *ngIf="accessInfo && !trackComponent">
32+
<ion-icon name="warning"></ion-icon> {{ 'addon.mod_h5pactivity.previewmode' | translate }}
33+
</ion-card>
34+
2435
<ion-list *ngIf="deployedFile && !playing">
2536
<ion-item text-wrap *ngIf="stateMessage">
2637
<p >{{ stateMessage | translate }}</p>
@@ -39,5 +50,5 @@ <h2 *ngIf="progressMessage">{{ progressMessage | translate }}</h2>
3950
</ion-item>
4051
</ion-list>
4152

42-
<core-h5p-iframe *ngIf="playing" [fileUrl]="fileUrl" [displayOptions]="displayOptions" [onlinePlayerUrl]="onlinePlayerUrl"></core-h5p-iframe>
53+
<core-h5p-iframe *ngIf="playing" [fileUrl]="fileUrl" [displayOptions]="displayOptions" [onlinePlayerUrl]="onlinePlayerUrl" [trackComponent]="trackComponent" [contextId]="h5pActivity.context"></core-h5p-iframe>
4354
</core-loading>

src/addon/mod/h5pactivity/components/index/index.ts

Lines changed: 143 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,15 @@ import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main
2424
import { CoreH5P } from '@core/h5p/providers/h5p';
2525
import { CoreH5PDisplayOptions } from '@core/h5p/classes/core';
2626
import { CoreH5PHelper } from '@core/h5p/classes/helper';
27+
import { CoreXAPI } from '@core/xapi/providers/xapi';
28+
import { CoreXAPIOffline } from '@core/xapi/providers/offline';
2729
import { CoreConstants } from '@core/constants';
2830
import { CoreSite } from '@classes/site';
2931

3032
import {
3133
AddonModH5PActivity, AddonModH5PActivityProvider, AddonModH5PActivityData, AddonModH5PActivityAccessInfo
3234
} from '../../providers/h5pactivity';
35+
import { AddonModH5PActivitySyncProvider, AddonModH5PActivitySync } from '../../providers/sync';
3336

3437
/**
3538
* Component that displays an H5P activity entry page.
@@ -57,17 +60,26 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
5760
fileUrl: string; // The fileUrl to use to play the package.
5861
state: string; // State of the file.
5962
siteCanDownload: boolean;
63+
trackComponent: string; // Component for tracking.
64+
hasOffline: boolean;
65+
isOpeningPage: boolean;
6066

6167
protected fetchContentDefaultError = 'addon.mod_h5pactivity.errorgetactivity';
68+
protected syncEventName = AddonModH5PActivitySyncProvider.AUTO_SYNCED;
6269
protected site: CoreSite;
6370
protected observer;
71+
protected messageListenerFunction: (event: MessageEvent) => Promise<void>;
6472

6573
constructor(injector: Injector,
6674
@Optional() protected content: Content) {
6775
super(injector, content);
6876

6977
this.site = this.sitesProvider.getCurrentSite();
7078
this.siteCanDownload = this.site.canDownloadFiles() && !CoreH5P.instance.isOfflineDisabledInSite();
79+
80+
// Listen for messages from the iframe.
81+
this.messageListenerFunction = this.onIframeMessage.bind(this);
82+
window.addEventListener('message', this.messageListenerFunction);
7183
}
7284

7385
/**
@@ -96,23 +108,30 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
96108
*/
97109
protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> {
98110
try {
99-
this.h5pActivity = await AddonModH5PActivity.instance.getH5PActivity(this.courseId, this.module.id);
111+
this.h5pActivity = await AddonModH5PActivity.instance.getH5PActivity(this.courseId, this.module.id, false, this.siteId);
100112

101113
this.dataRetrieved.emit(this.h5pActivity);
102114
this.description = this.h5pActivity.intro;
103115
this.displayOptions = CoreH5PHelper.decodeDisplayOptions(this.h5pActivity.displayoptions);
104116

105-
if (this.h5pActivity.package && this.h5pActivity.package[0]) {
106-
// The online player should use the original file, not the trusted one.
107-
this.onlinePlayerUrl = CoreH5P.instance.h5pPlayer.calculateOnlinePlayerUrl(
108-
this.site.getURL(), this.h5pActivity.package[0].fileurl, this.displayOptions);
117+
if (sync) {
118+
await this.syncActivity(showErrors);
109119
}
110120

111121
await Promise.all([
122+
this.checkHasOffline(),
112123
this.fetchAccessInfo(),
113124
this.fetchDeployedFileData(),
114125
]);
115126

127+
this.trackComponent = this.accessInfo.cansubmit ? AddonModH5PActivityProvider.TRACK_COMPONENT : '';
128+
129+
if (this.h5pActivity.package && this.h5pActivity.package[0]) {
130+
// The online player should use the original file, not the trusted one.
131+
this.onlinePlayerUrl = CoreH5P.instance.h5pPlayer.calculateOnlinePlayerUrl(
132+
this.site.getURL(), this.h5pActivity.package[0].fileurl, this.displayOptions, this.trackComponent);
133+
}
134+
116135
if (!this.siteCanDownload || this.state == CoreConstants.DOWNLOADED) {
117136
// Cannot download the file or already downloaded, play the package directly.
118137
this.play();
@@ -127,13 +146,22 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
127146
}
128147
}
129148

149+
/**
150+
* Fetch the access info and store it in the right variables.
151+
*
152+
* @return Promise resolved when done.
153+
*/
154+
protected async checkHasOffline(): Promise<void> {
155+
this.hasOffline = await CoreXAPIOffline.instance.contextHasStatements(this.h5pActivity.context, this.siteId);
156+
}
157+
130158
/**
131159
* Fetch the access info and store it in the right variables.
132160
*
133161
* @return Promise resolved when done.
134162
*/
135163
protected async fetchAccessInfo(): Promise<void> {
136-
this.accessInfo = await AddonModH5PActivity.instance.getAccessInformation(this.h5pActivity.id);
164+
this.accessInfo = await AddonModH5PActivity.instance.getAccessInformation(this.h5pActivity.id, false, this.siteId);
137165
}
138166

139167
/**
@@ -322,14 +350,121 @@ export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActiv
322350
/**
323351
* Go to view user events.
324352
*/
325-
viewMyAttempts(): void {
326-
this.navCtrl.push('AddonModH5PActivityUserAttemptsPage', {courseId: this.courseId, h5pActivityId: this.h5pActivity.id});
353+
async viewMyAttempts(): Promise<void> {
354+
this.isOpeningPage = true;
355+
356+
try {
357+
await this.navCtrl.push('AddonModH5PActivityUserAttemptsPage', {
358+
courseId: this.courseId,
359+
h5pActivityId: this.h5pActivity.id,
360+
});
361+
} finally {
362+
this.isOpeningPage = false;
363+
}
364+
}
365+
366+
/**
367+
* Treat an iframe message event.
368+
*
369+
* @param event Event.
370+
* @return Promise resolved when done.
371+
*/
372+
protected async onIframeMessage(event: MessageEvent): Promise<void> {
373+
if (!event.data || !CoreXAPI.instance.canPostStatementsInSite(this.site) || !this.isCurrentXAPIPost(event.data)) {
374+
return;
375+
}
376+
377+
try {
378+
const options = {
379+
offline: this.hasOffline,
380+
courseId: this.courseId,
381+
extra: this.h5pActivity.name,
382+
siteId: this.site.getId(),
383+
};
384+
385+
const sent = await CoreXAPI.instance.postStatements(this.h5pActivity.context, event.data.component,
386+
JSON.stringify(event.data.statements), options);
387+
388+
this.hasOffline = !sent;
389+
390+
if (sent) {
391+
try {
392+
// Invalidate attempts.
393+
await AddonModH5PActivity.instance.invalidateUserAttempts(this.h5pActivity.id, undefined, this.siteId);
394+
} catch (error) {
395+
// Ignore errors.
396+
}
397+
}
398+
} catch (error) {
399+
CoreDomUtils.instance.showErrorModalDefault(error, 'Error sending tracking data.');
400+
}
401+
}
402+
403+
/**
404+
* Check if an event is an XAPI post statement of the current activity.
405+
*
406+
* @param data Event data.
407+
* @return Whether it's an XAPI post statement of the current activity.
408+
*/
409+
protected isCurrentXAPIPost(data: any): boolean {
410+
if (data.context != 'moodleapp' || data.action != 'xapi_post_statement' || !data.statements) {
411+
return false;
412+
}
413+
414+
// Check the event belongs to this activity.
415+
const trackingUrl = data.statements[0] && data.statements[0].object && data.statements[0].object.id;
416+
if (!trackingUrl) {
417+
return false;
418+
}
419+
420+
if (!this.site.containsUrl(trackingUrl)) {
421+
// The event belongs to another site, weird scenario. Maybe some JS running in background.
422+
return false;
423+
}
424+
425+
const match = trackingUrl.match(/xapi\/activity\/(\d+)/);
426+
427+
return match && match[1] == this.h5pActivity.context;
428+
}
429+
430+
/**
431+
* Performs the sync of the activity.
432+
*
433+
* @return Promise resolved when done.
434+
*/
435+
protected sync(): Promise<any> {
436+
return AddonModH5PActivitySync.instance.syncActivity(this.h5pActivity.context, this.site.getId());
437+
}
438+
439+
/**
440+
* An autosync event has been received.
441+
*
442+
* @param syncEventData Data receiven on sync observer.
443+
*/
444+
protected autoSyncEventReceived(syncEventData: any): void {
445+
this.checkHasOffline();
446+
}
447+
448+
/**
449+
* Go to blog posts.
450+
*
451+
* @param event Event.
452+
*/
453+
async gotoBlog(event: any): Promise<void> {
454+
this.isOpeningPage = true;
455+
456+
try {
457+
await super.gotoBlog(event);
458+
} finally {
459+
this.isOpeningPage = false;
460+
}
327461
}
328462

329463
/**
330464
* Component destroyed.
331465
*/
332466
ngOnDestroy(): void {
333467
this.observer && this.observer.off();
468+
window.removeEventListener('message', this.messageListenerFunction);
334469
}
335470
}

src/addon/mod/h5pactivity/h5pactivity.module.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,24 @@
1414

1515
import { NgModule } from '@angular/core';
1616

17+
import { CoreCronDelegate } from '@providers/cron';
1718
import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate';
1819
import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate';
1920
import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
2021

2122
import { AddonModH5PActivityComponentsModule } from './components/components.module';
2223
import { AddonModH5PActivityModuleHandler } from './providers/module-handler';
2324
import { AddonModH5PActivityProvider } from './providers/h5pactivity';
25+
import { AddonModH5PActivitySyncProvider } from './providers/sync';
2426
import { AddonModH5PActivityPrefetchHandler } from './providers/prefetch-handler';
2527
import { AddonModH5PActivityIndexLinkHandler } from './providers/index-link-handler';
2628
import { AddonModH5PActivityReportLinkHandler } from './providers/report-link-handler';
29+
import { AddonModH5PActivitySyncCronHandler } from './providers/sync-cron-handler';
2730

2831
// List of providers (without handlers).
2932
export const ADDON_MOD_H5P_ACTIVITY_PROVIDERS: any[] = [
3033
AddonModH5PActivityProvider,
34+
AddonModH5PActivitySyncProvider,
3135
];
3236

3337
@NgModule({
@@ -38,10 +42,12 @@ export const ADDON_MOD_H5P_ACTIVITY_PROVIDERS: any[] = [
3842
],
3943
providers: [
4044
AddonModH5PActivityProvider,
45+
AddonModH5PActivitySyncProvider,
4146
AddonModH5PActivityModuleHandler,
4247
AddonModH5PActivityPrefetchHandler,
4348
AddonModH5PActivityIndexLinkHandler,
4449
AddonModH5PActivityReportLinkHandler,
50+
AddonModH5PActivitySyncCronHandler,
4551
]
4652
})
4753
export class AddonModH5PActivityModule {
@@ -51,11 +57,14 @@ export class AddonModH5PActivityModule {
5157
prefetchHandler: AddonModH5PActivityPrefetchHandler,
5258
linksDelegate: CoreContentLinksDelegate,
5359
indexHandler: AddonModH5PActivityIndexLinkHandler,
54-
reportLinkHandler: AddonModH5PActivityReportLinkHandler) {
60+
reportLinkHandler: AddonModH5PActivityReportLinkHandler,
61+
cronDelegate: CoreCronDelegate,
62+
syncHandler: AddonModH5PActivitySyncCronHandler) {
5563

5664
moduleDelegate.registerHandler(moduleHandler);
5765
prefetchDelegate.registerHandler(prefetchHandler);
5866
linksDelegate.registerHandler(indexHandler);
5967
linksDelegate.registerHandler(reportLinkHandler);
68+
cronDelegate.register(syncHandler);
6069
}
6170
}

src/addon/mod/h5pactivity/lang/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"no_compatible_track": "This interaction ({{$a}}) does not provide tracking information or the tracking provided is not compatible with the current activity version.",
2525
"offlinedisabledwarning": "You will need to be online to view the H5P package.",
2626
"outcome": "Outcome",
27+
"previewmode": "This content is displayed in preview mode. No attempt tracking will be stored.",
2728
"result_fill-in": "Fill-in text",
2829
"result_other": "Unkown interaction type",
2930
"review_my_attempts": "View my attempts",

src/addon/mod/h5pactivity/pages/index/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@
1414

1515
import { Component, ViewChild } from '@angular/core';
1616
import { IonicPage, NavParams } from 'ionic-angular';
17+
import { CoreDomUtils } from '@providers/utils/dom';
1718
import { AddonModH5PActivityIndexComponent } from '../../components/index/index';
1819
import { AddonModH5PActivityData } from '../../providers/h5pactivity';
1920

21+
import { Translate } from '@singletons/core.singletons';
22+
2023
/**
2124
* Page that displays an H5P activity.
2225
*/
@@ -46,4 +49,17 @@ export class AddonModH5PActivityIndexPage {
4649
updateData(h5p: AddonModH5PActivityData): void {
4750
this.title = h5p.name || this.title;
4851
}
52+
53+
/**
54+
* Check if we can leave the page or not.
55+
*
56+
* @return Resolved if we can leave it, rejected if not.
57+
*/
58+
ionViewCanLeave(): Promise<void> {
59+
if (!this.h5pComponent.playing || this.h5pComponent.isOpeningPage) {
60+
return;
61+
}
62+
63+
return CoreDomUtils.instance.showConfirm(Translate.instance.instant('core.confirmleaveunknownchanges'));
64+
}
4965
}

0 commit comments

Comments
 (0)