diff --git a/src/addons/calendar/services/calendar.ts b/src/addons/calendar/services/calendar.ts index 4dfb9fffd..0d3c84412 100644 --- a/src/addons/calendar/services/calendar.ts +++ b/src/addons/calendar/services/calendar.ts @@ -566,14 +566,14 @@ export class AddonCalendarProvider { */ async getCalendarLookAhead(siteId?: string): Promise { const site = await CoreSites.getSite(siteId); - let value: string | undefined; + let value: string | undefined | null; try { value = await CoreUser.getUserPreference('calendar_lookahead'); } catch { // Ignore errors. } - if (typeof value == 'undefined') { + if (typeof value == 'undefined' || value === null) { value = site.getStoredConfig('calendar_lookahead'); } @@ -588,7 +588,7 @@ export class AddonCalendarProvider { */ async getCalendarTimeFormat(siteId?: string): Promise { const site = await CoreSites.getSite(siteId); - let format: string | undefined; + let format: string | undefined | null; try { format = await CoreUser.getUserPreference('calendar_timeformat'); diff --git a/src/addons/mod/assign/submission/onlinetext/component/addon-mod-assign-submission-onlinetext.html b/src/addons/mod/assign/submission/onlinetext/component/addon-mod-assign-submission-onlinetext.html index 4883118eb..6660c6c4f 100644 --- a/src/addons/mod/assign/submission/onlinetext/component/addon-mod-assign-submission-onlinetext.html +++ b/src/addons/mod/assign/submission/onlinetext/component/addon-mod-assign-submission-onlinetext.html @@ -24,7 +24,7 @@ - + {{ plugin.name }} @@ -39,11 +39,11 @@ - + - + @@ -158,7 +158,7 @@ diff --git a/src/addons/mod/forum/components/index/index.ts b/src/addons/mod/forum/components/index/index.ts index 4e6178c66..2312f7ff4 100644 --- a/src/addons/mod/forum/components/index/index.ts +++ b/src/addons/mod/forum/components/index/index.ts @@ -73,7 +73,6 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom moduleName = 'forum'; descriptionNote?: string; forum?: AddonModForumData; - fetchMoreDiscussionsFailed = false; discussions: AddonModForumDiscussionsManager; canAddDiscussion = false; addDiscussionText!: string; @@ -237,7 +236,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom * @param showErrors Wether to show errors to the user or hide them. */ protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise { - this.fetchMoreDiscussionsFailed = false; + this.discussions.fetchFailed = false; const promises: Promise[] = []; @@ -259,7 +258,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom if (refresh) { CoreDomUtils.showErrorModalDefault(error, 'addon.mod_forum.errorgetforum', true); - this.fetchMoreDiscussionsFailed = true; // Set to prevent infinite calls with infinite-loading. + this.discussions.fetchFailed = true; // Set to prevent infinite calls with infinite-loading. } else { // Get forum failed, retry without using cache since it might be a new activity. await this.refreshContent(sync); @@ -422,7 +421,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom */ protected async fetchDiscussions(refresh: boolean): Promise { const forum = this.forum!; - this.fetchMoreDiscussionsFailed = false; + this.discussions.fetchFailed = false; if (refresh) { this.page = 0; @@ -497,7 +496,7 @@ export class AddonModForumIndexComponent extends CoreCourseModuleMainActivityCom } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'addon.mod_forum.errorgetforum', true); - this.fetchMoreDiscussionsFailed = true; + this.discussions.fetchFailed = true; } finally { complete(); } @@ -715,6 +714,7 @@ type DiscussionItem = AddonModForumDiscussion | AddonModForumOfflineDiscussion | class AddonModForumDiscussionsManager extends CorePageItemsListManager { onlineLoaded = false; + fetchFailed = false; private discussionsPathPrefix: string; private component: AddonModForumIndexComponent; @@ -726,6 +726,10 @@ class AddonModForumDiscussionsManager extends CorePageItemsListManager this.isOnlineDiscussion(discussion)) as AddonModForumDiscussion[]; } @@ -782,6 +786,7 @@ class AddonModForumDiscussionsManager extends CorePageItemsListManager !this.isOnlineDiscussion(discussion)); this.setItems(otherDiscussions.concat(onlineDiscussions), hasMoreItems); + this.onlineLoaded = true; } /** @@ -795,15 +800,6 @@ class AddonModForumDiscussionsManager extends CorePageItemsListManager this.isOnlineDiscussion(discussion)); - } - /** * @inheritdoc */ diff --git a/src/addons/mod/forum/pages/discussion/discussion.page.ts b/src/addons/mod/forum/pages/discussion/discussion.page.ts index d4e1857a7..e85857e4a 100644 --- a/src/addons/mod/forum/pages/discussion/discussion.page.ts +++ b/src/addons/mod/forum/pages/discussion/discussion.page.ts @@ -395,7 +395,7 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes offlineReplies.push(reply); // Disable reply of the parent. Reply in offline to the same post is not allowed, edit instead. - posts[reply.parentid!].capabilities.reply = false; + onlinePostsMap[reply.parentid!].capabilities.reply = false; return; }), diff --git a/src/addons/mod/lti/services/handlers/module.ts b/src/addons/mod/lti/services/handlers/module.ts index 59e2daa60..ea35c8d26 100644 --- a/src/addons/mod/lti/services/handlers/module.ts +++ b/src/addons/mod/lti/services/handlers/module.ts @@ -13,7 +13,6 @@ // limitations under the License. import { Injectable, Type } from '@angular/core'; -import { DomSanitizer } from '@angular/platform-browser'; import { CoreConstants } from '@/core/constants'; import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate'; @@ -24,7 +23,7 @@ import { CoreFilepool } from '@services/filepool'; import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; import { CoreUtils } from '@services/utils/utils'; -import { makeSingleton } from '@singletons'; +import { DomSanitizer, makeSingleton } from '@singletons'; import { AddonModLtiHelper } from '../lti-helper'; import { AddonModLti, AddonModLtiProvider } from '../lti'; import { AddonModLtiIndexComponent } from '../../components/index'; @@ -51,8 +50,6 @@ export class AddonModLtiModuleHandlerService implements CoreCourseModuleHandler [CoreConstants.FEATURE_SHOW_DESCRIPTION]: true, }; - constructor(protected sanitizer: DomSanitizer) {} - /** * @inheritdoc */ @@ -124,11 +121,11 @@ export class AddonModLtiModuleHandlerService implements CoreCourseModuleHandler // Get the internal URL. const url = await CoreFilepool.getSrcByUrl(siteId, icon, AddonModLtiProvider.COMPONENT, module.id); - handlerData.icon = this.sanitizer.bypassSecurityTrustUrl(url); + handlerData.icon = DomSanitizer.bypassSecurityTrustUrl(url); } catch { // Error downloading. If we're online we'll set the online url. if (CoreApp.isOnline()) { - handlerData.icon = this.sanitizer.bypassSecurityTrustUrl(icon); + handlerData.icon = DomSanitizer.bypassSecurityTrustUrl(icon); } } } diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 713c57567..18a5360d3 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core'; +import { AfterViewInit, ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core'; import { IonRouterOutlet } from '@ionic/angular'; import { BackButtonEvent } from '@ionic/core'; @@ -20,7 +20,7 @@ import { CoreLang } from '@services/lang'; import { CoreLoginHelper } from '@features/login/services/login-helper'; import { CoreEvents } from '@singletons/events'; import { Network, NgZone, Platform, SplashScreen } from '@singletons'; -import { CoreApp } from '@services/app'; +import { CoreApp, CoreAppProvider } from '@services/app'; import { CoreSites } from '@services/sites'; import { CoreNavigator } from '@services/navigator'; import { CoreSubscriptions } from '@singletons/subscriptions'; @@ -34,6 +34,10 @@ import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins'; const MOODLE_VERSION_PREFIX = 'version-'; const MOODLEAPP_VERSION_PREFIX = 'moodleapp-'; +type AutomatedTestsWindow = Window & { + changeDetector?: ChangeDetectorRef; +}; + @Component({ selector: 'app-root', templateUrl: 'app.component.html', @@ -46,6 +50,12 @@ export class AppComponent implements OnInit, AfterViewInit { protected lastUrls: Record = {}; protected lastInAppUrl?: string; + constructor(changeDetector: ChangeDetectorRef) { + if (CoreAppProvider.isAutomated()) { + (window as AutomatedTestsWindow).changeDetector = changeDetector; + } + } + /** * Component being initialized. * diff --git a/src/core/components/iframe/iframe.ts b/src/core/components/iframe/iframe.ts index cb1ecd059..3afcd69a2 100644 --- a/src/core/components/iframe/iframe.ts +++ b/src/core/components/iframe/iframe.ts @@ -15,7 +15,7 @@ import { Component, Input, Output, ViewChild, ElementRef, EventEmitter, OnChanges, SimpleChange, } from '@angular/core'; -import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; +import { SafeResourceUrl } from '@angular/platform-browser'; import { CoreFile } from '@services/file'; import { CoreDomUtils } from '@services/utils/dom'; @@ -23,6 +23,7 @@ import { CoreUrlUtils } from '@services/utils/url'; import { CoreIframeUtils } from '@services/utils/iframe'; import { CoreUtils } from '@services/utils/utils'; import { CoreLogger } from '@singletons/logger'; +import { DomSanitizer } from '@singletons'; @Component({ selector: 'core-iframe', @@ -46,10 +47,7 @@ export class CoreIframeComponent implements OnChanges { protected logger: CoreLogger; protected initialized = false; - constructor( - protected sanitizer: DomSanitizer, - ) { - + constructor() { this.logger = CoreLogger.getInstance('CoreIframe'); this.loaded = new EventEmitter(); } @@ -105,7 +103,7 @@ export class CoreIframeComponent implements OnChanges { await CoreIframeUtils.fixIframeCookies(url); - this.safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(CoreFile.convertFileSrc(url)); + this.safeUrl = DomSanitizer.bypassSecurityTrustResourceUrl(CoreFile.convertFileSrc(url)); // Now that the URL has been set, initialize the iframe. Wait for the iframe to the added to the DOM. setTimeout(() => { diff --git a/src/core/components/progress-bar/progress-bar.ts b/src/core/components/progress-bar/progress-bar.ts index 615aef875..ec65e69bc 100644 --- a/src/core/components/progress-bar/progress-bar.ts +++ b/src/core/components/progress-bar/progress-bar.ts @@ -13,8 +13,8 @@ // limitations under the License. import { Component, Input, OnChanges, SimpleChange, ChangeDetectionStrategy } from '@angular/core'; -import { DomSanitizer, SafeStyle } from '@angular/platform-browser'; -import { Translate } from '@singletons'; +import { SafeStyle } from '@angular/platform-browser'; +import { DomSanitizer, Translate } from '@singletons'; /** * Component to show a progress bar and its value. @@ -40,8 +40,6 @@ export class CoreProgressBarComponent implements OnChanges { protected textSupplied = false; - constructor(private sanitizer: DomSanitizer) { } - /** * Detect changes on input properties. */ @@ -69,7 +67,7 @@ export class CoreProgressBarComponent implements OnChanges { this.text = String(this.progress); } - this.width = this.sanitizer.bypassSecurityTrustStyle(this.progress + '%'); + this.width = DomSanitizer.bypassSecurityTrustStyle(this.progress + '%'); } } diff --git a/src/core/directives/format-text.ts b/src/core/directives/format-text.ts index 84c15bbe8..cf341587d 100644 --- a/src/core/directives/format-text.ts +++ b/src/core/directives/format-text.ts @@ -23,7 +23,6 @@ import { Optional, ViewContainerRef, } from '@angular/core'; -import { DomSanitizer } from '@angular/platform-browser'; import { IonContent } from '@ionic/angular'; import { CoreEventLoadingChangedData, CoreEventObserver, CoreEvents } from '@singletons/events'; @@ -94,7 +93,6 @@ export class CoreFormatTextDirective implements OnChanges { element: ElementRef, @Optional() protected content: IonContent, protected viewContainerRef: ViewContainerRef, - protected sanitizer: DomSanitizer, ) { this.element = element.nativeElement; this.element.classList.add('core-format-text-loading'); // Hide contents until they're treated. @@ -520,7 +518,7 @@ export class CoreFormatTextDirective implements OnChanges { // Important: We need to look for links first because in 'img' we add new links without core-link. anchors.forEach((anchor) => { // Angular 2 doesn't let adding directives dynamically. Create the CoreLinkDirective manually. - const linkDir = new CoreLinkDirective(new ElementRef(anchor), this.content, this.sanitizer); + const linkDir = new CoreLinkDirective(new ElementRef(anchor), this.content); linkDir.capture = this.captureLinks ?? true; linkDir.inApp = this.openLinksInApp; linkDir.ngOnInit(); diff --git a/src/core/directives/link.ts b/src/core/directives/link.ts index 94aae1bf7..472b28a83 100644 --- a/src/core/directives/link.ts +++ b/src/core/directives/link.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Directive, Input, OnInit, ElementRef, Optional, SecurityContext } from '@angular/core'; -import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; +import { SafeUrl } from '@angular/platform-browser'; import { IonContent } from '@ionic/angular'; import { CoreFileHelper } from '@services/file-helper'; @@ -25,6 +25,7 @@ import { CoreTextUtils } from '@services/utils/text'; import { CoreConstants } from '@/core/constants'; import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper'; import { CoreCustomURLSchemes } from '@services/urlschemes'; +import { DomSanitizer } from '@singletons'; /** * Directive to open a link in external browser or in the app. @@ -48,7 +49,6 @@ export class CoreLinkDirective implements OnInit { constructor( element: ElementRef, @Optional() protected content: IonContent, - protected sanitizer: DomSanitizer, ) { this.element = element.nativeElement; } @@ -96,7 +96,7 @@ export class CoreLinkDirective implements OnInit { let href: string | null = null; if (this.href) { // Convert the URL back to string if needed. - href = typeof this.href === 'string' ? this.href : this.sanitizer.sanitize(SecurityContext.URL, this.href); + href = typeof this.href === 'string' ? this.href : DomSanitizer.sanitize(SecurityContext.URL, this.href); } href = href || this.element.getAttribute('href') || this.element.getAttribute('xlink:href'); diff --git a/src/core/directives/tests/format-text.test.ts b/src/core/directives/tests/format-text.test.ts index f0fc1d7d0..3bfc5afa8 100644 --- a/src/core/directives/tests/format-text.test.ts +++ b/src/core/directives/tests/format-text.test.ts @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { DomSanitizer } from '@angular/platform-browser'; import { IonContent } from '@ionic/angular'; import { NgZone } from '@angular/core'; import Faker from 'faker'; @@ -39,7 +38,7 @@ describe('CoreFormatTextDirective', () => { mockSingleton(Platform, { ready: () => Promise.resolve() }); mockSingleton(CoreConfig, { get: (_, defaultValue) => defaultValue }); - CoreDomUtils.setInstance(new CoreDomUtilsProvider(mock())); + CoreDomUtils.setInstance(new CoreDomUtilsProvider()); CoreUrlUtils.setInstance(new CoreUrlUtilsProvider()); CoreUtils.setInstance(new CoreUtilsProvider(mock())); diff --git a/src/core/features/course/classes/activity-prefetch-handler.ts b/src/core/features/course/classes/activity-prefetch-handler.ts index 08b234392..4b58dc5eb 100644 --- a/src/core/features/course/classes/activity-prefetch-handler.ts +++ b/src/core/features/course/classes/activity-prefetch-handler.ts @@ -136,7 +136,7 @@ export class CoreCourseActivityPrefetchHandlerBase extends CoreCourseModulePrefe await this.setDownloaded(module.id, siteId, extra); } catch (error) { // Error prefetching, go back to previous status and reject the promise. - return this.setPreviousStatus(module.id, siteId); + await this.setPreviousStatus(module.id, siteId); throw error; } diff --git a/src/core/features/editor/components/rich-text-editor/core-editor-rich-text-editor.html b/src/core/features/editor/components/rich-text-editor/core-editor-rich-text-editor.html index c3ce29d1c..61bc945d4 100644 --- a/src/core/features/editor/components/rich-text-editor/core-editor-rich-text-editor.html +++ b/src/core/features/editor/components/rich-text-editor/core-editor-rich-text-editor.html @@ -5,6 +5,7 @@ class="core-rte-editor" role="textbox" contenteditable="true" + [attr.aria-labelledby]="ariaLabelledBy" [attr.data-placeholder-text]="placeholder" (focus)="showToolbar($event)" (blur)="hideToolbar($event)" diff --git a/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts b/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts index 0b53152f3..59e23bbad 100644 --- a/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts +++ b/src/core/features/editor/components/rich-text-editor/rich-text-editor.ts @@ -23,6 +23,7 @@ import { AfterContentInit, OnDestroy, Optional, + AfterViewInit, } from '@angular/core'; import { FormControl } from '@angular/forms'; import { IonTextarea, IonContent, IonSlides } from '@ionic/angular'; @@ -51,7 +52,7 @@ import { CoreEditorOffline } from '../../services/editor-offline'; templateUrl: 'core-editor-rich-text-editor.html', styleUrls: ['rich-text-editor.scss'], }) -export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentInit, OnDestroy { +export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentInit, AfterViewInit, OnDestroy { // Based on: https://github.com/judgewest2000/Ionic3RichText/ // @todo: Anchor button, fullscreen... @@ -87,6 +88,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn protected valueChangeSubscription?: Subscription; protected keyboardObserver?: CoreEventObserver; protected resetObserver?: CoreEventObserver; + protected labelObserver?: MutationObserver; protected initHeightInterval?: number; protected isCurrentView = true; protected toolbarButtonWidth = 44; @@ -109,6 +111,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn toolbarPrevHidden = true; toolbarNextHidden = false; canScanQR = false; + ariaLabelledBy?: string; infoMessage?: string; direction = 'ltr'; toolbarStyles = { @@ -209,6 +212,20 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn } } + /** + * @inheritdoc + */ + async ngAfterViewInit(): Promise { + const label = this.element.closest('ion-item')?.querySelector('ion-label'); + + if (!label) { + return; + } + + this.labelObserver = new MutationObserver(() => this.ariaLabelledBy = label.getAttribute('id') ?? undefined); + this.labelObserver.observe(label, { attributes: true, attributeFilter: ['id'] }); + } + /** * Set listeners and observers. */ @@ -1118,6 +1135,7 @@ export class CoreEditorRichTextEditorComponent implements OnInit, AfterContentIn this.resizeObserver?.disconnect(); this.resetObserver?.off(); this.keyboardObserver?.off(); + this.labelObserver?.disconnect(); } } diff --git a/src/core/features/rating/components/aggregate/aggregate.ts b/src/core/features/rating/components/aggregate/aggregate.ts index 77e34bda1..3c33cf348 100644 --- a/src/core/features/rating/components/aggregate/aggregate.ts +++ b/src/core/features/rating/components/aggregate/aggregate.ts @@ -47,7 +47,7 @@ export class CoreRatingAggregateComponent implements OnChanges, OnDestroy { disabled = false; labelKey = ''; - protected aggregateObserver: CoreEventObserver; + protected aggregateObserver?: CoreEventObserver; protected updateSiteObserver: CoreEventObserver; constructor() { @@ -57,27 +57,14 @@ export class CoreRatingAggregateComponent implements OnChanges, OnDestroy { this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => { this.disabled = CoreRating.isRatingDisabledInSite(); }, CoreSites.getCurrentSiteId()); - - // Update aggrgate when the user adds or edits a rating. - this.aggregateObserver = - CoreEvents.on(CoreRatingProvider.AGGREGATE_CHANGED_EVENT, (data) => { - if (this.item && - data.contextLevel == this.contextLevel && - data.instanceId == this.instanceId && - data.component == this.ratingInfo.component && - data.ratingArea == this.ratingInfo.ratingarea && - data.itemId == this.itemId) { - this.item.aggregatestr = data.aggregate; - this.item.count = data.count; - } - }); } /** * Detect changes on input properties. */ ngOnChanges(): void { - this.aggregateObserver && this.aggregateObserver.off(); + this.aggregateObserver?.off(); + delete this.aggregateObserver; this.item = (this.ratingInfo.ratings || []).find((rating) => rating.itemid == this.itemId); if (!this.item) { @@ -107,6 +94,20 @@ export class CoreRatingAggregateComponent implements OnChanges, OnDestroy { } this.showCount = (this.aggregateMethod != CoreRatingProvider.AGGREGATE_COUNT); + + // Update aggrgate when the user adds or edits a rating. + this.aggregateObserver = + CoreEvents.on(CoreRatingProvider.AGGREGATE_CHANGED_EVENT, (data) => { + if (this.item && + data.contextLevel == this.contextLevel && + data.instanceId == this.instanceId && + data.component == this.ratingInfo.component && + data.ratingArea == this.ratingInfo.ratingarea && + data.itemId == this.itemId) { + this.item.aggregatestr = data.aggregate; + this.item.count = data.count; + } + }); } /** @@ -135,7 +136,7 @@ export class CoreRatingAggregateComponent implements OnChanges, OnDestroy { * Component being destroyed. */ ngOnDestroy(): void { - this.aggregateObserver.off(); + this.aggregateObserver?.off(); this.updateSiteObserver.off(); } diff --git a/src/core/features/rating/services/rating.ts b/src/core/features/rating/services/rating.ts index 51f81c11a..25642d32a 100644 --- a/src/core/features/rating/services/rating.ts +++ b/src/core/features/rating/services/rating.ts @@ -122,7 +122,8 @@ export class CoreRatingProvider { try { await CoreRatingOffline.deleteRating(component, ratingArea, contextLevel, instanceId, itemId, siteId); - this.addRatingOnline( + + const response = await this.addRatingOnline( component, ratingArea, contextLevel, @@ -134,6 +135,8 @@ export class CoreRatingProvider { aggregateMethod, siteId, ); + + return response; } catch (error) { if (CoreUtils.isWebServiceError(error)) { // The WebService has thrown an error or offline not supported, reject. diff --git a/src/core/features/user/services/user.ts b/src/core/features/user/services/user.ts index 6033f8efd..6dfc08bfa 100644 --- a/src/core/features/user/services/user.ts +++ b/src/core/features/user/services/user.ts @@ -391,7 +391,7 @@ export class CoreUserProvider { * @param siteId Site Id. If not defined, use current site. * @return Preference value or null if preference not set. */ - async getUserPreference(name: string, siteId?: string): Promise { + async getUserPreference(name: string, siteId?: string): Promise { siteId = siteId || CoreSites.getCurrentSiteId(); const preference = await CoreUtils.ignoreErrors(CoreUserOffline.getPreference(name, siteId)); @@ -403,20 +403,15 @@ export class CoreUserProvider { const wsValue = await this.getUserPreferenceOnline(name, siteId); - if (!wsValue) { - if (preference) { - // Return the local value. - return preference.value; - } - - throw new CoreError('Preference not found'); - } - if (preference && preference.value != preference.onlinevalue && preference.onlinevalue == wsValue) { // Sync is pending for this preference, return stored value. return preference.value; } + if (!wsValue) { + return null; + } + await CoreUserOffline.setPreference(name, wsValue, wsValue); return wsValue; diff --git a/src/core/services/tests/navigator.test.ts b/src/core/services/tests/navigator.test.ts index 5dffe7899..d2c3fe25d 100644 --- a/src/core/services/tests/navigator.test.ts +++ b/src/core/services/tests/navigator.test.ts @@ -44,7 +44,7 @@ describe('CoreNavigator', () => { mockSingleton(Router, router); mockSingleton(CoreUtils, new CoreUtilsProvider(mock())); mockSingleton(CoreUrlUtils, new CoreUrlUtilsProvider()); - mockSingleton(CoreTextUtils, new CoreTextUtilsProvider(mock())); + mockSingleton(CoreTextUtils, new CoreTextUtilsProvider()); mockSingleton(CoreSites, { getCurrentSiteId: () => 42, isLoggedIn: () => true }); mockSingleton(CoreMainMenu, { isMainMenuTab: path => Promise.resolve(currentMainMenuHandlers.includes(path)) }); }); diff --git a/src/core/services/tests/utils/text.test.ts b/src/core/services/tests/utils/text.test.ts index 19ba8fd6e..1ac99af3d 100644 --- a/src/core/services/tests/utils/text.test.ts +++ b/src/core/services/tests/utils/text.test.ts @@ -12,24 +12,22 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { DomSanitizer } from '@angular/platform-browser'; - import { CoreApp } from '@services/app'; import { CoreTextUtilsProvider } from '@services/utils/text'; +import { DomSanitizer } from '@singletons'; -import { mock, mockSingleton } from '@/testing/utils'; +import { mockSingleton } from '@/testing/utils'; describe('CoreTextUtilsProvider', () => { const config = { platform: 'android' }; - let sanitizer: DomSanitizer; let textUtils: CoreTextUtilsProvider; beforeEach(() => { mockSingleton(CoreApp, [], { isAndroid: () => config.platform === 'android' }); + mockSingleton(DomSanitizer, [], { bypassSecurityTrustUrl: url => url }); - sanitizer = mock([], { bypassSecurityTrustUrl: url => url }); - textUtils = new CoreTextUtilsProvider(sanitizer); + textUtils = new CoreTextUtilsProvider(); }); it('adds ending slashes', () => { @@ -58,7 +56,7 @@ describe('CoreTextUtilsProvider', () => { // Assert expect(url).toEqual('geo:0,0?q=Moodle%20Spain%20HQ'); - expect(sanitizer.bypassSecurityTrustUrl).toHaveBeenCalled(); + expect(DomSanitizer.bypassSecurityTrustUrl).toHaveBeenCalled(); expect(CoreApp.isAndroid).toHaveBeenCalled(); }); @@ -74,7 +72,7 @@ describe('CoreTextUtilsProvider', () => { // Assert expect(url).toEqual('http://maps.google.com?q=Moodle%20Spain%20HQ'); - expect(sanitizer.bypassSecurityTrustUrl).toHaveBeenCalled(); + expect(DomSanitizer.bypassSecurityTrustUrl).toHaveBeenCalled(); expect(CoreApp.isAndroid).toHaveBeenCalled(); }); diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index b6ee0ee20..ffc7464f3 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -13,7 +13,6 @@ // limitations under the License. import { Injectable, SimpleChange, ElementRef, KeyValueChanges } from '@angular/core'; -import { DomSanitizer } from '@angular/platform-browser'; import { IonContent } from '@ionic/angular'; import { ModalOptions, PopoverOptions, AlertOptions, AlertButton, TextFieldTypes } from '@ionic/core'; import { Md5 } from 'ts-md5'; @@ -62,7 +61,7 @@ export class CoreDomUtilsProvider { protected activeLoadingModals: CoreIonLoadingElement[] = []; protected logger: CoreLogger; - constructor(protected domSanitizer: DomSanitizer) { + constructor() { this.logger = CoreLogger.getInstance('CoreDomUtilsProvider'); this.init(); diff --git a/src/core/services/utils/text.ts b/src/core/services/utils/text.ts index 2045bc547..572594b66 100644 --- a/src/core/services/utils/text.ts +++ b/src/core/services/utils/text.ts @@ -13,13 +13,13 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; +import { SafeUrl } from '@angular/platform-browser'; import { ModalOptions } from '@ionic/core'; import { CoreApp } from '@services/app'; import { CoreLang } from '@services/lang'; import { CoreAnyError, CoreError } from '@classes/errors/error'; -import { makeSingleton, Translate } from '@singletons'; +import { DomSanitizer, makeSingleton, Translate } from '@singletons'; import { CoreWSFile } from '@services/ws'; import { Locutus } from '@singletons/locutus'; import { CoreViewerTextComponent } from '@features/viewer/components/text/text'; @@ -94,8 +94,6 @@ export class CoreTextUtilsProvider { protected template: HTMLTemplateElement = document.createElement('template'); // A template element to convert HTML to element. - constructor(private sanitizer: DomSanitizer) { } - /** * Add ending slash from a path or URL. * @@ -156,7 +154,7 @@ export class CoreTextUtilsProvider { * @return URL to view the address. */ buildAddressURL(address: string): SafeUrl { - return this.sanitizer.bypassSecurityTrustUrl((CoreApp.isAndroid() ? 'geo:0,0?q=' : 'http://maps.google.com?q=') + + return DomSanitizer.bypassSecurityTrustUrl((CoreApp.isAndroid() ? 'geo:0,0?q=' : 'http://maps.google.com?q=') + encodeURIComponent(address)); } diff --git a/src/core/singletons/index.ts b/src/core/singletons/index.ts index 6d15cef42..d22a26290 100644 --- a/src/core/singletons/index.ts +++ b/src/core/singletons/index.ts @@ -12,9 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { ApplicationRef, ApplicationInitStatus, Injector, NgZone as NgZoneService, Type } from '@angular/core'; +import { AbstractType, ApplicationInitStatus, ApplicationRef, Injector, NgZone as NgZoneService, Type } from '@angular/core'; import { Router as RouterService } from '@angular/router'; import { HttpClient } from '@angular/common/http'; +import { DomSanitizer as DomSanitizerService } from '@angular/platform-browser'; import { Platform as PlatformService, @@ -109,7 +110,7 @@ export function setCreateSingletonMethodProxy(method: typeof createSingletonMeth * @return Singleton proxy. */ export function makeSingleton( // eslint-disable-line @typescript-eslint/ban-types - injectionToken: Type | Type | string, + injectionToken: Type | AbstractType | Type | string, ): CoreSingletonProxy { const singleton = { setInstance(instance: Service) { @@ -199,6 +200,7 @@ export const ApplicationInit = makeSingleton(ApplicationInitStatus); export const Application = makeSingleton(ApplicationRef); export const NavController = makeSingleton(NavControllerService); export const Router = makeSingleton(RouterService); +export const DomSanitizer = makeSingleton(DomSanitizerService); // Convert external libraries injectables. export const Translate = makeSingleton(TranslateService);