From 81d620cc9a22320e5fa01bc950e6852765840aa6 Mon Sep 17 00:00:00 2001 From: Kaan Date: Tue, 12 May 2026 03:25:25 +0300 Subject: [PATCH 1/3] test(mail): cover toolbar width recalculation trigger --- src/app/mailviewer/singlemailviewer.component.spec.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/app/mailviewer/singlemailviewer.component.spec.ts b/src/app/mailviewer/singlemailviewer.component.spec.ts index d4bc44f93..63e637e18 100644 --- a/src/app/mailviewer/singlemailviewer.component.spec.ts +++ b/src/app/mailviewer/singlemailviewer.component.spec.ts @@ -231,6 +231,14 @@ describe('SingleMailViewerComponent', () => { expect(component.mailObj.attachments[1].downloadURL.indexOf('blob:')).toBe(0); })); + it('should not recalculate toolbar width during every change detection check', () => { + const calculateSpy = spyOn(component, 'calculateWidthDependentElements'); + + component.ngDoCheck(); + + expect(calculateSpy).not.toHaveBeenCalled(); + }); + describe('mailto: link interceptor', () => { let messageContentsElement: HTMLElement; let mailtoLink: HTMLAnchorElement; From 2ad81055f93af0f28d86caccdb912f12f1330656 Mon Sep 17 00:00:00 2001 From: Kaan Date: Tue, 12 May 2026 03:26:37 +0300 Subject: [PATCH 2/3] fix(mail): observe toolbar width changes --- .../mailviewer/singlemailviewer.component.ts | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/src/app/mailviewer/singlemailviewer.component.ts b/src/app/mailviewer/singlemailviewer.component.ts index 1bb81b032..0d3d29039 100644 --- a/src/app/mailviewer/singlemailviewer.component.ts +++ b/src/app/mailviewer/singlemailviewer.component.ts @@ -24,7 +24,8 @@ import { QueryList, ElementRef, AfterViewInit, - DoCheck + DoCheck, + OnDestroy } from '@angular/core'; import { DomSanitizer } from '@angular/platform-browser'; import DOMPurify from 'dompurify'; @@ -70,8 +71,10 @@ type Mail = any; templateUrl: 'singlemailviewer.component.html', styleUrls: ['singlemailviewer.component.scss'] }) -export class SingleMailViewerComponent implements OnInit, DoCheck, AfterViewInit { +export class SingleMailViewerComponent implements OnInit, DoCheck, AfterViewInit, OnDestroy { private lastMailtoInterceptorNode: HTMLElement | null = null; + private toolbarButtonContainerElement: ElementRef | null = null; + private toolbarResizeObserver: ResizeObserver | null = null; _messageId = null; // Message id or filename @@ -96,7 +99,11 @@ export class SingleMailViewerComponent implements OnInit, DoCheck, AfterViewInit @ViewChildren('forwardMessageHeader', {read: ElementRef}) messageHeaderHTMLQuery: QueryList; @ViewChild(HorizResizerDirective) resizer: HorizResizerDirective; @ViewChildren(HorizResizerDirective) resizerQuery: QueryList; - @ViewChild('toolbarButtonContainer') toolbarButtonContainer: ElementRef; + @ViewChild('toolbarButtonContainer') + set toolbarButtonContainer(toolbarButtonContainer: ElementRef) { + this.toolbarButtonContainerElement = toolbarButtonContainer; + this.observeToolbarButtonContainer(); + } public downloadProgress: number; @@ -317,14 +324,32 @@ export class SingleMailViewerComponent implements OnInit, DoCheck, AfterViewInit } ngDoCheck() { - this.calculateWidthDependentElements(); // Rebind mailto interceptor if the underlying message or HTML view changes this.initMailtoInterceptor(); } + ngOnDestroy() { + this.toolbarResizeObserver?.disconnect(); + } + + private observeToolbarButtonContainer() { + this.toolbarResizeObserver?.disconnect(); + + if (!this.toolbarButtonContainerElement) return; + + this.calculateWidthDependentElements(); + + if (typeof ResizeObserver === 'undefined') return; + + this.toolbarResizeObserver = new ResizeObserver(() => { + this.calculateWidthDependentElements(); + }); + this.toolbarResizeObserver.observe(this.toolbarButtonContainerElement.nativeElement as HTMLDivElement); + } + calculateWidthDependentElements() { - if (this.toolbarButtonContainer) { - const toolbarwidth = (this.toolbarButtonContainer.nativeElement as HTMLDivElement).clientWidth; + if (this.toolbarButtonContainerElement) { + const toolbarwidth = (this.toolbarButtonContainerElement.nativeElement as HTMLDivElement).clientWidth; this.morebuttonindex = Math.floor( toolbarwidth / TOOLBAR_BUTTON_WIDTH ) - 1; From c5741b13bd71d3410a005e5ede3c948252785a8e Mon Sep 17 00:00:00 2001 From: Kaan Date: Tue, 12 May 2026 03:54:17 +0300 Subject: [PATCH 3/3] fix(mail): run toolbar resize updates in zone --- .../singlemailviewer.component.spec.ts | 27 +++++++++++++++++++ .../mailviewer/singlemailviewer.component.ts | 8 ++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/app/mailviewer/singlemailviewer.component.spec.ts b/src/app/mailviewer/singlemailviewer.component.spec.ts index 63e637e18..b2401c72c 100644 --- a/src/app/mailviewer/singlemailviewer.component.spec.ts +++ b/src/app/mailviewer/singlemailviewer.component.spec.ts @@ -18,6 +18,7 @@ // ---------- END RUNBOX LICENSE ---------- import { ComponentFixture, TestBed, tick, fakeAsync, waitForAsync, flush } from '@angular/core/testing'; +import { NgZone } from '@angular/core'; import { SingleMailViewerComponent } from './singlemailviewer.component'; import { ResizerModule } from '../directives/resizer.module'; @@ -239,6 +240,32 @@ describe('SingleMailViewerComponent', () => { expect(calculateSpy).not.toHaveBeenCalled(); }); + it('should run toolbar resize recalculation inside Angular zone', () => { + const originalResizeObserver = window.ResizeObserver; + let resizeObserverCallback: ResizeObserverCallback; + (window as any).ResizeObserver = class { + constructor(callback: ResizeObserverCallback) { + resizeObserverCallback = callback; + } + + observe() {} + + disconnect() {} + }; + const ngZone = TestBed.inject(NgZone); + const ngZoneRunSpy = spyOn(ngZone, 'run').and.callFake((callback: () => T) => callback()); + const toolbarElement = document.createElement('div'); + + component.toolbarButtonContainer = { + nativeElement: toolbarElement + } as any; + resizeObserverCallback([], {} as ResizeObserver); + + expect(ngZoneRunSpy).toHaveBeenCalled(); + + window.ResizeObserver = originalResizeObserver; + }); + describe('mailto: link interceptor', () => { let messageContentsElement: HTMLElement; let mailtoLink: HTMLAnchorElement; diff --git a/src/app/mailviewer/singlemailviewer.component.ts b/src/app/mailviewer/singlemailviewer.component.ts index 0d3d29039..53519f441 100644 --- a/src/app/mailviewer/singlemailviewer.component.ts +++ b/src/app/mailviewer/singlemailviewer.component.ts @@ -25,7 +25,8 @@ import { ElementRef, AfterViewInit, DoCheck, - OnDestroy + OnDestroy, + NgZone } from '@angular/core'; import { DomSanitizer } from '@angular/platform-browser'; import DOMPurify from 'dompurify'; @@ -149,6 +150,7 @@ export class SingleMailViewerComponent implements OnInit, DoCheck, AfterViewInit private snackBar: MatSnackBar, private contactsservice: ContactsService, private preferenceService: PreferencesService, + private ngZone: NgZone, ) { DOMPurify.addHook('afterSanitizeAttributes', function (node) { // set all elements owning target to target=_blank @@ -342,7 +344,9 @@ export class SingleMailViewerComponent implements OnInit, DoCheck, AfterViewInit if (typeof ResizeObserver === 'undefined') return; this.toolbarResizeObserver = new ResizeObserver(() => { - this.calculateWidthDependentElements(); + this.ngZone.run(() => { + this.calculateWidthDependentElements(); + }); }); this.toolbarResizeObserver.observe(this.toolbarButtonContainerElement.nativeElement as HTMLDivElement); }