From 99b6b322eaf90137940ff772a98ce8d45d7ade7b Mon Sep 17 00:00:00 2001 From: RunboxJoe Date: Mon, 15 Dec 2025 14:33:38 +0000 Subject: [PATCH 01/12] refactor(2fa): Change 2FA unlock code description Change made to the description of the 2FA unlock codes to match what is displayed in runbox 6 as an unlock code is now a requirement --- .../two-factor-authentication.component.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/account-security/two-factor-authentication.component.html b/src/app/account-security/two-factor-authentication.component.html index 9a5b925e8..47056cc25 100644 --- a/src/app/account-security/two-factor-authentication.component.html +++ b/src/app/account-security/two-factor-authentication.component.html @@ -10,8 +10,9 @@

Two-Factor Authentication

This is the Two-Factor Authentication (2FA) configuration screen, where you can configure the various options for enabling and using 2FA.

-

We recommended that you create an Unlock Code when enabling 2FA. This special code can - be used to disable 2FA if you have problems logging in with your 2FA codes.

+

Unlock Code: An unlock code is required so that if you lose access to your TOTP or OTP codes, you can still get access to your account. + Using the unlock code will turn off 2FA for your account. + You can easily re-instate 2FA on this page once you are successfully logged in again.

Note: For security reasons, enabling 2FA will disable your account password for non-web services. These include IMAP, POP, SMTP, FTP, CalDAV, and CardDAV services that may be used with progams or apps on your device. Therefore you will need to set up App From a38da3380a9f560d75e3ddeb82f828f95d9cf8ee Mon Sep 17 00:00:00 2001 From: Finn Date: Wed, 28 Jan 2026 02:08:10 +0000 Subject: [PATCH 02/12] feat(migration): migrate to RxJS 7 --- package.json | 1 - .../account-app/account-receipt.component.ts | 8 ++-- src/app/account-app/cart.service.spec.ts | 4 +- src/app/account-app/cart.service.ts | 8 ++-- .../stripe-add-card-dialog.component.ts | 4 +- .../no-products-for-subaccounts.guard.ts | 4 +- .../account-app/shopping-cart.component.ts | 8 ++-- .../stripe-payment-dialog.component.ts | 4 +- .../personal-details.component.ts | 12 +++--- src/app/app.component.ts | 18 ++++---- src/app/common/preferences.service.ts | 10 ++--- src/app/compose/draftdesk.service.ts | 12 +++--- src/app/compose/recipients.service.spec.ts | 14 +++---- .../contact-details.component.ts | 9 ++-- src/app/folder/folderlist.component.spec.ts | 42 +++++++++---------- src/app/folder/folderlist.component.ts | 4 +- src/app/http/progress.service.spec.ts | 15 ++++--- src/app/mailviewer/avatar-bar.component.ts | 4 +- .../mailviewer/singlemailviewer.component.ts | 18 ++++---- src/app/onscreen/onscreen.component.ts | 8 ++-- .../saved-searches/saved-searches.service.ts | 4 +- src/app/sentry-error-handler.ts | 5 ++- src/app/start/startdesk.component.ts | 4 +- src/app/storage.service.ts | 4 +- src/app/xapian/index.worker.ts | 22 +++++----- ...iple-search-fields-input.component.spec.ts | 11 ++--- src/app/xapian/searchservice.spec.ts | 17 ++++---- 27 files changed, 137 insertions(+), 137 deletions(-) diff --git a/package.json b/package.json index 04a3a6d52..5a2cf1c08 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,6 @@ "otpauth": "^9.1.1", "rrule": "^2.7.2", "rxjs": "^7.8.0", - "rxjs-compat": "^6.6.7", "tinymce": "^6.8.3", "ts-md5": "^1.3.1", "zone.js": "^0.13.3" diff --git a/src/app/account-app/account-receipt.component.ts b/src/app/account-app/account-receipt.component.ts index 0fd2b8451..0449436a2 100644 --- a/src/app/account-app/account-receipt.component.ts +++ b/src/app/account-app/account-receipt.component.ts @@ -20,7 +20,7 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { RunboxMe, RunboxWebmailAPI } from '../rmmapi/rbwebmail'; -import { AsyncSubject } from 'rxjs'; +import { AsyncSubject, firstValueFrom } from 'rxjs'; import { take } from 'rxjs/operators'; @Component({ @@ -45,12 +45,12 @@ export class AccountReceiptComponent implements OnInit { } async ngOnInit() { - this.me = await this.rmmapi.me.toPromise(); + this.me = await firstValueFrom(this.rmmapi.me); - const params = await this.route.params.pipe(take(1)).toPromise(); + const params = await firstValueFrom(this.route.params.pipe(take(1))); const receiptID = params.id; - this.receipt = await this.rmmapi.getReceipt(receiptID).toPromise(); + this.receipt = await firstValueFrom(this.rmmapi.getReceipt(receiptID)); this.receipt.time = this.receipt.time.replace('T', ' '); if (this.receipt.method === 'giro') { diff --git a/src/app/account-app/cart.service.spec.ts b/src/app/account-app/cart.service.spec.ts index 1389d2699..fe705f4fb 100644 --- a/src/app/account-app/cart.service.spec.ts +++ b/src/app/account-app/cart.service.spec.ts @@ -21,7 +21,7 @@ import { CartService } from './cart.service'; import { ProductOrder } from './product-order'; import { StorageService } from '../storage.service'; import { RunboxWebmailAPI } from '../rmmapi/rbwebmail'; -import { of } from 'rxjs'; +import { firstValueFrom, of } from 'rxjs'; import { take } from 'rxjs/operators'; import { Decimal } from 'decimal.js-light'; @@ -49,7 +49,7 @@ describe('CartService', () => { const order = new ProductOrder(401,'subscription', new Decimal(3), 402); await cart.add(order); - console.log(await cart.items.pipe(take(1)).toPromise()); + console.log(await firstValueFrom(cart.items)); expect(await cart.contains(401, 402)).toBe(true, 'cart contains the renewal product'); expect(await cart.contains(401)).toBe(false, 'cart does not contain the new product'); }); diff --git a/src/app/account-app/cart.service.ts b/src/app/account-app/cart.service.ts index afd1c2f23..8230390ff 100644 --- a/src/app/account-app/cart.service.ts +++ b/src/app/account-app/cart.service.ts @@ -18,7 +18,7 @@ // ---------- END RUNBOX LICENSE ---------- import { Injectable } from '@angular/core'; -import { ReplaySubject } from 'rxjs'; +import { firstValueFrom, ReplaySubject } from 'rxjs'; import { take } from 'rxjs/operators'; import { ProductOrder } from './product-order'; @@ -46,7 +46,7 @@ export class CartService { } async add(p: ProductOrder): Promise { - const items = await this.items.pipe(take(1)).toPromise(); + const items = await firstValueFrom(this.items.pipe(take(1))); for (const i of items) { // Cannot order multiples of subscription products @@ -70,7 +70,7 @@ export class CartService { } async contains(pid: number, apid?: number): Promise { - const items = await this.items.pipe(take(1)).toPromise(); + const items = await firstValueFrom(this.items.pipe(take(1))); for (const p of items) { if (p.pid === pid && p.apid === apid) { return true; @@ -80,7 +80,7 @@ export class CartService { } async remove(order: ProductOrder): Promise { - const items = await this.items.pipe(take(1)).toPromise(); + const items = await firstValueFrom(this.items.pipe(take(1))); // check if it's enough to just reduce the quantity on existing product for (const i of items) { if (i.isSameProduct(order)) { diff --git a/src/app/account-app/credit-cards/stripe-add-card-dialog.component.ts b/src/app/account-app/credit-cards/stripe-add-card-dialog.component.ts index 1cd98ee22..fbe56a54c 100644 --- a/src/app/account-app/credit-cards/stripe-add-card-dialog.component.ts +++ b/src/app/account-app/credit-cards/stripe-add-card-dialog.component.ts @@ -64,8 +64,8 @@ export class StripeAddCardDialogComponent implements AfterViewInit { } async ngAfterViewInit() { - await stripeLoader.toPromise(); - const stripePubkey = await this.paymentsservice.stripePubkey.toPromise(); + await firstValueFrom(stripeLoader); + const stripePubkey = await firstValueFrom(this.paymentsservice.stripePubkey); const customerSession = await firstValueFrom(this.paymentsservice.customerSession()); this.stripe = Stripe(stripePubkey); diff --git a/src/app/account-app/no-products-for-subaccounts.guard.ts b/src/app/account-app/no-products-for-subaccounts.guard.ts index 0e54f849c..d155f854f 100644 --- a/src/app/account-app/no-products-for-subaccounts.guard.ts +++ b/src/app/account-app/no-products-for-subaccounts.guard.ts @@ -19,7 +19,7 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router'; -import { Observable } from 'rxjs'; +import { firstValueFrom, Observable } from 'rxjs'; import { RunboxWebmailAPI } from '../rmmapi/rbwebmail'; import { RMMAuthGuardService } from '../rmmapi/rmmauthguard.service'; @@ -54,7 +54,7 @@ export class NoProductsForSubaccountsGuard { (success) => { if (typeof(success) === 'boolean' && success) { if (restricted) { - return this.rmmapi.me.toPromise().then(me => { + return firstValueFrom(this.rmmapi.me).then(me => { if (me.owner) { return this.router.parseUrl('/account/not-for-subaccounts'); } else { diff --git a/src/app/account-app/shopping-cart.component.ts b/src/app/account-app/shopping-cart.component.ts index 7349cb8fe..a31b8c06e 100644 --- a/src/app/account-app/shopping-cart.component.ts +++ b/src/app/account-app/shopping-cart.component.ts @@ -20,7 +20,7 @@ import { Component, OnInit } from '@angular/core'; import { MatDialog as MatDialog, MatDialogRef as MatDialogRef } from '@angular/material/dialog'; import { ActivatedRoute, Router } from '@angular/router'; -import { Subject } from 'rxjs'; +import { firstValueFrom, Subject } from 'rxjs'; import { CartService } from './cart.service'; import { BitpayPaymentDialogComponent } from './bitpay-payment-dialog.component'; @@ -177,7 +177,7 @@ export class ShoppingCartComponent implements OnInit { // maybe there is a more elegant way to do this, but it's concise and works :) const cartItems = JSON.parse(JSON.stringify(items)); - let products = await this.paymentsservice.products.toPromise(); + let products = await firstValueFrom(this.paymentsservice.products); // Check if all the products in the cart had their details in the paymentservice. // This may not be true if they're coming from the URL for instance, @@ -192,7 +192,7 @@ export class ShoppingCartComponent implements OnInit { } const neededPids = Array.from(neededPidsSet.values()); if (neededPids.length > 0) { - const extras = await this.rmmapi.getProducts(neededPids).toPromise(); + const extras = await firstValueFrom(this.rmmapi.getProducts(neededPids)); if (extras.length !== neededPids.length) { console.warn(`Failed to load products ${neededPids.join(',')} (got: ${JSON.stringify(extras)})`); } @@ -213,7 +213,7 @@ export class ShoppingCartComponent implements OnInit { } async checkIfLegal(items: CartItem[]) { - const me = await this.rmmapi.me.toPromise(); + const me = await firstValueFrom(this.rmmapi.me); this.orderError = undefined; // unless we find something else :) diff --git a/src/app/account-app/stripe-payment-dialog.component.ts b/src/app/account-app/stripe-payment-dialog.component.ts index 9046f46fa..75bbd3c69 100644 --- a/src/app/account-app/stripe-payment-dialog.component.ts +++ b/src/app/account-app/stripe-payment-dialog.component.ts @@ -87,7 +87,7 @@ export class StripePaymentDialogComponent implements AfterViewInit { } async ngAfterViewInit() { - await stripeLoader.toPromise(); + await firstValueFrom(stripeLoader); const stripePubkey = await firstValueFrom(this.paymentsservice.stripePubkey); const customerSession = await firstValueFrom(this.paymentsservice.customerSession()); @@ -173,7 +173,7 @@ export class StripePaymentDialogComponent implements AfterViewInit { this.state = 'failure'; return; } - + } handleConfirmationToken(cId: string) { diff --git a/src/app/account-details/personal-details.component.ts b/src/app/account-details/personal-details.component.ts index 24b2b4a94..451a34bc6 100644 --- a/src/app/account-details/personal-details.component.ts +++ b/src/app/account-details/personal-details.component.ts @@ -21,7 +21,7 @@ import { HttpClient, HttpResponse } from '@angular/common/http'; import { Component } from '@angular/core'; import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms'; import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; -import { Subject } from 'rxjs'; +import { firstValueFrom, Subject } from 'rxjs'; import { RMM } from '../rmm'; import { map } from 'rxjs/operators'; import { AccountDetailsInterface } from '../rmm/account-details'; @@ -109,10 +109,9 @@ export class PersonalDetailsComponent { } loadTimezones() { - this.http + firstValueFrom(this.http .get('/rest/v1/timezones') - .pipe(map((res: HttpResponse) => res['result'])) - .toPromise() + .pipe(map((res: HttpResponse) => res['result']))) .then((data) => this.timezones = data.timezones); } @@ -126,10 +125,9 @@ export class PersonalDetailsComponent { } private loadSelectFields() { - this.http + firstValueFrom(this.http .get('/rest/v1/account/details') - .pipe(map((res: HttpResponse) => res['result'])) - .toPromise() + .pipe(map((res: HttpResponse) => res['result']))) .then((data) => { this.selectedCountry = data.country; this.selectedTimezone = data.timezone; diff --git a/src/app/app.component.ts b/src/app/app.component.ts index b5a152f8f..055e4c6ae 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -47,7 +47,7 @@ import { map, take, skip, mergeMap, filter, tap, throttleTime, debounceTime, dis import { WebSocketSearchService } from './websocketsearch/websocketsearch.service'; import { WebSocketSearchMailList } from './websocketsearch/websocketsearchmaillist'; -import { from, Observable } from 'rxjs'; +import { from, Observable, lastValueFrom } from 'rxjs'; import { xapianLoadedSubject } from './xapian/xapianwebloader'; import { SwPush } from '@angular/service-worker'; import { exportKeysFromJWK } from './webpush/vapid.tools'; @@ -396,7 +396,7 @@ export class AppComponent implements OnInit, AfterViewInit, CanvasTableSelectLis && prev.every((f, index) => objectEqualWithKeys(f, curr[index], [ 'folderId', 'totalMessages', 'newMessages', 'folderName' - ])) + ])); })) .pipe(map((folders: FolderListEntry[]) => folders.filter(f => f.folderPath.indexOf('Drafts') !== 0)) ); @@ -616,11 +616,11 @@ export class AppComponent implements OnInit, AfterViewInit, CanvasTableSelectLis async emptySpam(spamFolderName) { console.log('found spam folder with name', spamFolderName); - const messageLists = await this.rmmapi.listAllMessages( + const messageLists = await lastValueFrom(this.rmmapi.listAllMessages( 0, 0, 0, RunboxWebmailAPI.LIST_ALL_MESSAGES_CHUNK_SIZE, true, spamFolderName - ).toPromise(); + )); const messageIds = messageLists.map(msg => msg.id); this.messageActionsHandler.updateMessages({ @@ -929,7 +929,7 @@ export class AppComponent implements OnInit, AfterViewInit, CanvasTableSelectLis } public rowSelected(rowIndex: number, columnIndex: number, multiSelect?: boolean) { - const isSelect = (columnIndex === 0) || multiSelect + const isSelect = (columnIndex === 0) || multiSelect; if ((this.selectedFolder === this.messagelistservice.templateFolderName) && !isSelect) { this.draftDeskService.newTemplateDraft( @@ -1452,14 +1452,14 @@ export class AppComponent implements OnInit, AfterViewInit, CanvasTableSelectLis } async enableNotification() { - console.log('request notification permission') + console.log('request notification permission'); try { - const result = await Notification.requestPermission() - console.log('Notification.permission', Notification.permission) + const result = await Notification.requestPermission(); + console.log('Notification.permission', Notification.permission); if (result === 'granted') this.subscribeToNotifications(); } catch (error) { - console.error(error) + console.error(error); } } } diff --git a/src/app/common/preferences.service.ts b/src/app/common/preferences.service.ts index 7ed303938..b1a358a11 100644 --- a/src/app/common/preferences.service.ts +++ b/src/app/common/preferences.service.ts @@ -18,7 +18,7 @@ // ---------- END RUNBOX LICENSE ---------- import { Injectable } from '@angular/core'; -import { ReplaySubject } from 'rxjs'; +import { firstValueFrom, ReplaySubject } from 'rxjs'; import { take } from 'rxjs/operators'; import { StorageService } from '../storage.service'; @@ -151,7 +151,7 @@ export class PreferencesService { }; } - const allPrefs = await this.preferences.pipe(take(1)).toPromise(); + const allPrefs = await firstValueFrom(this.preferences.pipe(take(1))); Object.keys(prefsdata[DefaultPrefGroups.Global]['entries']).forEach((key) => { allPrefs.set(`${DefaultPrefGroups.Global}:${key}`, prefsdata[DefaultPrefGroups.Global]['entries'][key]); }); @@ -179,7 +179,7 @@ export class PreferencesService { } private async uploadPreferenceData(level: string) { - const prefs = await this.preferences.pipe(take(1)).toPromise(); + const prefs = await firstValueFrom(this.preferences.pipe(take(1))); const entriesObj = {}; prefs.forEach((value, key) => { // for (const [key, value] of prefs) { @@ -219,7 +219,7 @@ export class PreferencesService { if (this.loadedOldStyle) { return; } - let prefs = await this.preferences.pipe(take(1)).toPromise(); + let prefs = await firstValueFrom(this.preferences.pipe(take(1))); if (!prefs) { // Already set / imported prefs = new Map(); @@ -235,7 +235,7 @@ export class PreferencesService { const showImagesDecisionKey = 'rmm7showimagesdecision'; const resizerPercentageKey = 'rmm7resizerpercentage'; - const uid = await this.storage.uid.toPromise(); + const uid = await firstValueFrom(this.storage.uid); if (level === DefaultPrefGroups.Global) { if (localStorage.getItem('rmm7experimentalFeatureEnabled') !== null) { diff --git a/src/app/compose/draftdesk.service.ts b/src/app/compose/draftdesk.service.ts index 0c1d02731..69b575ff7 100644 --- a/src/app/compose/draftdesk.service.ts +++ b/src/app/compose/draftdesk.service.ts @@ -26,7 +26,7 @@ import { MailAddressInfo } from '../common/mailaddressinfo'; import { MessageListService } from '../rmmapi/messagelist.service'; import { MessageTableRowTool} from '../messagetable/messagetablerow'; import { Identity, ProfileService } from '../profiles/profile.service'; -import { from, of, BehaviorSubject } from 'rxjs'; +import { from, of, BehaviorSubject, firstValueFrom } from 'rxjs'; import { map, mergeMap, bufferCount, take, distinctUntilChanged } from 'rxjs/operators'; import moment from 'moment'; @@ -353,9 +353,9 @@ export class DraftDeskService { '"Runbox 7 Bug Reports" ', 'Runbox 7 Bug Report' ); - const template = await this.http.get('assets/templates/bug_report.txt', - {responseType: 'text'}).toPromise(); - const me = await this.rmmapi.me.toPromise(); + const template = await firstValueFrom(this.http.get('assets/templates/bug_report.txt', + {responseType: 'text'})); + const me = await firstValueFrom(this.rmmapi.me); let body = `${template}` ; @@ -382,8 +382,8 @@ export class DraftDeskService { } public async newVideoCallInvite(to: string, url: URL) { - const template = await this.http.get('assets/templates/video_call.txt', - {responseType: 'text'}).toPromise(); + const template = await firstValueFrom(this.http.get('assets/templates/video_call.txt', + {responseType: 'text'})); const draftObj = DraftFormModel.create( -1, this.mainIdentity(), diff --git a/src/app/compose/recipients.service.spec.ts b/src/app/compose/recipients.service.spec.ts index ab2c353ba..cace40396 100644 --- a/src/app/compose/recipients.service.spec.ts +++ b/src/app/compose/recipients.service.spec.ts @@ -1,18 +1,18 @@ // --------- BEGIN RUNBOX LICENSE --------- // Copyright (C) 2016-2020 Runbox Solutions AS (runbox.com). -// +// // This file is part of Runbox 7. -// +// // Runbox 7 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, either version 3 of the License, or (at your // option) any later version. -// +// // Runbox 7 is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. -// +// // You should have received a copy of the GNU General Public License // along with Runbox 7. If not, see . // ---------- END RUNBOX LICENSE ---------- @@ -22,7 +22,7 @@ import { RecipientsService } from './recipients.service'; import { ContactsService } from '../contacts-app/contacts.service'; import { SearchService, XAPIAN_GLASS_WR } from '../xapian/searchservice'; import { StorageService } from '../storage.service'; -import { AsyncSubject, of, Subject } from 'rxjs'; +import { AsyncSubject, of, Subject, firstValueFrom, lastValueFrom } from 'rxjs'; import { take } from 'rxjs/operators'; import { Contact } from '../contacts-app/contact'; import { RunboxWebmailAPI } from '../rmmapi/rbwebmail'; @@ -109,7 +109,7 @@ describe('RecipientsService', () => { const recipientsService = TestBed.inject(RecipientsService); // Take 2 as searchindex+contacts service are separate updates - const recipients = await recipientsService.recipients.pipe(take(2)).toPromise(); + const recipients = await lastValueFrom(recipientsService.recipients.pipe(take(2))); console.log(recipients); expect(window['termlistresult'].length).toBe(5); @@ -136,7 +136,7 @@ describe('RecipientsService', () => { }; const recipientsService: RecipientsService = TestBed.inject(RecipientsService); - const suggested = await recipientsService.recentlyUsed.pipe(take(1)).toPromise(); + const suggested = await firstValueFrom(recipientsService.recentlyUsed); expect(suggested.length).toBe(2); }); diff --git a/src/app/contacts-app/contact-details/contact-details.component.ts b/src/app/contacts-app/contact-details/contact-details.component.ts index b342d67d1..f3621b0b7 100644 --- a/src/app/contacts-app/contact-details/contact-details.component.ts +++ b/src/app/contacts-app/contact-details/contact-details.component.ts @@ -31,6 +31,7 @@ import { MobileQueryService } from '../../mobile-query.service'; import { ContactPickerDialogComponent } from '../contact-picker-dialog.component'; import { AppSettings } from '../../app-settings'; import { DraftDeskService } from '../../compose/draftdesk.service'; +import { firstValueFrom } from 'rxjs'; import { RunboxWebmailAPI } from '../../rmmapi/rbwebmail'; import { OnscreenComponent } from '../../onscreen/onscreen.component'; import { Location, LocationStrategy, PathLocationStrategy } from '@angular/common'; @@ -329,7 +330,7 @@ export class ContactDetailsComponent { } async askForMoreMembers(): Promise { - let contacts = await this.contactsservice.contactsSubject.pipe(take(1)).toPromise(); + let contacts = await firstValueFrom(this.contactsservice.contactsSubject.pipe(take(1))); contacts = contacts.filter(c => { if (c.kind !== ContactKind.INVIDIDUAL) { return false; @@ -408,14 +409,14 @@ export class ContactDetailsComponent { } async newVideoCall(email: string) { - const name = await this.dialog.open(SimpleInputDialog, { + const name = await firstValueFrom(this.dialog.open(SimpleInputDialog, { data: new SimpleInputDialogParams( 'Meeting invitation', 'Pick a name for your meeting', 'Meeting name (optional)', ) - }).afterClosed().toPromise(); - const me = await this.rmmapi.me.toPromise(); + }).afterClosed()); + const me = await firstValueFrom(this.rmmapi.me); const meetingCode = OnscreenComponent.generateMeetingName(name, me.uid.toString()); const meetingUrl = new URL(this.location.prepareExternalUrl(`/onscreen/${meetingCode}`), window.location.origin); this.draftDeskService.newVideoCallInvite(email, meetingUrl).then(() => { diff --git a/src/app/folder/folderlist.component.spec.ts b/src/app/folder/folderlist.component.spec.ts index ecaf28f44..c7d05637d 100644 --- a/src/app/folder/folderlist.component.spec.ts +++ b/src/app/folder/folderlist.component.spec.ts @@ -20,7 +20,7 @@ import { FolderListComponent, DropPosition, CreateFolderEvent, MoveFolderEvent } from './folderlist.component'; import { RunboxWebmailAPI } from '../rmmapi/rbwebmail'; import { FolderListEntry } from '../common/folderlistentry'; -import { BehaviorSubject, of } from 'rxjs'; +import { BehaviorSubject, firstValueFrom, of } from 'rxjs'; import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; import { TestBed } from '@angular/core/testing'; import { take } from 'rxjs/operators'; @@ -76,19 +76,19 @@ describe('FolderListComponent', () => { console.log('move folder with id 6 above 5'); await comp.folderReorderingDrop(6, 5, DropPosition.ABOVE); - rearrangedFolders = await comp.folders.pipe(take(1)).toPromise(); + rearrangedFolders = await firstValueFrom(comp.folders); expect(rearrangedFolders.map(f => f.folderId)).toEqual([1, 2, 3, 4, 6, 5, 7]); expect(rearrangedFolders.map(f => f.folderLevel)).toEqual([0, 0, 1, 2, 2, 2, 0]); console.log('move folder with id 6 above 5 - should not cause any changes'); await comp.folderReorderingDrop(6, 5, DropPosition.ABOVE); - rearrangedFolders = await comp.folders.pipe(take(1)).toPromise(); + rearrangedFolders = await firstValueFrom(comp.folders); expect(rearrangedFolders.map(f => f.folderId)).toEqual([1, 2, 3, 4, 6, 5, 7]); expect(rearrangedFolders.map(f => f.folderLevel)).toEqual([0, 0, 1, 2, 2, 2, 0]); console.log('move folder with id 6 below 5'); comp.folderReorderingDrop(6, 5, DropPosition.BELOW); - rearrangedFolders = await comp.folders.pipe(take(1)).toPromise(); + rearrangedFolders = await firstValueFrom(comp.folders); console.log(rearrangedFolders.map(f => f.folderId)); expect(moveEvent.order).toEqual([1, 2, 3, 4, 5, 6, 7]); expect(rearrangedFolders.map(f => f.folderId)).toEqual([1, 2, 3, 4, 5, 6, 7]); @@ -96,7 +96,7 @@ describe('FolderListComponent', () => { console.log('move folder with id 5 below 7'); comp.folderReorderingDrop(5, 7, DropPosition.BELOW); - rearrangedFolders = await comp.folders.pipe(take(1)).toPromise(); + rearrangedFolders = await firstValueFrom(comp.folders); console.log(rearrangedFolders.map(f => f.folderId)); expect(moveEvent.order).toEqual([1, 2, 3, 4, 6, 7, 5]); expect(rearrangedFolders.map(f => f.folderId)).toEqual([1, 2, 3, 4, 6, 7, 5]); @@ -105,7 +105,7 @@ describe('FolderListComponent', () => { console.log('move folder with id 5 inside 7'); comp.folderReorderingDrop(5, 7, DropPosition.INSIDE); - rearrangedFolders = await comp.folders.pipe(take(1)).toPromise(); + rearrangedFolders = await firstValueFrom(comp.folders); console.log(rearrangedFolders.map(f => f.folderId)); expect(rearrangedFolders.map(f => f.folderId)).toEqual([1, 2, 3, 4, 6, 7, 5]); expect(rearrangedFolders[6].folderPath).toBe('folder3.subsubfolder2'); @@ -113,7 +113,7 @@ describe('FolderListComponent', () => { console.log('move folder with id 7 above 1'); comp.folderReorderingDrop(7, 1, DropPosition.ABOVE); - rearrangedFolders = await comp.folders.pipe(take(1)).toPromise(); + rearrangedFolders = await firstValueFrom(comp.folders); console.log(rearrangedFolders.map(f => f.folderId)); expect(rearrangedFolders.map(f => f.folderId)).toEqual([7, 5, 1, 2, 3, 4, 6]); expect(rearrangedFolders[0].folderPath).toBe('folder3'); @@ -122,7 +122,7 @@ describe('FolderListComponent', () => { console.log('move folder with id 7 below 1'); comp.folderReorderingDrop(7, 1, DropPosition.BELOW); - rearrangedFolders = await comp.folders.pipe(take(1)).toPromise(); + rearrangedFolders = await firstValueFrom(comp.folders); console.log(rearrangedFolders.map(f => f.folderId)); expect(rearrangedFolders.map(f => f.folderId)).toEqual([1, 7, 5, 2, 3, 4, 6]); expect(rearrangedFolders[1].folderPath).toBe('folder3'); @@ -131,7 +131,7 @@ describe('FolderListComponent', () => { console.log('move folder with id 4 above 7'); comp.folderReorderingDrop(4, 7, DropPosition.ABOVE); - rearrangedFolders = await comp.folders.pipe(take(1)).toPromise(); + rearrangedFolders = await firstValueFrom(comp.folders); console.log(rearrangedFolders.map(f => f.folderId)); expect(rearrangedFolders.map(f => f.folderId)).toEqual([1, 4, 7, 5, 2, 3, 6]); expect(rearrangedFolders[1].folderPath).toBe('subsubfolder'); @@ -141,7 +141,7 @@ describe('FolderListComponent', () => { console.log('move folder with id 5 below 7'); comp.folderReorderingDrop(5, 7, DropPosition.BELOW); - rearrangedFolders = await comp.folders.pipe(take(1)).toPromise(); + rearrangedFolders = await firstValueFrom(comp.folders); console.log(rearrangedFolders.map(f => f.folderId)); expect(rearrangedFolders[2].folderPath).toBe('folder3'); expect(rearrangedFolders[3].folderPath).toBe('subsubfolder2'); @@ -150,7 +150,7 @@ describe('FolderListComponent', () => { console.log('move folder with id 6 below 3'); comp.folderReorderingDrop(6, 3, DropPosition.BELOW); - rearrangedFolders = await comp.folders.pipe(take(1)).toPromise(); + rearrangedFolders = await firstValueFrom(comp.folders); console.log(rearrangedFolders.map(f => f.folderId)); expect(rearrangedFolders.map(f => f.folderId)).toEqual([1, 4, 7, 5, 2, 3, 6]); @@ -158,7 +158,7 @@ describe('FolderListComponent', () => { console.log('move folder with id 6 inside 7'); comp.folderReorderingDrop(6, 7, DropPosition.INSIDE); - rearrangedFolders = await comp.folders.pipe(take(1)).toPromise(); + rearrangedFolders = await firstValueFrom(comp.folders); console.log(rearrangedFolders.map(f => f.folderId)); expect(rearrangedFolders.map(f => f.folderId)).toEqual([1, 4, 7, 6, 5, 2, 3]); @@ -166,7 +166,7 @@ describe('FolderListComponent', () => { console.log('move folder with id 3 above 7'); comp.folderReorderingDrop(3, 7, DropPosition.ABOVE); - rearrangedFolders = await comp.folders.pipe(take(1)).toPromise(); + rearrangedFolders = await firstValueFrom(comp.folders); console.log(rearrangedFolders.map(f => f.folderId)); expect(rearrangedFolders.map(f => f.folderId)).toEqual([1, 4, 3, 7, 6, 5, 2]); @@ -174,7 +174,7 @@ describe('FolderListComponent', () => { console.log('move folder with id 6 above 4'); comp.folderReorderingDrop(6, 4, DropPosition.ABOVE); - rearrangedFolders = await comp.folders.pipe(take(1)).toPromise(); + rearrangedFolders = await firstValueFrom(comp.folders); console.log(rearrangedFolders.map(f => f.folderId)); expect(rearrangedFolders.map(f => f.folderId)).toEqual([1, 6, 4, 3, 7, 5, 2]); @@ -182,7 +182,7 @@ describe('FolderListComponent', () => { console.log('move folder with id 6 inside 4'); comp.folderReorderingDrop(6, 4, DropPosition.INSIDE); - rearrangedFolders = await comp.folders.pipe(take(1)).toPromise(); + rearrangedFolders = await firstValueFrom(comp.folders); console.log(rearrangedFolders.map(f => f.folderId)); expect(rearrangedFolders.map(f => f.folderId)).toEqual([1, 4, 6, 3, 7, 5, 2]); @@ -190,7 +190,7 @@ describe('FolderListComponent', () => { console.log('move folder with id 3 inside 5'); comp.folderReorderingDrop(3, 5, DropPosition.INSIDE); - rearrangedFolders = await comp.folders.pipe(take(1)).toPromise(); + rearrangedFolders = await firstValueFrom(comp.folders); console.log(rearrangedFolders.map(f => f.folderId)); expect(rearrangedFolders.map(f => f.folderId)).toEqual([1, 4, 6, 7, 5, 3, 2]); @@ -198,7 +198,7 @@ describe('FolderListComponent', () => { console.log('move folder with id 4 above 5'); comp.folderReorderingDrop(4, 5, DropPosition.ABOVE); - rearrangedFolders = await comp.folders.pipe(take(1)).toPromise(); + rearrangedFolders = await firstValueFrom(comp.folders); console.log(rearrangedFolders.map(f => f.folderId)); expect(rearrangedFolders.map(f => f.folderId)).toEqual([1, 7, 4, 6, 5, 3, 2]); @@ -206,7 +206,7 @@ describe('FolderListComponent', () => { console.log('move folder with id 6 below 7'); comp.folderReorderingDrop(6, 7, DropPosition.BELOW); - rearrangedFolders = await comp.folders.pipe(take(1)).toPromise(); + rearrangedFolders = await firstValueFrom(comp.folders); console.log(rearrangedFolders.map(f => f.folderId)); expect(rearrangedFolders.map(f => f.folderId)).toEqual([1, 7, 6, 4, 5, 3, 2]); @@ -214,7 +214,7 @@ describe('FolderListComponent', () => { console.log('move folder with id 2 inside 3'); comp.folderReorderingDrop(2, 3, DropPosition.INSIDE); - rearrangedFolders = await comp.folders.pipe(take(1)).toPromise(); + rearrangedFolders = await firstValueFrom(comp.folders); console.log(rearrangedFolders.map(f => f.folderId)); expect(rearrangedFolders.map(f => f.folderId)).toEqual([1, 7, 6, 4, 5, 3, 2]); @@ -222,7 +222,7 @@ describe('FolderListComponent', () => { console.log('move folder with id 4 below 3'); comp.folderReorderingDrop(4, 3, DropPosition.BELOW); - rearrangedFolders = await comp.folders.pipe(take(1)).toPromise(); + rearrangedFolders = await firstValueFrom(comp.folders); console.log(rearrangedFolders.map(f => f.folderId)); expect(rearrangedFolders.map(f => f.folderId)).toEqual([1, 7, 6, 5, 3, 2, 4]); @@ -244,7 +244,7 @@ describe('FolderListComponent', () => { foldersSubject.next([...folders, new FolderListEntry(3, 50, 40, 'user', ev.name, 'folder2', 0)]) ); comp.addFolder(); - const newListOfFolders = await comp.folders.pipe(take(1)).toPromise(); + const newListOfFolders = await firstValueFrom(comp.folders); console.log(newListOfFolders); diff --git a/src/app/folder/folderlist.component.ts b/src/app/folder/folderlist.component.ts index e0524080e..803a47aa9 100644 --- a/src/app/folder/folderlist.component.ts +++ b/src/app/folder/folderlist.component.ts @@ -25,7 +25,7 @@ import { FolderListEntry } from '../common/folderlistentry'; import { FolderMessageCountMap } from '../rmmapi/messagelist.service'; import { SimpleInputDialog, SimpleInputDialogParams } from '../dialog/simpleinput.dialog'; -import { Observable } from 'rxjs'; +import { Observable, firstValueFrom } from 'rxjs'; import { first, map, filter, take } from 'rxjs/operators'; import { FlatTreeControl } from '@angular/cdk/tree'; import {ExtendedKeyboardEvent, Hotkey, HotkeysService} from 'angular2-hotkeys'; @@ -366,7 +366,7 @@ export class FolderListComponent implements OnChanges { return; } - const folders = await this.folders.pipe(take(1)).toPromise(); + const folders = await firstValueFrom(this.folders.pipe(take(1))); let sourceIndex = folders.findIndex(fld => fld.folderId === sourceFolderId); let destinationIndex = folders.findIndex(folder => folder.folderId === destinationFolderId); diff --git a/src/app/http/progress.service.spec.ts b/src/app/http/progress.service.spec.ts index 8bb91f129..4576c8eb4 100644 --- a/src/app/http/progress.service.spec.ts +++ b/src/app/http/progress.service.spec.ts @@ -1,18 +1,18 @@ // --------- BEGIN RUNBOX LICENSE --------- // Copyright (C) 2016-2018 Runbox Solutions AS (runbox.com). -// +// // This file is part of Runbox 7. -// +// // Runbox 7 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, either version 3 of the License, or (at your // option) any later version. -// +// // Runbox 7 is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. -// +// // You should have received a copy of the GNU General Public License // along with Runbox 7. If not, see . // ---------- END RUNBOX LICENSE ---------- @@ -27,6 +27,7 @@ import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { RMMHttpInterceptorService } from '../rmmapi/rmmhttpinterceptor.service'; import { ProgressService } from './progress.service'; import { RouterTestingModule } from '@angular/router/testing'; +import { firstValueFrom } from 'rxjs'; import { filter, take } from 'rxjs/operators'; import { RMMAuthGuardService } from '../rmmapi/rmmauthguard.service'; import { RMMOfflineService } from '../rmmapi/rmmoffline.service'; @@ -72,14 +73,12 @@ describe('ProgressService', () => { const last_on_req = httpMock.expectOne('/rest/v1/last_on'); last_on_req.flush(200); - const me = await rmmapiservice.me.toPromise(); + const me = await firstValueFrom(rmmapiservice.me); expect(me.last_name).toBe('testuser'); expect(httpProgressSeen).toBeTruthy(); - const hasCurrentHttpActivity = await progressService.httpRequestInProgress.pipe( - take(1) - ).toPromise(); + const hasCurrentHttpActivity = await firstValueFrom(progressService.httpRequestInProgress); expect(hasCurrentHttpActivity).toBeFalsy(); expect(rmmapiservice.last_on_interval).toBeTruthy(); diff --git a/src/app/mailviewer/avatar-bar.component.ts b/src/app/mailviewer/avatar-bar.component.ts index 24bb623d7..fa700187b 100644 --- a/src/app/mailviewer/avatar-bar.component.ts +++ b/src/app/mailviewer/avatar-bar.component.ts @@ -18,7 +18,7 @@ // ---------- END RUNBOX LICENSE ---------- import { Component, Input, OnInit } from '@angular/core'; -import { ReplaySubject} from 'rxjs'; +import { firstValueFrom, ReplaySubject} from 'rxjs'; import { take } from 'rxjs/operators'; import { ProfileService } from '../profiles/profile.service'; import { ContactsService } from '../contacts-app/contacts.service'; @@ -82,7 +82,7 @@ export class AvatarBarComponent implements OnInit { async ngOnChanges() { this.emails = []; // so that we don't display the old, wrong ones while we're loading new ones - const own = await this.ownAddresses.pipe(take(1)).toPromise(); + const own = await firstValueFrom(this.ownAddresses.pipe(take(1))); const emails: string[] = [].concat.apply( [], [this.email.from, this.email.to, this.email.cc, this.email.bcc] diff --git a/src/app/mailviewer/singlemailviewer.component.ts b/src/app/mailviewer/singlemailviewer.component.ts index 0c632d961..1bb81b032 100644 --- a/src/app/mailviewer/singlemailviewer.component.ts +++ b/src/app/mailviewer/singlemailviewer.component.ts @@ -1,18 +1,18 @@ // --------- BEGIN RUNBOX LICENSE --------- // Copyright (C) 2016-2018 Runbox Solutions AS (runbox.com). -// +// // This file is part of Runbox 7. -// +// // Runbox 7 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, either version 3 of the License, or (at your // option) any later version. -// +// // Runbox 7 is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. -// +// // You should have received a copy of the GNU General Public License // along with Runbox 7. If not, see . // ---------- END RUNBOX LICENSE ---------- @@ -40,7 +40,7 @@ import { MobileQueryService } from '../mobile-query.service'; import { HorizResizerDirective } from '../directives/horizresizer.directive'; import { MessageContents, RunboxWebmailAPI } from '../rmmapi/rbwebmail'; -import { of } from 'rxjs'; +import { firstValueFrom, of } from 'rxjs'; import { Router } from '@angular/router'; import { MessageListService } from '../rmmapi/messagelist.service'; import { loadLocalMailParser } from './mailparser'; @@ -564,7 +564,7 @@ export class SingleMailViewerComponent implements OnInit, DoCheck, AfterViewInit res.date.setMinutes(res.date.getMinutes() - res.date.getTimezoneOffset()); res.sanitized_html = this.expandAttachmentData(res.attachments, res.sanitized_html); - res.visible_attachment_count = res.attachments.filter((att) => !att.internal).length; + res.visible_attachment_count = res.attachments.filter((att) => !att.internal).length; res.sanitized_html_without_images = this.expandAttachmentData(res.attachments, res.sanitized_html_without_images); @@ -689,7 +689,7 @@ export class SingleMailViewerComponent implements OnInit, DoCheck, AfterViewInit } print() { - // Can't access print view inside iFrame, so we need to + // Can't access print view inside iFrame, so we need to // temporary hide buttons while the view is rendering const messageContents = document.getElementById('messageContents'); const buttons = messageContents.getElementsByTagName('button'); @@ -732,7 +732,7 @@ export class SingleMailViewerComponent implements OnInit, DoCheck, AfterViewInit /** * EXPERIMENTAL, decrypt attachment (encrypted.asc) by sending it to pgpapp.no - * @param attachmentIndex + * @param attachmentIndex */ public decryptAttachment(attachmentIndex: number) { this.http.get('/rest/v1/email/' + this.messageId + '/attachment/' + attachmentIndex, @@ -746,7 +746,7 @@ export class SingleMailViewerComponent implements OnInit, DoCheck, AfterViewInit window.removeEventListener('message', pgpapplistener); pgpapp.close(); - const parseMail = await loadLocalMailParser().toPromise(); + const parseMail = await firstValueFrom(loadLocalMailParser()); const parsed = await parseMail(msg.data.decryptedContent); this.mailObj.text = parsed.text; this.mailObj.subject = parsed.subject; diff --git a/src/app/onscreen/onscreen.component.ts b/src/app/onscreen/onscreen.component.ts index a9d5d5eaf..8997dc4c8 100644 --- a/src/app/onscreen/onscreen.component.ts +++ b/src/app/onscreen/onscreen.component.ts @@ -20,7 +20,7 @@ import { Component, OnDestroy } from '@angular/core'; import { Location } from '@angular/common'; import { MobileQueryService } from '../mobile-query.service'; -import { AsyncSubject } from 'rxjs'; +import { AsyncSubject, firstValueFrom } from 'rxjs'; import { RunboxWebmailAPI, RunboxMe } from '../rmmapi/rbwebmail'; import { ContactsService } from '../contacts-app/contacts.service'; import { ActivatedRoute } from '@angular/router'; @@ -115,7 +115,7 @@ export class OnscreenComponent implements OnDestroy { } async createMeeting() { - await jitsiLoader.toPromise(); + await firstValueFrom(jitsiLoader); const name = await this.encodeMeetingName(this.form.meetingName); this.joinMeeting(name).then(() => { @@ -132,7 +132,7 @@ export class OnscreenComponent implements OnDestroy { } async joinMeeting(code: string) { - await jitsiLoader.toPromise(); + await firstValueFrom(jitsiLoader); this.jitsiAPI = new JitsiMeetExternalAPI('video.runbox.com', { roomName: code, @@ -188,7 +188,7 @@ export class OnscreenComponent implements OnDestroy { } async encodeMeetingName(name: string): Promise { - const me = await this.me.toPromise(); + const me = await firstValueFrom(this.me); return Promise.resolve(OnscreenComponent.generateMeetingName(name, me.uid.toString())); } diff --git a/src/app/saved-searches/saved-searches.service.ts b/src/app/saved-searches/saved-searches.service.ts index 21763d5a6..7c58e14d4 100644 --- a/src/app/saved-searches/saved-searches.service.ts +++ b/src/app/saved-searches/saved-searches.service.ts @@ -18,7 +18,7 @@ // ---------- END RUNBOX LICENSE ---------- import { Injectable } from '@angular/core'; -import { ReplaySubject } from 'rxjs'; +import { firstValueFrom, ReplaySubject } from 'rxjs'; import { take } from 'rxjs/operators'; import { StorageService } from '../storage.service'; @@ -83,7 +83,7 @@ export class SavedSearchesService { private async uploadSeachData() { const data: SavedSearchStorage = { version: this.version, - entries: await this.searches.pipe(take(1)).toPromise(), + entries: await firstValueFrom(this.searches.pipe(take(1))), }; this.rmmapi.setSavedSearches(data).subscribe( newData => this.applySyncedData(newData) diff --git a/src/app/sentry-error-handler.ts b/src/app/sentry-error-handler.ts index d005c9f4b..af3d56711 100644 --- a/src/app/sentry-error-handler.ts +++ b/src/app/sentry-error-handler.ts @@ -22,6 +22,7 @@ import * as Sentry from '@sentry/browser'; import './sentry'; import { RMMAuthGuardService } from './rmmapi/rmmauthguard.service'; +import { firstValueFrom } from 'rxjs'; import { RunboxWebmailAPI } from './rmmapi/rbwebmail'; @Injectable() @@ -40,10 +41,10 @@ export class SentryErrorHandler implements ErrorHandler { return; } const authguard = this.injector.get(RMMAuthGuardService); - const isLoggedIn = await authguard.isLoggedIn().toPromise(); + const isLoggedIn = await firstValueFrom(authguard.isLoggedIn()); if (isLoggedIn) { const rmmapi = this.injector.get(RunboxWebmailAPI); - const me = await rmmapi.me.toPromise(); + const me = await firstValueFrom(rmmapi.me); Sentry.setUser({ uid: me.uid, username: me.username, diff --git a/src/app/start/startdesk.component.ts b/src/app/start/startdesk.component.ts index 26cf8c34f..53faca1dd 100644 --- a/src/app/start/startdesk.component.ts +++ b/src/app/start/startdesk.component.ts @@ -27,7 +27,7 @@ import { Contact } from '../contacts-app/contact'; import { SearchService, SearchIndexDocumentData } from '../xapian/searchservice'; import { isValidEmail } from '../compose/emailvalidator'; import { filter, take } from 'rxjs/operators'; -import { ReplaySubject } from 'rxjs'; +import { firstValueFrom, ReplaySubject } from 'rxjs'; import { ProfileService } from '../profiles/profile.service'; import { UsageReportsService } from '../common/usage-reports.service'; @@ -267,7 +267,7 @@ export class StartDeskComponent implements OnInit { private async extractMailingLists(messages: SearchIndexDocumentData[]): Promise> { const possibleMailingLists = new Map(); - const ownAddresses = await this.ownAddresses.pipe(take(1)).toPromise(); + const ownAddresses = await firstValueFrom(this.ownAddresses.pipe(take(1))); for (const message of messages) { if (!message.recipients.find(r => ownAddresses.has(r.toLowerCase()))) { diff --git a/src/app/storage.service.ts b/src/app/storage.service.ts index 3af77f90f..31e57e397 100644 --- a/src/app/storage.service.ts +++ b/src/app/storage.service.ts @@ -19,7 +19,7 @@ import { RunboxWebmailAPI } from './rmmapi/rbwebmail'; import { Injectable } from '@angular/core'; -import { AsyncSubject, ReplaySubject } from 'rxjs'; +import { AsyncSubject, firstValueFrom, ReplaySubject } from 'rxjs'; @Injectable() export class StorageService { @@ -36,7 +36,7 @@ export class StorageService { } private async userKey(key: string): Promise { - const uid = await this.uid.toPromise(); + const uid = await firstValueFrom(this.uid); return `${uid}:${key}`; } diff --git a/src/app/xapian/index.worker.ts b/src/app/xapian/index.worker.ts index 378cbdc51..a1cfb24bd 100644 --- a/src/app/xapian/index.worker.ts +++ b/src/app/xapian/index.worker.ts @@ -21,7 +21,7 @@ import '../sentry'; -import { Observer, Observable, of, from, AsyncSubject } from 'rxjs'; +import { Observer, Observable, of, from, AsyncSubject, firstValueFrom, lastValueFrom } from 'rxjs'; import { mergeMap, map, filter, catchError, tap, take, bufferCount } from 'rxjs/operators'; import { XapianAPI } from '@runboxcom/runbox-searchindex'; @@ -147,7 +147,7 @@ class SearchIndexService { db.close(); }; } catch (e) { - console.error(e) + console.error(e); console.log('Worker: Unable to open local xapian index', (e ? e.message : '')); db.close(); // console.log('Worker: Req failed'); @@ -237,7 +237,7 @@ class SearchIndexService { } }); } catch (e) { - console.error(e) + console.error(e); } } @@ -273,7 +273,7 @@ class SearchIndexService { try { FS.unlink('xapianglass'); } catch (e) { - console.error(e) + console.error(e); } @@ -535,8 +535,8 @@ not matching with rest api counts for current folder`); (result) => console.log(result.result.result.msg) ).catch( (err) => { - console.error(err) - console.log('Error updating folder counts: ' + err.errors.join(',')) + console.error(err); + console.log('Error updating folder counts: ' + err.errors.join(',')); } ); } @@ -556,7 +556,7 @@ not matching with rest api counts for current folder`); try { this.api.deleteDocumentByUniqueTerm(uniqueIdTerm); } catch (e) { - console.error(e) + console.error(e); console.error('Worker: Unable to delete message from index', msgid); } }) @@ -681,7 +681,7 @@ not matching with rest api counts for current folder`); this.numberOfMessagesSyncedLastTime = searchIndexDocumentUpdates.length; if (searchIndexDocumentUpdates.length > 0) { - await this.postMessagesToXapianWorker(searchIndexDocumentUpdates).toPromise(); + await lastValueFrom(this.postMessagesToXapianWorker(searchIndexDocumentUpdates)); } // Look up messages with missing body text term and add the missing text to the index @@ -713,7 +713,7 @@ not matching with rest api counts for current folder`); console.error('Worker: Failed to add text to document', messageId, e); } }); - })).toPromise(); + })); } } else { // localsearchactivated is off @@ -861,7 +861,7 @@ not matching with rest api counts for current folder`); if (this.persistIndexInProgressSubject) { // Wait for persistence of index to finish before doing more work on the index - await this.persistIndexInProgressSubject.toPromise(); + await firstValueFrom(this.persistIndexInProgressSubject); } setTimeout(() => processMessage(), 1); @@ -872,7 +872,7 @@ not matching with rest api counts for current folder`); this.indexNotPersisted = true; } this.api.commitXapianUpdates(); - await this.persistIndex().toPromise(); + await lastValueFrom(this.persistIndex()); if (hasProgressSnackBar) { ctx.postMessage({'action': PostMessageAction.closeProgressSnackBar}); diff --git a/src/app/xapian/multiple-search-fields-input/multiple-search-fields-input.component.spec.ts b/src/app/xapian/multiple-search-fields-input/multiple-search-fields-input.component.spec.ts index dd9d0deed..a17304417 100644 --- a/src/app/xapian/multiple-search-fields-input/multiple-search-fields-input.component.spec.ts +++ b/src/app/xapian/multiple-search-fields-input/multiple-search-fields-input.component.spec.ts @@ -1,18 +1,18 @@ // --------- BEGIN RUNBOX LICENSE --------- // Copyright (C) 2016-2019 Runbox Solutions AS (runbox.com). -// +// // This file is part of Runbox 7. -// +// // Runbox 7 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, either version 3 of the License, or (at your // option) any later version. -// +// // Runbox 7 is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. -// +// // You should have received a copy of the GNU General Public License // along with Runbox 7. If not, see . // ---------- END RUNBOX LICENSE ---------- @@ -28,6 +28,7 @@ import { MatLegacyInputModule as MatInputModule } from '@angular/material/legacy import { CommonModule } from '@angular/common'; import { ReactiveFormsModule } from '@angular/forms'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { firstValueFrom } from 'rxjs'; import { take } from 'rxjs/operators'; describe('MultipleSearchFieldsInputComponent', () => { @@ -63,7 +64,7 @@ describe('MultipleSearchFieldsInputComponent', () => { component.currentFolder = 'Testfolder'; component.formGroup.get('currentfolderonly').setValue(true); - const searchExpressionPromise = component.searchexpression.pipe(take(1)).toPromise(); + const searchExpressionPromise = firstValueFrom(component.searchexpression); component.formGroup.get('subject').setValue('testsubject'); const searchExpression = await searchExpressionPromise; diff --git a/src/app/xapian/searchservice.spec.ts b/src/app/xapian/searchservice.spec.ts index b7e9277f2..6252a1654 100644 --- a/src/app/xapian/searchservice.spec.ts +++ b/src/app/xapian/searchservice.spec.ts @@ -1,18 +1,18 @@ // --------- BEGIN RUNBOX LICENSE --------- // Copyright (C) 2016-2018 Runbox Solutions AS (runbox.com). -// +// // This file is part of Runbox 7. -// +// // Runbox 7 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, either version 3 of the License, or (at your // option) any later version. -// +// // Runbox 7 is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. -// +// // You should have received a copy of the GNU General Public License // along with Runbox 7. If not, see . // ---------- END RUNBOX LICENSE ---------- @@ -28,6 +28,7 @@ import { MatLegacySnackBarModule as MatSnackBarModule } from '@angular/material/ import { MessageListService } from '../rmmapi/messagelist.service'; import { XapianAPI } from '@runboxcom/runbox-searchindex/rmmxapianapi'; +import { firstValueFrom } from 'rxjs'; import { xapianLoadedSubject } from './xapianwebloader'; import { PostMessageAction } from './messageactions'; @@ -123,12 +124,12 @@ describe('SearchService', () => { xit('should load searchservice, but no local index', async () => { const searchService = TestBed.inject(SearchService); - await xapianLoadedSubject.toPromise(); + await firstValueFrom(xapianLoadedSubject); const req = httpMock.expectOne('/rest/v1/email_folder/list'); req.flush(listEmailFoldersResponse); - expect(await searchService.initSubject.toPromise()).toBeFalsy(); + expect(await firstValueFrom(searchService.initSubject)).toBeFalsy(); expect(searchService.localSearchActivated).toBeFalsy(); httpMock.verify(); @@ -193,7 +194,7 @@ describe('SearchService', () => { const testuserid = 444; const localdir = 'rmmsearchservice' + testuserid; - await xapianLoadedSubject.toPromise(); + await firstValueFrom(xapianLoadedSubject); FS.mkdir(localdir); FS.mount(IDBFS, {}, '/' + localdir); @@ -246,7 +247,7 @@ describe('SearchService', () => { req.flush(listEmailFoldersResponse); - expect(await searchService.initSubject.toPromise()).toBeTruthy(); + expect(await firstValueFrom(searchService.initSubject)).toBeTruthy(); console.log('search service initialised'); expect(searchService.localSearchActivated).toBeTruthy(); expect(localdir).toEqual(searchService.localdir); From 6718dd69212f8e77d12f1ed2942398b46e08dcf4 Mon Sep 17 00:00:00 2001 From: Finn Date: Wed, 28 Jan 2026 02:54:13 +0000 Subject: [PATCH 03/12] ci(tooling): update ESLint configuration --- .eslintrc.json | 11 ++++++- .../account-app/account-renewals.component.ts | 4 +-- .../account-settings.component.ts | 4 +-- .../personal-details.component.ts | 4 +-- .../account-details/storage-data.component.ts | 4 +-- .../account-security/sessions.component.ts | 4 +-- src/app/aliases/aliases.lister.spec.ts | 6 ++-- src/app/aliases/aliases.lister.ts | 2 +- src/app/common/util.spec.ts | 30 +++++++++---------- src/app/common/util.ts | 2 +- src/app/compose/compose.component.ts | 10 +++---- src/app/compose/draftdesk.service.ts | 10 +++---- src/app/compose/recipients.service.ts | 8 ++--- .../contacts-app/contact-list.component.ts | 4 +-- .../contacts-app/contacts-app.component.ts | 2 +- src/app/folder/folderlist.component.ts | 6 ++-- src/app/login/login.component.ts | 2 +- src/app/mailviewer/avatar-bar.component.ts | 4 +-- src/app/menu/headertoolbar.component.ts | 4 +-- src/app/profiles/profile.service.spec.ts | 12 ++++---- src/app/rmm/account-security.ts | 4 +-- src/app/rmm6/rmm6.module.ts | 4 +-- src/app/rmmapi/rblocale.ts | 2 +- src/app/sentry.ts | 2 +- src/app/welcome/welcomedesk.component.ts | 4 +-- .../multiple-search-fields-input.component.ts | 2 +- src/app/xapian/searchservice.ts | 12 ++++---- src/environments/environment.prod.ts | 4 +-- src/environments/environment.ts | 2 +- 29 files changed, 89 insertions(+), 80 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 0c3f077cf..6eb370fcb 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -24,14 +24,23 @@ "plugin:@typescript-eslint/recommended" ], "rules": { + "@typescript-eslint/semi":"error", "@typescript-eslint/no-namespace": "warn", "@typescript-eslint/no-empty-function": "warn", "@typescript-eslint/no-useless-constructor": "off", "@typescript-eslint/no-explicit-any": "warn", "@typescript-eslint/quotes": ["error", "single"], "@typescript-eslint/no-unused-expressions": "error", - "@typescript-eslint/member-ordering": "warn", + "@typescript-eslint/member-ordering": "off", "@typescript-eslint/no-shadow": "error", + "@typescript-eslint/no-unused-vars": [ + "warn", + { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "caughtErrorsIgnorePattern": "^_" + } + ], "@angular-eslint/component-selector": [ "error", { diff --git a/src/app/account-app/account-renewals.component.ts b/src/app/account-app/account-renewals.component.ts index d3d0110ea..cad14cff2 100644 --- a/src/app/account-app/account-renewals.component.ts +++ b/src/app/account-app/account-renewals.component.ts @@ -17,7 +17,7 @@ // along with Runbox 7. If not, see . // ---------- END RUNBOX LICENSE ---------- -import { Component, Input, Output, EventEmitter } from '@angular/core'; +import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; import { MatLegacySnackBar as MatSnackBar } from '@angular/material/legacy-snack-bar'; @@ -232,7 +232,7 @@ Warning: You are close to your quotas for this product `, }) -export class AccountRenewalsRenewNowButtonComponent { +export class AccountRenewalsRenewNowButtonComponent implements OnInit { @Input() p: ActiveProduct; @Input() usage: DataUsageInterface; @Output() clicked: EventEmitter = new EventEmitter(); diff --git a/src/app/account-details/account-settings.component.ts b/src/app/account-details/account-settings.component.ts index 7097ecd60..422e837cc 100644 --- a/src/app/account-details/account-settings.component.ts +++ b/src/app/account-details/account-settings.component.ts @@ -17,7 +17,7 @@ // along with Runbox 7. If not, see . // ---------- END RUNBOX LICENSE ---------- -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { AsyncSubject } from 'rxjs'; import { share, timeout } from 'rxjs/operators'; import { RMM } from '../rmm'; @@ -28,7 +28,7 @@ import { AccountSettingsInterface } from '../rmm/account-settings'; templateUrl: './account-settings.component.html', styleUrls: ['account-details.component.scss'], }) -export class AccountSettingsComponent { +export class AccountSettingsComponent implements OnInit { displayedColumns: string[] = ['description', 'status']; settings = new AsyncSubject(); settingsArray = []; diff --git a/src/app/account-details/personal-details.component.ts b/src/app/account-details/personal-details.component.ts index 451a34bc6..75416d191 100644 --- a/src/app/account-details/personal-details.component.ts +++ b/src/app/account-details/personal-details.component.ts @@ -18,7 +18,7 @@ // ---------- END RUNBOX LICENSE ---------- import { HttpClient, HttpResponse } from '@angular/common/http'; -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms'; import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; import { firstValueFrom, Subject } from 'rxjs'; @@ -39,7 +39,7 @@ interface CountryAndTimezone { templateUrl: './personal-details.component.html', styleUrls: ['account-details.component.scss'], }) -export class PersonalDetailsComponent { +export class PersonalDetailsComponent implements OnInit { hide = true; myControl = new UntypedFormControl(); countriesAndTimezones: CountryAndTimezone[] = []; diff --git a/src/app/account-details/storage-data.component.ts b/src/app/account-details/storage-data.component.ts index 5dfb34dfb..abcad5a5c 100644 --- a/src/app/account-details/storage-data.component.ts +++ b/src/app/account-details/storage-data.component.ts @@ -17,7 +17,7 @@ // along with Runbox 7. If not, see . // ---------- END RUNBOX LICENSE ---------- -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { AsyncSubject } from 'rxjs'; import { RMM } from '../rmm'; @@ -34,7 +34,7 @@ export interface DataUsage { templateUrl: './storage-data.component.html', styleUrls: ['account-details.component.scss'], }) -export class StorageDataComponent { +export class StorageDataComponent implements OnInit { dataUsage = new AsyncSubject(); displayedColumns: string[] = ['type', 'quota', 'usage', 'percentage_used']; diff --git a/src/app/account-security/sessions.component.ts b/src/app/account-security/sessions.component.ts index ccc26f8c0..a95d29f94 100644 --- a/src/app/account-security/sessions.component.ts +++ b/src/app/account-security/sessions.component.ts @@ -16,7 +16,7 @@ // You should have received a copy of the GNU General Public License // along with Runbox 7. If not, see . // ---------- END RUNBOX LICENSE ---------- -import { Component, Output, EventEmitter, ViewChild } from '@angular/core'; +import { Component, Output, EventEmitter, ViewChild, OnInit } from '@angular/core'; import { MatLegacyPaginator as MatPaginator } from '@angular/material/legacy-paginator'; import { MatLegacySnackBar as MatSnackBar } from '@angular/material/legacy-snack-bar'; import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; @@ -28,7 +28,7 @@ import { RMM } from '../rmm'; styleUrls: ['account.security.component.scss'], templateUrl: 'sessions.component.html', }) -export class SessionsComponent { +export class SessionsComponent implements OnInit { panelOpenState = false; @ViewChild(MatPaginator, { static: false }) paginator: MatPaginator; @Output() Close: EventEmitter = new EventEmitter(); diff --git a/src/app/aliases/aliases.lister.spec.ts b/src/app/aliases/aliases.lister.spec.ts index 6effe5bf4..8a3ffb893 100644 --- a/src/app/aliases/aliases.lister.spec.ts +++ b/src/app/aliases/aliases.lister.spec.ts @@ -70,7 +70,7 @@ describe('AliasesListerComponent', () => { return of({ status: 'error', result: 'unimplemented' - }) + }); } } } }, @@ -94,11 +94,11 @@ describe('AliasesListerComponent', () => { }); fixture = TestBed.createComponent(AliasesListerComponent); component = fixture.componentInstance; - }) + }); it('loads aliases through RMM', () => { expect(component.aliases).toEqual(ALIASES); - }) + }); it('sets the default email to current user email', () => { expect(component.defaultEmail).toBe(DEFAULT_EMAIL); diff --git a/src/app/aliases/aliases.lister.ts b/src/app/aliases/aliases.lister.ts index a8781079d..847a1fe0e 100644 --- a/src/app/aliases/aliases.lister.ts +++ b/src/app/aliases/aliases.lister.ts @@ -58,7 +58,7 @@ export class AliasesListerComponent { if (result !== undefined) { this.aliases.push(result); } - }) + }); } edit (item: object) { diff --git a/src/app/common/util.spec.ts b/src/app/common/util.spec.ts index b50d532ea..3fa34121f 100644 --- a/src/app/common/util.spec.ts +++ b/src/app/common/util.spec.ts @@ -104,53 +104,53 @@ describe('withKeys', () => { }); it('can fetch nested items', () => { - expect(withKeys(o, ['c'])).toEqual({c: [3, 4]}) + expect(withKeys(o, ['c'])).toEqual({c: [3, 4]}); }); it('returns an empty object if all keys dont exist', () => { - expect(withKeys(o, ['x', 'y', 'z'])).toEqual({}) + expect(withKeys(o, ['x', 'y', 'z'])).toEqual({}); }); it('returns an empty object if no keys are provided', () => { expect(withKeys(o, [])).toEqual({}); }); -}) +}); describe('objectEqualWithKeys', () => { const o = {a: 1, b: 2, c: [3, 4], d: {e: 5}}; // since they are both empty objects it('matches when no keys are specified', () => { - expect(objectEqualWithKeys(o, o, [])).toBeTrue() - expect(objectEqualWithKeys(o, {}, [])).toBeTrue() - expect(objectEqualWithKeys({}, {}, [])).toBeTrue() + expect(objectEqualWithKeys(o, o, [])).toBeTrue(); + expect(objectEqualWithKeys(o, {}, [])).toBeTrue(); + expect(objectEqualWithKeys({}, {}, [])).toBeTrue(); }); it('matches when provided keys that exist', () => { - expect(objectEqualWithKeys(o, o, ['a', 'b'])).toBeTrue() - expect(objectEqualWithKeys(o, o, ['a', 'b', 'c', 'd'])).toBeTrue() + expect(objectEqualWithKeys(o, o, ['a', 'b'])).toBeTrue(); + expect(objectEqualWithKeys(o, o, ['a', 'b', 'c', 'd'])).toBeTrue(); }); // again, they are both empty objects it('matches when provided keys that dont exist', () => { - expect(objectEqualWithKeys(o, o, ['f', 'g', 'h'])).toBeTrue() + expect(objectEqualWithKeys(o, o, ['f', 'g', 'h'])).toBeTrue(); }); it('matches only by keys, ignoring other values', () => { - expect(objectEqualWithKeys(o, {a: 1, b: 2, c: 32}, ['a', 'b'])).toBeTrue() + expect(objectEqualWithKeys(o, {a: 1, b: 2, c: 32}, ['a', 'b'])).toBeTrue(); }); it('matches nested objects and arrays', () => { - expect(objectEqualWithKeys(o, {a: 25, c: [3, 4], d: {e: 5}}, ['c', 'd'])).toBeTrue() + expect(objectEqualWithKeys(o, {a: 25, c: [3, 4], d: {e: 5}}, ['c', 'd'])).toBeTrue(); }); it('fails to match if values are different', () => { - expect(objectEqualWithKeys(o, {a: 222, b: 123}, ['a', 'b'])).toBeFalse() - expect(objectEqualWithKeys(o, {c: [4, 5]}, ['c'])).toBeFalse() - expect(objectEqualWithKeys(o, {d: {e: 6}}, ['d'])).toBeFalse() + expect(objectEqualWithKeys(o, {a: 222, b: 123}, ['a', 'b'])).toBeFalse(); + expect(objectEqualWithKeys(o, {c: [4, 5]}, ['c'])).toBeFalse(); + expect(objectEqualWithKeys(o, {d: {e: 6}}, ['d'])).toBeFalse(); }); it('fails to match if comparison object is empty', () => { - expect(objectEqualWithKeys(o, {}, ['a', 'b'])).toBeFalse() + expect(objectEqualWithKeys(o, {}, ['a', 'b'])).toBeFalse(); }); }); \ No newline at end of file diff --git a/src/app/common/util.ts b/src/app/common/util.ts index fd94fe97f..3c8f045ff 100644 --- a/src/app/common/util.ts +++ b/src/app/common/util.ts @@ -73,7 +73,7 @@ export function withKeys(o: object, keys: any[]): object { return keys.reduce((acc, k) => { if (k in o) { acc[k] = o[k]; } return acc; - }, {}) + }, {}); } /** diff --git a/src/app/compose/compose.component.ts b/src/app/compose/compose.component.ts index 7d52848c8..e1aa9ecf2 100644 --- a/src/app/compose/compose.component.ts +++ b/src/app/compose/compose.component.ts @@ -46,7 +46,7 @@ declare const MailParser; const LOCAL_STORAGE_SHOW_POPULAR_RECIPIENTS = 'showPopularRecipients'; const LOCAL_STORAGE_DEFAULT_HTML_COMPOSE = 'composeInHTMLByDefault'; -const DOWNLOAD_DRAFT_URL = '/ajax/download_draft_attachment?filename=' +const DOWNLOAD_DRAFT_URL = '/ajax/download_draft_attachment?filename='; @Component({ // eslint-disable-next-line @angular-eslint/component-selector @@ -216,7 +216,7 @@ export class ComposeComponent implements AfterViewInit, OnDestroy, OnInit { // have autosave until it was saved as a draft. if (this.model.tid) return; - this.submit(false) + this.submit(false); }); this.formGroup.controls.from.valueChanges @@ -471,8 +471,8 @@ export class ComposeComponent implements AfterViewInit, OnDestroy, OnInit { public loadDraft(msgObj) { if (msgObj.errors) { - this.snackBar.open(msgObj.errors[0], 'Ok') - throw msgObj + this.snackBar.open(msgObj.errors[0], 'Ok'); + throw msgObj; } const model = new DraftFormModel(); @@ -869,7 +869,7 @@ export class ComposeComponent implements AfterViewInit, OnDestroy, OnInit { } public saveTemplate() { - this.isTemplate = true + this.isTemplate = true; this.submit(false); } diff --git a/src/app/compose/draftdesk.service.ts b/src/app/compose/draftdesk.service.ts index 69b575ff7..82e6f452b 100644 --- a/src/app/compose/draftdesk.service.ts +++ b/src/app/compose/draftdesk.service.ts @@ -264,7 +264,7 @@ export class DraftDeskService { && prev.every((f, index) => objectEqualWithKeys(f, curr[index], [ 'folderId', 'totalMessages', 'newMessages' - ])) + ])); })) .subscribe((folders) => { this.refreshDrafts(); @@ -317,8 +317,8 @@ export class DraftDeskService { this.rmmapi.getMessageContents(messageId).subscribe((contents) => { const res: any = Object.assign({}, contents); - const {subject} = res.headers - let { to } = res.headers + const {subject} = res.headers; + let { to } = res.headers; if (to) { to = new MailAddressInfo(to.value.name, to.value.address).nameAndAddress; @@ -329,14 +329,14 @@ export class DraftDeskService { this.mainIdentity(), to, subject - ) + ); draftFormModel.tid = messageId; draftFormModel.msg_body = contents.text.text; draftFormModel.html = contents.text.html; return this.newDraft(draftFormModel); - }) + }); } public async newBugReport( diff --git a/src/app/compose/recipients.service.ts b/src/app/compose/recipients.service.ts index 436e34aad..f0886a1cf 100644 --- a/src/app/compose/recipients.service.ts +++ b/src/app/compose/recipients.service.ts @@ -120,8 +120,8 @@ export class RecipientsService { ).then((updateGroups) => { this.updateRecipients(updateGroups); }).catch((error) => { - console.error(error) - return this.recipients.next([]) + console.error(error); + return this.recipients.next([]); }); }); @@ -198,8 +198,8 @@ export class RecipientsService { } } ).catch((error) => { - console.error(error) - return null + console.error(error); + return null; }) ); } else if (m.email) { diff --git a/src/app/contacts-app/contact-list.component.ts b/src/app/contacts-app/contact-list.component.ts index 53e02aed7..393988461 100644 --- a/src/app/contacts-app/contact-list.component.ts +++ b/src/app/contacts-app/contact-list.component.ts @@ -17,7 +17,7 @@ // along with Runbox 7. If not, see . // ---------- END RUNBOX LICENSE ---------- -import { Component, Input, EventEmitter, Output } from '@angular/core'; +import { Component, Input, EventEmitter, Output, OnChanges } from '@angular/core'; import { MatSidenav } from '@angular/material/sidenav'; import { Contact } from './contact'; @@ -28,7 +28,7 @@ type SelectionEvent = Set; styleUrls: ['contacts-app.component.scss'], templateUrl: './contact-list.component.html' }) -export class ContactListComponent { +export class ContactListComponent implements OnChanges { @Input() contacts: Contact[]; @Input() categories: string[] = []; diff --git a/src/app/contacts-app/contacts-app.component.ts b/src/app/contacts-app/contacts-app.component.ts index d55a4054b..7e7ba2d10 100644 --- a/src/app/contacts-app/contacts-app.component.ts +++ b/src/app/contacts-app/contacts-app.component.ts @@ -258,7 +258,7 @@ export class ContactsAppComponent { try { contacts = Contact.fromVcf(vcf); } catch (e) { - console.error(e) + console.error(e); if (warning) { // we predicted this: this.showError('Only .vcf contacts files are supported, this does not look like one'); diff --git a/src/app/folder/folderlist.component.ts b/src/app/folder/folderlist.component.ts index 803a47aa9..c6a96fc9e 100644 --- a/src/app/folder/folderlist.component.ts +++ b/src/app/folder/folderlist.component.ts @@ -117,7 +117,7 @@ export class FolderListComponent implements OnChanges { } } catch (e) { /* we don't care why it failed, it just means that we'll show all folders as collapsed */ - console.error(e) + console.error(e); } this.treeControl = new FlatTreeControl(this._getLevel, this._isExpandable); @@ -255,8 +255,8 @@ export class FolderListComponent implements OnChanges { } onFolderClick($event, folder) { - $event.preventDefault() - this.selectFolder(folder) + $event.preventDefault(); + this.selectFolder(folder); } selectFolder(folder: string): void { diff --git a/src/app/login/login.component.ts b/src/app/login/login.component.ts index d73e32a74..60e5c0e46 100644 --- a/src/app/login/login.component.ts +++ b/src/app/login/login.component.ts @@ -93,7 +93,7 @@ export class LoginComponent implements OnInit { } public onSubmit(ngForm: NgForm) { - const { value } = ngForm + const { value } = ngForm; this.login_errors_reset(); diff --git a/src/app/mailviewer/avatar-bar.component.ts b/src/app/mailviewer/avatar-bar.component.ts index fa700187b..76b23948a 100644 --- a/src/app/mailviewer/avatar-bar.component.ts +++ b/src/app/mailviewer/avatar-bar.component.ts @@ -17,7 +17,7 @@ // along with Runbox 7. If not, see . // ---------- END RUNBOX LICENSE ---------- -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input, OnInit, OnChanges } from '@angular/core'; import { firstValueFrom, ReplaySubject} from 'rxjs'; import { take } from 'rxjs/operators'; import { ProfileService } from '../profiles/profile.service'; @@ -53,7 +53,7 @@ import { PreferencesService } from '../common/preferences.service';

`, }) -export class AvatarBarComponent implements OnInit { +export class AvatarBarComponent implements OnInit, OnChanges { @Input() email: { from: string[], to: string[], diff --git a/src/app/menu/headertoolbar.component.ts b/src/app/menu/headertoolbar.component.ts index 2a6964181..ae584a1f3 100644 --- a/src/app/menu/headertoolbar.component.ts +++ b/src/app/menu/headertoolbar.component.ts @@ -17,7 +17,7 @@ // along with Runbox 7. If not, see . // ---------- END RUNBOX LICENSE ---------- -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { RunboxWebmailAPI } from '../rmmapi/rbwebmail'; import { RMMOfflineService } from '../rmmapi/rmmoffline.service'; import { Router } from '@angular/router'; @@ -29,7 +29,7 @@ import { RunboxMe } from '../rmmapi/rbwebmail'; selector: 'rmm-headertoolbar', templateUrl: 'headertoolbar.component.html' }) -export class HeaderToolbarComponent { +export class HeaderToolbarComponent implements OnInit { rmm6tooltip = 'This area isn\'t upgraded to Runbox 7 yet and will open in a new tab'; user_is_trial = false; diff --git a/src/app/profiles/profile.service.spec.ts b/src/app/profiles/profile.service.spec.ts index de80daf6c..5be6dcaad 100644 --- a/src/app/profiles/profile.service.spec.ts +++ b/src/app/profiles/profile.service.spec.ts @@ -157,36 +157,36 @@ describe('ProfileService', () => { }).compileComponents(); })); - beforeEach(() => { service = service = TestBed.inject(ProfileService) }); + beforeEach(() => { service = service = TestBed.inject(ProfileService); }); it('loads valid profile subsets', (done) => { service.validProfiles.subscribe(profiles => { expect(profiles.length).toBe(4); done(); }); - }) + }); it('loads all profiles', (done) => { service.profiles.subscribe(profiles => { expect(profiles.length).toBe(PROFILES.length); done(); }); - }) + }); it('loads alias profile subsets', (done) => { service.aliases.subscribe(profiles => { expect(profiles.length).toBe(PROFILES.filter(p => p.reference_type === 'aliases').length); done(); }); - }) + }); it('loads non alias profile subsets', (done) => { service.otherProfiles.subscribe(profiles => { expect(profiles.length).toBe(2); done(); }); - }) + }); it('loads a compose profile', () => { expect(service.composeProfile).toBeDefined(); expect(service.composeProfile.email).toEqual('a2@example.com'); - }) + }); it('adds a new profile on create', (done) => { service.create({ name: 'New Profile Name', diff --git a/src/app/rmm/account-security.ts b/src/app/rmm/account-security.ts index 748072da8..33a505202 100644 --- a/src/app/rmm/account-security.ts +++ b/src/app/rmm/account-security.ts @@ -80,7 +80,7 @@ export class AccountSecurity { + ' \\____\\/ \\_____/\\ \n' + ' \\____\\/ \n'; return txt; - } + }; txt_footer: any = () => { const dt = new Date(); @@ -93,7 +93,7 @@ export class AccountSecurity { ' \n' + ' https://runbox.com Generated by Runbox at ' + dt_str + ' \n'; return txt; - } + }; check_password(password): Observable { this.is_busy = true; diff --git a/src/app/rmm6/rmm6.module.ts b/src/app/rmm6/rmm6.module.ts index ccbb4ffb6..52abc3aa8 100644 --- a/src/app/rmm6/rmm6.module.ts +++ b/src/app/rmm6/rmm6.module.ts @@ -17,7 +17,7 @@ // along with Runbox 7. If not, see . // ---------- END RUNBOX LICENSE ---------- -import { NgModule, ApplicationRef, ComponentFactoryResolver, Injector, NgZone } from '@angular/core'; +import { NgModule, ApplicationRef, ComponentFactoryResolver, Injector, NgZone, DoBootstrap } from '@angular/core'; import { RMM6AngularGateway } from './rmm6angulargateway'; import { MailViewerModule } from '../mailviewer/mailviewer.module'; import { DomainRegisterModule } from '../domainregister/domainregister.module'; @@ -78,7 +78,7 @@ import { SearchExpressionBuilderModule } from '../xapian/search-expression-build ], providers: [ProgressService] }) -export class RMM6Module { +export class RMM6Module implements DoBootstrap { rmmAngularGW: RMM6AngularGateway; diff --git a/src/app/rmmapi/rblocale.ts b/src/app/rmmapi/rblocale.ts index cdbcdb0c5..738eb3c0f 100644 --- a/src/app/rmmapi/rblocale.ts +++ b/src/app/rmmapi/rblocale.ts @@ -45,7 +45,7 @@ export class RunboxLocale { try { translated = window['getLocale'](key.split('.')); } catch (ex) { - console.error(ex) + console.error(ex); console.log('locale translations not found'); } return translated; diff --git a/src/app/sentry.ts b/src/app/sentry.ts index d0980ca07..93677fad9 100644 --- a/src/app/sentry.ts +++ b/src/app/sentry.ts @@ -17,7 +17,7 @@ // along with Runbox 7. If not, see . // ---------- END RUNBOX LICENSE ---------- -import { environment } from '../environments/environment' +import { environment } from '../environments/environment'; import * as Sentry from '@sentry/browser'; Sentry.init({ diff --git a/src/app/welcome/welcomedesk.component.ts b/src/app/welcome/welcomedesk.component.ts index 0a1ab40a4..1b1580248 100644 --- a/src/app/welcome/welcomedesk.component.ts +++ b/src/app/welcome/welcomedesk.component.ts @@ -37,11 +37,11 @@ export class WelcomeDeskComponent implements OnInit { this.rmmapi.me.subscribe(me => this.me = me); } - public postSignup = '' + public postSignup = ''; ngOnInit() { this.activatedRoute.queryParams.subscribe(params => { this.postSignup = params['postSignup']; - }) + }); } } diff --git a/src/app/xapian/multiple-search-fields-input/multiple-search-fields-input.component.ts b/src/app/xapian/multiple-search-fields-input/multiple-search-fields-input.component.ts index 029e45252..d5854fcaf 100644 --- a/src/app/xapian/multiple-search-fields-input/multiple-search-fields-input.component.ts +++ b/src/app/xapian/multiple-search-fields-input/multiple-search-fields-input.component.ts @@ -54,7 +54,7 @@ export class MultipleSearchFieldsInputComponent implements OnChanges { .pipe(distinctUntilChanged()) .subscribe(() => { this.enableDisableUnread(); - this.buildSearchExpression() + this.buildSearchExpression(); }); } diff --git a/src/app/xapian/searchservice.ts b/src/app/xapian/searchservice.ts index efe5c9420..b83c992c4 100644 --- a/src/app/xapian/searchservice.ts +++ b/src/app/xapian/searchservice.ts @@ -223,7 +223,7 @@ export class SearchService { db.close(); }; } catch (e) { - console.error(e) + console.error(e); console.log('Unable to open local xapian index', (e ? e.message : '')); db.close(); observer.next(false); @@ -366,7 +366,7 @@ export class SearchService { this.indexLastUpdateTime = new Date().getTime(); } } catch (e) { - console.error(e) + console.error(e); if (!this.updateIndexLastUpdateTime()) { // Corrupt xapian index - delete it and subscribe to changes (fallback to websocket search) // Deal with this on the non-worker side, then tell it to @@ -386,7 +386,7 @@ export class SearchService { } catch (e) { - console.error(e) + console.error(e); console.log('No xapian db'); this.initSubject.next(false); } @@ -411,7 +411,7 @@ export class SearchService { } }); } catch (e) { - console.error(e) + console.error(e); } } @@ -560,7 +560,7 @@ export class SearchService { try { FS.stat(XAPIAN_GLASS_WR); } catch (e) { - console.error(e) + console.error(e); FS.mkdir(XAPIAN_GLASS_WR); } @@ -787,7 +787,7 @@ export class SearchService { try { FS.stat(`${this.partitionsdir}/${dirname}`); } catch (e) { - console.error(e) + console.error(e); FS.mkdir(`${this.partitionsdir}/${dirname}`); } diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index cf62cf7e2..5bec9c6ac 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -17,9 +17,9 @@ // along with Runbox 7. If not, see . // ---------- END RUNBOX LICENSE ---------- -import env from './env' +import env from './env'; export const environment = { ...env, production: true -} +}; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 3d2dfca83..1e1e66b03 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -17,7 +17,7 @@ // along with Runbox 7. If not, see . // ---------- END RUNBOX LICENSE ---------- -import env from './env' +import env from './env'; export const environment = { ...env, From 3ea607c590130a311f0ee22c107856652b047500 Mon Sep 17 00:00:00 2001 From: Finn Date: Wed, 28 Jan 2026 02:56:04 +0000 Subject: [PATCH 04/12] fix(stability): gate service worker registration --- src/app/app.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/app.module.ts b/src/app/app.module.ts index bced66edb..72427892d 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -202,7 +202,7 @@ const routes: Routes = [ { provide: HTTP_INTERCEPTORS, useClass: RMMHttpInterceptorService, multi: true }, { provide: ErrorHandler, useClass: SentryErrorHandler }, { provide: SwRegistrationOptions, - useFactory: () => ({ registrationStrategy: 'registerWithDelay:5000' }) } + useFactory: () => ({ registrationStrategy: 'registerWhenStable:30000' }) } ], bootstrap: [MainContainerComponent] }) From 796fecff05be73e547a4d217d2a4a776a8389f6f Mon Sep 17 00:00:00 2001 From: Finn Date: Wed, 28 Jan 2026 02:57:16 +0000 Subject: [PATCH 05/12] refactor(cleanup): remove unused code and constants --- src/app/http/progress.service.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/app/http/progress.service.ts b/src/app/http/progress.service.ts index 647df750b..4c5882bf6 100644 --- a/src/app/http/progress.service.ts +++ b/src/app/http/progress.service.ts @@ -1,26 +1,30 @@ // --------- BEGIN RUNBOX LICENSE --------- // Copyright (C) 2016-2018 Runbox Solutions AS (runbox.com). -// +// // This file is part of Runbox 7. -// +// // Runbox 7 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, either version 3 of the License, or (at your // option) any later version. -// +// // Runbox 7 is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. -// +// // You should have received a copy of the GNU General Public License // along with Runbox 7. If not, see . // ---------- END RUNBOX LICENSE ---------- import {Injectable} from '@angular/core'; -import {Subject, BehaviorSubject} from 'rxjs'; +import {Subject, BehaviorSubject, Observable} from 'rxjs'; +import {delay} from 'rxjs/operators'; @Injectable() export class ProgressService { httpRequestInProgress: Subject = new BehaviorSubject(false); + + // Delayed version to avoid ExpressionChangedAfterItHasBeenCheckedError in templates + httpRequestInProgress$: Observable = this.httpRequestInProgress.pipe(delay(0)); } From 47e1c3bd479353fd7d88030aee873350c8e8b4cd Mon Sep 17 00:00:00 2001 From: Finn Date: Wed, 28 Jan 2026 02:58:33 +0000 Subject: [PATCH 06/12] style(cleanup): remove obsolete CSS styles --- src/app/changelog/changelog.component.scss | 3 +- src/styles.scss | 104 +++++++++------------ 2 files changed, 45 insertions(+), 62 deletions(-) diff --git a/src/app/changelog/changelog.component.scss b/src/app/changelog/changelog.component.scss index 33643b198..ac11df38c 100644 --- a/src/app/changelog/changelog.component.scss +++ b/src/app/changelog/changelog.component.scss @@ -6,7 +6,8 @@ h4 { margin: 0.75em 0; } -.mat-list-base[dense] .mat-list-item, .mat-list-base[dense] .mat-list-option { +.mat-list-base[dense] .mat-list-item, +.mat-list-base[dense] .mat-list-option { height: 30px; } diff --git a/src/styles.scss b/src/styles.scss index fb10e6bf9..7ff559a45 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -32,7 +32,7 @@ $rmm-darker-background: #01001c; $rmm-gray: #dddddd; $rmm-gray-light: #eeeeee; $rmm-gray-lighter: #f3f3f3; - + $rmm-default-theme: mat.define-light-theme($rmm-default-primary, $rmm-default-accent, $rmm-default-warn); $rmm-default-lighter-gray: #eeeeee; @@ -42,24 +42,6 @@ $rmm-default-black: #444444; @include mat.all-legacy-component-themes($rmm-default-theme); -// GTA 13.06.2018: Load custom fonts - -@font-face { - font-family: "Avenir Next Pro Regular"; - src: url("assets/AvenirNextLTPro-Regular.otf"); - src: url("assets/Avenir-Next-LT-Pro.ttf"); - font-style: normal; - font-weight: normal; -} - -@font-face { - font-family: "Avenir Next Pro Medium"; - src: url("assets/AvenirNextLTPro-Medium.otf"); - src: url("assets/AvenirNextLTPro-Medium.ttf"); - font-style: normal; - font-weight: normal; -} - // GTA 13.06.2018: Override default fonts as per https://material.angular.io/guide/typography $custom-typography: mat.define-legacy-typography-config( @@ -159,7 +141,7 @@ a[mat-list-item] .mat-list-item-content { min-height: 24px !important; } -.mat-list[dense] .mat-list-item .mat-list-text, +.mat-list[dense] .mat-list-item .mat-list-text, .mat-nav-list[dense] .mat-list-item .mat-list-text>*, mat-list-item .mat-list-text, a[mat-list-item] .mat-list-text { @@ -313,21 +295,21 @@ mat-grid-tile.tableTitle { height: 16px; width: 42px; } - + .mat-slide-toggle.mat-checked .mat-slide-toggle-thumb-container { top: -5px; transform: translate3d(20px, 0, 0); } - + .mat-slide-toggle.mat-checked .mat-slide-toggle-thumb { height: 24px; width: 24px; } - + .mat-slide-toggle-label { font-size: 16px; } - + .mat-slide-toggle-content { margin-left: 2px; } @@ -394,12 +376,12 @@ mat-grid-tile.tableTitle { /*** Main ***/ #main { - position: fixed; - top: 0px; - bottom: 0px; - left: 0px; - right: 0px; - width: 100%; + position: fixed; + top: 0px; + bottom: 0px; + left: 0px; + right: 0px; + width: 100%; height: 100%; min-height: 100%; display: flex; @@ -439,7 +421,7 @@ mat-grid-tile.tableTitle { display: none; margin: 0; } - + #logo { margin: 0; width: 300px; @@ -518,7 +500,7 @@ div.loginScreen { mat-form-field { width: 200px; } - } + } #loginOptions { display: flex; margin: 0.5em; @@ -724,8 +706,8 @@ rmm-headertoolbar { /* Sidenav pane */ mat-sidenav-container { - position: absolute !important; - bottom: 0px !important; + position: absolute !important; + bottom: 0px !important; left: 0px !important; right: 0px !important; width: 100% !important; @@ -782,7 +764,7 @@ mat-sidenav-container { .mat-mini-fab .mat-button-wrapper { line-height: 18px; } - + a { width: 30%; } @@ -936,7 +918,7 @@ rmm-folderlist { } .foldersidebarcount { - font-size: 10px; + font-size: 10px; } .draftsFolder { @@ -982,7 +964,7 @@ app-saved-searches .mat-list-base[dense] .mat-list-item { flex-grow: 1; overflow: hidden; } - + .messageListActionButtonsRight button { width: 30px; // Remember to also update TOOLBAR_LIST_BUTTON_WIDTH in app.component.ts in order to show the correct number of menu items } @@ -1008,7 +990,7 @@ app-saved-searches .mat-list-base[dense] .mat-list-item { button { margin-right: 10px; - + @media(max-width: 540px) { margin-right: 2px; height: 30px; @@ -1022,7 +1004,7 @@ app-saved-searches .mat-list-base[dense] .mat-list-item { #offerLocalIndex .mat-list-item-content { padding: 0 5px; -} +} #searchField { flex-grow: 10; @@ -1097,7 +1079,7 @@ app-saved-searches .mat-list-base[dense] .mat-list-item { .mat-icon, .mat-icon-button { color: mat.get-color-from-palette($rmm-default-primary); } - + @media (max-width: 540px) { #threadedCheckbox { display: none; @@ -1172,7 +1154,7 @@ app-saved-searches .mat-list-base[dense] .mat-list-item { .mat-radio-label-content { padding: 0 !important; } - + button, .mat-radio-button, .mat-checkbox { margin-left: 5px; } @@ -1195,7 +1177,7 @@ app-saved-searches .mat-list-base[dense] .mat-list-item { .mat-icon { margin: 0 3px !important; } - + mat-flat-button, .mat-flat-button, mat-raised-button, .mat-raised-button { min-width: 30px !important; width: 30px !important; @@ -1307,7 +1289,7 @@ compose #fieldFrom .mat-form-field-wrapper, compose .fieldRecipient .mat-form-fi .recipientSuggestionContainer { max-height: 90px; overflow: auto; - + span { font-size: 12.5px; } @@ -1347,7 +1329,7 @@ compose #fieldFrom .mat-form-field-wrapper, compose .fieldRecipient .mat-form-fi .mat-nav-list[dense], .mat-list-item, .mat-list-text, .mat-form-field { height: 48px !important; } -} +} .contactList .mat-form-field-infix { font-size: 16px; @@ -1370,7 +1352,7 @@ compose #fieldFrom .mat-form-field-wrapper, compose .fieldRecipient .mat-form-fi .mat-form-field-appearance-legacy .mat-form-field-infix { padding-top: 0 !important; } - + .mat-form-field-appearance-legacy .mat-form-field-label { top: 0.75em; } @@ -1519,7 +1501,7 @@ app-calendar-event-editor-dialog p { .productGrid mat-card.recommended { border: 1px solid mat.get-color-from-palette($rmm-default-primary); -} +} #pricePlans td { /* border-right: 1px solid $rmm-dark-background !important; */ @@ -1573,7 +1555,7 @@ app-payment-method { cursor: pointer; } } - + #otherPaymentMethods { margin: 20px 0 50px 0; max-width: 90%; @@ -1594,7 +1576,7 @@ app-payment-method { color: #0F0; } -.dev.runbox-components .nice_green_timer .timeunit { +.dev.runbox-components .nice_green_timer .timeunit { border: 1px solid #0F0; width: 40px; height: 40px; @@ -1609,22 +1591,22 @@ app-payment-method { .dev.runbox-components .nice_green_timer .timeunit.hours { color: #0f0; } -.dev.runbox-components .nice_green_timer .timeunit.years::after { +.dev.runbox-components .nice_green_timer .timeunit.years::after { content: "y"; } -.dev.runbox-components .nice_green_timer .timeunit.months::after { +.dev.runbox-components .nice_green_timer .timeunit.months::after { content: "m"; } -.dev.runbox-components .nice_green_timer .timeunit.days::after { +.dev.runbox-components .nice_green_timer .timeunit.days::after { content: "d"; } -.dev.runbox-components .nice_green_timer .timeunit.hours::after { +.dev.runbox-components .nice_green_timer .timeunit.hours::after { content: "h"; } -.dev.runbox-components .nice_green_timer .timeunit.minutes::after { +.dev.runbox-components .nice_green_timer .timeunit.minutes::after { content: "m"; } -.dev.runbox-components .nice_green_timer .timeunit.seconds::after { +.dev.runbox-components .nice_green_timer .timeunit.seconds::after { content: "s"; } @@ -1640,22 +1622,22 @@ app-payment-method { align-items: center; } -.dev.runbox-components .nice_blue_timer .timeunit.years::after { +.dev.runbox-components .nice_blue_timer .timeunit.years::after { content: " years"; } -.dev.runbox-components .nice_blue_timer .timeunit.months::after { +.dev.runbox-components .nice_blue_timer .timeunit.months::after { content: " months"; } -.dev.runbox-components .nice_blue_timer .timeunit.days::after { +.dev.runbox-components .nice_blue_timer .timeunit.days::after { content: " days"; } -.dev.runbox-components .nice_blue_timer .timeunit.hours::after { +.dev.runbox-components .nice_blue_timer .timeunit.hours::after { content: " hours"; } -.dev.runbox-components .nice_blue_timer .timeunit.minutes::after { +.dev.runbox-components .nice_blue_timer .timeunit.minutes::after { content: " mins"; } -.dev.runbox-components .nice_blue_timer .timeunit.seconds::after { +.dev.runbox-components .nice_blue_timer .timeunit.seconds::after { content: " secs"; } @@ -1743,7 +1725,7 @@ td.mat-cell.cdk-column-renewal_name.mat-column-renewal_name { table.renewalsTable td, table.paymentsTable td { padding: 5px 10px 0px 10px !important; -} +} table.detailsTable { width: 100%; From a9730ff086b89c835cc0aa88df6cbdc42366eb24 Mon Sep 17 00:00:00 2001 From: Finn Date: Wed, 28 Jan 2026 21:24:31 +0000 Subject: [PATCH 07/12] build(deps): update package lock json --- package-lock.json | 7 ------- 1 file changed, 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 673ae650c..73cf4479b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,7 +44,6 @@ "otpauth": "^9.1.1", "rrule": "^2.7.2", "rxjs": "^7.8.0", - "rxjs-compat": "^6.6.7", "tinymce": "^6.8.3", "ts-md5": "^1.3.1", "zone.js": "^0.13.3" @@ -20500,12 +20499,6 @@ "tslib": "^2.1.0" } }, - "node_modules/rxjs-compat": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs-compat/-/rxjs-compat-6.6.7.tgz", - "integrity": "sha512-szN4fK+TqBPOFBcBcsR0g2cmTTUF/vaFEOZNuSdfU8/pGFnNmmn2u8SystYXG1QMrjOPBc6XTKHMVfENDf6hHw==", - "license": "Apache-2.0" - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", From 1b5feaae80e6144416fa8057e02fda31465acbc6 Mon Sep 17 00:00:00 2001 From: Finn Date: Wed, 28 Jan 2026 21:56:15 +0000 Subject: [PATCH 08/12] fix(build): gen-env --- src/build/gen-env.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/build/gen-env.js b/src/build/gen-env.js index 903816a2d..8db3a977b 100644 --- a/src/build/gen-env.js +++ b/src/build/gen-env.js @@ -49,6 +49,6 @@ export default ${JSON.stringify( return acc }, {}) -, null, 2)}`); +, null, 2)};`); debug(`Wrote env file to ${target}.`) From 995f055aaadf75f82729421c55d2b58e76f87688 Mon Sep 17 00:00:00 2001 From: Finn Date: Tue, 3 Feb 2026 16:01:57 +0000 Subject: [PATCH 09/12] refactor(cleanup): remove code that turned out to be unnecessary --- src/app/http/progress.service.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/app/http/progress.service.ts b/src/app/http/progress.service.ts index 4c5882bf6..8beb8c662 100644 --- a/src/app/http/progress.service.ts +++ b/src/app/http/progress.service.ts @@ -18,13 +18,9 @@ // ---------- END RUNBOX LICENSE ---------- import {Injectable} from '@angular/core'; -import {Subject, BehaviorSubject, Observable} from 'rxjs'; -import {delay} from 'rxjs/operators'; +import {Subject, BehaviorSubject} from 'rxjs'; @Injectable() export class ProgressService { httpRequestInProgress: Subject = new BehaviorSubject(false); - - // Delayed version to avoid ExpressionChangedAfterItHasBeenCheckedError in templates - httpRequestInProgress$: Observable = this.httpRequestInProgress.pipe(delay(0)); } From aeed9e931c258b9566d5e26750f4a60a28083f2c Mon Sep 17 00:00:00 2001 From: Finn Date: Tue, 3 Feb 2026 16:21:52 +0000 Subject: [PATCH 10/12] refactor(rxjs): remove redundant pipe take 1s on firstValueFrom --- src/app/account-app/account-receipt.component.ts | 2 +- src/app/account-app/cart.service.ts | 6 +++--- src/app/common/preferences.service.ts | 6 +++--- .../contact-details/contact-details.component.ts | 2 +- src/app/folder/folderlist.component.ts | 2 +- src/app/mailviewer/avatar-bar.component.ts | 2 +- src/app/saved-searches/saved-searches.service.ts | 2 +- src/app/start/startdesk.component.ts | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/app/account-app/account-receipt.component.ts b/src/app/account-app/account-receipt.component.ts index 0449436a2..b40b03b5b 100644 --- a/src/app/account-app/account-receipt.component.ts +++ b/src/app/account-app/account-receipt.component.ts @@ -47,7 +47,7 @@ export class AccountReceiptComponent implements OnInit { async ngOnInit() { this.me = await firstValueFrom(this.rmmapi.me); - const params = await firstValueFrom(this.route.params.pipe(take(1))); + const params = await firstValueFrom(this.route.params); const receiptID = params.id; this.receipt = await firstValueFrom(this.rmmapi.getReceipt(receiptID)); diff --git a/src/app/account-app/cart.service.ts b/src/app/account-app/cart.service.ts index 8230390ff..9f7493164 100644 --- a/src/app/account-app/cart.service.ts +++ b/src/app/account-app/cart.service.ts @@ -46,7 +46,7 @@ export class CartService { } async add(p: ProductOrder): Promise { - const items = await firstValueFrom(this.items.pipe(take(1))); + const items = await firstValueFrom(this.items); for (const i of items) { // Cannot order multiples of subscription products @@ -70,7 +70,7 @@ export class CartService { } async contains(pid: number, apid?: number): Promise { - const items = await firstValueFrom(this.items.pipe(take(1))); + const items = await firstValueFrom(this.items); for (const p of items) { if (p.pid === pid && p.apid === apid) { return true; @@ -80,7 +80,7 @@ export class CartService { } async remove(order: ProductOrder): Promise { - const items = await firstValueFrom(this.items.pipe(take(1))); + const items = await firstValueFrom(this.items); // check if it's enough to just reduce the quantity on existing product for (const i of items) { if (i.isSameProduct(order)) { diff --git a/src/app/common/preferences.service.ts b/src/app/common/preferences.service.ts index b1a358a11..6d602823a 100644 --- a/src/app/common/preferences.service.ts +++ b/src/app/common/preferences.service.ts @@ -151,7 +151,7 @@ export class PreferencesService { }; } - const allPrefs = await firstValueFrom(this.preferences.pipe(take(1))); + const allPrefs = await firstValueFrom(this.preferences); Object.keys(prefsdata[DefaultPrefGroups.Global]['entries']).forEach((key) => { allPrefs.set(`${DefaultPrefGroups.Global}:${key}`, prefsdata[DefaultPrefGroups.Global]['entries'][key]); }); @@ -179,7 +179,7 @@ export class PreferencesService { } private async uploadPreferenceData(level: string) { - const prefs = await firstValueFrom(this.preferences.pipe(take(1))); + const prefs = await firstValueFrom(this.preferences); const entriesObj = {}; prefs.forEach((value, key) => { // for (const [key, value] of prefs) { @@ -219,7 +219,7 @@ export class PreferencesService { if (this.loadedOldStyle) { return; } - let prefs = await firstValueFrom(this.preferences.pipe(take(1))); + let prefs = await firstValueFrom(this.preferences); if (!prefs) { // Already set / imported prefs = new Map(); diff --git a/src/app/contacts-app/contact-details/contact-details.component.ts b/src/app/contacts-app/contact-details/contact-details.component.ts index f3621b0b7..e0a849cac 100644 --- a/src/app/contacts-app/contact-details/contact-details.component.ts +++ b/src/app/contacts-app/contact-details/contact-details.component.ts @@ -330,7 +330,7 @@ export class ContactDetailsComponent { } async askForMoreMembers(): Promise { - let contacts = await firstValueFrom(this.contactsservice.contactsSubject.pipe(take(1))); + let contacts = await firstValueFrom(this.contactsservice.contactsSubject); contacts = contacts.filter(c => { if (c.kind !== ContactKind.INVIDIDUAL) { return false; diff --git a/src/app/folder/folderlist.component.ts b/src/app/folder/folderlist.component.ts index c6a96fc9e..831826457 100644 --- a/src/app/folder/folderlist.component.ts +++ b/src/app/folder/folderlist.component.ts @@ -366,7 +366,7 @@ export class FolderListComponent implements OnChanges { return; } - const folders = await firstValueFrom(this.folders.pipe(take(1))); + const folders = await firstValueFrom(this.folders); let sourceIndex = folders.findIndex(fld => fld.folderId === sourceFolderId); let destinationIndex = folders.findIndex(folder => folder.folderId === destinationFolderId); diff --git a/src/app/mailviewer/avatar-bar.component.ts b/src/app/mailviewer/avatar-bar.component.ts index 76b23948a..6c155e72b 100644 --- a/src/app/mailviewer/avatar-bar.component.ts +++ b/src/app/mailviewer/avatar-bar.component.ts @@ -82,7 +82,7 @@ export class AvatarBarComponent implements OnInit, OnChanges { async ngOnChanges() { this.emails = []; // so that we don't display the old, wrong ones while we're loading new ones - const own = await firstValueFrom(this.ownAddresses.pipe(take(1))); + const own = await firstValueFrom(this.ownAddresses); const emails: string[] = [].concat.apply( [], [this.email.from, this.email.to, this.email.cc, this.email.bcc] diff --git a/src/app/saved-searches/saved-searches.service.ts b/src/app/saved-searches/saved-searches.service.ts index 7c58e14d4..0b2419f4e 100644 --- a/src/app/saved-searches/saved-searches.service.ts +++ b/src/app/saved-searches/saved-searches.service.ts @@ -83,7 +83,7 @@ export class SavedSearchesService { private async uploadSeachData() { const data: SavedSearchStorage = { version: this.version, - entries: await firstValueFrom(this.searches.pipe(take(1))), + entries: await firstValueFrom(this.searches), }; this.rmmapi.setSavedSearches(data).subscribe( newData => this.applySyncedData(newData) diff --git a/src/app/start/startdesk.component.ts b/src/app/start/startdesk.component.ts index 53faca1dd..0fe32a1e4 100644 --- a/src/app/start/startdesk.component.ts +++ b/src/app/start/startdesk.component.ts @@ -267,7 +267,7 @@ export class StartDeskComponent implements OnInit { private async extractMailingLists(messages: SearchIndexDocumentData[]): Promise> { const possibleMailingLists = new Map(); - const ownAddresses = await firstValueFrom(this.ownAddresses.pipe(take(1))); + const ownAddresses = await firstValueFrom(this.ownAddresses); for (const message of messages) { if (!message.recipients.find(r => ownAddresses.has(r.toLowerCase()))) { From 08d1476b077db67058c09619f4887f5f1a1dedb0 Mon Sep 17 00:00:00 2001 From: Finn Date: Wed, 4 Feb 2026 13:49:22 +0000 Subject: [PATCH 11/12] refactor(cleanup): remove unused imports --- .../account-app/account-receipt.component.ts | 1 - src/app/account-app/cart.service.ts | 1 - src/app/changelog/changes.ts | 628 +++++++++++++++--- .../contact-details.component.ts | 2 +- src/app/mailviewer/avatar-bar.component.ts | 1 - src/app/start/startdesk.component.ts | 2 +- 6 files changed, 543 insertions(+), 92 deletions(-) diff --git a/src/app/account-app/account-receipt.component.ts b/src/app/account-app/account-receipt.component.ts index b40b03b5b..1f63f721d 100644 --- a/src/app/account-app/account-receipt.component.ts +++ b/src/app/account-app/account-receipt.component.ts @@ -21,7 +21,6 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { RunboxMe, RunboxWebmailAPI } from '../rmmapi/rbwebmail'; import { AsyncSubject, firstValueFrom } from 'rxjs'; -import { take } from 'rxjs/operators'; @Component({ selector: 'app-account-receipt-component', diff --git a/src/app/account-app/cart.service.ts b/src/app/account-app/cart.service.ts index 9f7493164..5c9a9c376 100644 --- a/src/app/account-app/cart.service.ts +++ b/src/app/account-app/cart.service.ts @@ -19,7 +19,6 @@ import { Injectable } from '@angular/core'; import { firstValueFrom, ReplaySubject } from 'rxjs'; -import { take } from 'rxjs/operators'; import { ProductOrder } from './product-order'; import { StorageService } from '../storage.service'; diff --git a/src/app/changelog/changes.ts b/src/app/changelog/changes.ts index 125deeb28..d19af7eaa 100644 --- a/src/app/changelog/changes.ts +++ b/src/app/changelog/changes.ts @@ -1,81 +1,535 @@ -// --------- BEGIN RUNBOX LICENSE --------- -// Copyright (C) 2016-2019 Runbox Solutions AS (runbox.com). -// -// This file is part of Runbox 7. -// -// Runbox 7 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, either version 3 of the License, or (at your -// option) any later version. -// -// Runbox 7 is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Runbox 7. If not, see . -// ---------- END RUNBOX LICENSE ---------- - -import moment from 'moment'; - -export enum EntryType { - BUILD, - CI, - DOCS, - FEAT, - FIX, - PERF, - REFACTOR, - STYLE, - TEST -} - -const typeMapping = { - 'build': EntryType.BUILD, - 'ci': EntryType.CI, - 'docs': EntryType.DOCS, - 'feat': EntryType.FEAT, - 'feature': EntryType.FEAT, - 'fix': EntryType.FIX, - 'perf': EntryType.PERF, - 'refactor': EntryType.REFACTOR, - 'style': EntryType.STYLE, - 'test': EntryType.TEST, - // known typos - 'refator': EntryType.REFACTOR, -}; - -export class ChangelogEntry { - public datetime: moment.Moment; - - constructor( - public hash: string, - public epoch: number, - public type: EntryType, - public component: string, - public description: string, - ) { - this.datetime = moment(this.epoch * 1000); // in ms - } - - get url(): string { - return `https://github.com/runbox/runbox7/commit/${this.hash}`; - } -} - - -// These entries are auto-generated from build-changelog.js. -// Manual edits will be overwritten. -/* eslint-disable @typescript-eslint/quotes */ -// BEGIN:AUTOGENERATED -const changes = [ - [ - "2b05c475", - "1747382829", +// --------- BEGIN RUNBOX LICENSE --------- +// Copyright (C) 2016-2019 Runbox Solutions AS (runbox.com). +// +// This file is part of Runbox 7. +// +// Runbox 7 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, either version 3 of the License, or (at your +// option) any later version. +// +// Runbox 7 is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Runbox 7. If not, see . +// ---------- END RUNBOX LICENSE ---------- + +import moment from 'moment'; + +export enum EntryType { + BUILD, + CI, + DOCS, + FEAT, + FIX, + PERF, + REFACTOR, + STYLE, + TEST +} + +const typeMapping = { + 'build': EntryType.BUILD, + 'ci': EntryType.CI, + 'docs': EntryType.DOCS, + 'feat': EntryType.FEAT, + 'feature': EntryType.FEAT, + 'fix': EntryType.FIX, + 'perf': EntryType.PERF, + 'refactor': EntryType.REFACTOR, + 'style': EntryType.STYLE, + 'test': EntryType.TEST, + // known typos + 'refator': EntryType.REFACTOR, +}; + +export class ChangelogEntry { + public datetime: moment.Moment; + + constructor( + public hash: string, + public epoch: number, + public type: EntryType, + public component: string, + public description: string, + ) { + this.datetime = moment(this.epoch * 1000); // in ms + } + + get url(): string { + return `https://github.com/runbox/runbox7/commit/${this.hash}`; + } +} + + +// These entries are auto-generated from build-changelog.js. +// Manual edits will be overwritten. +/* eslint-disable @typescript-eslint/quotes */ +// BEGIN:AUTOGENERATED const changes = [ + [ + "137575af", + "1769569113", + "style", + "cleanup", + "remove obsolete CSS styles" + ], + [ + "dac39ac9", + "1769569036", + "refactor", + "cleanup", + "remove unused code and constants" + ], + [ + "b383a4e0", + "1769568964", + "fix", + "stability", + "gate service worker registration" + ], + [ + "84c8a297", + "1769566090", + "feat", + "migration", + "migrate to RxJS 7" + ], + [ + "15d5b4f1", + "1769272608", + "style", + "account-upgrades", + "amend text regarding sub-accounts" + ], + [ + "9f4c2fb3", + "1769009210", + "refactor", + "2fa", + "Change 2FA unlock code description" + ], + [ + "f2d51985", + "1768489312", + "fix", + "cart", + "Fix add/remove cart item" + ], + [ + "3bc39da0", + "1768480595", + "feat", + "upgrades", + "Show recommended plans with addons if needed to downgrade" + ], + [ + "eaf16ee2", + "1768388642", + "refactor", + "upgrades", + "Ensure currency calculations are correct" + ], + [ + "1b76a7d4", + "1767483281", + "fix", + "mailviewer", + "intercept `mailto:` links and open a compose window" + ], + [ + "b52c415a", + "1765455095", + "fix", + "notifications", + "Check Notifcation API exists when showing button" + ], + [ + "ad665a71", + "1764772951", + "fix", + "plans", + "Enable downgrade to Micro including Email Hosting" + ], + [ + "840522fb", + "1764245620", + "style", + "payment", + "Change \"pending\" to \"incomplete\"." + ], + [ + "9ada126e", + "1764175688", + "fix", + "stripe", + "Fail early if stripe submission errors" + ], + [ + "757de0ed", + "1762962071", + "fix", + "stripe", + "Improve on-page status updates for Stripe payments" + ], + [ + "35cb13aa", + "1761061904", + "fix", + "notification", + "notification permission on click" + ], + [ + "524e9134", + "1760020202", + "style", + "payment", + "Correct image source URLs." + ], + [ + "42c6b9d2", + "1759989960", + "style", + "payment", + "Move shopping cart style back to main stylesheet for it to work." + ], + [ + "2154f5a8", + "1759989960", + "style", + "payment", + "Include missing SCSS file." + ], + [ + "07f415a6", + "1759989960", + "style", + "payment", + "Add Apple Pay logo and imrpove formatting." + ], + [ + "09968dc8", + "1759916636", + "feat", + "dkim", + "Display all users domains" + ], + [ + "a6316817", + "1759692383", + "style", + "payment", + "Minor textual correction." + ], + [ + "89bf44f6", + "1759692157", + "style", + "payment", + "Add cryptocurrency logos." + ], + [ + "2cc33c58", + "1759689457", + "style", + "payment", + "Bundle Stripe and PayPal, and improve layout." + ], + [ + "89de7403", + "1759202407", + "style", + "payment", + "Add note about number of email aliases on sub-accounts." + ], + [ + "a04b4700", + "1759144876", + "fix", + "mailviewer", + "Show images with ngsw bypass" + ], + [ + "0eb7556f", + "1757937300", + "fix", + "2fa", + "Show QR codes for 2FA generation" + ], + [ + "440fbdfc", + "1756825896", + "fix", + "sentry", + "Downgrade client to make it work with sentry server" + ], + [ + "10e135be", + "1756794090", + "style", + "payment", + "Fix typo." + ], + [ + "7ebeca44", + "1756335277", + "style", + "settings", + "Moved and updated link to documentation." + ], + [ + "a1faab26", + "1756305250", + "fix", + "paymentcards", + "Make add card dialog fit on screen" + ], + [ + "50b8e5db", + "1755783796", "feat", "worker", - "Add sentry to xapian worker scope" + "Improve Sentry integration" + ], + [ + "db8ccdef", + "1754909871", + "fix", + "stripe", + "Allow dialog to scroll (make Submit reachable)" + ], + [ + "4bdcff06", + "1751829714", + "style", + "dkim", + "Add note about format." + ], + [ + "7d6ddd10", + "1751477343", + "fix", + "cart", + "Renewing a subscription now sets a quantity of 1" + ], + [ + "1eebaa8a", + "1751367740", + "fix", + "commit-lint", + "allow hyphen in commit subject" + ], + [ + "67c0a118", + "1751367409", + "fix", + "login", + "make login inputs required" + ], + [ + "29eb8869", + "1751367360", + "fix", + "side-menu", + "make buttons keyboard navigable" + ], + [ + "8d22da9f", + "1751363648", + "fix", + "upgrades", + "Highlight correct \"current sub\" in plans table" + ], + [ + "d40892f9", + "1751296588", + "build", + "lint", + "Remove typos from commit-lint tags" + ], + [ + "e7be3886", + "1751296588", + "fix", + "upgrades", + "Indicate which plans would put the user over quota" + ], + [ + "ea501342", + "1751291013", + "fix", + "subscriptions", + "Only warn about close-quota for Disk/File quotas" + ], + [ + "1a198352", + "1751277492", + "fix", + "upgrades", + "Prevent two subscriptions being bought at once" + ], + [ + "9e4d014c", + "1750951971", + "fix", + "mailviewer", + "Text/HTML views blank when attachment has odd types" + ], + [ + "290ebe7e", + "1750694678", + "fix", + "upgrades", + "Remove ability to buy plans when over usage" + ], + [ + "8b4ad27a", + "1750691421", + "fix", + "upgrades", + "Checks quota usage for plan upgrades table" + ], + [ + "77657d08", + "1750691117", + "build", + "lint", + "Fix allowed commit-message tags" + ], + [ + "19ee3d5f", + "1750410342", + "style", + "upgrades", + "Improve formatting of recommended plans and other fixes." + ], + [ + "e53115a7", + "1750354119", + "fix", + "upgrades", + "Compare user's quotas when displaying plans" + ], + [ + "6d14aa2b", + "1750088797", + "fix", + "renewals", + "Removes confusing Yes/No for renewal checkboxes" + ], + [ + "b2d769b7", + "1750084083", + "fix", + "mailviewer", + "Fixes attachment display with HTML (again)" + ], + [ + "4a4e08e2", + "1749740369", + "fix", + "mailviewer", + "Only show attachments that are not inline in HTML" + ], + [ + "1beabc35", + "1749571399", + "fix", + "folderlist", + "place dir expander button above the anchor overlay" + ], + [ + "1e5cee80", + "1749544633", + "fix", + "github", + "make checkout of fork work in commit lint workflow" + ], + [ + "8d0d3cde", + "1749227244", + "style", + "mail", + "Make Allow and Block Sender descriptions consistent." + ], + [ + "652e3748", + "1749031846", + "build", + "ci", + "Remove redundant cache action" + ], + [ + "9a0189bb", + "1749031810", + "build", + "ci", + "prevent improper commit formatting from being merged into master" + ], + [ + "6cfc7dd4", + "1748872985", + "fix", + "details", + "show alternative email resend on email change" + ], + [ + "0036ce78", + "1748517484", + "fix", + "payments", + "Handle post stripe 3d secure correctly" + ], + [ + "ea2c77ca", + "1747908332", + "fix", + "build", + "Remove unused dependencies (revert)" + ], + [ + "2ef0626f", + "1747734901", + "build", + "deps", + "Remove unused dependencies" + ], + [ + "183d42ad", + "1747729699", + "build", + "deps", + "Remove unused dependencies" + ], + [ + "90d46835", + "1747328794", + "fix", + "payments", + "Renew or create products after Stripe payment" + ], + [ + "9b2580c3", + "1747302470", + "fix", + "folderlist", + "Make folder buttons keyboard navigable" + ], + [ + "51cb8dfe", + "1747297115", + "feat", + "login", + "Add autcomplete attrs in accordance with WCAG guidelines" + ], + [ + "403f7367", + "1747296965", + "fix", + "login", + "Fix user not sent with login req even when field is filled" ], [ "111120bd", @@ -10718,14 +11172,14 @@ const changes = [ "make sure folders are handled correctly regadless of order" ] ]; -// END:AUTOGENERATED - -export const changelog: ChangelogEntry[] = changes.map(entry => { - const type = typeMapping[entry[2]]; - if (type === undefined) { - throw new Error(`Invalid change type "${entry[2]}" in ${entry}`); - } - return new ChangelogEntry( - entry[0], parseInt(entry[1], 10), type, entry[3], entry[4] - ); -}); +// END:AUTOGENERATED + +export const changelog: ChangelogEntry[] = changes.map(entry => { + const type = typeMapping[entry[2]]; + if (type === undefined) { + throw new Error(`Invalid change type "${entry[2]}" in ${entry}`); + } + return new ChangelogEntry( + entry[0], parseInt(entry[1], 10), type, entry[3], entry[4] + ); +}); diff --git a/src/app/contacts-app/contact-details/contact-details.component.ts b/src/app/contacts-app/contact-details/contact-details.component.ts index e0a849cac..68b26ad6e 100644 --- a/src/app/contacts-app/contact-details/contact-details.component.ts +++ b/src/app/contacts-app/contact-details/contact-details.component.ts @@ -24,7 +24,7 @@ import { MatLegacySnackBar as MatSnackBar } from '@angular/material/legacy-snack import { Router, ActivatedRoute } from '@angular/router'; import { Contact, ContactKind, AddressDetails, Address, GroupMember } from '../contact'; import { ErrorDialog, ConfirmDialog, SimpleInputDialog, SimpleInputDialogParams } from '../../dialog/dialog.module'; -import { filter, take } from 'rxjs/operators'; +import { filter } from 'rxjs/operators'; import { ContactsService } from '../contacts.service'; import { MobileQueryService } from '../../mobile-query.service'; diff --git a/src/app/mailviewer/avatar-bar.component.ts b/src/app/mailviewer/avatar-bar.component.ts index 6c155e72b..cbe6aa201 100644 --- a/src/app/mailviewer/avatar-bar.component.ts +++ b/src/app/mailviewer/avatar-bar.component.ts @@ -19,7 +19,6 @@ import { Component, Input, OnInit, OnChanges } from '@angular/core'; import { firstValueFrom, ReplaySubject} from 'rxjs'; -import { take } from 'rxjs/operators'; import { ProfileService } from '../profiles/profile.service'; import { ContactsService } from '../contacts-app/contacts.service'; import { PreferencesService } from '../common/preferences.service'; diff --git a/src/app/start/startdesk.component.ts b/src/app/start/startdesk.component.ts index 0fe32a1e4..69d62d7be 100644 --- a/src/app/start/startdesk.component.ts +++ b/src/app/start/startdesk.component.ts @@ -26,7 +26,7 @@ import moment from 'moment'; import { Contact } from '../contacts-app/contact'; import { SearchService, SearchIndexDocumentData } from '../xapian/searchservice'; import { isValidEmail } from '../compose/emailvalidator'; -import { filter, take } from 'rxjs/operators'; +import { filter } from 'rxjs/operators'; import { firstValueFrom, ReplaySubject } from 'rxjs'; import { ProfileService } from '../profiles/profile.service'; import { UsageReportsService } from '../common/usage-reports.service'; From 5efd36f298a02e48061304e5d829b07c9d2f4901 Mon Sep 17 00:00:00 2001 From: Finn Date: Wed, 4 Feb 2026 14:30:27 +0000 Subject: [PATCH 12/12] refactor(rxjs): remove unused take import in test --- src/app/account-app/cart.service.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/account-app/cart.service.spec.ts b/src/app/account-app/cart.service.spec.ts index fe705f4fb..5900f395b 100644 --- a/src/app/account-app/cart.service.spec.ts +++ b/src/app/account-app/cart.service.spec.ts @@ -22,7 +22,6 @@ import { ProductOrder } from './product-order'; import { StorageService } from '../storage.service'; import { RunboxWebmailAPI } from '../rmmapi/rbwebmail'; import { firstValueFrom, of } from 'rxjs'; -import { take } from 'rxjs/operators'; import { Decimal } from 'decimal.js-light'; Decimal.set({ precision: 2, rounding: Decimal.ROUND_HALF_EVEN });