From 7128e8964e00ad0b9735997049e7cfc8186a8cbd Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Tue, 2 Jun 2026 23:04:52 +0800 Subject: [PATCH] fix(reader): suppress Android image callout freezing the image viewer (#4425) Long-pressing an image in the zoom viewer on Android triggers the WebView's native image callout (context menu / drag / magnifier) at the same time as the viewer's own pinch/pan/zoom touch handlers, locking up the whole app until restart. Same root cause as the book-cover freeze (PR #4345): `-webkit-touch-callout: none` doesn't inherit, so the class must sit on an ancestor of the ``. Apply the existing `.no-context-menu` class to the viewer container so the `.no-context-menu img` rule reaches the zoomed image and disables the native callout. Harmless on desktop (the property is a no-op there and right-click-save still works). Closes #4420 Co-authored-by: Claude Opus 4.8 (1M context) --- .../__tests__/components/ImageViewer.test.tsx | 39 +++++++++++++++++++ .../src/app/reader/components/ImageViewer.tsx | 5 ++- 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 apps/readest-app/src/__tests__/components/ImageViewer.test.tsx diff --git a/apps/readest-app/src/__tests__/components/ImageViewer.test.tsx b/apps/readest-app/src/__tests__/components/ImageViewer.test.tsx new file mode 100644 index 00000000..93e44dd6 --- /dev/null +++ b/apps/readest-app/src/__tests__/components/ImageViewer.test.tsx @@ -0,0 +1,39 @@ +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { render, cleanup } from '@testing-library/react'; + +import ImageViewer from '@/app/reader/components/ImageViewer'; + +vi.mock('@/hooks/useTranslation', () => ({ + useTranslation: () => (s: string) => s, +})); + +// useKeyDownActions pulls in EnvContext + device store; not under test here. +vi.mock('@/hooks/useKeyDownActions', () => ({ + useKeyDownActions: () => {}, +})); + +// ZoomControls reaches into the theme store and Tauri window APIs; stub it out. +vi.mock('@/app/reader/components/ZoomControls', () => ({ + __esModule: true, + default: () => null, +})); + +afterEach(cleanup); + +const gridInsets = { top: 0, right: 0, bottom: 0, left: 0 }; + +describe('ImageViewer', () => { + it('suppresses the native image callout on the zoomed image', () => { + // The WebView's native long-press image callout collides with the + // viewer's own pinch/pan handlers on Android and freezes the app. The + // zoomed must live under a `.no-context-menu` ancestor so the + // global `.no-context-menu img { -webkit-touch-callout: none }` rule + // disables that callout. Mirrors the book-cover fix (PR #4345). + const { container } = render( + , + ); + + const calloutSafeImage = container.querySelector('.no-context-menu img'); + expect(calloutSafeImage).toBeTruthy(); + }); +}); diff --git a/apps/readest-app/src/app/reader/components/ImageViewer.tsx b/apps/readest-app/src/app/reader/components/ImageViewer.tsx index d8e6fa9a..4af6df17 100644 --- a/apps/readest-app/src/app/reader/components/ImageViewer.tsx +++ b/apps/readest-app/src/app/reader/components/ImageViewer.tsx @@ -367,12 +367,15 @@ const ImageViewer: React.FC = ({ if (!src) return null; return ( + // `no-context-menu` suppresses the WebView's native long-press image + // callout (via the `.no-context-menu img` rule). On Android it otherwise + // collides with the pinch/pan handlers below and freezes the app.