From b1662464008335ba168b0833f126f0ccc4939923 Mon Sep 17 00:00:00 2001 From: codedogQBY <1369175442@qq.com> Date: Thu, 13 Aug 2026 10:48:31 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix(reader):=20iOS=20=E5=88=92=E7=BA=BF/?= =?UTF-8?q?=E9=95=BF=E6=8C=89=E9=80=89=E8=AF=8D=E7=BB=93=E6=9D=9F=E6=97=B6?= =?UTF-8?q?=E6=8A=91=E5=88=B6=E8=AF=AF=E8=A7=A6=E7=BF=BB=E9=A1=B5=20(#618?= =?UTF-8?q?=20#624=20#663)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - onSelection 回调后设置 900ms 翻页/tap 抑制窗口 - onSelectionCleared 回调后同样抑制 - 复用 suppressReaderTapUntilRef,隔离选词手势与点击翻页判定 --- packages/app-expo/src/screens/ReaderScreen.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/app-expo/src/screens/ReaderScreen.tsx b/packages/app-expo/src/screens/ReaderScreen.tsx index 3eb75832..9c1dabb2 100644 --- a/packages/app-expo/src/screens/ReaderScreen.tsx +++ b/packages/app-expo/src/screens/ReaderScreen.tsx @@ -259,6 +259,10 @@ export function ReaderScreen({ route, navigation }: Props) { const noteTooltipTimer = useRef | null>(null); const noteTooltipVisibleRef = useRef(false); const suppressReaderTapUntilRef = useRef(0); + // iOS can dispatch the WebView tap that ends a long-press/drag selection + // after the selection event, especially when the handle is near an edge. + // Keep the suppression in a ref so the bridge callback always sees it. + const selectionGestureSuppressMs = 900; const assetLoadedRef = useRef(false); // Mediator ref so onRelocate can fire TTS continuation without direct hook dependency const ttsPendingContinueRef = useRef<{ @@ -786,6 +790,9 @@ export function ReaderScreen({ route, navigation }: Props) { setToc(items); }, onSelection: (detail: SelectionEvent) => { + // A selection release must never be interpreted as the reader tap that + // toggles controls (or advances a page at the WebView layer). + suppressReaderTapUntilRef.current = Date.now() + selectionGestureSuppressMs; setSelection(detail); // Sync selection for AI tools if (detail.cfi) { @@ -798,6 +805,10 @@ export function ReaderScreen({ route, navigation }: Props) { } }, onSelectionCleared: () => { + // Clearing a selection is also commonly the tail end of a long press. + // Suppress the trailing tap so an edge selection cannot turn into a page + // navigation when iOS sends both events in the same gesture. + suppressReaderTapUntilRef.current = Date.now() + selectionGestureSuppressMs; setSelection(null); readingContextService.clearSelection(); }, From a62a60f27afebc7ff880ae3540bb09969ade2c17 Mon Sep 17 00:00:00 2001 From: codedogQBY <1369175442@qq.com> Date: Fri, 14 Aug 2026 11:41:07 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix(reader):=20=E9=80=89=E8=AF=8D=E5=B0=BE?= =?UTF-8?q?=E9=9A=8F=20tap=20=E4=B8=80=E6=AC=A1=E6=80=A7=E6=8B=A6=E6=88=AA?= =?UTF-8?q?=EF=BC=8C=E4=B8=8D=E5=86=8D=E6=8C=A1=E9=80=89=E4=B8=AD=E5=90=8E?= =?UTF-8?q?=E7=BF=BB=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RN 层移除 900ms 抑制(不再挡选中后立即翻页) - WebView 层:选区提交后设 250ms 一次性 tap 保护 - 只消费选词手势自带的尾随 tap(iOS 100-200ms 补发) - 后续用户主动 tap 完全放行 - 模板与编译产物同步 --- packages/app-expo/assets/reader/reader.html | 13 +++++++++++++ .../app-expo/assets/reader/reader.template.html | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/packages/app-expo/assets/reader/reader.html b/packages/app-expo/assets/reader/reader.html index ee88b5df..4c575869 100644 --- a/packages/app-expo/assets/reader/reader.html +++ b/packages/app-expo/assets/reader/reader.html @@ -2510,6 +2510,10 @@ const state = tapState; tapState = null; + if (doc.__readany_selection_tap_guard_until > Date.now() && !doc.__readany_selection_tap_guard_consumed) { + doc.__readany_selection_tap_guard_consumed = true; + return; + } if (!state || state.moved || e.changedTouches.length !== 1) return; if (Date.now() - state.startedAt > MAX_TAP_MS) return; const touch = e.changedTouches[0]; @@ -2800,6 +2804,10 @@ function emitSelection(doc, sel, range) { const text = getRangeTextWithoutRuby(range, sel.toString()); if (!text) return; + // iOS may emit one synthetic tap 100-200ms after the selection handle is + // released. Consume only that one trailing tap; later user taps remain + // available for immediate page navigation. + armSelectionTapGuardForDocument(doc); const rect = range.getBoundingClientRect(); const rects = Array.from(range.getClientRects()).filter(r => r.width > 0 && r.height > 0); @@ -2829,6 +2837,11 @@ }); } + function armSelectionTapGuardForDocument(doc) { + doc.__readany_selection_tap_guard_until = Date.now() + 250; + doc.__readany_selection_tap_guard_consumed = false; + } + // ─── TOC conversion ─── function convertTOC(toc, level) { if (!toc) return []; diff --git a/packages/app-expo/assets/reader/reader.template.html b/packages/app-expo/assets/reader/reader.template.html index 37b71bc7..89fe18ad 100644 --- a/packages/app-expo/assets/reader/reader.template.html +++ b/packages/app-expo/assets/reader/reader.template.html @@ -2510,6 +2510,10 @@ const state = tapState; tapState = null; + if (doc.__readany_selection_tap_guard_until > Date.now() && !doc.__readany_selection_tap_guard_consumed) { + doc.__readany_selection_tap_guard_consumed = true; + return; + } if (!state || state.moved || e.changedTouches.length !== 1) return; if (Date.now() - state.startedAt > MAX_TAP_MS) return; const touch = e.changedTouches[0]; @@ -2800,6 +2804,10 @@ function emitSelection(doc, sel, range) { const text = getRangeTextWithoutRuby(range, sel.toString()); if (!text) return; + // iOS may emit one synthetic tap 100-200ms after the selection handle is + // released. Consume only that one trailing tap; later user taps remain + // available for immediate page navigation. + armSelectionTapGuardForDocument(doc); const rect = range.getBoundingClientRect(); const rects = Array.from(range.getClientRects()).filter(r => r.width > 0 && r.height > 0); @@ -2829,6 +2837,11 @@ }); } + function armSelectionTapGuardForDocument(doc) { + doc.__readany_selection_tap_guard_until = Date.now() + 250; + doc.__readany_selection_tap_guard_consumed = false; + } + // ─── TOC conversion ─── function convertTOC(toc, level) { if (!toc) return []; From 188b0d90f5b8dee773a5550fbba40cc85cbe2a03 Mon Sep 17 00:00:00 2001 From: codedogQBY <1369175442@qq.com> Date: Fri, 14 Aug 2026 11:41:25 +0800 Subject: [PATCH 3/3] =?UTF-8?q?revert:=20=E7=A7=BB=E9=99=A4=20RN=20?= =?UTF-8?q?=E5=B1=82=20900ms=20=E6=8A=91=E5=88=B6=EF=BC=88=E6=94=B9?= =?UTF-8?q?=E7=94=A8=20WebView=20=E4=B8=80=E6=AC=A1=E6=80=A7=20tap=20?= =?UTF-8?q?=E6=8B=A6=E6=88=AA=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/app-expo/src/screens/ReaderScreen.tsx | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/packages/app-expo/src/screens/ReaderScreen.tsx b/packages/app-expo/src/screens/ReaderScreen.tsx index 9c1dabb2..3eb75832 100644 --- a/packages/app-expo/src/screens/ReaderScreen.tsx +++ b/packages/app-expo/src/screens/ReaderScreen.tsx @@ -259,10 +259,6 @@ export function ReaderScreen({ route, navigation }: Props) { const noteTooltipTimer = useRef | null>(null); const noteTooltipVisibleRef = useRef(false); const suppressReaderTapUntilRef = useRef(0); - // iOS can dispatch the WebView tap that ends a long-press/drag selection - // after the selection event, especially when the handle is near an edge. - // Keep the suppression in a ref so the bridge callback always sees it. - const selectionGestureSuppressMs = 900; const assetLoadedRef = useRef(false); // Mediator ref so onRelocate can fire TTS continuation without direct hook dependency const ttsPendingContinueRef = useRef<{ @@ -790,9 +786,6 @@ export function ReaderScreen({ route, navigation }: Props) { setToc(items); }, onSelection: (detail: SelectionEvent) => { - // A selection release must never be interpreted as the reader tap that - // toggles controls (or advances a page at the WebView layer). - suppressReaderTapUntilRef.current = Date.now() + selectionGestureSuppressMs; setSelection(detail); // Sync selection for AI tools if (detail.cfi) { @@ -805,10 +798,6 @@ export function ReaderScreen({ route, navigation }: Props) { } }, onSelectionCleared: () => { - // Clearing a selection is also commonly the tail end of a long press. - // Suppress the trailing tap so an edge selection cannot turn into a page - // navigation when iOS sends both events in the same gesture. - suppressReaderTapUntilRef.current = Date.now() + selectionGestureSuppressMs; setSelection(null); readingContextService.clearSelection(); },