MOBILE-4015 core: Support refresh on resume with data-app-url

main
Dani Palou 2022-04-01 11:48:54 +02:00
parent 910d557b87
commit dc6ca1f085
4 changed files with 102 additions and 12 deletions

View File

@ -24,6 +24,7 @@ import {
ViewContainerRef, ViewContainerRef,
ViewChild, ViewChild,
OnDestroy, OnDestroy,
Inject,
} from '@angular/core'; } from '@angular/core';
import { IonContent } from '@ionic/angular'; import { IonContent } from '@ionic/angular';
@ -33,7 +34,7 @@ import { CoreIframeUtils, CoreIframeUtilsProvider } from '@services/utils/iframe
import { CoreTextUtils } from '@services/utils/text'; import { CoreTextUtils } from '@services/utils/text';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { CoreSite } from '@classes/site'; import { CoreSite } from '@classes/site';
import { Translate } from '@singletons'; import { NgZone, Platform, Translate } from '@singletons';
import { CoreExternalContentDirective } from './external-content'; import { CoreExternalContentDirective } from './external-content';
import { CoreLinkDirective } from './link'; import { CoreLinkDirective } from './link';
import { CoreFilter, CoreFilterFilter, CoreFilterFormatTextOptions } from '@features/filter/services/filter'; import { CoreFilter, CoreFilterFilter, CoreFilterFormatTextOptions } from '@features/filter/services/filter';
@ -46,6 +47,8 @@ import { CoreCancellablePromise } from '@classes/cancellable-promise';
import { AsyncComponent } from '@classes/async-component'; import { AsyncComponent } from '@classes/async-component';
import { CoreText } from '@singletons/text'; import { CoreText } from '@singletons/text';
import { CoreDom } from '@singletons/dom'; import { CoreDom } from '@singletons/dom';
import { CoreEvents } from '@singletons/events';
import { CoreRefreshContext, CORE_REFRESH_CONTEXT } from '@/core/utils/refresh-context';
/** /**
* Directive to format text rendered. It renders the HTML and treats all links and media, using CoreLinkDirective * Directive to format text rendered. It renders the HTML and treats all links and media, using CoreLinkDirective
@ -99,6 +102,7 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo
element: ElementRef, element: ElementRef,
@Optional() protected content: IonContent, @Optional() protected content: IonContent,
protected viewContainerRef: ViewContainerRef, protected viewContainerRef: ViewContainerRef,
@Optional() @Inject(CORE_REFRESH_CONTEXT) protected refreshContext?: CoreRefreshContext,
) { ) {
CoreComponentsRegistry.register(element.nativeElement, this); CoreComponentsRegistry.register(element.nativeElement, this);
@ -457,8 +461,8 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo
// Walk through the content to find the links and add our directive to it. // Walk through the content to find the links and add our directive to it.
// Important: We need to look for links first because in 'img' we add new links without core-link. // Important: We need to look for links first because in 'img' we add new links without core-link.
anchors.forEach((anchor) => { anchors.forEach((anchor) => {
if (anchor.getAttribute('data-app-url')) { if (anchor.dataset.appUrl) {
// Link already treated in data-app-url, ignore it. // Link already treated in treatAppUrlElements, ignore it.
return; return;
} }
@ -563,7 +567,7 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo
const appUrlElements = Array.from(div.querySelectorAll<HTMLElement>('*[data-app-url]')); const appUrlElements = Array.from(div.querySelectorAll<HTMLElement>('*[data-app-url]'));
appUrlElements.forEach((element) => { appUrlElements.forEach((element) => {
const url = element.getAttribute('data-app-url'); const url = element.dataset.appUrl;
if (!url) { if (!url) {
return; return;
} }
@ -582,8 +586,9 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo
return; return;
} }
const confirmMessage = element.getAttribute('data-app-url-confirm'); const confirmMessage = element.dataset.appUrlConfirm;
const openInApp = element.getAttribute('data-open-in') === 'app'; const openInApp = element.dataset.openIn === 'app';
const refreshOnResume = element.dataset.appUrlResumeAction === 'refresh';
if (confirmMessage) { if (confirmMessage) {
try { try {
@ -595,10 +600,26 @@ export class CoreFormatTextDirective implements OnChanges, OnDestroy, AsyncCompo
if (openInApp) { if (openInApp) {
site.openInAppWithAutoLoginIfSameSite(url); site.openInAppWithAutoLoginIfSameSite(url);
if (refreshOnResume && this.refreshContext) {
// Refresh the context when the IAB is closed.
CoreEvents.once(CoreEvents.IAB_EXIT, () => {
this.refreshContext?.refreshContext();
});
}
} else { } else {
site.openInBrowserWithAutoLoginIfSameSite(url, undefined, { site.openInBrowserWithAutoLoginIfSameSite(url, undefined, {
showBrowserWarning: !confirmMessage, showBrowserWarning: !confirmMessage,
}); });
if (refreshOnResume && this.refreshContext) {
// Refresh the context when the app is resumed.
CoreSubscriptions.once(Platform.resume, () => {
NgZone.run(async () => {
this.refreshContext?.refreshContext();
});
});
}
} }
}); });
}); });

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component, ViewChild, OnInit, OnDestroy } from '@angular/core'; import { Component, ViewChild, OnInit, OnDestroy, forwardRef } from '@angular/core';
import { IonContent, IonRefresher } from '@ionic/angular'; import { IonContent, IonRefresher } from '@ionic/angular';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
@ -36,6 +36,7 @@ import {
CoreEventObserver, CoreEventObserver,
} from '@singletons/events'; } from '@singletons/events';
import { CoreNavigator } from '@services/navigator'; import { CoreNavigator } from '@services/navigator';
import { CoreRefreshContext, CORE_REFRESH_CONTEXT } from '@/core/utils/refresh-context';
/** /**
* Page that displays the contents of a course. * Page that displays the contents of a course.
@ -43,8 +44,12 @@ import { CoreNavigator } from '@services/navigator';
@Component({ @Component({
selector: 'page-core-course-contents', selector: 'page-core-course-contents',
templateUrl: 'contents.html', templateUrl: 'contents.html',
providers: [{
provide: CORE_REFRESH_CONTEXT,
useExisting: forwardRef(() => CoreCourseContentsPage),
}],
}) })
export class CoreCourseContentsPage implements OnInit, OnDestroy { export class CoreCourseContentsPage implements OnInit, OnDestroy, CoreRefreshContext {
@ViewChild(IonContent) content?: IonContent; @ViewChild(IonContent) content?: IonContent;
@ViewChild(CoreCourseFormatComponent) formatComponent?: CoreCourseFormatComponent; @ViewChild(CoreCourseFormatComponent) formatComponent?: CoreCourseFormatComponent;
@ -127,7 +132,7 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
CoreEvents.COMPLETION_MODULE_VIEWED, CoreEvents.COMPLETION_MODULE_VIEWED,
(data) => { (data) => {
if (data && data.courseId == this.course.id) { if (data && data.courseId == this.course.id) {
this.refreshAfterCompletionChange(true); this.showLoadingAndRefresh(true, false);
} }
}, },
); );
@ -141,7 +146,7 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
return; return;
} }
this.refreshAfterCompletionChange(false); this.showLoadingAndRefresh(false, false);
if (data.warnings && data.warnings[0]) { if (data.warnings && data.warnings[0]) {
CoreDomUtils.showErrorModal(data.warnings[0]); CoreDomUtils.showErrorModal(data.warnings[0]);
@ -315,7 +320,7 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
await CoreUtils.ignoreErrors(this.invalidateData()); await CoreUtils.ignoreErrors(this.invalidateData());
await this.refreshAfterCompletionChange(true); await this.showLoadingAndRefresh(true, false);
} }
/** /**
@ -341,9 +346,10 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
* Refresh list after a completion change since there could be new activities. * Refresh list after a completion change since there could be new activities.
* *
* @param sync If it should try to sync. * @param sync If it should try to sync.
* @param invalidateData Whether to invalidate data. Set it to false if data has already been invalidated.
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
protected async refreshAfterCompletionChange(sync?: boolean): Promise<void> { protected async showLoadingAndRefresh(sync = false, invalidateData = true): Promise<void> {
// Save scroll position to restore it once done. // Save scroll position to restore it once done.
const scrollElement = await this.content?.getScrollElement(); const scrollElement = await this.content?.getScrollElement();
const scrollTop = scrollElement?.scrollTop || 0; const scrollTop = scrollElement?.scrollTop || 0;
@ -353,6 +359,10 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
this.content?.scrollToTop(0); // Scroll top so the spinner is seen. this.content?.scrollToTop(0); // Scroll top so the spinner is seen.
try { try {
if (invalidateData) {
await CoreUtils.ignoreErrors(this.invalidateData());
}
await this.loadData(true, sync); await this.loadData(true, sync);
await this.formatComponent?.doRefresh(undefined, undefined, true); await this.formatComponent?.doRefresh(undefined, undefined, true);
@ -366,6 +376,13 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
} }
} }
/**
* @inheritdoc
*/
async refreshContext(): Promise<void> {
await this.showLoadingAndRefresh(true, true);
}
/** /**
* @inheritdoc * @inheritdoc
*/ */

View File

@ -173,6 +173,31 @@ export class CoreEvents {
}; };
} }
/**
* Listen once for a certain event. To stop listening to the event (in case it wasn't triggered):
* let observer = eventsProvider.on('something', myCallBack);
* ...
* observer.off();
*
* @param eventName Name of the event to listen to.
* @param callBack Function to call when the event is triggered.
* @param siteId Site where to trigger the event. Undefined won't check the site.
* @return Observer to stop listening.
*/
static once<Fallback = unknown, Event extends string = string>(
eventName: Event,
callBack: (value: CoreEventData<Event, Fallback> & CoreEventSiteData) => void,
siteId?: string,
): CoreEventObserver {
const listener = CoreEvents.on<Fallback, Event>(eventName, (value) => {
setTimeout(() => listener.off(), 0);
callBack(value);
}, siteId);
return listener;
}
/** /**
* Listen for several events. To stop listening to the events: * Listen for several events. To stop listening to the events:
* let observer = eventsProvider.onMultiple(['something', 'another'], myCallBack); * let observer = eventsProvider.onMultiple(['something', 'another'], myCallBack);

View File

@ -0,0 +1,27 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { InjectionToken } from '@angular/core';
/**
* Context to refresh data when a certain action happens.
*/
export interface CoreRefreshContext {
/**
* Refresh the context.
*/
refreshContext(): Promise<void>;
}
export const CORE_REFRESH_CONTEXT = new InjectionToken<CoreRefreshContext>('CORE_REFRESH_CONTEXT');