Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions src/app/mailviewer/singlemailviewer.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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(<T>(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;
Expand Down
41 changes: 35 additions & 6 deletions src/app/mailviewer/singlemailviewer.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand All @@ -96,7 +100,11 @@ export class SingleMailViewerComponent implements OnInit, DoCheck, AfterViewInit
@ViewChildren('forwardMessageHeader', {read: ElementRef}) messageHeaderHTMLQuery: QueryList<ElementRef>;
@ViewChild(HorizResizerDirective) resizer: HorizResizerDirective;
@ViewChildren(HorizResizerDirective) resizerQuery: QueryList<HorizResizerDirective>;
@ViewChild('toolbarButtonContainer') toolbarButtonContainer: ElementRef;
@ViewChild('toolbarButtonContainer')
set toolbarButtonContainer(toolbarButtonContainer: ElementRef) {
this.toolbarButtonContainerElement = toolbarButtonContainer;
this.observeToolbarButtonContainer();
}

public downloadProgress: number;

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
});
});
Comment on lines +346 to +350
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Run toolbar resize callback inside Angular zone

The new ResizeObserver callback updates morebuttonindex/attachmentAreaCols directly, but this app does not import the Zone.js ResizeObserver patch (src/polyfills.ts only imports zone.js). In this setup, ResizeObserver callbacks can run outside Angular change detection, so toolbar state changes after a resize may not render until some unrelated UI event triggers another check. Wrap the callback in NgZone.run(...) (or explicitly patch ResizeObserver) so width-driven toolbar updates are reliably reflected immediately.

Useful? React with 👍 / 👎.

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;
Expand Down