diff --git a/local_moodleappbehat/tests/behat/behat_app.php b/local_moodleappbehat/tests/behat/behat_app.php index 20c957c2b2f..95d456154f2 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('wifi');"); break; case 'cellular': - $this->runtime_js("network.setForceConnectionMode('$mode');"); + $this->runtime_js("network.setForceConnectionMode('cellular');"); 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/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/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/classes/sites/authenticated-site.ts b/src/core/classes/sites/authenticated-site.ts index 345eb651cba..eb3309254d7 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.isCellular()) { // 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..04d608f272e 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.isWifi()) { // 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/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/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; } 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/fileuploader/services/fileuploader-helper.ts b/src/core/features/fileuploader/services/fileuploader-helper.ts index 06ddb44ba9f..edd9e963bc1 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.isCellular() && size >= limitedThreshold)) { const readableSize = CoreText.bytesToSize(size, 2); return CoreAlerts.confirm(Translate.instant('core.fileuploader.confirmuploadfile', { size: readableSize })); 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/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..9821a3f1280 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, CoreNetworkConnectionType } 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()() === CoreNetworkConnectionType.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/features/settings/pages/site/site.ts b/src/core/features/settings/pages/site/site.ts index 7063aeee1e7..8bab0db3284 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.isCellular(); 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.isCellular(); }); }); } diff --git a/src/core/features/settings/pages/synchronization/synchronization.ts b/src/core/features/settings/pages/synchronization/synchronization.ts index 627ed67ef7a..9da24d55b43 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.isCellular(); 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.isCellular(); }); }); diff --git a/src/core/features/settings/services/settings-helper.ts b/src/core/features/settings/services/settings-helper.ts index 2dd2f95bcea..6f7dd3b69a5 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.isCellular()) { throw new CoreError(Translate.instant('core.settings.cannotsyncwithoutwifi')); } 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/services/network.ts b/src/core/services/network.ts index 35597f4f51a..f2d13169e26 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 { Injectable } 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 { NgZone, makeSingleton } from '@singletons'; +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', + WIFI = 'wifi', // Usually a non-metered connection. + CELL = 'cellular', // Usually a metered connection. + OFFLINE = 'offline', +} + /** * Service to manage network connections. */ @@ -41,27 +48,44 @@ export class CoreNetworkService extends Network { protected connectObservable = new Subject<'connected'>(); protected connectStableObservable = new Subject<'connected'>(); protected disconnectObservable = new Subject<'disconnected'>(); - protected forceConnectionMode?: CoreNetworkConnection; - protected online = false; + protected forceConnectionMode?: CoreNetworkConnectionType; protected connectStableTimeout?: number; + protected readonly online = signal(false); + private readonly _connectionType = signal(CoreNetworkConnectionType.UNKNOWN); - 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); + } + }); + } - return this.online ? CoreNetworkConnection.WIFI : CoreNetworkConnection.NONE; + get connectionType(): CoreNetworkConnectionType { + CoreNetwork.updateConnectionType(); + + 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 @@ -73,6 +97,7 @@ export class CoreNetworkService extends Network { this.fireObservable(); }); } else { + // Match the Cordova constants to the ones used in the app. // eslint-disable-next-line @typescript-eslint/no-explicit-any ( window).Connection = { UNKNOWN: CoreNetworkConnection.UNKNOWN, // eslint-disable-line @typescript-eslint/naming-convention @@ -103,30 +128,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()); } /** @@ -135,7 +137,7 @@ export class CoreNetworkService extends Network { * * @param value Value to set. */ - setForceConnectionMode(value: CoreNetworkConnection): void { + setForceConnectionMode(value: CoreNetworkConnectionType): void { this.forceConnectionMode = value; this.fireObservable(); } @@ -146,15 +148,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 { - if (this.forceConnectionMode === CoreNetworkConnection.NONE) { - this.online = false; + protected updateOnline(): void { + // Recalculate connection type. + CoreNetwork.updateConnectionType(); + + if (this.forceConnectionMode === CoreNetworkConnectionType.OFFLINE) { + this.online.set(false); return; } @@ -162,20 +167,59 @@ 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; - let online = type !== null && type !== CoreNetworkConnection.NONE && type !== CoreNetworkConnection.UNKNOWN; + const type = this._connectionType(); + 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) { 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()) { + switch (this.type) { + case CoreNetworkConnection.WIFI: + case CoreNetworkConnection.ETHERNET: + this._connectionType.set(CoreNetworkConnectionType.WIFI); + + return; + case CoreNetworkConnection.CELL: + case CoreNetworkConnection.CELL_2G: + case CoreNetworkConnection.CELL_3G: + case CoreNetworkConnection.CELL_4G: + this._connectionType.set(CoreNetworkConnectionType.CELL); + + 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() ? CoreNetworkConnectionType.WIFI : CoreNetworkConnectionType.OFFLINE); } /** @@ -187,6 +231,24 @@ 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 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 +286,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'); @@ -240,18 +302,10 @@ export class CoreNetworkService extends Network { * Check if device uses a limited connection. * * @returns Whether the device uses a limited connection. + * @deprecated since 5.1. Use isCellular instead. */ isNetworkAccessLimited(): boolean { - const limited: CoreNetworkConnection[] = [ - CoreNetworkConnection.CELL_2G, - CoreNetworkConnection.CELL_3G, - CoreNetworkConnection.CELL_4G, - CoreNetworkConnection.CELL, - ]; - - const type = this.connectionType; - - return limited.indexOf(type) > -1; + return this.isCellular(); } /** @@ -260,7 +314,16 @@ export class CoreNetworkService extends Network { * @returns Whether the device uses a wifi connection. */ isWifi(): boolean { - return this.isOnline() && !this.isNetworkAccessLimited(); + return this.connectionType === CoreNetworkConnectionType.WIFI; + } + + /** + * Check if device uses a limited connection. + * + * @returns Whether the device uses a limited connection. + */ + isCellular(): boolean { + return this.connectionType === CoreNetworkConnectionType.CELL; } } diff --git a/src/core/services/overlays/alerts.ts b/src/core/services/overlays/alerts.ts index c69f500057f..c8e901ccc24 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.isCellular()) { 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.isCellular() && 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..6b9072f7a9b 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.isCellular() ? CoreConstants.WS_TIMEOUT : CoreConstants.WS_TIMEOUT_WIFI; } /** 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..4da3cd1dc36 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,80 @@ export class CoreKeyboard { } } + /** + * Get a signal that indicates whether the keyboard is shown or not. + * + * @returns Signal indicating whether the keyboard is shown. + */ + static getKeyboardShownSignal(): Signal { + return CoreKeyboard.IS_KEYBOARD_SHOWN.asReadonly(); + } + + /** + * Get a signal that indicates the keyboard height. + * + * @returns Signal indicating the keyboard height. + */ + 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 +135,7 @@ export class CoreKeyboard { * @returns Whether keyboard is closing (animating). */ static isKeyboardClosing(): boolean { - return CoreKeyboard.keyboardClosing; + return CoreKeyboard.KEYBOARD_CLOSING(); } /** @@ -113,7 +144,7 @@ export class CoreKeyboard { * @returns Whether keyboard is opening (animating). */ static isKeyboardOpening(): boolean { - return CoreKeyboard.keyboardOpening; + return CoreKeyboard.KEYBOARD_OPENING(); } /** @@ -122,7 +153,7 @@ export class CoreKeyboard { * @returns Whether keyboard is visible. */ static isKeyboardVisible(): boolean { - return CoreKeyboard.isKeyboardShown; + return CoreKeyboard.IS_KEYBOARD_SHOWN(); } }