From d0d2e0895ed8e17361b04f0fce21188cd45a323a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 16 Jul 2025 13:30:49 +0200 Subject: [PATCH 1/4] MOBILE-4842 network: Add signals --- src/addons/blog/pages/index/index.ts | 11 +- .../settings/pages/deviceinfo/deviceinfo.html | 8 +- .../settings/pages/deviceinfo/deviceinfo.ts | 40 ++---- src/core/services/network.ts | 134 ++++++++++++------ 4 files changed, 110 insertions(+), 83 deletions(-) diff --git a/src/addons/blog/pages/index/index.ts b/src/addons/blog/pages/index/index.ts index 293823fec74..f9f392a66d4 100644 --- a/src/addons/blog/pages/index/index.ts +++ b/src/addons/blog/pages/index/index.ts @@ -41,7 +41,6 @@ import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreTime } from '@singletons/time'; import { CorePopovers } from '@services/overlays/popovers'; import { CoreLoadings } from '@services/overlays/loadings'; -import { Subscription } from 'rxjs'; import { CoreAlerts } from '@services/overlays/alerts'; import { Translate } from '@singletons'; import { CoreCommentsCommentsComponent } from '@features/comments/components/comments/comments'; @@ -89,10 +88,9 @@ export default class AddonBlogIndexPage implements OnInit, OnDestroy { contextInstanceId = 0; entryUpdateObserver: CoreEventObserver; syncObserver: CoreEventObserver; - onlineObserver: Subscription; optionsAvailable = false; readonly hasOfflineDataToSync = signal(false); - readonly isOnline = signal(false); + readonly isOnline = CoreNetwork.onlineSignal(); siteId: string; syncIcon = CoreConstants.ICON_SYNC; readonly syncHidden = computed(() => !this.loaded() || !this.isOnline() || !this.hasOfflineDataToSync()); @@ -101,7 +99,6 @@ export default class AddonBlogIndexPage implements OnInit, OnDestroy { this.currentUserId = CoreSites.getCurrentSiteUserId(); this.siteHomeId = CoreSites.getCurrentSiteHomeId(); this.siteId = CoreSites.getCurrentSiteId(); - this.isOnline.set(CoreNetwork.isOnline()); this.logView = CoreTime.once(async () => { await CorePromiseUtils.ignoreErrors(AddonBlog.logView(this.filter)); @@ -137,11 +134,6 @@ export default class AddonBlogIndexPage implements OnInit, OnDestroy { await CorePromiseUtils.ignoreErrors(this.refresh(false)); this.loaded.set(true); }); - - // Refresh online status when changes. - this.onlineObserver = CoreNetwork.onChange().subscribe(async () => { - this.isOnline.set(CoreNetwork.isOnline()); - }); } /** @@ -516,7 +508,6 @@ export default class AddonBlogIndexPage implements OnInit, OnDestroy { ngOnDestroy(): void { this.entryUpdateObserver.off(); this.syncObserver.off(); - this.onlineObserver.unsubscribe(); } } diff --git a/src/core/features/settings/pages/deviceinfo/deviceinfo.html b/src/core/features/settings/pages/deviceinfo/deviceinfo.html index 57d2ae90a37..e5386aabb0b 100644 --- a/src/core/features/settings/pages/deviceinfo/deviceinfo.html +++ b/src/core/features/settings/pages/deviceinfo/deviceinfo.html @@ -139,7 +139,7 @@

{{ 'core.settings.networkstatus' | translate}}

- @if (deviceInfo.isOnline) { + @if (deviceInfo.isOnline()) {

{{ 'core.online' | translate }}

} @else {

{{ 'core.offline' | translate }}

@@ -149,7 +149,11 @@

{{ 'core.settings.wificonnection' | translate}}

-

{{ 'core.' + deviceInfo.wifiConnection | translate }}

+ @if (deviceInfo.wifiConnection()) { +

{{ 'core.yes' | translate }}

+ } @else { +

{{ 'core.no' | translate }}

+ }
@if (deviceInfo.cordovaVersion) { diff --git a/src/core/features/settings/pages/deviceinfo/deviceinfo.ts b/src/core/features/settings/pages/deviceinfo/deviceinfo.ts index 03e89d80c29..9824ac5b6b7 100644 --- a/src/core/features/settings/pages/deviceinfo/deviceinfo.ts +++ b/src/core/features/settings/pages/deviceinfo/deviceinfo.ts @@ -12,21 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnDestroy } from '@angular/core'; +import { Component, computed, Signal } from '@angular/core'; import { CoreConstants } from '@/core/constants'; import { CoreLocalNotifications } from '@services/local-notifications'; -import { Device, Translate, NgZone } from '@singletons'; +import { Device, Translate } from '@singletons'; import { CoreLang } from '@services/lang'; import { CoreFile } from '@services/file'; import { CoreSites } from '@services/sites'; import { CorePromiseUtils } from '@singletons/promise-utils'; -import { Subscription } from 'rxjs'; import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications'; import { CoreConfig } from '@services/config'; import { CoreToasts } from '@services/overlays/toasts'; import { CoreNavigator } from '@services/navigator'; import { CorePlatform } from '@services/platform'; -import { CoreNetwork } from '@services/network'; +import { CoreNetwork, CoreNetworkConnection } from '@services/network'; import { CoreLoginHelper } from '@features/login/services/login-helper'; import { CoreSitesFactory } from '@services/sites-factory'; import { CoreText } from '@singletons/text'; @@ -53,8 +52,8 @@ interface CoreSettingsDeviceInfo { locationHref?: string; deviceType: string; screen?: string; - isOnline: boolean; - wifiConnection: string; + isOnline: Signal; + wifiConnection: Signal; cordovaVersion?: string; platform?: string; osVersion?: string; @@ -76,7 +75,7 @@ interface CoreSettingsDeviceInfo { CoreSharedModule, ], }) -export default class CoreSettingsDeviceInfoPage implements OnDestroy { +export default class CoreSettingsDeviceInfoPage { deviceInfo: CoreSettingsDeviceInfo; deviceOsTranslated?: string; @@ -88,8 +87,6 @@ export default class CoreSettingsDeviceInfoPage implements OnDestroy { protected devOptionsForced = false; protected devOptionsClickTimeout?: number; - protected onlineObserver?: Subscription; - constructor() { const navigator = window.navigator; @@ -98,8 +95,8 @@ export default class CoreSettingsDeviceInfoPage implements OnDestroy { versionCode: CoreConstants.CONFIG.versioncode, compilationTime: CoreConstants.BUILD.compilationTime || 0, lastCommit: CoreConstants.BUILD.lastCommitHash || '', - isOnline: CoreNetwork.isOnline(), - wifiConnection: CoreNetwork.isWifi() ? 'yes' : 'no', + isOnline: CoreNetwork.onlineSignal(), + wifiConnection: computed(() => CoreNetwork.connectionTypeSignal()() === CoreNetworkConnection.WIFI), localNotifAvailable: CoreLocalNotifications.isPluginAvailable() ? 'yes' : 'no', pushId: CorePushNotifications.getPushId(), deviceType: '', @@ -172,14 +169,6 @@ export default class CoreSettingsDeviceInfoPage implements OnDestroy { this.deviceInfo.siteId = currentSite?.getId(); this.deviceInfo.siteVersion = currentSite?.getInfo()?.release; - // Refresh online status when changes. - this.onlineObserver = CoreNetwork.onChange().subscribe(() => { - // Execute the callback in the Angular zone, so change detection doesn't stop working. - NgZone.run(() => { - this.deviceInfo.isOnline = CoreNetwork.isOnline(); - }); - }); - this.asyncInit(); } @@ -220,6 +209,12 @@ export default class CoreSettingsDeviceInfoPage implements OnDestroy { */ copyInfo(): void { CoreText.copyToClipboard(JSON.stringify(this.deviceInfo)); + const deviceInfo = { + ...this.deviceInfo, + isOnline: this.deviceInfo.isOnline(), + wifiConnection: this.deviceInfo.wifiConnection(), + }; + CoreText.copyToClipboard(JSON.stringify(deviceInfo)); } /** @@ -234,13 +229,6 @@ export default class CoreSettingsDeviceInfoPage implements OnDestroy { text && CoreText.copyToClipboard(text); } - /** - * Page destroyed. - */ - ngOnDestroy(): void { - this.onlineObserver && this.onlineObserver.unsubscribe(); - } - /** * 5 clicks will enable dev options. */ diff --git a/src/core/services/network.ts b/src/core/services/network.ts index 35597f4f51a..0d3493d2a61 100644 --- a/src/core/services/network.ts +++ b/src/core/services/network.ts @@ -12,10 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { computed, effect, Injectable, Signal, signal } from '@angular/core'; import { CorePlatform } from '@services/platform'; import { Network } from '@awesome-cordova-plugins/network/ngx'; -import { NgZone, makeSingleton } from '@singletons'; +import { makeSingleton } from '@singletons'; import { Observable, Subject, merge } from 'rxjs'; import { CoreHTMLClasses } from '@singletons/html-classes'; @@ -42,26 +42,44 @@ export class CoreNetworkService extends Network { protected connectStableObservable = new Subject<'connected'>(); protected disconnectObservable = new Subject<'disconnected'>(); protected forceConnectionMode?: CoreNetworkConnection; - protected online = false; + protected readonly online = signal(false); protected connectStableTimeout?: number; + private readonly _connectionType = signal(CoreNetworkConnection.UNKNOWN); + private readonly _limitedConnection = computed(() => this.online() && CoreNetwork.isNetworkAccessLimited()); - get connectionType(): CoreNetworkConnection { - if (this.forceConnectionMode !== undefined) { - return this.forceConnectionMode; - } + constructor() { + super(); - if (CorePlatform.isMobile()) { - return this.type as CoreNetworkConnection; - } + effect(() => { + const isOnline = this.online(); + + const hadOfflineMessage = CoreHTMLClasses.hasModeClass('core-offline'); + + CoreHTMLClasses.toggleModeClass('core-offline', !isOnline); + + if (isOnline && hadOfflineMessage) { + CoreHTMLClasses.toggleModeClass('core-online', true); + + setTimeout(() => { + CoreHTMLClasses.toggleModeClass('core-online', false); + }, 3000); + } else if (!isOnline) { + CoreHTMLClasses.toggleModeClass('core-online', false); + } + }); + } + + get connectionType(): CoreNetworkConnection { + CoreNetwork.updateConnectionType(); - return this.online ? CoreNetworkConnection.WIFI : CoreNetworkConnection.NONE; + return this._connectionType(); } /** * Initialize the service. */ initialize(): void { - this.checkOnline(); + this.updateOnline(); if (CorePlatform.isMobile()) { // We cannot directly listen to onChange because it depends on @@ -103,30 +121,7 @@ export class CoreNetworkService extends Network { async onPlaformReady(): Promise { await CorePlatform.ready(); - // Refresh online status when changes. - CoreNetwork.onChange().subscribe(() => { - // Execute the callback in the Angular zone, so change detection doesn't stop working. - NgZone.run(() => { - const isOnline = this.isOnline(); - - const hadOfflineMessage = CoreHTMLClasses.hasModeClass('core-offline'); - - CoreHTMLClasses.toggleModeClass('core-offline', !isOnline); - - if (isOnline && hadOfflineMessage) { - CoreHTMLClasses.toggleModeClass('core-online', true); - - setTimeout(() => { - CoreHTMLClasses.toggleModeClass('core-online', false); - }, 3000); - } else if (!isOnline) { - CoreHTMLClasses.toggleModeClass('core-online', false); - } - }); - }); - - const isOnline = this.isOnline(); - CoreHTMLClasses.toggleModeClass('core-offline', !isOnline); + CoreHTMLClasses.toggleModeClass('core-offline', !this.online()); } /** @@ -146,15 +141,18 @@ export class CoreNetworkService extends Network { * @returns Whether the app is online. */ isOnline(): boolean { - return this.online; + return this.online(); } /** - * Returns whether we are online. + * Updates online status. */ - checkOnline(): void { + protected updateOnline(): void { + // Recalculate connection type. + CoreNetwork.updateConnectionType(); + if (this.forceConnectionMode === CoreNetworkConnection.NONE) { - this.online = false; + this.online.set(false); return; } @@ -162,12 +160,12 @@ export class CoreNetworkService extends Network { // We cannot use navigator.onLine because it has issues in some devices. // See https://bugs.chromium.org/p/chromium/issues/detail?id=811122 if (!CorePlatform.isAndroid()) { - this.online = navigator.onLine; + this.online.set(navigator.onLine); return; } - const type = this.connectionType; + const type = this._connectionType(); let online = type !== null && type !== CoreNetworkConnection.NONE && type !== CoreNetworkConnection.UNKNOWN; // Double check we are not online because we cannot rely 100% in Cordova APIs. @@ -175,7 +173,26 @@ export class CoreNetworkService extends Network { online = true; } - this.online = online; + this.online.set(online); + } + + /** + * Check and update the connection type. + */ + protected updateConnectionType(): void { + if (this.forceConnectionMode !== undefined) { + this._connectionType.set(this.forceConnectionMode); + + return; + } + + if (CorePlatform.isMobile()) { + this._connectionType.set(this.type as CoreNetworkConnection); + + return; + } + + this._connectionType.set(this.online() ? CoreNetworkConnection.WIFI : CoreNetworkConnection.NONE); } /** @@ -187,6 +204,33 @@ export class CoreNetworkService extends Network { return merge(this.connectObservable, this.disconnectObservable); } + /** + * Returns a signal to watch online status. + * + * @returns Signal. + */ + onlineSignal(): Signal { + return this.online.asReadonly(); + } + + /** + * Returns a signal to watch limited connection status. + * + * @returns Signal. + */ + limitedConnectionSignal(): Signal { + return this._limitedConnection; + } + + /** + * Returns a signal to watch connection type. + * + * @returns Signal. + */ + connectionTypeSignal(): Signal { + return this._connectionType.asReadonly(); + } + /** * Returns an observable to notify when the app is connected. * It will also be fired when connection type changes. @@ -224,9 +268,9 @@ export class CoreNetworkService extends Network { */ protected fireObservable(): void { clearTimeout(this.connectStableTimeout); - this.checkOnline(); + this.updateOnline(); - if (this.online) { + if (this.online()) { this.connectObservable.next('connected'); this.connectStableTimeout = window.setTimeout(() => { this.connectStableObservable.next('connected'); From ae34d226821d5f89e5a7905cfc0518ff713a6270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 29 Jan 2025 12:33:46 +0100 Subject: [PATCH 2/4] MOBILE-4842 module: Change BehaviorSubject to signal --- .../mod/page/tests/behat/basic_usage.feature | 2 +- .../components/module/core-course-module.html | 6 +++--- .../course/components/module/module.ts | 19 +++++++++---------- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/addons/mod/page/tests/behat/basic_usage.feature b/src/addons/mod/page/tests/behat/basic_usage.feature index c9c8b85d69c..a44160b5ca2 100644 --- a/src/addons/mod/page/tests/behat/basic_usage.feature +++ b/src/addons/mod/page/tests/behat/basic_usage.feature @@ -24,7 +24,7 @@ Feature: Test basic usage of page activity in app | name | activity | activityname | course | | \mod_page\event\course_module_viewed | page | Test page title | Course 1 | - Scenario: Prefecth page + Scenario: Prefetch page Given I entered the course "Course 1" as "student1" in the app When I press "Course downloads" in the app And I press "Download" within "Test page title" "ion-item" in the app diff --git a/src/core/features/course/components/module/core-course-module.html b/src/core/features/course/components/module/core-course-module.html index 9685d51309e..640616f9f55 100644 --- a/src/core/features/course/components/module/core-course-module.html +++ b/src/core/features/course/components/module/core-course-module.html @@ -30,9 +30,9 @@ @if (module.visible === 0 || module.uservisible === false) { } - @if (prefetchStatusIcon$ | async; as prefetchStatusIcon) { - + @if (prefetchStatusIcon()) { + }

diff --git a/src/core/features/course/components/module/module.ts b/src/core/features/course/components/module/module.ts index 4e6b43e1cec..a097a80cf6b 100644 --- a/src/core/features/course/components/module/module.ts +++ b/src/core/features/course/components/module/module.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input, Output, EventEmitter, OnInit, OnDestroy, HostBinding } from '@angular/core'; +import { Component, Input, Output, EventEmitter, OnInit, OnDestroy, HostBinding, signal } from '@angular/core'; import { CoreSites } from '@services/sites'; import { @@ -29,7 +29,6 @@ import { } from '@features/course/services/module-prefetch-delegate'; import { CoreConstants, DownloadStatus } from '@/core/constants'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; -import { BehaviorSubject } from 'rxjs'; import { toBoolean } from '@/core/transforms/boolean'; import { CoreRemindersDateComponent } from '../../../reminders/components/date/date'; import { CoreCourseModuleCompletionComponent } from '../module-completion/module-completion'; @@ -74,9 +73,9 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy { modNameTranslated = ''; hasCompletion = false; // Whether activity has completion to be shown. showManualCompletion = false; // Whether to show manual completion when completion conditions are disabled. - prefetchStatusIcon$ = new BehaviorSubject(''); // Module prefetch status icon. - prefetchStatusText$ = new BehaviorSubject(''); // Module prefetch status text. moduleHasView = true; + readonly prefetchStatusIcon = signal(''); // Module prefetch status icon. + readonly prefetchStatusText = signal(''); // Module prefetch status text. protected prefetchHandler?: CoreCourseModulePrefetchHandler; @@ -152,16 +151,16 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy { switch (prefetchStatus) { case DownloadStatus.OUTDATED: - this.prefetchStatusIcon$.next(CoreConstants.ICON_OUTDATED); - this.prefetchStatusText$.next('core.outdated'); + this.prefetchStatusIcon.set(CoreConstants.ICON_OUTDATED); + this.prefetchStatusText.set('core.outdated'); break; case DownloadStatus.DOWNLOADED: - this.prefetchStatusIcon$.next(CoreConstants.ICON_DOWNLOADED); - this.prefetchStatusText$.next('core.downloaded'); + this.prefetchStatusIcon.set(CoreConstants.ICON_DOWNLOADED); + this.prefetchStatusText.set('core.downloaded'); break; default: - this.prefetchStatusIcon$.next(''); - this.prefetchStatusText$.next(''); + this.prefetchStatusIcon.set(''); + this.prefetchStatusText.set(''); break; } From 0fe16318f4b9e7c3620ff6784abb7f776f4c701a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 29 Jan 2025 12:43:24 +0100 Subject: [PATCH 3/4] MOBILE-4842 keyboard: Change keyboard events to signals --- .../features/comments/pages/viewer/viewer.ts | 13 ++-- .../rich-text-editor/rich-text-editor.ts | 19 +++--- src/core/features/mainmenu/pages/menu/menu.ts | 33 ++++----- .../subscribe-to-keyboard-events.ts | 2 +- src/core/services/app.ts | 2 +- src/core/singletons/events.ts | 3 + src/core/singletons/keyboard.ts | 67 ++++++++++++------- 7 files changed, 85 insertions(+), 54 deletions(-) diff --git a/src/core/features/comments/pages/viewer/viewer.ts b/src/core/features/comments/pages/viewer/viewer.ts index 96448472f83..e502b36de7c 100644 --- a/src/core/features/comments/pages/viewer/viewer.ts +++ b/src/core/features/comments/pages/viewer/viewer.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnDestroy, OnInit, ViewChild, AfterViewInit, inject } from '@angular/core'; +import { Component, OnDestroy, OnInit, ViewChild, AfterViewInit, effect, inject } from '@angular/core'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { ActivatedRoute } from '@angular/router'; import { CoreSites } from '@services/sites'; @@ -49,6 +49,7 @@ import { CoreDom } from '@singletons/dom'; import { CoreSharedModule } from '@/core/shared.module'; import { ADDON_MOD_ASSIGN_COMMENTS_COMPONENT_NAME } from '@addons/mod/assign/submission/comments/constants'; import { CoreCourses } from '@features/courses/services/courses'; +import { CoreKeyboard } from '@singletons/keyboard'; /** * Page that displays comments. @@ -93,7 +94,6 @@ export default class CoreCommentsViewerPage implements OnInit, OnDestroy, AfterV protected addDeleteCommentsAvailable = false; protected syncObserver?: CoreEventObserver; protected onlineObserver: Subscription; - protected keyboardObserver: CoreEventObserver; protected viewDestroyed = false; protected scrollBottom = true; protected scrollElement?: HTMLElement; @@ -128,9 +128,11 @@ export default class CoreCommentsViewerPage implements OnInit, OnDestroy, AfterV }); }); - this.keyboardObserver = CoreEvents.on(CoreEvents.KEYBOARD_CHANGE, (keyboardHeight: number) => { - // Force when opening. - this.scrollToBottom(keyboardHeight > 0); + effect(() => { + const shown = CoreKeyboard.getKeyboardShownSignal(); + + /// Force when opening. + this.scrollToBottom(shown()); }); } @@ -700,7 +702,6 @@ export default class CoreCommentsViewerPage implements OnInit, OnDestroy, AfterV this.syncObserver?.off(); this.onlineObserver.unsubscribe(); this.viewDestroyed = true; - this.keyboardObserver.off(); } } 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 f85f0440c72..6c69ca584a3 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 @@ -25,6 +25,7 @@ import { Output, EventEmitter, inject, + effect, } from '@angular/core'; import { IonContent } from '@ionic/angular'; import { CoreSharedModule } from '@/core/shared.module'; @@ -50,6 +51,7 @@ import { CoreLoadingComponent } from '@components/loading/loading'; import { CoreToasts } from '@services/overlays/toasts'; import { CorePromiseUtils } from '@singletons/promise-utils'; import { convertTextToHTMLElement } from '@/core/utils/create-html-element'; +import { CoreKeyboard } from '@singletons/keyboard'; /** * Component that displays a rich text editor. @@ -85,7 +87,6 @@ export class CoreEditorRichTextEditorComponent implements AfterViewInit, OnDestr @ViewChild(CoreDynamicComponent) dynamicComponent!: CoreDynamicComponent; - protected keyboardObserver?: CoreEventObserver; protected resizeListener?: CoreEventObserver; protected editorComponentClass?: Type; protected editorComponentData: Record = {}; @@ -107,6 +108,15 @@ export class CoreEditorRichTextEditorComponent implements AfterViewInit, OnDestr constructor() { // Generate a "unique" ID based on timestamp. this.pageInstance = `app_${Date.now()}`; + + effect(() => { + // Signal will be triggered when the keyboard is shown or hidden. + CoreKeyboard.getKeyboardShownSignal(); + + // Opening or closing the keyboard also calls the resize function, but sometimes the resize is called too soon. + // Check the height again, now the window height should have been updated. + this.maximizeEditorSize(); + }); } /** @@ -188,12 +198,6 @@ export class CoreEditorRichTextEditorComponent implements AfterViewInit, OnDestr this.controlSubscription = this.control?.valueChanges.subscribe((newValue) => { this.onControlValueChange(newValue); }); - - // Opening or closing the keyboard also calls the resize function, but sometimes the resize is called too soon. - // Check the height again, now the window height should have been updated. - this.keyboardObserver = CoreEvents.on(CoreEvents.KEYBOARD_CHANGE, () => { - this.maximizeEditorSize(); - }); } /** @@ -201,7 +205,6 @@ export class CoreEditorRichTextEditorComponent implements AfterViewInit, OnDestr */ ngOnDestroy(): void { this.resizeListener?.off(); - this.keyboardObserver?.off(); this.controlSubscription?.unsubscribe(); this.resetObserver?.off(); this.labelObserver?.disconnect(); diff --git a/src/core/features/mainmenu/pages/menu/menu.ts b/src/core/features/mainmenu/pages/menu/menu.ts index ca201752e0a..d8c80850d1b 100644 --- a/src/core/features/mainmenu/pages/menu/menu.ts +++ b/src/core/features/mainmenu/pages/menu/menu.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; +import { Component, OnInit, OnDestroy, ViewChild, effect } from '@angular/core'; import { IonTabs } from '@ionic/angular'; import { BackButtonEvent } from '@ionic/core'; import { Subscription } from 'rxjs'; @@ -44,6 +44,7 @@ import { import { CoreSharedModule } from '@/core/shared.module'; import { CoreMainMenuUserButtonComponent } from '../../components/user-menu-button/user-menu-button'; import { BackButtonPriority } from '@/core/constants'; +import { CoreKeyboard } from '@singletons/keyboard'; const ANIMATION_DURATION = 500; @@ -119,6 +120,22 @@ export default class CoreMainMenuPage implements OnInit, OnDestroy { this.isMainScreen = !this.mainTabs?.outlet.canGoBack(); this.updateVisibility(); }); + + if (CorePlatform.isIOS()) { + effect(() => { + const shown = CoreKeyboard.getKeyboardShownSignal(); + // In iOS, the resize event is triggered before the keyboard is opened/closed and not triggered again once done. + // Init handlers again once keyboard is closed since the resize event doesn't have the updated height. + if (!shown) { + this.updateHandlers(); + + // If the device is slow it can take a bit more to update the window height. Retry in a few ms. + setTimeout(() => { + this.updateHandlers(); + }, 250); + } + }); + } } /** @@ -150,20 +167,6 @@ export default class CoreMainMenuPage implements OnInit, OnDestroy { }); document.addEventListener('ionBackButton', this.backButtonFunction); - if (CorePlatform.isIOS()) { - // In iOS, the resize event is triggered before the keyboard is opened/closed and not triggered again once done. - // Init handlers again once keyboard is closed since the resize event doesn't have the updated height. - this.keyboardObserver = CoreEvents.on(CoreEvents.KEYBOARD_CHANGE, (kbHeight: number) => { - if (kbHeight === 0) { - this.updateHandlers(); - - // If the device is slow it can take a bit more to update the window height. Retry in a few ms. - setTimeout(() => { - this.updateHandlers(); - }, 250); - } - }); - } CoreEvents.trigger(CoreEvents.MAIN_HOME_LOADED); } diff --git a/src/core/initializers/subscribe-to-keyboard-events.ts b/src/core/initializers/subscribe-to-keyboard-events.ts index a160dabc0cc..b4f9f6684f1 100644 --- a/src/core/initializers/subscribe-to-keyboard-events.ts +++ b/src/core/initializers/subscribe-to-keyboard-events.ts @@ -25,6 +25,6 @@ export default function(): void { // Execute callbacks in the Angular zone, so change detection doesn't stop working. keyboard.onKeyboardShow().subscribe(data => zone.run(() => CoreKeyboard.onKeyboardShow(data.keyboardHeight))); keyboard.onKeyboardHide().subscribe(() => zone.run(() => CoreKeyboard.onKeyboardHide())); - keyboard.onKeyboardWillShow().subscribe(() => zone.run(() => CoreKeyboard.onKeyboardWillShow())); + keyboard.onKeyboardWillShow().subscribe((data) => zone.run(() => CoreKeyboard.onKeyboardWillShow(data.keyboardHeight))); keyboard.onKeyboardWillHide().subscribe(() => zone.run(() => CoreKeyboard.onKeyboardWillHide())); } diff --git a/src/core/services/app.ts b/src/core/services/app.ts index 1492fd5ea3b..ca7ff7f82a8 100644 --- a/src/core/services/app.ts +++ b/src/core/services/app.ts @@ -90,7 +90,7 @@ export class CoreAppProvider { /** * Closes the keyboard. * - * @deprecated sinde 4.5.0. Use CoreKeyboard.closeKeyboard instead. + * @deprecated since 4.5.0. Use CoreKeyboard.closeKeyboard instead. */ closeKeyboard(): void { CoreKeyboard.close(); diff --git a/src/core/singletons/events.ts b/src/core/singletons/events.ts index f5aba91d55b..dfd305c5864 100644 --- a/src/core/singletons/events.ts +++ b/src/core/singletons/events.ts @@ -107,6 +107,9 @@ export class CoreEvents { static readonly IAB_MESSAGE = 'inappbrowser_message'; static readonly APP_LAUNCHED_URL = 'app_launched_url'; // App opened with a certain URL (custom URL scheme). static readonly FILE_SHARED = 'file_shared'; + /** + * @deprecated since 5.0.0. Use CoreKeyboard.getKeyboardShownSignal signal. + */ static readonly KEYBOARD_CHANGE = 'keyboard_change'; static readonly ORIENTATION_CHANGE = 'orientation_change'; static readonly SEND_ON_ENTER_CHANGED = 'send_on_enter_changed'; diff --git a/src/core/singletons/keyboard.ts b/src/core/singletons/keyboard.ts index 2c3c0126943..87d0d4be27d 100644 --- a/src/core/singletons/keyboard.ts +++ b/src/core/singletons/keyboard.ts @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { effect, Signal, signal } from '@angular/core'; import { CorePlatform } from '@services/platform'; import { Keyboard } from '@singletons'; import { CoreEvents } from '@singletons/events'; @@ -21,13 +22,16 @@ import { CoreEvents } from '@singletons/events'; */ export class CoreKeyboard { - protected static isKeyboardShown = false; - protected static keyboardOpening = false; - protected static keyboardClosing = false; + protected static readonly IS_KEYBOARD_SHOWN = signal(false); + protected static readonly KEYBOARD_OPENING = signal(false); + protected static readonly KEYBOARD_CLOSING = signal(false); + protected static readonly KEYBOARD_HEIGHT = signal(0); // Avoid creating singleton instances. private constructor() { - // Nothing to do. + effect(() => { + document.body.classList.toggle('keyboard-is-open', CoreKeyboard.IS_KEYBOARD_SHOWN()); + }); } /** @@ -49,53 +53,70 @@ export class CoreKeyboard { } } + static getKeyboardShownSignal(): Signal { + return CoreKeyboard.IS_KEYBOARD_SHOWN.asReadonly(); + } + + static getKeyboardHeightSignal(): Signal { + return CoreKeyboard.KEYBOARD_HEIGHT.asReadonly(); + } + /** * Notify that Keyboard has been shown. * * @param keyboardHeight Keyboard height. */ static onKeyboardShow(keyboardHeight: number): void { - document.body.classList.add('keyboard-is-open'); - CoreKeyboard.setKeyboardShown(true); // Error on iOS calculating size. - // More info: https://github.com/ionic-team/ionic-plugin-keyboard/issues/276 . - CoreEvents.trigger(CoreEvents.KEYBOARD_CHANGE, keyboardHeight); + // More info: https://github.com/ionic-team/ionic-plugin-keyboard/issues/276 + CoreKeyboard.setKeyboardShown(true, keyboardHeight); } /** * Notify that Keyboard has been hidden. */ static onKeyboardHide(): void { - document.body.classList.remove('keyboard-is-open'); - CoreKeyboard.setKeyboardShown(false); - CoreEvents.trigger(CoreEvents.KEYBOARD_CHANGE, 0); + CoreKeyboard.setKeyboardShown(false, 0); } /** * Notify that Keyboard is about to be shown. + * + * @param keyboardHeight Keyboard height. */ - static onKeyboardWillShow(): void { - CoreKeyboard.keyboardOpening = true; - CoreKeyboard.keyboardClosing = false; + static onKeyboardWillShow(keyboardHeight?: number): void { + CoreKeyboard.KEYBOARD_OPENING.set(true); + CoreKeyboard.KEYBOARD_CLOSING.set(false); + + if (keyboardHeight !== undefined) { + this.KEYBOARD_HEIGHT.set(keyboardHeight); + } } /** * Notify that Keyboard is about to be hidden. */ static onKeyboardWillHide(): void { - CoreKeyboard.keyboardOpening = false; - CoreKeyboard.keyboardClosing = true; + CoreKeyboard.KEYBOARD_OPENING.set(false); + CoreKeyboard.KEYBOARD_CLOSING.set(true); + + this.KEYBOARD_HEIGHT.set(0); } /** * Set keyboard shown or hidden. * * @param shown Whether the keyboard is shown or hidden. + * @param keyboardHeight Keyboard height. */ - protected static setKeyboardShown(shown: boolean): void { - CoreKeyboard.isKeyboardShown = shown; - CoreKeyboard.keyboardOpening = false; - CoreKeyboard.keyboardClosing = false; + protected static setKeyboardShown(shown: boolean, keyboardHeight: number): void { + CoreKeyboard.IS_KEYBOARD_SHOWN.set(shown); + CoreKeyboard.KEYBOARD_OPENING.set(false); + CoreKeyboard.KEYBOARD_CLOSING.set(false); + this.KEYBOARD_HEIGHT.set(keyboardHeight); + + // eslint-disable-next-line @typescript-eslint/no-deprecated + CoreEvents.trigger(CoreEvents.KEYBOARD_CHANGE, keyboardHeight); } /** @@ -104,7 +125,7 @@ export class CoreKeyboard { * @returns Whether keyboard is closing (animating). */ static isKeyboardClosing(): boolean { - return CoreKeyboard.keyboardClosing; + return CoreKeyboard.KEYBOARD_CLOSING(); } /** @@ -113,7 +134,7 @@ export class CoreKeyboard { * @returns Whether keyboard is opening (animating). */ static isKeyboardOpening(): boolean { - return CoreKeyboard.keyboardOpening; + return CoreKeyboard.KEYBOARD_OPENING(); } /** @@ -122,7 +143,7 @@ export class CoreKeyboard { * @returns Whether keyboard is visible. */ static isKeyboardVisible(): boolean { - return CoreKeyboard.isKeyboardShown; + return CoreKeyboard.IS_KEYBOARD_SHOWN(); } } From c7465427453eab4e6ea34067688a583ed52c0409 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 29 Jan 2025 15:51:27 +0100 Subject: [PATCH 4/4] MOBILE-4842 network: Simplify network type management --- .../tests/behat/behat_app.php | 7 +- scripts/langindex.json | 2 +- src/core/classes/sites/authenticated-site.ts | 2 +- src/core/directives/external-content.ts | 3 +- .../features/course/services/course-helper.ts | 2 +- .../services/fileuploader-helper.ts | 2 +- src/core/features/settings/lang.json | 2 +- .../settings/pages/deviceinfo/deviceinfo.html | 4 +- .../settings/pages/deviceinfo/deviceinfo.ts | 8 +- src/core/features/settings/pages/site/site.ts | 4 +- .../pages/synchronization/synchronization.ts | 4 +- .../settings/services/settings-helper.ts | 2 +- src/core/services/cron.ts | 2 +- src/core/services/file-helper.ts | 2 +- src/core/services/filepool.ts | 4 +- src/core/services/network.ts | 108 ++++++++++-------- src/core/services/overlays/alerts.ts | 4 +- src/core/services/ws.ts | 2 +- 18 files changed, 91 insertions(+), 73 deletions(-) diff --git a/local_moodleappbehat/tests/behat/behat_app.php b/local_moodleappbehat/tests/behat/behat_app.php index 20c957c2b2f..cdb7d95c898 100644 --- a/local_moodleappbehat/tests/behat/behat_app.php +++ b/local_moodleappbehat/tests/behat/behat_app.php @@ -1179,15 +1179,16 @@ public function i_switch_offline_mode(string $offline) { public function i_switch_network_connection(string $mode) { switch ($mode) { case 'wifi': - $this->runtime_js("network.setForceConnectionMode('$mode');"); + $this->runtime_js("network.setForceConnectionMode('not_measured');"); break; case 'cellular': - $this->runtime_js("network.setForceConnectionMode('$mode');"); + $this->runtime_js("network.setForceConnectionMode('measured');"); break; case 'offline': - $this->runtime_js("network.setForceConnectionMode('none');"); + $this->runtime_js("network.setForceConnectionMode('offline');"); break; default: + $this->runtime_js("network.setForceConnectionMode('unknown');"); break; } } diff --git a/scripts/langindex.json b/scripts/langindex.json index 777bb28b359..a81f93ba243 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -2537,6 +2537,7 @@ "core.settings.loggedin": "message", "core.settings.loggedoff": "message", "core.settings.logintosync": "local_moodlemobileapp", + "core.settings.measuredconnection": "local_moodlemobileapp", "core.settings.navigatorlanguage": "local_moodlemobileapp", "core.settings.navigatoruseragent": "local_moodlemobileapp", "core.settings.networkstatus": "local_moodlemobileapp", @@ -2560,7 +2561,6 @@ "core.settings.synchronizenowhelp": "local_moodlemobileapp", "core.settings.syncsettings": "local_moodlemobileapp", "core.settings.total": "moodle", - "core.settings.wificonnection": "local_moodlemobileapp", "core.settings.youradev": "local_moodlemobileapp", "core.share": "moodle", "core.sharedfiles.chooseaccountstorefile": "local_moodlemobileapp", diff --git a/src/core/classes/sites/authenticated-site.ts b/src/core/classes/sites/authenticated-site.ts index 345eb651cba..d39cca582ba 100644 --- a/src/core/classes/sites/authenticated-site.ts +++ b/src/core/classes/sites/authenticated-site.ts @@ -1592,7 +1592,7 @@ export class CoreAuthenticatedSite extends CoreUnauthenticatedSite { let expirationDelay = CoreAuthenticatedSite.UPDATE_FREQUENCIES[updateFrequency] || CoreAuthenticatedSite.UPDATE_FREQUENCIES[CoreCacheUpdateFrequency.USUALLY]; - if (CoreNetwork.isNetworkAccessLimited()) { + if (CoreNetwork.connectionIsMeasured()) { // Not WiFi, increase the expiration delay a 50% to decrease the data usage in this case. expirationDelay *= 1.5; } diff --git a/src/core/directives/external-content.ts b/src/core/directives/external-content.ts index 6345c16bd55..54921a7db64 100644 --- a/src/core/directives/external-content.ts +++ b/src/core/directives/external-content.ts @@ -518,7 +518,8 @@ export class CoreExternalContentDirective implements AfterViewInit, OnChanges, O clickableEl.addEventListener(eventName, () => { // User played media or opened a downloadable link. // Download the file if in wifi and it hasn't been downloaded already (for big files). - if (state !== DownloadStatus.DOWNLOADED && state !== DownloadStatus.DOWNLOADING && CoreNetwork.isWifi()) { + if (state !== DownloadStatus.DOWNLOADED && state !== DownloadStatus.DOWNLOADING && + CoreNetwork.connectionIsNotMeasured()) { // We aren't using the result, so it doesn't matter which of the 2 functions we call. CoreFilepool.getUrlByUrl(site.getId(), url, this.component, this.componentId, 0, false); } diff --git a/src/core/features/course/services/course-helper.ts b/src/core/features/course/services/course-helper.ts index 7ea007c3c3d..5ce43462e39 100644 --- a/src/core/features/course/services/course-helper.ts +++ b/src/core/features/course/services/course-helper.ts @@ -890,7 +890,7 @@ export class CoreCourseHelperProvider { } // Start the download if in wifi, but return the URL right away so the file is opened. - if (CoreNetwork.isWifi()) { + if (CoreNetwork.connectionIsNotMeasured()) { this.downloadModule(module, courseId, component, componentId, files, siteId); } diff --git a/src/core/features/fileuploader/services/fileuploader-helper.ts b/src/core/features/fileuploader/services/fileuploader-helper.ts index 06ddb44ba9f..7a38d1d7e78 100644 --- a/src/core/features/fileuploader/services/fileuploader-helper.ts +++ b/src/core/features/fileuploader/services/fileuploader-helper.ts @@ -140,7 +140,7 @@ export class CoreFileUploaderHelperProvider { if (size < 0) { return CoreAlerts.confirm(Translate.instant('core.fileuploader.confirmuploadunknownsize')); - } else if (size >= wifiThreshold || (CoreNetwork.isNetworkAccessLimited() && size >= limitedThreshold)) { + } else if (size >= wifiThreshold || (CoreNetwork.connectionIsMeasured() && size >= limitedThreshold)) { const readableSize = CoreText.bytesToSize(size, 2); return CoreAlerts.confirm(Translate.instant('core.fileuploader.confirmuploadfile', { size: readableSize })); diff --git a/src/core/features/settings/lang.json b/src/core/features/settings/lang.json index 00202ecea47..f2df5c70b94 100644 --- a/src/core/features/settings/lang.json +++ b/src/core/features/settings/lang.json @@ -79,6 +79,6 @@ "synchronizenowhelp": "Synchronising a site will send pending changes and all offline activity stored in the device and will synchronise some data like messages and notifications.", "syncsettings": "Synchronisation settings", "total": "Total", - "wificonnection": "Wi-Fi connection", + "measuredconnection": "Measured data connection", "youradev": "You are now a developer" } diff --git a/src/core/features/settings/pages/deviceinfo/deviceinfo.html b/src/core/features/settings/pages/deviceinfo/deviceinfo.html index e5386aabb0b..e665d0054b0 100644 --- a/src/core/features/settings/pages/deviceinfo/deviceinfo.html +++ b/src/core/features/settings/pages/deviceinfo/deviceinfo.html @@ -148,8 +148,8 @@

-

{{ 'core.settings.wificonnection' | translate}}

- @if (deviceInfo.wifiConnection()) { +

{{ 'core.settings.measuredconnection' | translate}}

+ @if (deviceInfo.measuredConnection()) {

{{ 'core.yes' | translate }}

} @else {

{{ 'core.no' | translate }}

diff --git a/src/core/features/settings/pages/deviceinfo/deviceinfo.ts b/src/core/features/settings/pages/deviceinfo/deviceinfo.ts index 9824ac5b6b7..fa7cb93230d 100644 --- a/src/core/features/settings/pages/deviceinfo/deviceinfo.ts +++ b/src/core/features/settings/pages/deviceinfo/deviceinfo.ts @@ -25,7 +25,7 @@ import { CoreConfig } from '@services/config'; import { CoreToasts } from '@services/overlays/toasts'; import { CoreNavigator } from '@services/navigator'; import { CorePlatform } from '@services/platform'; -import { CoreNetwork, CoreNetworkConnection } from '@services/network'; +import { CoreNetwork, CoreNetworkConnectionType } from '@services/network'; import { CoreLoginHelper } from '@features/login/services/login-helper'; import { CoreSitesFactory } from '@services/sites-factory'; import { CoreText } from '@singletons/text'; @@ -53,7 +53,7 @@ interface CoreSettingsDeviceInfo { deviceType: string; screen?: string; isOnline: Signal; - wifiConnection: Signal; + measuredConnection: Signal; cordovaVersion?: string; platform?: string; osVersion?: string; @@ -96,7 +96,7 @@ export default class CoreSettingsDeviceInfoPage { compilationTime: CoreConstants.BUILD.compilationTime || 0, lastCommit: CoreConstants.BUILD.lastCommitHash || '', isOnline: CoreNetwork.onlineSignal(), - wifiConnection: computed(() => CoreNetwork.connectionTypeSignal()() === CoreNetworkConnection.WIFI), + measuredConnection: computed(() => CoreNetwork.connectionTypeSignal()() === CoreNetworkConnectionType.MEASURED), localNotifAvailable: CoreLocalNotifications.isPluginAvailable() ? 'yes' : 'no', pushId: CorePushNotifications.getPushId(), deviceType: '', @@ -212,7 +212,7 @@ export default class CoreSettingsDeviceInfoPage { const deviceInfo = { ...this.deviceInfo, isOnline: this.deviceInfo.isOnline(), - wifiConnection: this.deviceInfo.wifiConnection(), + measuredConnection: this.deviceInfo.measuredConnection(), }; CoreText.copyToClipboard(JSON.stringify(deviceInfo)); } diff --git a/src/core/features/settings/pages/site/site.ts b/src/core/features/settings/pages/site/site.ts index 7063aeee1e7..8b948be73d8 100644 --- a/src/core/features/settings/pages/site/site.ts +++ b/src/core/features/settings/pages/site/site.ts @@ -73,13 +73,13 @@ export default class CoreSitePreferencesPage implements AfterViewInit, OnDestroy }, this.siteId); this.isOnline = CoreNetwork.isOnline(); - this.limitedConnection = this.isOnline && CoreNetwork.isNetworkAccessLimited(); + this.limitedConnection = CoreNetwork.connectionIsMeasured(); this.networkObserver = CoreNetwork.onChange().subscribe(() => { // Execute the callback in the Angular zone, so change detection doesn't stop working. NgZone.run(() => { this.isOnline = CoreNetwork.isOnline(); - this.limitedConnection = this.isOnline && CoreNetwork.isNetworkAccessLimited(); + this.limitedConnection = CoreNetwork.connectionIsMeasured(); }); }); } diff --git a/src/core/features/settings/pages/synchronization/synchronization.ts b/src/core/features/settings/pages/synchronization/synchronization.ts index 627ed67ef7a..4f67d2ef718 100644 --- a/src/core/features/settings/pages/synchronization/synchronization.ts +++ b/src/core/features/settings/pages/synchronization/synchronization.ts @@ -93,13 +93,13 @@ export default class CoreSettingsSynchronizationPage implements OnInit, OnDestro }); this.isOnline = CoreNetwork.isOnline(); - this.limitedConnection = this.isOnline && CoreNetwork.isNetworkAccessLimited(); + this.limitedConnection = CoreNetwork.connectionIsMeasured(); this.networkObserver = CoreNetwork.onChange().subscribe(() => { // Execute the callback in the Angular zone, so change detection doesn't stop working. NgZone.run(() => { this.isOnline = CoreNetwork.isOnline(); - this.limitedConnection = this.isOnline && CoreNetwork.isNetworkAccessLimited(); + this.limitedConnection = CoreNetwork.connectionIsMeasured(); }); }); diff --git a/src/core/features/settings/services/settings-helper.ts b/src/core/features/settings/services/settings-helper.ts index 2dd2f95bcea..dceaf2486e9 100644 --- a/src/core/features/settings/services/settings-helper.ts +++ b/src/core/features/settings/services/settings-helper.ts @@ -241,7 +241,7 @@ export class CoreSettingsHelperProvider { } else if (hasSyncHandlers && !CoreNetwork.isOnline()) { // We need connection to execute sync. throw new CoreError(Translate.instant('core.settings.cannotsyncoffline')); - } else if (hasSyncHandlers && syncOnlyOnWifi && CoreNetwork.isNetworkAccessLimited()) { + } else if (hasSyncHandlers && syncOnlyOnWifi && CoreNetwork.connectionIsMeasured()) { throw new CoreError(Translate.instant('core.settings.cannotsyncwithoutwifi')); } diff --git a/src/core/services/cron.ts b/src/core/services/cron.ts index 15d3a8dc3a7..30d1b03049c 100644 --- a/src/core/services/cron.ts +++ b/src/core/services/cron.ts @@ -99,7 +99,7 @@ export class CoreCronDelegateService { // Check network connection. const syncOnlyOnWifi = await CoreConfig.get(CoreConstants.SETTINGS_SYNC_ONLY_ON_WIFI, false); - if (syncOnlyOnWifi && !CoreNetwork.isWifi()) { + if (syncOnlyOnWifi && !CoreNetwork.connectionIsNotMeasured()) { // Cannot execute in this network connection, retry soon. this.logger.debug(`Cron job failed because your device has a limited internet connection: ${name}`); this.scheduleNextExecution(name, CoreCronDelegateService.MIN_INTERVAL); diff --git a/src/core/services/file-helper.ts b/src/core/services/file-helper.ts index 53a94287cef..e51f044a792 100644 --- a/src/core/services/file-helper.ts +++ b/src/core/services/file-helper.ts @@ -165,7 +165,7 @@ export class CoreFileHelperProvider { } // The file system is available. - const isWifi = CoreNetwork.isWifi(); + const isWifi = CoreNetwork.connectionIsNotMeasured(); const isOnline = CoreNetwork.isOnline(); if (state === DownloadStatus.DOWNLOADED) { diff --git a/src/core/services/filepool.ts b/src/core/services/filepool.ts index 2ecbb520d34..88a9be0bb17 100644 --- a/src/core/services/filepool.ts +++ b/src/core/services/filepool.ts @@ -828,7 +828,7 @@ export class CoreFilepoolProvider { } // Calculate the size of the file. - const isWifi = CoreNetwork.isWifi(); + const isWifi = CoreNetwork.connectionIsNotMeasured(); const sizeUnknown = size <= 0; if (!sizeUnknown) { @@ -3027,7 +3027,7 @@ export class CoreFilepoolProvider { */ shouldDownload(size: number): boolean { return size <= CoreFilepoolProvider.DOWNLOAD_THRESHOLD || - (CoreNetwork.isWifi() && size <= CoreFilepoolProvider.WIFI_DOWNLOAD_THRESHOLD); + (CoreNetwork.connectionIsNotMeasured() && size <= CoreFilepoolProvider.WIFI_DOWNLOAD_THRESHOLD); } /** diff --git a/src/core/services/network.ts b/src/core/services/network.ts index 0d3493d2a61..42d17b303db 100644 --- a/src/core/services/network.ts +++ b/src/core/services/network.ts @@ -12,14 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { computed, effect, Injectable, Signal, signal } from '@angular/core'; +import { effect, Injectable, Signal, signal } from '@angular/core'; import { CorePlatform } from '@services/platform'; import { Network } from '@awesome-cordova-plugins/network/ngx'; import { makeSingleton } from '@singletons'; import { Observable, Subject, merge } from 'rxjs'; import { CoreHTMLClasses } from '@singletons/html-classes'; -export enum CoreNetworkConnection { +enum CoreNetworkConnection { UNKNOWN = 'unknown', ETHERNET = 'ethernet', WIFI = 'wifi', @@ -30,6 +30,13 @@ export enum CoreNetworkConnection { NONE = 'none', } +export enum CoreNetworkConnectionType { + UNKNOWN = 'unknown', + NOT_MEASURED = 'not_measured', + MEASURED = 'measured', + OFFLINE = 'offline', +} + /** * Service to manage network connections. */ @@ -41,11 +48,10 @@ export class CoreNetworkService extends Network { protected connectObservable = new Subject<'connected'>(); protected connectStableObservable = new Subject<'connected'>(); protected disconnectObservable = new Subject<'disconnected'>(); - protected forceConnectionMode?: CoreNetworkConnection; - protected readonly online = signal(false); + protected forceConnectionMode?: CoreNetworkConnectionType; protected connectStableTimeout?: number; - private readonly _connectionType = signal(CoreNetworkConnection.UNKNOWN); - private readonly _limitedConnection = computed(() => this.online() && CoreNetwork.isNetworkAccessLimited()); + protected readonly online = signal(false); + private readonly _connectionType = signal(CoreNetworkConnectionType.UNKNOWN); constructor() { super(); @@ -69,7 +75,7 @@ export class CoreNetworkService extends Network { }); } - get connectionType(): CoreNetworkConnection { + get connectionType(): CoreNetworkConnectionType { CoreNetwork.updateConnectionType(); return this._connectionType(); @@ -91,18 +97,6 @@ export class CoreNetworkService extends Network { this.fireObservable(); }); } else { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ( window).Connection = { - UNKNOWN: CoreNetworkConnection.UNKNOWN, // eslint-disable-line @typescript-eslint/naming-convention - ETHERNET: CoreNetworkConnection.ETHERNET, // eslint-disable-line @typescript-eslint/naming-convention - WIFI: CoreNetworkConnection.WIFI, // eslint-disable-line @typescript-eslint/naming-convention - CELL_2G: CoreNetworkConnection.CELL_2G, // eslint-disable-line @typescript-eslint/naming-convention - CELL_3G: CoreNetworkConnection.CELL_3G, // eslint-disable-line @typescript-eslint/naming-convention - CELL_4G: CoreNetworkConnection.CELL_4G, // eslint-disable-line @typescript-eslint/naming-convention - CELL: CoreNetworkConnection.CELL, // eslint-disable-line @typescript-eslint/naming-convention - NONE: CoreNetworkConnection.NONE, // eslint-disable-line @typescript-eslint/naming-convention - }; - window.addEventListener('online', () => { this.fireObservable(); }, false); @@ -130,7 +124,7 @@ export class CoreNetworkService extends Network { * * @param value Value to set. */ - setForceConnectionMode(value: CoreNetworkConnection): void { + setForceConnectionMode(value: CoreNetworkConnectionType): void { this.forceConnectionMode = value; this.fireObservable(); } @@ -151,7 +145,7 @@ export class CoreNetworkService extends Network { // Recalculate connection type. CoreNetwork.updateConnectionType(); - if (this.forceConnectionMode === CoreNetworkConnection.NONE) { + if (this.forceConnectionMode === CoreNetworkConnectionType.OFFLINE) { this.online.set(false); return; @@ -166,7 +160,7 @@ export class CoreNetworkService extends Network { } const type = this._connectionType(); - let online = type !== null && type !== CoreNetworkConnection.NONE && type !== CoreNetworkConnection.UNKNOWN; + let online = type !== null && type !== CoreNetworkConnectionType.OFFLINE && type !== CoreNetworkConnectionType.UNKNOWN; // Double check we are not online because we cannot rely 100% in Cordova APIs. if (!online && navigator.onLine) { @@ -187,12 +181,32 @@ export class CoreNetworkService extends Network { } if (CorePlatform.isMobile()) { - this._connectionType.set(this.type as CoreNetworkConnection); - - return; + switch (this.type) { + case CoreNetworkConnection.WIFI: + case CoreNetworkConnection.ETHERNET: + this._connectionType.set(CoreNetworkConnectionType.NOT_MEASURED); + + return; + case CoreNetworkConnection.CELL: + case CoreNetworkConnection.CELL_2G: + case CoreNetworkConnection.CELL_3G: + case CoreNetworkConnection.CELL_4G: + this._connectionType.set(CoreNetworkConnectionType.MEASURED); + + return; + case CoreNetworkConnection.NONE: + this._connectionType.set(CoreNetworkConnectionType.OFFLINE); + + return; + default: + case CoreNetworkConnection.UNKNOWN: + this._connectionType.set(CoreNetworkConnectionType.UNKNOWN); + + return; + } } - this._connectionType.set(this.online() ? CoreNetworkConnection.WIFI : CoreNetworkConnection.NONE); + this._connectionType.set(this.online() ? CoreNetworkConnectionType.NOT_MEASURED : CoreNetworkConnectionType.OFFLINE); } /** @@ -213,21 +227,12 @@ export class CoreNetworkService extends Network { return this.online.asReadonly(); } - /** - * Returns a signal to watch limited connection status. - * - * @returns Signal. - */ - limitedConnectionSignal(): Signal { - return this._limitedConnection; - } - /** * Returns a signal to watch connection type. * * @returns Signal. */ - connectionTypeSignal(): Signal { + connectionTypeSignal(): Signal { return this._connectionType.asReadonly(); } @@ -284,18 +289,29 @@ export class CoreNetworkService extends Network { * Check if device uses a limited connection. * * @returns Whether the device uses a limited connection. + * @deprecated since 5.0. Use connectionIsMeasured instead. */ isNetworkAccessLimited(): boolean { - const limited: CoreNetworkConnection[] = [ - CoreNetworkConnection.CELL_2G, - CoreNetworkConnection.CELL_3G, - CoreNetworkConnection.CELL_4G, - CoreNetworkConnection.CELL, - ]; + return this.connectionIsMeasured(); + } - const type = this.connectionType; + /** + * Check if device uses a wifi connection. + * + * @returns Whether the device uses a wifi connection. + * @deprecated since 5.0. Use connectionIsNotMeasured instead. + */ + isWifi(): boolean { + return this.connectionIsNotMeasured(); + } - return limited.indexOf(type) > -1; + /** + * Check if device uses a limited connection. + * + * @returns Whether the device uses a limited connection. + */ + connectionIsMeasured(): boolean { + return this.connectionType === CoreNetworkConnectionType.MEASURED; } /** @@ -303,8 +319,8 @@ export class CoreNetworkService extends Network { * * @returns Whether the device uses a wifi connection. */ - isWifi(): boolean { - return this.isOnline() && !this.isNetworkAccessLimited(); + connectionIsNotMeasured(): boolean { + return this.connectionType === CoreNetworkConnectionType.NOT_MEASURED; } } diff --git a/src/core/services/overlays/alerts.ts b/src/core/services/overlays/alerts.ts index c69f500057f..5759c9a7e6e 100644 --- a/src/core/services/overlays/alerts.ts +++ b/src/core/services/overlays/alerts.ts @@ -186,7 +186,7 @@ export class CoreAlertsService { const limitedThreshold = options.limitedThreshold ?? CoreConstants.DOWNLOAD_THRESHOLD; let wifiPrefix = ''; - if (CoreNetwork.isNetworkAccessLimited()) { + if (CoreNetwork.connectionIsMeasured()) { wifiPrefix = Translate.instant('core.course.confirmlimiteddownload'); } @@ -203,7 +203,7 @@ export class CoreAlertsService { { size: readableSize, availableSpace: availableSpace }, )); } else if (options.alwaysConfirm || size.size >= wifiThreshold || - (CoreNetwork.isNetworkAccessLimited() && size.size >= limitedThreshold)) { + (CoreNetwork.connectionIsMeasured() && size.size >= limitedThreshold)) { return this.confirm(wifiPrefix + Translate.instant( options.message ?? (size.size === 0 ? 'core.course.confirmdownloadzerosize' : 'core.course.confirmdownload'), diff --git a/src/core/services/ws.ts b/src/core/services/ws.ts index e038ceba1cd..5f94635c959 100644 --- a/src/core/services/ws.ts +++ b/src/core/services/ws.ts @@ -398,7 +398,7 @@ export class CoreWSProvider { * @returns Timeout in ms. */ getRequestTimeout(): number { - return CoreNetwork.isNetworkAccessLimited() ? CoreConstants.WS_TIMEOUT : CoreConstants.WS_TIMEOUT_WIFI; + return CoreNetwork.connectionIsMeasured() ? CoreConstants.WS_TIMEOUT : CoreConstants.WS_TIMEOUT_WIFI; } /**