Skip to content

Commit

Permalink
Fix ResizeObserver loop completed with undelivered notifications
Browse files Browse the repository at this point in the history
Close #8517
  • Loading branch information
hrb-hub committed Feb 11, 2025
1 parent e6ed691 commit 9405eb7
Show file tree
Hide file tree
Showing 3 changed files with 27 additions and 15 deletions.
18 changes: 18 additions & 0 deletions packages/tutanota-utils/lib/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -616,3 +616,21 @@ export function assertValidURL(url: string) {
return false
}
}

/**
* Excessive resizing of an observed element can result in one or more resize events being deferred to the next render cycle.
* When this happens, the browser sends a `ResizeObserver loop completed with undelivered notifications.` error.
* To avoid this, we handle resize events in a `requestAnimationFrame` making sure to cancel any pending requests
*/
export function createResizeObserver(cb: ResizeObserverCallback): ResizeObserver {
let afRequestId: number | null = null

return new ResizeObserver((entries, observer) => {
if (afRequestId != null) {
cancelAnimationFrame(afRequestId)
}
afRequestId = requestAnimationFrame(() => {
cb(entries, observer)
})
})
}
8 changes: 5 additions & 3 deletions src/common/gui/base/List.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { theme, ThemeId } from "../theme.js"
import { ProgrammingError } from "../../api/common/error/ProgrammingError.js"
import { Coordinate2D } from "./SwipeHandler.js"
import { styles } from "../styles.js"
import { createResizeObserver } from "@tutao/tutanota-utils/dist/Utils"

export type ListState<T> = Readonly<{
items: ReadonlyArray<T>
Expand Down Expand Up @@ -120,6 +121,7 @@ export class List<T, VH extends ViewHolder<T>> implements ClassComponent<ListAtt
// remember the last time we needed to scroll somewhere
private activeIndex: number | null = null
private lastThemeId: ThemeId = theme.themeId
private resizeObserver: ResizeObserver | null = null

view({ attrs }: Vnode<ListAttrs<T, VH>>) {
const oldAttrs = this.lastAttrs
Expand All @@ -134,9 +136,9 @@ export class List<T, VH extends ViewHolder<T>> implements ClassComponent<ListAtt
// Some of the tech-savvy users like to disable *all* "experimental features" in their Safari devices and there's also a toggle to disable
// ResizeObserver. Since the app works without it anyway we just fall back to not handling the resize events.
if (typeof ResizeObserver !== "undefined") {
new ResizeObserver(() => {
this.updateSize()
}).observe(this.containerDom)
this.resizeObserver?.disconnect()
this.resizeObserver = createResizeObserver(() => this.updateSize())
this.resizeObserver.observe(this.containerDom)
} else {
requestAnimationFrame(() => this.updateSize())
}
Expand Down
16 changes: 4 additions & 12 deletions src/mail-app/mail/view/MailViewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { responsiveCardHMargin, responsiveCardHPadding } from "../../../common/g
import { Dialog } from "../../../common/gui/base/Dialog.js"
import { createNewContact } from "../../../common/mailFunctionality/SharedMailUtils.js"
import { getExistingRuleForType } from "../model/MailUtils.js"
import { createResizeObserver } from "@tutao/tutanota-utils/dist/Utils"

assertMainOrNode()

Expand Down Expand Up @@ -93,9 +94,6 @@ export class MailViewer implements Component<MailViewerAttrs> {
/** for block quotes in mail bodies, whether to display placeholder or original quote */
private quoteState: "noquotes" | "unset" | "collapsed" | "expanded" = "unset"

/** most recent resize animation frame request ID */
private resizeRaf: number | undefined

constructor(vnode: Vnode<MailViewerAttrs>) {
this.setViewModel(vnode.attrs.viewModel, vnode.attrs.isPrimary)

Expand Down Expand Up @@ -298,7 +296,7 @@ export class MailViewer implements Component<MailViewerAttrs> {
this.renderShadowMailBody(sanitizedMailBody, attrs, vnode.dom as HTMLElement)
if (client.isMobileDevice()) {
this.resizeObserverViewport?.disconnect()
this.resizeObserverViewport = new ResizeObserver((entries) => {
this.resizeObserverViewport = createResizeObserver(() => {
if (this.pinchZoomable) {
// recreate if the orientation of the device changes -> size of the viewport / mail-body changes
this.createPinchZoom(this.pinchZoomable.getZoomable(), vnode.dom as HTMLElement)
Expand Down Expand Up @@ -423,14 +421,8 @@ export class MailViewer implements Component<MailViewerAttrs> {
if (client.isMobileDevice()) {
this.pinchZoomable = null
this.resizeObserverZoomable?.disconnect()
this.resizeObserverZoomable = new ResizeObserver((entries) => {
if (this.resizeRaf) {
// did we already schedule a reset for pinch to zoom in the frame
cancelAnimationFrame(this.resizeRaf)
}
this.resizeRaf = requestAnimationFrame(() => {
this.createPinchZoom(wrapNode, parent) // recreate for example if images are loaded slowly
})
this.resizeObserverZoomable = createResizeObserver(() => {
this.createPinchZoom(wrapNode, parent) // recreate for example if images are loaded slowly
})
this.resizeObserverZoomable.observe(wrapNode)
} else {
Expand Down

0 comments on commit 9405eb7

Please sign in to comment.