From 06f31203453a3a26e04c21fae32b119033230761 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thea=20Sch=C3=B6bl?= Date: Wed, 26 Jul 2023 16:56:41 +0000 Subject: [PATCH] feat: use takeUntilDestroy instead of manually unsubscribing --- .../detail/assessments-detail.component.ts | 23 +++-- .../assessments-simple-data-list.component.ts | 21 ++--- .../page/assessments-page.component.ts | 29 ++++--- .../page/auth-callback-page.component.ts | 34 +++----- .../schedule/schedule-sync.service.ts | 30 +++---- .../app/modules/calendar/schedule.provider.ts | 10 +-- .../app/modules/catalog/catalog.component.ts | 29 ++----- .../modules/dashboard/dashboard.component.ts | 46 ++++------ .../data/add-event-action-chip.component.ts | 60 +++++++------ .../data/detail/data-path.component.ts | 24 +++--- .../modules/data/list/data-list.component.ts | 29 +++---- .../data/list/food-data-list.component.ts | 48 +++++------ .../data/list/search-page.component.ts | 75 ++++++++--------- .../data/list/simple-data-list.component.ts | 29 ++----- .../place/place-detail-content.component.ts | 33 +++----- .../mensa/place-mensa-detail.component.ts | 35 ++------ .../favorites/favorites-page.component.ts | 45 +++++----- .../hebis/list/hebis-search-page.component.ts | 44 +++++----- .../modules/map/page/map-page.component.ts | 84 +++++++++---------- .../app/modules/map/position.service.spec.ts | 36 +++----- .../src/app/modules/map/position.service.ts | 28 +++---- .../menu/context/context-menu.component.ts | 42 +++------- .../navigation/offline-notice.component.ts | 29 ++----- .../menu/navigation/root-link.directive.ts | 57 ++++++------- .../page/profile-page-section.component.ts | 26 +++--- .../profile/page/profile-page.component.ts | 27 +++--- .../schedule/page/calendar-view.component.ts | 19 ++--- .../page/components/calendar.component.ts | 32 ++----- .../page/schedule-single-events.component.ts | 30 ++----- .../schedule/page/schedule-view.component.ts | 22 ++--- .../translation/thing-translate.service.ts | 19 ++--- .../app/translation/translate-simple.pipe.ts | 19 +++-- .../ion-icon/ion-back-button.directive.ts | 39 +++++---- .../util/ion-icon/ion-breadcrumb.directive.ts | 1 - .../util/ion-icon/ion-reorder.directive.ts | 1 - .../app/src/app/util/ion-icon/replace-util.ts | 22 +---- frontend/app/src/app/util/pause-when.ts | 12 +++ 37 files changed, 476 insertions(+), 713 deletions(-) create mode 100644 frontend/app/src/app/util/pause-when.ts diff --git a/frontend/app/src/app/modules/assessments/detail/assessments-detail.component.ts b/frontend/app/src/app/modules/assessments/detail/assessments-detail.component.ts index bca37c5e..c935c230 100644 --- a/frontend/app/src/app/modules/assessments/detail/assessments-detail.component.ts +++ b/frontend/app/src/app/modules/assessments/detail/assessments-detail.component.ts @@ -13,21 +13,23 @@ * this program. If not, see . */ -import {Component, Input, OnDestroy, OnInit, ViewChild} from '@angular/core'; +import {Component, DestroyRef, inject, Input, OnInit, ViewChild} from '@angular/core'; import {ActivatedRoute} from '@angular/router'; import {AssessmentsProvider} from '../assessments.provider'; import {DataDetailComponent, ExternalDataLoadEvent} from '../../data/detail/data-detail.component'; import {NavController, ViewWillEnter} from '@ionic/angular'; -import {Subscription} from 'rxjs'; import {DataRoutingService} from '../../data/data-routing.service'; import {SCAssessment} from '@openstapps/core'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; @Component({ selector: 'assessments-detail', templateUrl: 'assessments-detail.html', styleUrls: ['assessments-detail.scss'], }) -export class AssessmentsDetailComponent implements ViewWillEnter, OnInit, OnDestroy { +export class AssessmentsDetailComponent implements ViewWillEnter, OnInit { + destroy$ = inject(DestroyRef); + constructor( readonly route: ActivatedRoute, readonly assessmentsProvider: AssessmentsProvider, @@ -36,8 +38,6 @@ export class AssessmentsDetailComponent implements ViewWillEnter, OnInit, OnDest readonly activatedRoute: ActivatedRoute, ) {} - subscriptions: Subscription[] = []; - @Input() dataPathAutoRouting = true; @ViewChild(DataDetailComponent) @@ -47,19 +47,16 @@ export class AssessmentsDetailComponent implements ViewWillEnter, OnInit, OnDest ngOnInit() { if (!this.dataPathAutoRouting) return; - this.subscriptions.push( - this.dataRoutingService.pathSelectListener().subscribe(item => { + this.dataRoutingService + .pathSelectListener() + .pipe(takeUntilDestroyed(this.destroy$)) + .subscribe(item => { void this.navController.navigateBack(['assessments', 'detail', item.uid], { queryParams: { token: this.activatedRoute.snapshot.queryParamMap.get('token'), }, }); - }), - ); - } - - ngOnDestroy() { - for (const sub of this.subscriptions) sub.unsubscribe(); + }); } getItem(event: ExternalDataLoadEvent) { diff --git a/frontend/app/src/app/modules/assessments/list/assessments-simple-data-list.component.ts b/frontend/app/src/app/modules/assessments/list/assessments-simple-data-list.component.ts index f78afe99..30a7dd5a 100644 --- a/frontend/app/src/app/modules/assessments/list/assessments-simple-data-list.component.ts +++ b/frontend/app/src/app/modules/assessments/list/assessments-simple-data-list.component.ts @@ -13,18 +13,18 @@ * this program. If not, see . */ -import {Component, Input, OnDestroy, OnInit} from '@angular/core'; +import {Component, DestroyRef, inject, Input, OnInit} from '@angular/core'; import {SCThings} from '@openstapps/core'; -import {Subscription} from 'rxjs'; import {DataRoutingService} from '../../data/data-routing.service'; import {ActivatedRoute, Router} from '@angular/router'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; @Component({ selector: 'assessments-simple-data-list', templateUrl: 'assessments-simple-data-list.html', styleUrls: ['assessments-simple-data-list.scss'], }) -export class AssessmentsSimpleDataListComponent implements OnInit, OnDestroy { +export class AssessmentsSimpleDataListComponent implements OnInit { /** * All SCThings to display */ @@ -44,7 +44,7 @@ export class AssessmentsSimpleDataListComponent implements OnInit, OnDestroy { this._items = new Promise(resolve => resolve(items)); } - subscriptions: Subscription[] = []; + destroy$ = inject(DestroyRef); constructor( readonly dataRoutingService: DataRoutingService, @@ -53,18 +53,15 @@ export class AssessmentsSimpleDataListComponent implements OnInit, OnDestroy { ) {} ngOnInit() { - this.subscriptions.push( - this.dataRoutingService.itemSelectListener().subscribe(thing => { + this.dataRoutingService + .itemSelectListener() + .pipe(takeUntilDestroyed(this.destroy$)) + .subscribe(thing => { void this.router.navigate(['assessments', 'detail', thing.uid], { queryParams: { token: this.activatedRoute.snapshot.queryParamMap.get('token'), }, }); - }), - ); - } - - ngOnDestroy() { - for (const subscription of this.subscriptions) subscription.unsubscribe(); + }); } } diff --git a/frontend/app/src/app/modules/assessments/page/assessments-page.component.ts b/frontend/app/src/app/modules/assessments/page/assessments-page.component.ts index 3e1be673..759ea087 100644 --- a/frontend/app/src/app/modules/assessments/page/assessments-page.component.ts +++ b/frontend/app/src/app/modules/assessments/page/assessments-page.component.ts @@ -12,17 +12,17 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -import {AfterViewInit, Component, OnDestroy, OnInit, ViewChild} from '@angular/core'; +import {AfterViewInit, Component, DestroyRef, inject, OnInit, ViewChild} from '@angular/core'; import {AssessmentsProvider} from '../assessments.provider'; import {SCAssessment, SCCourseOfStudy} from '@openstapps/core'; import {ActivatedRoute, Router} from '@angular/router'; -import {Subscription} from 'rxjs'; import {NGXLogger} from 'ngx-logger'; import {materialSharedAxisX} from '../../../animation/material-motion'; import {SharedAxisChoreographer} from '../../../animation/animation-choreographer'; import {DataProvider, DataScope} from '../../data/data.provider'; import {DataRoutingService} from '../../data/data-routing.service'; import {groupBy, mapValues} from '@openstapps/collection-utils'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; @Component({ selector: 'app-assessments-page', @@ -30,7 +30,7 @@ import {groupBy, mapValues} from '@openstapps/collection-utils'; styleUrls: ['assessments-page.scss'], animations: [materialSharedAxisX], }) -export class AssessmentsPageComponent implements OnInit, AfterViewInit, OnDestroy { +export class AssessmentsPageComponent implements OnInit, AfterViewInit { assessments: Promise< Record< string, @@ -43,12 +43,12 @@ export class AssessmentsPageComponent implements OnInit, AfterViewInit, OnDestro assessmentKeys: string[] = []; - routingSubscription: Subscription; - @ViewChild('segment') segmentView!: HTMLIonSegmentElement; sharedAxisChoreographer: SharedAxisChoreographer = new SharedAxisChoreographer('', []); + destroy$ = inject(DestroyRef); + constructor( readonly logger: NGXLogger, readonly assessmentsProvider: AssessmentsProvider, @@ -62,18 +62,17 @@ export class AssessmentsPageComponent implements OnInit, AfterViewInit, OnDestro this.segmentView.value = this.sharedAxisChoreographer.currentValue; } - ngOnDestroy() { - this.routingSubscription.unsubscribe(); - } - ngOnInit() { - this.routingSubscription = this.dataRoutingService.itemSelectListener().subscribe(thing => { - void this.router.navigate(['assessments', 'detail', thing.uid], { - queryParams: { - token: this.activatedRoute.snapshot.queryParamMap.get('token'), - }, + this.dataRoutingService + .itemSelectListener() + .pipe(takeUntilDestroyed(this.destroy$)) + .subscribe(thing => { + void this.router.navigate(['assessments', 'detail', thing.uid], { + queryParams: { + token: this.activatedRoute.snapshot.queryParamMap.get('token'), + }, + }); }); - }); this.activatedRoute.queryParams.subscribe(parameters => { try { diff --git a/frontend/app/src/app/modules/auth/auth-callback/page/auth-callback-page.component.ts b/frontend/app/src/app/modules/auth/auth-callback/page/auth-callback-page.component.ts index 3305de70..9b815412 100644 --- a/frontend/app/src/app/modules/auth/auth-callback/page/auth-callback-page.component.ts +++ b/frontend/app/src/app/modules/auth/auth-callback/page/auth-callback-page.component.ts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 StApps + * Copyright (C) 2023 StApps * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation, version 3. @@ -12,41 +12,29 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ - -import {OnInit, OnDestroy, Component} from '@angular/core'; +import {Component} from '@angular/core'; import {NavController} from '@ionic/angular'; import {Router} from '@angular/router'; import {AuthActions, IAuthAction} from 'ionic-appauth'; -import {Subscription} from 'rxjs'; import {SCAuthorizationProviderType} from '@openstapps/core'; import {AuthHelperService} from '../../auth-helper.service'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {Observable} from 'rxjs'; +import {IPAIAAuthAction} from '../../paia/paia-auth-action'; @Component({ templateUrl: 'auth-callback-page.component.html', styleUrls: ['auth-callback-page.component.scss'], }) -export class AuthCallbackPageComponent implements OnInit, OnDestroy { +export class AuthCallbackPageComponent { PROVIDER_TYPE: SCAuthorizationProviderType = 'default'; - private authEvents: Subscription; + constructor(private navCtrl: NavController, private router: Router, private authHelper: AuthHelperService) { + const provider = this.authHelper.getProvider(this.PROVIDER_TYPE); + const events: Observable = provider.events$; - constructor( - private navCtrl: NavController, - private router: Router, - private authHelper: AuthHelperService, - ) {} - - ngOnInit() { - this.authEvents = this.authHelper - .getProvider(this.PROVIDER_TYPE) - .events$.subscribe((action: IAuthAction) => this.postCallback(action)); - this.authHelper - .getProvider(this.PROVIDER_TYPE) - .authorizationCallback(window.location.origin + this.router.url); - } - - ngOnDestroy() { - this.authEvents.unsubscribe(); + events.pipe(takeUntilDestroyed()).subscribe((action: IAuthAction) => this.postCallback(action)); + provider.authorizationCallback(window.location.origin + this.router.url); } async postCallback(action: IAuthAction) { diff --git a/frontend/app/src/app/modules/background/schedule/schedule-sync.service.ts b/frontend/app/src/app/modules/background/schedule/schedule-sync.service.ts index da88dc54..3ceaa38b 100644 --- a/frontend/app/src/app/modules/background/schedule/schedule-sync.service.ts +++ b/frontend/app/src/app/modules/background/schedule/schedule-sync.service.ts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 StApps + * Copyright (C) 2023 StApps * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation, version 3. @@ -12,8 +12,7 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ - -import {Injectable, OnDestroy} from '@angular/core'; +import {DestroyRef, inject, Injectable} from '@angular/core'; import { DateSeriesRelevantData, dateSeriesRelevantKeys, @@ -28,7 +27,6 @@ import {BackgroundFetch} from '@transistorsoft/capacitor-background-fetch'; import {StorageProvider} from '../../storage/storage.provider'; import {CalendarService} from '../../calendar/calendar.service'; import {toICal} from '../../calendar/ical/ical'; -import {Subscription} from 'rxjs'; import {ChangesOf} from './changes'; import {hashStringToInt} from './hash'; import { @@ -38,9 +36,12 @@ import { } from '../../settings/page/calendar-sync-settings-keys'; import {filter} from 'rxjs/operators'; import {Capacitor} from '@capacitor/core'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; @Injectable() -export class ScheduleSyncService implements OnDestroy { +export class ScheduleSyncService { + destroy$ = inject(DestroyRef); + constructor( private scheduleProvider: ScheduleProvider, private storageProvider: StorageProvider, @@ -51,20 +52,19 @@ export class ScheduleSyncService implements OnDestroy { ) {} init() { - this.scheduleProvider.uuids$.pipe(filter(uuids => uuids?.length > 0)).subscribe(uuids => { - this.uuids = uuids; - void this.syncNativeCalendar(); - }); + this.scheduleProvider.uuids$ + .pipe( + takeUntilDestroyed(this.destroy$), + filter(uuids => uuids?.length > 0), + ) + .subscribe(uuids => { + this.uuids = uuids; + void this.syncNativeCalendar(); + }); } uuids: SCUuid[]; - uuidSubscription: Subscription; - - ngOnDestroy() { - this.uuidSubscription?.unsubscribe(); - } - private async isSyncEnabled(): Promise { return getCalendarSetting(this.storageProvider, CALENDAR_SYNC_ENABLED_KEY); } diff --git a/frontend/app/src/app/modules/calendar/schedule.provider.ts b/frontend/app/src/app/modules/calendar/schedule.provider.ts index 5c6c2cba..c94eef2c 100644 --- a/frontend/app/src/app/modules/calendar/schedule.provider.ts +++ b/frontend/app/src/app/modules/calendar/schedule.provider.ts @@ -13,7 +13,7 @@ * this program. If not, see . */ /* eslint-disable unicorn/no-null */ -import {Injectable, OnDestroy} from '@angular/core'; +import {DestroyRef, inject, Injectable, OnDestroy} from '@angular/core'; import { Bounds, SCDateSeries, @@ -23,11 +23,12 @@ import { SCThingType, SCUuid, } from '@openstapps/core'; -import {BehaviorSubject, Observable, Subscription} from 'rxjs'; +import {BehaviorSubject, Observable} from 'rxjs'; import {DataProvider} from '../data/data.provider'; import {map} from 'rxjs/operators'; import {DateFormatPipe, DurationPipe} from 'ngx-moment'; import {pick} from '@openstapps/collection-utils'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; /** * @@ -73,7 +74,7 @@ export class ScheduleProvider implements OnDestroy { private _partialEvents$?: BehaviorSubject; - private _partialEventsSubscription?: Subscription; + destroy$ = inject(DestroyRef); constructor(private readonly dataProvider: DataProvider) { window.addEventListener('storage', this.storageListener); @@ -125,7 +126,7 @@ export class ScheduleProvider implements OnDestroy { const data = ScheduleProvider.get(ScheduleProvider.partialEventsStorageKey); this._partialEvents$ = new BehaviorSubject(data ?? []); - this._partialEventsSubscription = this._partialEvents$.subscribe(result => { + this._partialEvents$.pipe(takeUntilDestroyed(this.destroy$)).subscribe(result => { ScheduleProvider.set(ScheduleProvider.partialEventsStorageKey, result); }); } @@ -257,7 +258,6 @@ export class ScheduleProvider implements OnDestroy { * TODO */ ngOnDestroy(): void { - this._partialEventsSubscription?.unsubscribe(); window.removeEventListener('storage', this.storageListener); } } diff --git a/frontend/app/src/app/modules/catalog/catalog.component.ts b/frontend/app/src/app/modules/catalog/catalog.component.ts index beefbf4c..2a1fa921 100644 --- a/frontend/app/src/app/modules/catalog/catalog.component.ts +++ b/frontend/app/src/app/modules/catalog/catalog.component.ts @@ -12,22 +12,22 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -import {Component, OnInit, OnDestroy} from '@angular/core'; +import {Component, OnInit} from '@angular/core'; import {Router, ActivatedRoute} from '@angular/router'; import {SCCatalog, SCSemester} from '@openstapps/core'; import moment from 'moment'; -import {Subscription} from 'rxjs'; import {CatalogProvider} from './catalog.provider'; import {NGXLogger} from 'ngx-logger'; import {Location} from '@angular/common'; import {DataRoutingService} from '../data/data-routing.service'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; @Component({ selector: 'app-catalog', templateUrl: './catalog.component.html', styleUrls: ['./catalog.component.scss'], }) -export class CatalogComponent implements OnInit, OnDestroy { +export class CatalogComponent implements OnInit { /** * SCSemester to show */ @@ -48,11 +48,6 @@ export class CatalogComponent implements OnInit, OnDestroy { */ catalogs: SCCatalog[] | undefined; - /** - * Array of all subscriptions to Observables - */ - subscriptions: Subscription[] = []; - /** * Supercatalog (SCCatalog) to refer to */ @@ -66,11 +61,12 @@ export class CatalogComponent implements OnInit, OnDestroy { protected router: Router, public location: Location, ) { - this.subscriptions.push( - this.dataRoutingService.itemSelectListener().subscribe(item => { + this.dataRoutingService + .itemSelectListener() + .pipe(takeUntilDestroyed()) + .subscribe(item => { void this.router.navigate(['data-detail', item.uid]); - }), - ); + }); } ngOnInit() { @@ -78,15 +74,6 @@ export class CatalogComponent implements OnInit, OnDestroy { void this.fetchCatalog(); } - /** - * Remove subscriptions when the component is removed - */ - ngOnDestroy() { - for (const sub of this.subscriptions) { - sub.unsubscribe(); - } - } - async fetchCatalog() { try { if (this.availableSemesters.length === 0) { diff --git a/frontend/app/src/app/modules/dashboard/dashboard.component.ts b/frontend/app/src/app/modules/dashboard/dashboard.component.ts index e4fe0f3d..b5fa34b5 100644 --- a/frontend/app/src/app/modules/dashboard/dashboard.component.ts +++ b/frontend/app/src/app/modules/dashboard/dashboard.component.ts @@ -12,21 +12,18 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -import {Component, ElementRef, NgZone, OnDestroy, OnInit, ViewChild} from '@angular/core'; +import {Component, DestroyRef, ElementRef, inject, NgZone, OnDestroy, OnInit, ViewChild} from '@angular/core'; import {Router} from '@angular/router'; import {Location} from '@angular/common'; -import {Subscription} from 'rxjs'; import moment from 'moment'; import {SCDateSeries, SCUuid} from '@openstapps/core'; import {SplashScreen} from '@capacitor/splash-screen'; - import {DataRoutingService} from '../data/data-routing.service'; import {ScheduleProvider} from '../calendar/schedule.provider'; import {AnimationController, IonContent} from '@ionic/angular'; import {DashboardCollapse} from './dashboard-collapse'; import {BreakpointObserver} from '@angular/cdk/layout'; - -// const scrollTimeline = new ScrollTimeline(); +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; @Component({ selector: 'app-dashboard', @@ -34,11 +31,6 @@ import {BreakpointObserver} from '@angular/cdk/layout'; styleUrls: ['./dashboard.component.scss', '/dashboard.collapse.component.scss'], }) export class DashboardComponent implements OnInit, OnDestroy { - /** - * Array of all subscriptions to Observables - */ - subscriptions: Subscription[] = []; - @ViewChild('toolbar', {read: ElementRef}) toolbarRef: ElementRef; @ViewChild('schedule', {read: ElementRef}) scheduleRef: ElementRef; @@ -47,11 +39,6 @@ export class DashboardComponent implements OnInit, OnDestroy { collapseAnimation: DashboardCollapse; - /** - * UUID subscription - */ - private _eventUuidSubscription: Subscription; - /** * The events to display */ @@ -74,6 +61,8 @@ export class DashboardComponent implements OnInit, OnDestroy { }, }; + destroy$ = inject(DestroyRef); + constructor( private readonly dataRoutingService: DataRoutingService, private scheduleProvider: ScheduleProvider, @@ -83,15 +72,16 @@ export class DashboardComponent implements OnInit, OnDestroy { private breakpointObserver: BreakpointObserver, private zone: NgZone, ) { - this.subscriptions.push( - this.dataRoutingService.itemSelectListener().subscribe(item => { + this.dataRoutingService + .itemSelectListener() + .pipe(takeUntilDestroyed()) + .subscribe(item => { void this.router.navigate(['data-detail', item.uid]); - }), - ); + }); } async ngOnInit() { - this._eventUuidSubscription = this.scheduleProvider.uuids$.subscribe(async result => { + this.scheduleProvider.uuids$.pipe(takeUntilDestroyed(this.destroy$)).subscribe(async result => { this.eventUuids = result; await this.loadNextEvent(); }); @@ -105,12 +95,13 @@ export class DashboardComponent implements OnInit, OnDestroy { this.scheduleRef.nativeElement, ); - this.subscriptions.push( - this.breakpointObserver.observe(['(min-width: 768px)']).subscribe(async state => { + this.breakpointObserver + .observe(['(min-width: 768px)']) + .pipe(takeUntilDestroyed(this.destroy$)) + .subscribe(async state => { await this.collapseAnimation.ready; this.collapseAnimation.active = !state.matches; - }), - ); + }); } async loadNextEvent() { @@ -133,14 +124,7 @@ export class DashboardComponent implements OnInit, OnDestroy { .find(({time}) => !!time)?.series; } - /** - * Remove subscriptions when the component is removed - */ ngOnDestroy() { - for (const sub of this.subscriptions) { - sub.unsubscribe(); - } - this._eventUuidSubscription.unsubscribe(); this.collapseAnimation.destroy(); } } diff --git a/frontend/app/src/app/modules/data/chips/data/add-event-action-chip.component.ts b/frontend/app/src/app/modules/data/chips/data/add-event-action-chip.component.ts index 39a2dfe1..aa962081 100644 --- a/frontend/app/src/app/modules/data/chips/data/add-event-action-chip.component.ts +++ b/frontend/app/src/app/modules/data/chips/data/add-event-action-chip.component.ts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 StApps + * Copyright (C) 2023 StApps * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation, version 3. @@ -12,9 +12,7 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ - -/* tslint:disable:prefer-function-over-method */ -import {Component, Input, OnDestroy, ViewChild} from '@angular/core'; +import {Component, DestroyRef, inject, Input, ViewChild} from '@angular/core'; import {IonRouterOutlet, ModalController} from '@ionic/angular'; import {SCDateSeries, SCThing, SCThingType, SCUuid} from '@openstapps/core'; import {Subscription} from 'rxjs'; @@ -27,6 +25,7 @@ import { import {AddEventStates, AddEventStatesMap} from './add-event-action-chip.config'; import {EditEventSelectionComponent} from '../edit-event-selection.component'; import {AddEventReviewModalComponent} from '../../../calendar/add-event-review-modal.component'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; /** * Shows a horizontal list of action chips @@ -37,7 +36,7 @@ import {AddEventReviewModalComponent} from '../../../calendar/add-event-review-m styleUrls: ['add-event-action-chip.scss'], animations: [chipSkeletonTransition, chipTransition], }) -export class AddEventActionChipComponent implements OnDestroy { +export class AddEventActionChipComponent { /** * Associated date series */ @@ -91,6 +90,8 @@ export class AddEventActionChipComponent implements OnDestroy { @ViewChild('selection', {static: false}) selection: EditEventSelectionComponent; + destroy$ = inject(DestroyRef); + constructor( readonly dataProvider: CoordinatedSearchProvider, readonly modalController: ModalController, @@ -111,13 +112,6 @@ export class AddEventActionChipComponent implements OnDestroy { this.color = color; } - /** - * TODO - */ - ngOnDestroy() { - this.uuidSubscription?.unsubscribe(); - } - async export() { const modal = await this.modalController.create({ component: AddEventReviewModalComponent, @@ -180,28 +174,30 @@ export class AddEventActionChipComponent implements OnDestroy { }) .then(it => it.data as SCDateSeries[]); - this.uuidSubscription = this.scheduleProvider.uuids$.subscribe(async result => { - this.uuids = result; - const associatedDateSeries = await this.associatedDateSeries; - if (associatedDateSeries.length === 0) { - this.applyState(AddEventStates.UNAVAILABLE); + this.uuidSubscription = this.scheduleProvider.uuids$ + .pipe(takeUntilDestroyed(this.destroy$)) + .subscribe(async result => { + this.uuids = result; + const associatedDateSeries = await this.associatedDateSeries; + if (associatedDateSeries.length === 0) { + this.applyState(AddEventStates.UNAVAILABLE); - return; - } - switch (associatedDateSeries.map(it => it.uid).filter(it => !this.uuids.includes(it)).length) { - case 0: { - this.applyState(AddEventStates.ADDED_ALL); - break; + return; } - case associatedDateSeries.length: { - this.applyState(AddEventStates.REMOVED_ALL); - break; + switch (associatedDateSeries.map(it => it.uid).filter(it => !this.uuids.includes(it)).length) { + case 0: { + this.applyState(AddEventStates.ADDED_ALL); + break; + } + case associatedDateSeries.length: { + this.applyState(AddEventStates.REMOVED_ALL); + break; + } + default: { + this.applyState(AddEventStates.ADDED_SOME); + break; + } } - default: { - this.applyState(AddEventStates.ADDED_SOME); - break; - } - } - }); + }); } } diff --git a/frontend/app/src/app/modules/data/detail/data-path.component.ts b/frontend/app/src/app/modules/data/detail/data-path.component.ts index 04749168..d65069ff 100644 --- a/frontend/app/src/app/modules/data/detail/data-path.component.ts +++ b/frontend/app/src/app/modules/data/detail/data-path.component.ts @@ -12,27 +12,26 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -import {Component, Input, OnDestroy, OnInit} from '@angular/core'; +import {Component, DestroyRef, inject, Input, OnInit} from '@angular/core'; import {RoutingStackService} from '../../../util/routing-stack.service'; import {SCCatalog, SCThings, SCThingType, SCThingWithoutReferences} from '@openstapps/core'; import {DataProvider, DataScope} from '../data.provider'; -import {fromEvent, Observable, Subscription} from 'rxjs'; +import {fromEvent, Observable} from 'rxjs'; import {map, startWith} from 'rxjs/operators'; import {DataRoutingService} from '../data-routing.service'; import {NavController} from '@ionic/angular'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; @Component({ selector: 'stapps-data-path', templateUrl: './data-path.html', styleUrls: ['./data-path.scss'], }) -export class DataPathComponent implements OnInit, OnDestroy { +export class DataPathComponent implements OnInit { path: Promise; $width: Observable; - subscriptions: Subscription[] = []; - @Input() autoRouting = true; @Input() maxItems = 2; @@ -74,6 +73,8 @@ export class DataPathComponent implements OnInit, OnDestroy { } } + destroy$ = inject(DestroyRef); + constructor( readonly dataRoutingService: DataRoutingService, readonly navController: NavController, @@ -88,14 +89,11 @@ export class DataPathComponent implements OnInit, OnDestroy { ); if (!this.autoRouting) return; - this.subscriptions.push( - this.dataRoutingService.pathSelectListener().subscribe(item => { + this.dataRoutingService + .pathSelectListener() + .pipe(takeUntilDestroyed(this.destroy$)) + .subscribe(item => { void this.navController.navigateBack(['data-detail', item.uid]); - }), - ); - } - - ngOnDestroy() { - for (const sub of this.subscriptions) sub.unsubscribe(); + }); } } diff --git a/frontend/app/src/app/modules/data/list/data-list.component.ts b/frontend/app/src/app/modules/data/list/data-list.component.ts index bc7f8c22..a8883cc1 100644 --- a/frontend/app/src/app/modules/data/list/data-list.component.ts +++ b/frontend/app/src/app/modules/data/list/data-list.component.ts @@ -15,11 +15,12 @@ import { Component, ContentChild, + DestroyRef, EventEmitter, HostListener, + inject, Input, OnChanges, - OnDestroy, OnInit, Output, SimpleChanges, @@ -27,8 +28,9 @@ import { ViewChild, } from '@angular/core'; import {SCThings} from '@openstapps/core'; -import {BehaviorSubject, Observable, Subscription} from 'rxjs'; +import {BehaviorSubject, Observable} from 'rxjs'; import {IonInfiniteScroll} from '@ionic/angular'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; export interface DataListContext { $implicit: T; @@ -42,7 +44,7 @@ export interface DataListContext { templateUrl: 'data-list.html', styleUrls: ['data-list.scss'], }) -export class DataListComponent implements OnChanges, OnInit, OnDestroy { +export class DataListComponent implements OnChanges, OnInit { /** * All SCThings to display */ @@ -76,11 +78,6 @@ export class DataListComponent implements OnChanges, OnInit, OnDestroy { */ skeletonItems: number; - /** - * Array of all subscriptions to Observables - */ - subscriptions: Subscription[] = []; - @ViewChild(IonInfiniteScroll) infiniteScroll: IonInfiniteScroll; /** @@ -88,6 +85,8 @@ export class DataListComponent implements OnChanges, OnInit, OnDestroy { */ @Input() loading = true; + destroy$ = inject(DestroyRef); + /** * Calculate how many items would fill the screen */ @@ -112,20 +111,12 @@ export class DataListComponent implements OnChanges, OnInit, OnDestroy { } } - ngOnDestroy(): void { - for (const subscription of this.subscriptions) { - subscription.unsubscribe(); - } - } - ngOnInit(): void { this.calcSkeletonItems(); if (this.resetToTop !== undefined) { - this.subscriptions.push( - this.resetToTop.subscribe(() => { - // this.viewPort.scrollToIndex(0); - }), - ); + this.resetToTop.pipe(takeUntilDestroyed(this.destroy$)).subscribe(() => { + // this.viewPort.scrollToIndex(0); + }); } } diff --git a/frontend/app/src/app/modules/data/list/food-data-list.component.ts b/frontend/app/src/app/modules/data/list/food-data-list.component.ts index 95e6afbc..4c72f9dc 100644 --- a/frontend/app/src/app/modules/data/list/food-data-list.component.ts +++ b/frontend/app/src/app/modules/data/list/food-data-list.component.ts @@ -12,11 +12,13 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -import {Component, OnDestroy, OnInit} from '@angular/core'; +import {Component, OnInit} from '@angular/core'; import {MapPosition} from '../../map/position.service'; import {SearchPageComponent} from './search-page.component'; import {Geolocation} from '@capacitor/geolocation'; -import {Subscription} from 'rxjs'; +import {BehaviorSubject} from 'rxjs'; +import {pauseWhen} from '../../../util/pause-when'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; /** * Presents a list of places for eating/drinking @@ -25,19 +27,29 @@ import {Subscription} from 'rxjs'; templateUrl: 'search-page.html', styleUrls: ['../../data/list/search-page.scss'], }) -export class FoodDataListComponent extends SearchPageComponent implements OnInit, OnDestroy { +export class FoodDataListComponent extends SearchPageComponent implements OnInit { title = 'canteens.title'; showNavigation = false; - locationWatch?: Subscription; + isNotInView$ = new BehaviorSubject(true); /** * Sets the forced filter to present only places for eating/drinking */ ngOnInit() { - this.locationWatch?.unsubscribe(); - this.locationWatch = this.createLocationWatch(); + this.positionService + .watchCurrentLocation({enableHighAccuracy: false, maximumAge: 1000}) + .pipe(pauseWhen(this.isNotInView$), takeUntilDestroyed(this.destroy$)) + .subscribe({ + next: (position: MapPosition) => { + this.positionService.position = position; + }, + error: async _error => { + this.positionService.position = undefined; + await Geolocation.checkPermissions(); + }, + }); this.showDefaultData = true; this.sortQuery = [ @@ -101,32 +113,12 @@ export class FoodDataListComponent extends SearchPageComponent implements OnInit super.ngOnInit(); } - private createLocationWatch(): Subscription { - return this.positionService - .watchCurrentLocation(this.constructor.name, {enableHighAccuracy: false, maximumAge: 1000}) - .subscribe({ - next: (position: MapPosition) => { - this.positionService.position = position; - }, - error: async _error => { - this.positionService.position = undefined; - await Geolocation.checkPermissions(); - }, - }); - } - async ionViewWillEnter() { await super.ionViewWillEnter(); - this.locationWatch?.unsubscribe(); - this.locationWatch = this.createLocationWatch(); + this.isNotInView$.next(false); } ionViewWillLeave() { - this.locationWatch?.unsubscribe(); - } - - ngOnDestroy() { - super.ngOnDestroy(); - this.locationWatch?.unsubscribe(); + this.isNotInView$.next(true); } } diff --git a/frontend/app/src/app/modules/data/list/search-page.component.ts b/frontend/app/src/app/modules/data/list/search-page.component.ts index f108a5bb..22a4af8c 100644 --- a/frontend/app/src/app/modules/data/list/search-page.component.ts +++ b/frontend/app/src/app/modules/data/list/search-page.component.ts @@ -12,7 +12,7 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -import {Component, Input, OnDestroy, OnInit} from '@angular/core'; +import {Component, DestroyRef, inject, Input, OnInit} from '@angular/core'; import {ActivatedRoute, Router} from '@angular/router'; import {Keyboard} from '@capacitor/keyboard'; import {AlertController, AnimationBuilder, AnimationController} from '@ionic/angular'; @@ -26,7 +26,7 @@ import { SCThings, } from '@openstapps/core'; import {NGXLogger} from 'ngx-logger'; -import {combineLatest, Subject, Subscription} from 'rxjs'; +import {combineLatest, Subject} from 'rxjs'; import {debounceTime, distinctUntilChanged, startWith} from 'rxjs/operators'; import {ContextMenuService} from '../../menu/context/context-menu.service'; import {SettingsProvider} from '../../settings/settings.provider'; @@ -35,6 +35,7 @@ import {DataProvider} from '../data.provider'; import {PositionService} from '../../map/position.service'; import {ConfigProvider} from '../../config/config.provider'; import {searchPageSwitchAnimation} from './search-page-switch-animation'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; /** * SearchPageComponent queries things and shows list of things as search results and filter as context menu @@ -45,7 +46,7 @@ import {searchPageSwitchAnimation} from './search-page-switch-animation'; styleUrls: ['search-page.scss'], providers: [ContextMenuService], }) -export class SearchPageComponent implements OnInit, OnDestroy { +export class SearchPageComponent implements OnInit { @Input() title = 'search.title'; @Input() placeholder = 'search.search_bar.placeholder'; @@ -141,10 +142,7 @@ export class SearchPageComponent implements OnInit, OnDestroy { */ sortQuery: SCSearchSort[] | undefined; - /** - * Array of all subscriptions to Observables - */ - subscriptions: Subscription[] = []; + destroy$ = inject(DestroyRef); routeAnimation: AnimationBuilder; @@ -286,7 +284,7 @@ export class SearchPageComponent implements OnInit, OnDestroy { this.contextMenuService.updateContextFilter(facets); } - ngOnInit() { + ngOnInit(defaultListeners = true) { this.initialize(); this.contextMenuService.setContextSort({ name: 'sort', @@ -308,7 +306,7 @@ export class SearchPageComponent implements OnInit, OnDestroy { ], }); - this.subscriptions.push( + if (defaultListeners) { combineLatest([ this.queryTextChanged.pipe( debounceTime(this.searchQueryDueTime), @@ -317,30 +315,37 @@ export class SearchPageComponent implements OnInit, OnDestroy { ), this.contextMenuService.filterQueryChanged$.pipe(startWith(this.filterQuery)), this.contextMenuService.sortQueryChanged$.pipe(startWith(this.sortQuery)), - ]).subscribe(async query => { - this.queryText = query[0]; - this.filterQuery = query[1]; - this.sortQuery = query[2]; - this.from = 0; - if (this.filterQuery !== undefined || this.queryText?.length > 0 || this.showDefaultData) { - await this.fetchAndUpdateItems(); - this.queryChanged.next(); - } - }), - this.settingsProvider.settingsActionChanged$.subscribe(({type, payload}) => { - if (type === 'stapps.settings.changed') { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const {category, name, value} = payload!; - this.logger.log(`received event "settings.changed" with category: + ]) + .pipe(takeUntilDestroyed(this.destroy$)) + .subscribe(async query => { + this.queryText = query[0]; + this.filterQuery = query[1]; + this.sortQuery = query[2]; + this.from = 0; + if (this.filterQuery !== undefined || this.queryText?.length > 0 || this.showDefaultData) { + await this.fetchAndUpdateItems(); + this.queryChanged.next(); + } + }); + this.settingsProvider.settingsActionChanged$ + .pipe(takeUntilDestroyed(this.destroy$)) + .subscribe(({type, payload}) => { + if (type === 'stapps.settings.changed') { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const {category, name, value} = payload!; + this.logger.log(`received event "settings.changed" with category: ${category}, name: ${name}, value: ${JSON.stringify(value)}`); - } - }), - this.dataRoutingService.itemSelectListener().subscribe(item => { - if (this.itemRouting) { - void this.router.navigate(['/data-detail', item.uid]); - } - }), - ); + } + }); + this.dataRoutingService + .itemSelectListener() + .pipe(takeUntilDestroyed(this.destroy$)) + .subscribe(item => { + if (this.itemRouting) { + void this.router.navigate(['/data-detail', item.uid]); + } + }); + } try { const features = this.configProvider.getValue('features') as SCFeatureConfiguration; this.isHebisAvailable = !!features.plugins?.['hebis-plugin']?.urlPath; @@ -359,10 +364,4 @@ export class SearchPageComponent implements OnInit, OnDestroy { this.searchStringChanged(term); } } - - ngOnDestroy() { - for (const subscription of this.subscriptions) { - subscription.unsubscribe(); - } - } } diff --git a/frontend/app/src/app/modules/data/list/simple-data-list.component.ts b/frontend/app/src/app/modules/data/list/simple-data-list.component.ts index 9e4b0f80..7cf789cd 100644 --- a/frontend/app/src/app/modules/data/list/simple-data-list.component.ts +++ b/frontend/app/src/app/modules/data/list/simple-data-list.component.ts @@ -12,12 +12,12 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -import {Component, ContentChild, Input, OnDestroy, OnInit, TemplateRef} from '@angular/core'; +import {Component, ContentChild, DestroyRef, inject, Input, OnInit, TemplateRef} from '@angular/core'; import {SCThings} from '@openstapps/core'; -import {Subscription} from 'rxjs'; import {Router} from '@angular/router'; import {DataRoutingService} from '../data-routing.service'; import {DataListContext} from './data-list.component'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; /** * Shows the list of items @@ -27,7 +27,7 @@ import {DataListContext} from './data-list.component'; templateUrl: 'simple-data-list.html', styleUrls: ['simple-data-list.scss'], }) -export class SimpleDataListComponent implements OnInit, OnDestroy { +export class SimpleDataListComponent implements OnInit { @Input() items?: Promise; /** @@ -51,28 +51,17 @@ export class SimpleDataListComponent implements OnInit, OnDestroy { */ skeletonItems = 6; - /** - * Array of all subscriptions to Observables - */ - subscriptions: Subscription[] = []; + destroy$ = inject(DestroyRef); constructor(protected router: Router, private readonly dataRoutingService: DataRoutingService) {} ngOnInit(): void { if (!this.autoRouting) return; - this.subscriptions.push( - this.dataRoutingService.itemSelectListener().subscribe(item => { + this.dataRoutingService + .itemSelectListener() + .pipe(takeUntilDestroyed(this.destroy$)) + .subscribe(item => { void this.router.navigate(['/data-detail', item.uid]); - }), - ); - } - - /** - * Remove subscriptions when the component is removed - */ - ngOnDestroy() { - for (const sub of this.subscriptions) { - sub.unsubscribe(); - } + }); } } diff --git a/frontend/app/src/app/modules/data/types/place/place-detail-content.component.ts b/frontend/app/src/app/modules/data/types/place/place-detail-content.component.ts index 48fbcb3f..79f35af7 100644 --- a/frontend/app/src/app/modules/data/types/place/place-detail-content.component.ts +++ b/frontend/app/src/app/modules/data/types/place/place-detail-content.component.ts @@ -12,42 +12,30 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -import {Component, Input, OnDestroy, OnInit} from '@angular/core'; +import {Component, Input, OnInit} from '@angular/core'; import {SCBuilding, SCFloor, SCPointOfInterest, SCRoom, SCThings} from '@openstapps/core'; import {DataProvider} from '../../data.provider'; import {hasValidLocation, isSCFloor} from './place-types'; import {DataRoutingService} from '../../data-routing.service'; import {Router} from '@angular/router'; -import {Subscription} from 'rxjs'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; -/** - * TODO - */ @Component({ providers: [DataProvider], styleUrls: ['place-detail-content.scss'], selector: 'stapps-place-detail-content', templateUrl: 'place-detail-content.html', }) -export class PlaceDetailContentComponent implements OnInit, OnDestroy { - /** - * TODO - */ +export class PlaceDetailContentComponent implements OnInit { @Input() item: SCBuilding | SCRoom | SCPointOfInterest | SCFloor; @Input() openAsModal = false; /** - * Does it have valid location or not (for showing in in a map widget) + * Does it have a valid location or not (for showing in a map widget) */ hasValidLocation = false; - itemRouting: Subscription; - - /** - * TODO - * @param item TODO - */ hasCategories(item: SCThings): item is SCThings & {categories: string[]} { return (item as {categories: string[]}).categories !== undefined; } @@ -67,16 +55,15 @@ export class PlaceDetailContentComponent implements OnInit, OnDestroy { } constructor(dataRoutingService: DataRoutingService, router: Router) { - this.itemRouting = dataRoutingService.itemSelectListener().subscribe(item => { - void router.navigate(['/data-detail', item.uid]); - }); + dataRoutingService + .itemSelectListener() + .pipe(takeUntilDestroyed()) + .subscribe(item => { + void router.navigate(['/data-detail', item.uid]); + }); } ngOnInit() { this.hasValidLocation = !isSCFloor(this.item) && hasValidLocation(this.item); } - - ngOnDestroy() { - this.itemRouting.unsubscribe(); - } } diff --git a/frontend/app/src/app/modules/data/types/place/special/mensa/place-mensa-detail.component.ts b/frontend/app/src/app/modules/data/types/place/special/mensa/place-mensa-detail.component.ts index bb54238a..b709e7d5 100644 --- a/frontend/app/src/app/modules/data/types/place/special/mensa/place-mensa-detail.component.ts +++ b/frontend/app/src/app/modules/data/types/place/special/mensa/place-mensa-detail.component.ts @@ -13,14 +13,14 @@ * this program. If not, see . */ import moment, {Moment} from 'moment'; -import {AfterViewInit, Component, Input, OnDestroy} from '@angular/core'; +import {AfterViewInit, Component, DestroyRef, inject, Input} from '@angular/core'; import {SCDish, SCISO8601Date, SCPlace} from '@openstapps/core'; import {PlaceMensaService} from './place-mensa-service'; import {Router} from '@angular/router'; -import {Subscription} from 'rxjs'; import {IonRouterOutlet} from '@ionic/angular'; import {DataRoutingService} from '../../../../data-routing.service'; import {groupBy} from '@openstapps/collection-utils'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; /** * TODO @@ -31,7 +31,7 @@ import {groupBy} from '@openstapps/collection-utils'; templateUrl: 'place-mensa.html', styleUrls: ['place-mensa.scss'], }) -export class PlaceMensaDetailComponent implements AfterViewInit, OnDestroy { +export class PlaceMensaDetailComponent implements AfterViewInit { /** * Map of dishes for each day */ @@ -44,9 +44,6 @@ export class PlaceMensaDetailComponent implements AfterViewInit, OnDestroy { */ @Input() displayRange = 7; - /** - * TODO - */ @Input() item: SCPlace; @Input() openAsModal = false; @@ -61,10 +58,7 @@ export class PlaceMensaDetailComponent implements AfterViewInit, OnDestroy { */ startingDay: Moment; - /** - * Array of all subscriptions to Observables - */ - subscriptions: Subscription[] = []; + destroy$ = inject(DestroyRef); constructor( private readonly mensaService: PlaceMensaService, @@ -75,16 +69,14 @@ export class PlaceMensaDetailComponent implements AfterViewInit, OnDestroy { this.startingDay = moment().startOf('day'); } - /** - * TODO - */ ngAfterViewInit() { if (!this.openAsModal) { - this.subscriptions.push( - this.dataRoutingService.itemSelectListener().subscribe(item => { + this.dataRoutingService + .itemSelectListener() + .pipe(takeUntilDestroyed(this.destroy$)) + .subscribe(item => { void this.router.navigate(['/data-detail', item.uid]); - }), - ); + }); } const dishesByDay = this.mensaService.getAllDishes(this.item, this.displayRange); @@ -111,13 +103,4 @@ export class PlaceMensaDetailComponent implements AfterViewInit, OnDestroy { } }); } - - /** - * Remove subscriptions when the component is removed - */ - ngOnDestroy() { - for (const sub of this.subscriptions) { - sub.unsubscribe(); - } - } } diff --git a/frontend/app/src/app/modules/favorites/favorites-page.component.ts b/frontend/app/src/app/modules/favorites/favorites-page.component.ts index 5f6b59af..ee549979 100644 --- a/frontend/app/src/app/modules/favorites/favorites-page.component.ts +++ b/frontend/app/src/app/modules/favorites/favorites-page.component.ts @@ -27,6 +27,7 @@ import {DataProvider} from '../data/data.provider'; import {SettingsProvider} from '../settings/settings.provider'; import {PositionService} from '../map/position.service'; import {ConfigProvider} from '../config/config.provider'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; /** * The page for showing favorites @@ -71,23 +72,21 @@ export class FavoritesPageComponent extends SearchPageComponent implements OnIni } ngOnInit() { - super.ngOnInit(); - for (const subscription of this.subscriptions) { - subscription.unsubscribe(); - } + super.ngOnInit(false); // Recreate subscriptions to handle different routing - this.subscriptions.push( - combineLatest([ - this.queryTextChanged.pipe( - debounceTime(this.searchQueryDueTime), - distinctUntilChanged(), - startWith(this.queryText), - ), - this.contextMenuService.filterQueryChanged$.pipe(startWith(this.filterQuery)), - this.contextMenuService.sortQueryChanged$.pipe(startWith(this.sortQuery)), - this.favoritesService.favoritesChanged$, - ]).subscribe(async query => { + combineLatest([ + this.queryTextChanged.pipe( + debounceTime(this.searchQueryDueTime), + distinctUntilChanged(), + startWith(this.queryText), + ), + this.contextMenuService.filterQueryChanged$.pipe(startWith(this.filterQuery)), + this.contextMenuService.sortQueryChanged$.pipe(startWith(this.sortQuery)), + this.favoritesService.favoritesChanged$, + ]) + .pipe(takeUntilDestroyed(this.destroy$)) + .subscribe(async query => { this.queryText = query[0]; this.filterQuery = query[1]; this.sortQuery = query[2]; @@ -96,16 +95,21 @@ export class FavoritesPageComponent extends SearchPageComponent implements OnIni await this.fetchAndUpdateItems(); this.queryChanged.next(); } - }), - this.settingsProvider.settingsActionChanged$.subscribe(({type, payload}) => { + }); + this.settingsProvider.settingsActionChanged$ + .pipe(takeUntilDestroyed(this.destroy$)) + .subscribe(({type, payload}) => { if (type === 'stapps.settings.changed') { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const {category, name, value} = payload!; this.logger.log(`received event "settings.changed" with category: ${category}, name: ${name}, value: ${JSON.stringify(value)}`); } - }), - this.dataRoutingService.itemSelectListener().subscribe(item => { + }); + this.dataRoutingService + .itemSelectListener() + .pipe(takeUntilDestroyed(this.destroy$)) + .subscribe(item => { if (this.itemRouting) { if ([SCThingType.Book, SCThingType.Periodical, SCThingType.Article].includes(item.type)) { void this.router.navigate([ @@ -116,8 +120,7 @@ export class FavoritesPageComponent extends SearchPageComponent implements OnIni void this.router.navigate(['data-detail', item.uid]); } } - }), - ); + }); } /** diff --git a/frontend/app/src/app/modules/hebis/list/hebis-search-page.component.ts b/frontend/app/src/app/modules/hebis/list/hebis-search-page.component.ts index 18672e19..eb79648f 100644 --- a/frontend/app/src/app/modules/hebis/list/hebis-search-page.component.ts +++ b/frontend/app/src/app/modules/hebis/list/hebis-search-page.component.ts @@ -12,7 +12,7 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -import {Component, Input, OnDestroy, OnInit} from '@angular/core'; +import {Component, Input, OnInit} from '@angular/core'; import {ActivatedRoute, Router} from '@angular/router'; import {AlertController, AnimationController} from '@ionic/angular'; import {NGXLogger} from 'ngx-logger'; @@ -25,6 +25,7 @@ import {SearchPageComponent} from '../../data/list/search-page.component'; import {HebisDataProvider} from '../hebis-data.provider'; import {PositionService} from '../../map/position.service'; import {ConfigProvider} from '../../config/config.provider'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; /** * HebisSearchPageComponent queries things and shows list of things as search results and filter as context menu @@ -34,7 +35,7 @@ import {ConfigProvider} from '../../config/config.provider'; templateUrl: 'hebis-search-page.html', styleUrls: ['../../data/list/search-page.scss'], }) -export class HebisSearchPageComponent extends SearchPageComponent implements OnInit, OnDestroy { +export class HebisSearchPageComponent extends SearchPageComponent implements OnInit { /** * If routing should be done if the user clicks on an item */ @@ -144,43 +145,42 @@ export class HebisSearchPageComponent extends SearchPageComponent implements OnI //this.fetchAndUpdateItems(); this.initialize(); - this.subscriptions.push( - combineLatest([ - this.queryTextChanged.pipe( - debounceTime(this.searchQueryDueTime), - distinctUntilChanged(), - startWith(this.queryText), - ), - ]).subscribe(async query => { + combineLatest([ + this.queryTextChanged.pipe( + debounceTime(this.searchQueryDueTime), + distinctUntilChanged(), + startWith(this.queryText), + ), + ]) + .pipe(takeUntilDestroyed(this.destroy$)) + .subscribe(async query => { this.queryText = query[0]; this.page = 0; if (this.queryText?.length > 0 || this.showDefaultData) { await this.fetchAndUpdateItems(); this.queryChanged.next(); } - }), - this.settingsProvider.settingsActionChanged$.subscribe(({type, payload}) => { + }); + this.settingsProvider.settingsActionChanged$ + .pipe(takeUntilDestroyed(this.destroy$)) + .subscribe(({type, payload}) => { if (type === 'stapps.settings.changed') { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const {category, name, value} = payload!; this.logger.log(`received event "settings.changed" with category: ${category}, name: ${name}, value: ${JSON.stringify(value)}`); } - }), - this.dataRoutingService.itemSelectListener().subscribe(async item => { + }); + this.dataRoutingService + .itemSelectListener() + .pipe(takeUntilDestroyed(this.destroy$)) + .subscribe(async item => { if (this.itemRouting) { void this.router.navigate([ 'hebis-detail', (item.origin && 'originalId' in item.origin && item.origin['originalId']) || '', ]); } - }), - ); - } - - ngOnDestroy() { - for (const subscription of this.subscriptions) { - subscription.unsubscribe(); - } + }); } } diff --git a/frontend/app/src/app/modules/map/page/map-page.component.ts b/frontend/app/src/app/modules/map/page/map-page.component.ts index d28b182c..373a9892 100644 --- a/frontend/app/src/app/modules/map/page/map-page.component.ts +++ b/frontend/app/src/app/modules/map/page/map-page.component.ts @@ -13,20 +13,22 @@ * this program. If not, see . */ import {Location} from '@angular/common'; -import {ChangeDetectorRef, Component, ElementRef, ViewChild} from '@angular/core'; +import {ChangeDetectorRef, Component, DestroyRef, ElementRef, inject, OnInit, ViewChild} from '@angular/core'; import {ActivatedRoute, Router} from '@angular/router'; import {Keyboard} from '@capacitor/keyboard'; import {AlertController, IonRouterOutlet, ModalController} from '@ionic/angular'; import {TranslateService} from '@ngx-translate/core'; import {SCBuilding, SCPlace, SCRoom, SCSearchFilter, SCUuid} from '@openstapps/core'; import {featureGroup, geoJSON, LatLng, Layer, Map, MapOptions, Marker, tileLayer} from 'leaflet'; -import {Subscription} from 'rxjs'; +import {BehaviorSubject} from 'rxjs'; import {DataRoutingService} from '../../data/data-routing.service'; import {ContextMenuService} from '../../menu/context/context-menu.service'; import {MapProvider} from '../map.provider'; import {MapPosition, PositionService} from '../position.service'; import {Geolocation, PermissionStatus} from '@capacitor/geolocation'; import {Capacitor} from '@capacitor/core'; +import {pauseWhen} from '../../../util/pause-when'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; /** * The main page of the map @@ -36,7 +38,7 @@ import {Capacitor} from '@capacitor/core'; templateUrl: './map-page.html', providers: [ContextMenuService], }) -export class MapPageComponent { +export class MapPageComponent implements OnInit { /** * Default map zoom level */ @@ -115,10 +117,9 @@ export class MapPageComponent { */ queryText: string; - /** - * Subscriptions used by the page - */ - subscriptions: Subscription[] = []; + isNotInView$ = new BehaviorSubject(true); + + destroy$ = inject(DestroyRef); constructor( private translateService: TranslateService, @@ -148,6 +149,34 @@ export class MapPageComponent { }; } + ngOnInit() { + this.dataRoutingService + .itemSelectListener() + .pipe(pauseWhen(this.isNotInView$), takeUntilDestroyed(this.destroy$)) + .subscribe(async item => { + // in case the list item is clicked + if (this.items.length > 1) { + await Promise.all([this.modalController.dismiss(), this.showItem(item.uid)]); + } else { + void this.router.navigate(['/data-detail', item.uid]); + } + }); + this.positionService + .watchCurrentLocation({enableHighAccuracy: true, maximumAge: 1000}) + .pipe(pauseWhen(this.isNotInView$), takeUntilDestroyed(this.destroy$)) + .subscribe({ + next: (position: MapPosition) => { + this.position = position; + this.positionMarker = MapProvider.getPositionMarker(position, 'stapps-device-location', 32); + }, + error: async _error => { + this.locationStatus = await Geolocation.checkPermissions(); + // eslint-disable-next-line unicorn/no-null + this.position = null; + }, + }); + } + /** * Animate to coordinates * @param latLng Coordinates to animate to @@ -257,47 +286,18 @@ export class MapPageComponent { * Subscribe to needed observables and get the location status when user is entering the page */ async ionViewWillEnter() { + this.isNotInView$.next(false); if (this.positionService.position) { this.position = this.positionService.position; this.positionMarker = MapProvider.getPositionMarker(this.position, 'stapps-device-location', 32); } - this.subscriptions.push( - this.dataRoutingService.itemSelectListener().subscribe(async item => { - // in case the list item is clicked - if (this.items.length > 1) { - await Promise.all([this.modalController.dismiss(), this.showItem(item.uid)]); - } else { - void this.router.navigate(['/data-detail', item.uid]); - } - }), - this.positionService - .watchCurrentLocation(this.constructor.name, {enableHighAccuracy: true, maximumAge: 1000}) - .subscribe({ - next: (position: MapPosition) => { - this.position = position; - this.positionMarker = MapProvider.getPositionMarker(position, 'stapps-device-location', 32); - }, - error: async _error => { - this.locationStatus = await Geolocation.checkPermissions(); - // eslint-disable-next-line unicorn/no-null - this.position = null; - }, - }), - ); - // get detailed location status (diagnostics only supports devices) this.locationStatus = await Geolocation.checkPermissions(); } - /** - * Unsubscribe from all subscriptions when user leaves page - */ ionViewWillLeave() { - void this.positionService.clearWatcher(this.constructor.name); - for (const sub of this.subscriptions) { - sub.unsubscribe(); - } + this.isNotInView$.next(true); } /** @@ -322,12 +322,12 @@ export class MapPageComponent { this.addToMap(this.items, true, uid !== null); this.contextMenuService.updateContextFilter(response.facets); - this.subscriptions.push( - this.contextMenuService.filterQueryChanged$.subscribe(query => { + this.contextMenuService.filterQueryChanged$ + .pipe(pauseWhen(this.isNotInView$), takeUntilDestroyed(this.destroy$)) + .subscribe(query => { this.filterQuery = query; this.fetchAndUpdateItems(false, true); - }), - ); + }); this.distance = this.positionService.getDistance(this.items[0].geo.point); } diff --git a/frontend/app/src/app/modules/map/position.service.spec.ts b/frontend/app/src/app/modules/map/position.service.spec.ts index fa231449..a7e2abc7 100644 --- a/frontend/app/src/app/modules/map/position.service.spec.ts +++ b/frontend/app/src/app/modules/map/position.service.spec.ts @@ -19,6 +19,8 @@ import {StorageModule} from '../storage/storage.module'; import {MapPosition, PositionService} from './position.service'; import {ConfigProvider} from '../config/config.provider'; import {LoggerTestingModule} from 'ngx-logger/testing'; +import {firstValueFrom} from 'rxjs'; +import {Geolocation} from '@capacitor/geolocation'; describe('PositionService', () => { let positionService: PositionService; @@ -54,31 +56,17 @@ describe('PositionService', () => { expect(currentLocation).toEqual(sampleMapPosition); }); - it('should continuously provide (watch) location of the device', done => { - positionService.watchCurrentLocation('testCaller').subscribe(location => { - expect(location).toBeDefined(); - done(); - }); + it('should continuously provide (watch) location of the device', async () => { + expect(await firstValueFrom(positionService.watchCurrentLocation())).toBeDefined(); }); - it('should stop to continuously provide (watch) location of the device', done => { - positionService.watchers.set( - 'clearWatch', - new Promise(resolve => { - setTimeout(function () { - resolve(`watcherID123`); - }, 20); - }), - ); - positionService - .clearWatcher('clearWatch') - .then(result => { - expect(result).toBeUndefined(); - done(); - }) - .catch(error => { - expect(error).toBeUndefined(); - done(); - }); + it('should stop to continuously provide (watch) location of the device', async () => { + const watchPosition = spyOn(Geolocation, 'watchPosition').and.resolveTo('abc'); + const clearWatch = spyOn(Geolocation, 'clearWatch').and.callThrough(); + const subscription = positionService.watchCurrentLocation().subscribe(); + expect(watchPosition).toHaveBeenCalled(); + subscription.unsubscribe(); + await new Promise(resolve => setTimeout(resolve, 100)); + expect(clearWatch).toHaveBeenCalledWith({id: 'abc'}); }); }); diff --git a/frontend/app/src/app/modules/map/position.service.ts b/frontend/app/src/app/modules/map/position.service.ts index bb89f5f0..f29c96cd 100644 --- a/frontend/app/src/app/modules/map/position.service.ts +++ b/frontend/app/src/app/modules/map/position.service.ts @@ -85,11 +85,10 @@ export class PositionService { } /** - * Watches (continuously gets) current coordinates information of the device - * @param caller Identifier for later reference. (I.e use of `clearWatcher`) - * @param options Options which define which data should be provided (e.g. how accurate or how old) + * Watches (continuously gets) the current coordinates information of the device + * @param options Options which define which data should be provided (e.g., how accurate or how old) */ - watchCurrentLocation(caller: string, options: PositionOptions = {}): Observable { + watchCurrentLocation(options: PositionOptions = {}): Observable { return new Observable(subscriber => { const watcherID = Geolocation.watchPosition(options, (position, error) => { if (error) { @@ -106,18 +105,15 @@ export class PositionService { subscriber.next(this.position); } }); - this.watchers.set(caller, watcherID); + watcherID.then(console.log); + return { + unsubscribe() { + watcherID.then(id => { + console.log(id); + void Geolocation.clearWatch({id}); + }); + }, + }; }); } - - /** - * Clears watcher for a certain caller - * @param caller Identifier of the caller wanting to clear the watcher - */ - async clearWatcher(caller: string): Promise { - const watcherID = await this.watchers.get(caller); - if (watcherID) { - Geolocation.clearWatch({id: watcherID}); - } - } } diff --git a/frontend/app/src/app/modules/menu/context/context-menu.component.ts b/frontend/app/src/app/modules/menu/context/context-menu.component.ts index 89270d69..fe00d10d 100644 --- a/frontend/app/src/app/modules/menu/context/context-menu.component.ts +++ b/frontend/app/src/app/modules/menu/context/context-menu.component.ts @@ -12,12 +12,12 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -import {Component, Input, OnDestroy} from '@angular/core'; +import {Component, Input} from '@angular/core'; import {LangChangeEvent, TranslateService} from '@ngx-translate/core'; import {SCLanguage, SCThingTranslator, SCThingType, SCTranslations} from '@openstapps/core'; -import {Subscription} from 'rxjs'; import {ContextMenuService} from './context-menu.service'; import {FilterContext, FilterFacet, SortContext, SortContextOption} from './context-type.js'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; /** * The context menu @@ -32,7 +32,7 @@ import {FilterContext, FilterFacet, SortContext, SortContextOption} from './cont selector: 'stapps-context', templateUrl: 'context-menu.html', }) -export class ContextMenuComponent implements OnDestroy { +export class ContextMenuComponent { /** * Id of the content the menu is used for */ @@ -77,11 +77,6 @@ export class ContextMenuComponent implements OnDestroy { */ sortOption: SortContext; - /** - * Array of all Subscriptions - */ - subscriptions: Subscription[] = []; - /** * Core translator */ @@ -94,18 +89,16 @@ export class ContextMenuComponent implements OnDestroy { this.language = this.translateService.currentLang as keyof SCTranslations; this.translator = new SCThingTranslator(this.language); - this.subscriptions.push( - this.translateService.onLangChange.subscribe((event: LangChangeEvent) => { - this.language = event.lang as keyof SCTranslations; - this.translator = new SCThingTranslator(this.language); - }), - this.contextMenuService.filterContextChanged$.subscribe(filterContext => { - this.filterOption = filterContext; - }), - this.contextMenuService.sortOptions.subscribe(sortContext => { - this.sortOption = sortContext; - }), - ); + this.translateService.onLangChange.pipe(takeUntilDestroyed()).subscribe((event: LangChangeEvent) => { + this.language = event.lang as keyof SCTranslations; + this.translator = new SCThingTranslator(this.language); + }); + this.contextMenuService.filterContextChanged$.pipe(takeUntilDestroyed()).subscribe(filterContext => { + this.filterOption = filterContext; + }); + this.contextMenuService.sortOptions.pipe(takeUntilDestroyed()).subscribe(sortContext => { + this.sortOption = sortContext; + }); } /** @@ -122,15 +115,6 @@ export class ContextMenuComponent implements OnDestroy { return this.translator.translatedPropertyValue(onlyForType, field, key); } - /** - * Unsubscribe from Observables - */ - ngOnDestroy() { - for (const sub of this.subscriptions) { - sub.unsubscribe(); - } - } - /** * Resets filter options */ diff --git a/frontend/app/src/app/modules/menu/navigation/offline-notice.component.ts b/frontend/app/src/app/modules/menu/navigation/offline-notice.component.ts index 758fc8b3..a7ff281e 100644 --- a/frontend/app/src/app/modules/menu/navigation/offline-notice.component.ts +++ b/frontend/app/src/app/modules/menu/navigation/offline-notice.component.ts @@ -12,40 +12,35 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ - -import {Component, ElementRef, HostBinding, OnDestroy, ViewChild} from '@angular/core'; +import {Component, ElementRef, HostBinding, ViewChild} from '@angular/core'; import {InternetConnectionService} from '../../../util/internet-connection.service'; -import {Subscription} from 'rxjs'; import {Router} from '@angular/router'; import {NGXLogger} from 'ngx-logger'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; @Component({ selector: 'stapps-offline-notice', templateUrl: 'offline-notice.html', styleUrls: ['offline-notice.scss'], }) -export class OfflineNoticeComponent implements OnDestroy { +export class OfflineNoticeComponent { @HostBinding('class.is-offline') isOffline = false; @HostBinding('class.has-error') hasError = false; @ViewChild('spinIcon', {read: ElementRef}) spinIcon: ElementRef; - readonly subscriptions: Subscription[]; - constructor( readonly offlineProvider: InternetConnectionService, readonly router: Router, readonly logger: NGXLogger, ) { - this.subscriptions = [ - this.offlineProvider.offline$.subscribe(isOffline => { - this.isOffline = isOffline; - }), - this.offlineProvider.error$.subscribe(hasError => { - this.hasError = hasError; - }), - ]; + this.offlineProvider.offline$.pipe(takeUntilDestroyed()).subscribe(isOffline => { + this.isOffline = isOffline; + }); + this.offlineProvider.error$.pipe(takeUntilDestroyed()).subscribe(hasError => { + this.hasError = hasError; + }); } retry() { @@ -54,10 +49,4 @@ export class OfflineNoticeComponent implements OnDestroy { this.spinIcon.nativeElement.classList.add('spin'); this.offlineProvider.retry(); } - - ngOnDestroy() { - for (const subscription of this.subscriptions) { - subscription.unsubscribe(); - } - } } diff --git a/frontend/app/src/app/modules/menu/navigation/root-link.directive.ts b/frontend/app/src/app/modules/menu/navigation/root-link.directive.ts index 28345a40..5522072a 100644 --- a/frontend/app/src/app/modules/menu/navigation/root-link.directive.ts +++ b/frontend/app/src/app/modules/menu/navigation/root-link.directive.ts @@ -12,28 +12,26 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -import {Directive, ElementRef, Input, OnDestroy, OnInit, Renderer2} from '@angular/core'; +import {DestroyRef, Directive, ElementRef, inject, Input, OnInit, Renderer2} from '@angular/core'; import {AnimationController, NavController} from '@ionic/angular'; import {Router, RouterEvent} from '@angular/router'; import {tabsTransition} from './tabs-transition'; -import {Subscription} from 'rxjs'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; @Directive({ selector: '[rootLink]', }) -export class RootLinkDirective implements OnInit, OnDestroy { +export class RootLinkDirective implements OnInit { @Input() rootLink: string; @Input() redirectedFrom: string; - dispose: () => void; - - subscriptions: Subscription[] = []; - private readonly classNames = ['tab-selected', 'link-active']; private needsInit = true; + destroy$ = inject(DestroyRef); + constructor( private element: ElementRef, private renderer: Renderer2, @@ -52,28 +50,28 @@ export class RootLinkDirective implements OnInit, OnDestroy { this.needsInit = false; } - this.subscriptions.push( - this.router.events.subscribe(event => { - if ( - event instanceof RouterEvent && - // @ts-expect-error access private member - (this.navController.direction === 'root' || this.needsInit) - ) { - if (event.url === this.rootLink || (this.redirectedFrom && event.url === this.redirectedFrom)) { - this.setActive(); - } else { - this.setInactive(); - } - this.needsInit = false; + this.router.events.pipe(takeUntilDestroyed(this.destroy$)).subscribe(event => { + if ( + event instanceof RouterEvent && + // @ts-expect-error access private member + (this.navController.direction === 'root' || this.needsInit) + ) { + if (event.url === this.rootLink || (this.redirectedFrom && event.url === this.redirectedFrom)) { + this.setActive(); + } else { + this.setInactive(); } + this.needsInit = false; + } + }); + + this.destroy$.onDestroy( + this.renderer.listen(this.element.nativeElement, 'click', () => { + this.setActive(); + this.navController.setDirection('root', true, 'back', animation); + void this.router.navigate([this.rootLink]); }), ); - - this.dispose = this.renderer.listen(this.element.nativeElement, 'click', () => { - this.setActive(); - this.navController.setDirection('root', true, 'back', animation); - void this.router.navigate([this.rootLink]); - }); } setActive() { @@ -87,11 +85,4 @@ export class RootLinkDirective implements OnInit, OnDestroy { this.renderer.removeClass(this.element.nativeElement, className); } } - - ngOnDestroy() { - this.dispose(); - for (const subscription of this.subscriptions) { - subscription.unsubscribe(); - } - } } diff --git a/frontend/app/src/app/modules/profile/page/profile-page-section.component.ts b/frontend/app/src/app/modules/profile/page/profile-page-section.component.ts index f9103d52..7ff56bd3 100644 --- a/frontend/app/src/app/modules/profile/page/profile-page-section.component.ts +++ b/frontend/app/src/app/modules/profile/page/profile-page-section.component.ts @@ -12,20 +12,20 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ - -import {Component, Input, OnDestroy, OnInit} from '@angular/core'; +import {Component, DestroyRef, inject, Input, OnInit} from '@angular/core'; import {SCSection} from '../../../../config/profile-page-sections'; import {AuthHelperService} from '../../auth/auth-helper.service'; -import {Observable, Subscription} from 'rxjs'; +import {Observable} from 'rxjs'; import {SCAuthorizationProviderType} from '@openstapps/core'; import Swiper from 'swiper'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; @Component({ selector: 'stapps-profile-page-section', templateUrl: 'profile-page-section.html', styleUrls: ['profile-page-section.scss'], }) -export class ProfilePageSectionComponent implements OnInit, OnDestroy { +export class ProfilePageSectionComponent implements OnInit { @Input() item: SCSection; @Input() minSlideWidth = 110; @@ -36,8 +36,6 @@ export class ProfilePageSectionComponent implements OnInit, OnDestroy { isBeginning = true; - subscriptions: Subscription[] = []; - slidesPerView: number; slidesFillScreen = false; @@ -53,15 +51,17 @@ export class ProfilePageSectionComponent implements OnInit, OnDestroy { }, }; + destroy$ = inject(DestroyRef); + constructor(private authHelper: AuthHelperService) {} ngOnInit() { if (this.item.authProvider) { - this.subscriptions.push( - this.data[this.item.authProvider].loggedIn$.subscribe(loggedIn => { + this.data[this.item.authProvider].loggedIn$ + .pipe(takeUntilDestroyed(this.destroy$)) + .subscribe(loggedIn => { this.isLoggedIn = loggedIn; - }), - ); + }); } } @@ -96,10 +96,4 @@ export class ProfilePageSectionComponent implements OnInit, OnDestroy { await this.authHelper.getProvider(providerType).signOut(); await this.authHelper.endBrowserSession(providerType); } - - ngOnDestroy() { - for (const subscription of this.subscriptions) { - subscription.unsubscribe(); - } - } } diff --git a/frontend/app/src/app/modules/profile/page/profile-page.component.ts b/frontend/app/src/app/modules/profile/page/profile-page.component.ts index ffdd4154..5bbe4068 100644 --- a/frontend/app/src/app/modules/profile/page/profile-page.component.ts +++ b/frontend/app/src/app/modules/profile/page/profile-page.component.ts @@ -12,9 +12,8 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ - import {Component, OnInit} from '@angular/core'; -import {Observable, of, Subscription} from 'rxjs'; +import {firstValueFrom, Observable, of, Subscription} from 'rxjs'; import {AuthHelperService} from '../../auth/auth-helper.service'; import {SCAuthorizationProviderType, SCDateSeries, SCUserConfiguration} from '@openstapps/core'; import {ActivatedRoute} from '@angular/router'; @@ -93,22 +92,20 @@ export class ProfilePageComponent implements OnInit { } async getMyCourses() { - const uuidSubscription = this.scheduleProvider.uuids$.subscribe(async result => { - const courses = await this.scheduleProvider.getDateSeries(result); + const result = await firstValueFrom(this.scheduleProvider.uuids$); + const courses = await this.scheduleProvider.getDateSeries(result); - for (const course of courses.dates) { - for (const date of course.dates) { - if (moment(date).startOf('day').format() === this.todayDate) { - this.myCoursesToday[this.myCoursesToday.length] = { - startTime: moment(date).format('LT'), - endTime: moment(date).add(course.duration).format('LT'), - course, - }; - } + for (const course of courses.dates) { + for (const date of course.dates) { + if (moment(date).startOf('day').format() === this.todayDate) { + this.myCoursesToday[this.myCoursesToday.length] = { + startTime: moment(date).format('LT'), + endTime: moment(date).add(course.duration).format('LT'), + course, + }; } } - uuidSubscription.unsubscribe(); - }); + } } async signIn(providerType: SCAuthorizationProviderType) { diff --git a/frontend/app/src/app/modules/schedule/page/calendar-view.component.ts b/frontend/app/src/app/modules/schedule/page/calendar-view.component.ts index 68faff56..31799277 100644 --- a/frontend/app/src/app/modules/schedule/page/calendar-view.component.ts +++ b/frontend/app/src/app/modules/schedule/page/calendar-view.component.ts @@ -12,7 +12,7 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -import {AfterViewInit, Component, Input, OnDestroy, OnInit, ViewChild} from '@angular/core'; +import {AfterViewInit, Component, Input, OnInit, ViewChild} from '@angular/core'; import {ActivatedRoute} from '@angular/router'; import moment from 'moment'; import {materialFade, materialManualFade, materialSharedAxisX} from '../../../animation/material-motion'; @@ -22,6 +22,7 @@ import {CalendarComponent} from './components/calendar.component'; import {CalendarService} from '../../calendar/calendar.service'; import {InfiniteSwiperComponent} from './grid/infinite-swiper.component'; import {IonContent} from '@ionic/angular'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; /** * Component that displays the schedule @@ -32,7 +33,7 @@ import {IonContent} from '@ionic/angular'; styleUrls: ['calendar-view.scss', './components/calendar-component.scss'], animations: [materialFade, materialSharedAxisX, materialManualFade], }) -export class CalendarViewComponent extends CalendarComponent implements OnInit, OnDestroy, AfterViewInit { +export class CalendarViewComponent extends CalendarComponent implements OnInit, AfterViewInit { @ViewChild('mainSwiper') mainSwiper: InfiniteSwiperComponent; @ViewChild('headerSwiper') headerSwiper: InfiniteSwiperComponent; @@ -73,11 +74,8 @@ export class CalendarViewComponent extends CalendarComponent implements OnInit, * Initialize */ ngOnInit() { - super.onInit(); - if (this.calendarServiceSubscription) { - this.calendarServiceSubscription.unsubscribe(); - } - this.calendarServiceSubscription = this.calendarService.goToDateClicked.subscribe(async newIndex => { + super.ngOnInit(); + this.calendarService.goToDateClicked.pipe(takeUntilDestroyed(this.destroy$)).subscribe(async newIndex => { await this.mainSwiper.goToIndex(newIndex); this.setDateRange(newIndex); await this.scrollCursorIntoView(this.content); @@ -88,13 +86,6 @@ export class CalendarViewComponent extends CalendarComponent implements OnInit, void this.scrollCursorIntoView(this.content); } - /** - * OnDestroy - */ - ngOnDestroy(): void { - super.onDestroy(); - } - /** * Load events */ diff --git a/frontend/app/src/app/modules/schedule/page/components/calendar.component.ts b/frontend/app/src/app/modules/schedule/page/components/calendar.component.ts index f116459f..ae6ce37e 100644 --- a/frontend/app/src/app/modules/schedule/page/components/calendar.component.ts +++ b/frontend/app/src/app/modules/schedule/page/components/calendar.component.ts @@ -12,7 +12,7 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -import {Component, Input, OnDestroy, OnInit} from '@angular/core'; +import {Component, DestroyRef, inject, Input, OnInit} from '@angular/core'; import {ActivatedRoute} from '@angular/router'; import {SCISO8601Date, SCUuid} from '@openstapps/core'; import moment, {Moment} from 'moment'; @@ -22,9 +22,9 @@ import {ScheduleEvent, ScheduleResponsiveBreakpoint} from '../schema/schema'; import {SwiperComponent} from 'swiper/angular'; import {InfiniteSwiperComponent} from '../grid/infinite-swiper.component'; import {IonContent, IonDatetime} from '@ionic/angular'; -import {Subscription} from 'rxjs'; import {CalendarService} from '../../../calendar/calendar.service'; import {getScheduleCursorOffset} from '../grid/schedule-cursor-offset'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; /** * Component that displays the schedule @@ -35,7 +35,7 @@ import {getScheduleCursorOffset} from '../grid/schedule-cursor-offset'; styleUrls: ['calendar-component.scss'], animations: [materialFade, materialSharedAxisX, materialManualFade], }) -export class CalendarComponent implements OnInit, OnDestroy { +export class CalendarComponent implements OnInit { /** * The day that the schedule started out on */ @@ -52,8 +52,6 @@ export class CalendarComponent implements OnInit, OnDestroy { endDate: '', }; - calendarServiceSubscription: Subscription; - prevHeaderIndex = 0; /** @@ -98,15 +96,12 @@ export class CalendarComponent implements OnInit, OnDestroy { */ @Input() uuids: SCUuid[]; - /** - * UUID subscription - */ - uuidSubscription: Subscription; - @Input() useInfiniteSwiper = true; @Input() weekDates: Array; + destroy$ = inject(DestroyRef); + constructor( protected readonly activatedRoute: ActivatedRoute, protected readonly calendarService: CalendarService, @@ -114,14 +109,6 @@ export class CalendarComponent implements OnInit, OnDestroy { ) {} ngOnInit() { - this.onInit(); - } - - ngOnDestroy() { - this.onDestroy(); - } - - onInit() { let dayString: string | number | null = this.activatedRoute.snapshot.paramMap.get('date'); if (dayString == undefined || dayString === 'now') { const fragments = window.location.href.split('/'); @@ -133,7 +120,7 @@ export class CalendarComponent implements OnInit, OnDestroy { this.baselineDate = moment(dayString).startOf('day'); this.initialSlideIndex = new Promise(resolve => { - this.uuidSubscription = this.scheduleProvider.uuids$.subscribe(async result => { + this.scheduleProvider.uuids$.pipe(takeUntilDestroyed(this.destroy$)).subscribe(async result => { this.uuids = result; resolve(await this.loadEvents()); }); @@ -150,13 +137,6 @@ export class CalendarComponent implements OnInit, OnDestroy { }); } - onDestroy() { - this.uuidSubscription.unsubscribe(); - if (this.calendarServiceSubscription) { - this.calendarServiceSubscription.unsubscribe(); - } - } - /** * Get date from baseline date and index of current slide. * @param index number diff --git a/frontend/app/src/app/modules/schedule/page/schedule-single-events.component.ts b/frontend/app/src/app/modules/schedule/page/schedule-single-events.component.ts index c26026f4..f4f8a6f4 100644 --- a/frontend/app/src/app/modules/schedule/page/schedule-single-events.component.ts +++ b/frontend/app/src/app/modules/schedule/page/schedule-single-events.component.ts @@ -12,14 +12,14 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -import {Component, Input, OnDestroy, OnInit} from '@angular/core'; +import {Component, DestroyRef, inject, Input, OnInit} from '@angular/core'; import {SCDateSeries, SCUuid} from '@openstapps/core'; import moment from 'moment'; -import {Subscription} from 'rxjs'; import {materialFade} from '../../../animation/material-motion'; import {ScheduleProvider} from '../../calendar/schedule.provider'; import {ScheduleEvent} from './schema/schema'; import {groupBy, omit, stringSortBy} from '@openstapps/collection-utils'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; /** * A single event @@ -31,7 +31,7 @@ export interface ScheduleSingleEvent { day: string; /** - * Event the date is referring to + * The event the date is referring to */ event: ScheduleEvent; } @@ -45,12 +45,7 @@ export interface ScheduleSingleEvent { styleUrls: ['schedule-single-events.scss'], animations: [materialFade], }) -export class ScheduleSingleEventsComponent implements OnInit, OnDestroy { - /** - * UUID subscription - */ - private _uuidSubscription: Subscription; - +export class ScheduleSingleEventsComponent implements OnInit { /** * The events to display */ @@ -66,6 +61,8 @@ export class ScheduleSingleEventsComponent implements OnInit, OnDestroy { */ @Input() scale = 60; + destroy$ = inject(DestroyRef); + /** * Sorts dates to a list of days with events on each */ @@ -116,24 +113,13 @@ export class ScheduleSingleEventsComponent implements OnInit, OnDestroy { ); // TODO: replace with filter - const test = ScheduleSingleEventsComponent.groupDateSeriesToDays( + return ScheduleSingleEventsComponent.groupDateSeriesToDays( dateSeries.dates.filter(it => !it.repeatFrequency), ); - return test; } - /** - * OnDestroy - */ - ngOnDestroy(): void { - this._uuidSubscription.unsubscribe(); - } - - /** - * Initialize - */ ngOnInit() { - this._uuidSubscription = this.scheduleProvider.uuids$.subscribe(async result => { + this.scheduleProvider.uuids$.pipe(takeUntilDestroyed(this.destroy$)).subscribe(async result => { this.uuids = result; this.events = this.fetchDateSeries(); }); diff --git a/frontend/app/src/app/modules/schedule/page/schedule-view.component.ts b/frontend/app/src/app/modules/schedule/page/schedule-view.component.ts index 10729688..235814cf 100644 --- a/frontend/app/src/app/modules/schedule/page/schedule-view.component.ts +++ b/frontend/app/src/app/modules/schedule/page/schedule-view.component.ts @@ -12,7 +12,7 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -import {AfterViewInit, Component, Input, OnDestroy, OnInit, ViewChild} from '@angular/core'; +import {AfterViewInit, Component, Input, OnInit, ViewChild} from '@angular/core'; import {ActivatedRoute} from '@angular/router'; import moment, {Moment} from 'moment'; import {materialFade, materialManualFade, materialSharedAxisX} from '../../../animation/material-motion'; @@ -23,6 +23,7 @@ import {CalendarService} from '../../calendar/calendar.service'; import {CalendarComponent} from './components/calendar.component'; import {IonContent, IonDatetime} from '@ionic/angular'; import {SwiperComponent} from 'swiper/angular'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; /** * Component that displays the schedule @@ -33,10 +34,7 @@ import {SwiperComponent} from 'swiper/angular'; styleUrls: ['schedule-view.scss', './components/calendar-component.scss'], animations: [materialFade, materialSharedAxisX, materialManualFade], }) -export class ScheduleViewComponent - extends CalendarComponent - implements OnInit, AfterViewInit, OnDestroy, AfterViewInit -{ +export class ScheduleViewComponent extends CalendarComponent implements OnInit, AfterViewInit { @ViewChild('mainSwiper') mainSwiper: SwiperComponent; @ViewChild('headerSwiper') headerSwiper: SwiperComponent; @@ -99,11 +97,8 @@ export class ScheduleViewComponent * Initialize */ ngOnInit() { - super.onInit(); - if (this.calendarServiceSubscription) { - this.calendarServiceSubscription.unsubscribe(); - } - this.calendarServiceSubscription = this.calendarService.goToDateClicked.subscribe(() => { + super.ngOnInit(); + this.calendarService.goToDateClicked.pipe(takeUntilDestroyed(this.destroy$)).subscribe(() => { this.slideToToday(); }); } @@ -112,13 +107,6 @@ export class ScheduleViewComponent this.slideToToday(); } - /** - * OnDestroy - */ - ngOnDestroy(): void { - super.onDestroy(); - } - /** * Slide today into view. */ diff --git a/frontend/app/src/app/translation/thing-translate.service.ts b/frontend/app/src/app/translation/thing-translate.service.ts index 1ae833d2..b1f16544 100644 --- a/frontend/app/src/app/translation/thing-translate.service.ts +++ b/frontend/app/src/app/translation/thing-translate.service.ts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 StApps + * Copyright (C) 2023 StApps * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation, version 3. @@ -12,8 +12,7 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ - -import {Injectable, OnDestroy} from '@angular/core'; +import {Injectable} from '@angular/core'; import {LangChangeEvent, TranslateService} from '@ngx-translate/core'; import { SCLanguage, @@ -24,8 +23,8 @@ import { SCTranslations, } from '@openstapps/core'; import moment from 'moment'; -import {Subscription} from 'rxjs'; import {isDefined, ThingTranslateParser} from './thing-translate.parser'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; // export const DEFAULT_LANGUAGE = new InjectionToken('DEFAULT_LANGUAGE'); @@ -34,9 +33,7 @@ import {isDefined, ThingTranslateParser} from './thing-translate.parser'; @Injectable({ providedIn: 'root', }) -export class ThingTranslateService implements OnDestroy { - onLangChange: Subscription; - +export class ThingTranslateService { translator: SCThingTranslator; /** @@ -49,7 +46,7 @@ export class ThingTranslateService implements OnDestroy { (translateService.currentLang ?? translateService.defaultLang) as SCLanguageCode, ); /** set the default language from configuration */ - this.onLangChange = this.translateService.onLangChange.subscribe((event: LangChangeEvent) => { + this.translateService.onLangChange.pipe(takeUntilDestroyed()).subscribe((event: LangChangeEvent) => { this.translator.language = event.lang as keyof SCTranslations; moment.locale(event.lang); }); @@ -104,10 +101,4 @@ export class ThingTranslateService implements OnDestroy { return this.getParsedResult(translatedPropertyNames, keyPath); } - - ngOnDestroy() { - if (!this.onLangChange.closed) { - this.onLangChange.unsubscribe(); - } - } } diff --git a/frontend/app/src/app/translation/translate-simple.pipe.ts b/frontend/app/src/app/translation/translate-simple.pipe.ts index 11edf52f..a0ea2cc8 100644 --- a/frontend/app/src/app/translation/translate-simple.pipe.ts +++ b/frontend/app/src/app/translation/translate-simple.pipe.ts @@ -12,17 +12,18 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -import {Injectable, OnDestroy, Pipe, PipeTransform} from '@angular/core'; +import {DestroyRef, inject, Injectable, Pipe, PipeTransform} from '@angular/core'; import {TranslateService} from '@ngx-translate/core'; import {get} from '@openstapps/collection-utils'; import {Subscription} from 'rxjs'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; @Injectable() @Pipe({ name: 'translateSimple', pure: false, }) -export class TranslateSimplePipe implements PipeTransform, OnDestroy { +export class TranslateSimplePipe implements PipeTransform { value: unknown; query: unknown; @@ -31,6 +32,8 @@ export class TranslateSimplePipe implements PipeTransform, OnDestroy { onLangChange: Subscription; + destroy$ = inject(DestroyRef); + constructor(private readonly translate: TranslateService) {} // eslint-disable-next-line @typescript-eslint/ban-types @@ -58,14 +61,12 @@ export class TranslateSimplePipe implements PipeTransform, OnDestroy { this.updateValue(); - this.onLangChange ??= this.translate.onLangChange.subscribe(() => { - this.updateValue(); - }); + this.onLangChange ??= this.translate.onLangChange + .pipe(takeUntilDestroyed(this.destroy$)) + .subscribe(() => { + this.updateValue(); + }); return this.value as never; } - - ngOnDestroy(): void { - this.onLangChange?.unsubscribe(); - } } diff --git a/frontend/app/src/app/util/ion-icon/ion-back-button.directive.ts b/frontend/app/src/app/util/ion-icon/ion-back-button.directive.ts index dd9df2bc..1b742ba9 100644 --- a/frontend/app/src/app/util/ion-icon/ion-back-button.directive.ts +++ b/frontend/app/src/app/util/ion-icon/ion-back-button.directive.ts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 StApps + * Copyright (C) 2023 StApps * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation, version 3. @@ -12,20 +12,29 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ - -import {Directive, ElementRef, Host, Optional, Self, ViewContainerRef} from '@angular/core'; +import { + DestroyRef, + Directive, + ElementRef, + Host, + inject, + OnInit, + Optional, + Self, + ViewContainerRef, +} from '@angular/core'; import {SCIcon} from './icon'; import {IconReplacer} from './replace-util'; import {TranslateService} from '@ngx-translate/core'; -import {Subscription} from 'rxjs'; import {IonBackButton} from '@ionic/angular'; import {TitleCasePipe} from '@angular/common'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; @Directive({ selector: 'ion-back-button', }) -export class IonBackButtonDirective extends IconReplacer { - private subscriptions: Subscription[] = []; +export class IonBackButtonDirective extends IconReplacer implements OnInit { + destroy$ = inject(DestroyRef); constructor( element: ElementRef, @@ -45,17 +54,13 @@ export class IonBackButtonDirective extends IconReplacer { }); } - init() { - this.subscriptions.push( - this.translateService.stream('back').subscribe((value: string) => { + async ngOnInit() { + await super.ngOnInit(); + this.translateService + .stream('back') + .pipe(takeUntilDestroyed(this.destroy$)) + .subscribe((value: string) => { this.ionBackButton.text = this.titleCasePipe.transform(value); - }), - ); - } - - destroy() { - for (const subscription of this.subscriptions) { - subscription.unsubscribe(); - } + }); } } diff --git a/frontend/app/src/app/util/ion-icon/ion-breadcrumb.directive.ts b/frontend/app/src/app/util/ion-icon/ion-breadcrumb.directive.ts index e9139ae1..2e9de757 100644 --- a/frontend/app/src/app/util/ion-icon/ion-breadcrumb.directive.ts +++ b/frontend/app/src/app/util/ion-icon/ion-breadcrumb.directive.ts @@ -12,7 +12,6 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ - import {Directive, ElementRef, ViewContainerRef} from '@angular/core'; import {SCIcon} from './icon'; import {IconReplacer} from './replace-util'; diff --git a/frontend/app/src/app/util/ion-icon/ion-reorder.directive.ts b/frontend/app/src/app/util/ion-icon/ion-reorder.directive.ts index bbac84b9..2a8f74e9 100644 --- a/frontend/app/src/app/util/ion-icon/ion-reorder.directive.ts +++ b/frontend/app/src/app/util/ion-icon/ion-reorder.directive.ts @@ -12,7 +12,6 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ - import {Directive, ElementRef, ViewContainerRef} from '@angular/core'; import {SCIcon} from './icon'; import {IconReplacer} from './replace-util'; diff --git a/frontend/app/src/app/util/ion-icon/replace-util.ts b/frontend/app/src/app/util/ion-icon/replace-util.ts index cdadf44a..5dd03d32 100644 --- a/frontend/app/src/app/util/ion-icon/replace-util.ts +++ b/frontend/app/src/app/util/ion-icon/replace-util.ts @@ -65,24 +65,7 @@ export abstract class IconReplacer implements OnInit, OnDestroy { */ abstract replace(): void; - /** - * If any additional work needs to be done, this - * is called during ngOnInit - */ - init() { - // noop - } - - /** - * If you need to do cleanup, this method is called during ngOnDestroy - */ - destroy() { - // noop - } - async ngOnInit() { - this.init(); - if (this.host) { this.attachObserver(); } else { @@ -110,8 +93,8 @@ export abstract class IconReplacer implements OnInit, OnDestroy { scIcon.location.nativeElement.classList.add(...icon.classList); if (this.iconDomLocation === 'shadow') { - // shadow dom needs to utilize slotting, to put it outside - // the shadow dom, otherwise it won't receive any css data + // shadow dom needs to utilize slotting, to put it outside the shadow dom + // otherwise it won't receive any css data const slot = document.createElement('slot'); slot.name = this.slotName + slotName; icon.replaceWith(slot); @@ -137,6 +120,5 @@ export abstract class IconReplacer implements OnInit, OnDestroy { ngOnDestroy() { this.mutationObserver?.disconnect(); - this.destroy(); } } diff --git a/frontend/app/src/app/util/pause-when.ts b/frontend/app/src/app/util/pause-when.ts new file mode 100644 index 00000000..4e89b7cb --- /dev/null +++ b/frontend/app/src/app/util/pause-when.ts @@ -0,0 +1,12 @@ +import {filter, MonoTypeOperatorFunction, Observable, repeat, takeUntil} from 'rxjs'; + +/** + * Pause the observable if the notifier emits true, and resume when it emits false + */ +export function pauseWhen(notifier: Observable): MonoTypeOperatorFunction { + return value => + value.pipe( + takeUntil(notifier.pipe(filter(it => it))), + repeat({delay: () => notifier.pipe(filter(it => !it))}), + ); +}