diff --git a/src/app/mailviewer/singlemailviewer.component.spec.ts b/src/app/mailviewer/singlemailviewer.component.spec.ts index d4bc44f93..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'; @@ -231,6 +232,40 @@ 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(); + }); + + 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 1bb81b032..53519f441 100644 --- a/src/app/mailviewer/singlemailviewer.component.ts +++ b/src/app/mailviewer/singlemailviewer.component.ts @@ -24,7 +24,9 @@ import { QueryList, ElementRef, AfterViewInit, - DoCheck + DoCheck, + OnDestroy, + NgZone } from '@angular/core'; import { DomSanitizer } from '@angular/platform-browser'; import DOMPurify from 'dompurify'; @@ -70,8 +72,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 +100,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; @@ -142,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 @@ -317,14 +326,34 @@ 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.ngZone.run(() => { + 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;