Skip to content
Merged
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: 22 additions & 13 deletions apps/mobile/src/features/threads/HtmlRenderWebView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,20 +51,26 @@ function postTheme(view: WebView<object> | null, theme: HtmlRenderTheme) {

const OVERFLOW_MESSAGE_TYPE = "t3-html-render-overflow";

// Reports whether the page overflows its frame, so a feed row only takes scroll
// gestures from a page that can use them.
const OVERFLOW_SCRIPT = `(function(){var last;function report(){var d=document.documentElement,b=document.body;var o=Math.max(d.scrollHeight,b?b.scrollHeight:0)>window.innerHeight+1||Math.max(d.scrollWidth,b?b.scrollWidth:0)>window.innerWidth+1;if(o===last)return;last=o;window.ReactNativeWebView.postMessage(JSON.stringify({type:${JSON.stringify(OVERFLOW_MESSAGE_TYPE)},overflow:o}));}report();if(window.ResizeObserver){var r=new ResizeObserver(report);r.observe(document.documentElement);if(document.body)r.observe(document.body);}window.addEventListener("resize",report);})();true;`;
// Reports which ways the page overflows its frame, so a feed row only takes
// scroll gestures from a page that can use them.
const OVERFLOW_SCRIPT = `(function(){var last;function report(){var d=document.documentElement,b=document.body;var x=Math.max(d.scrollWidth,b?b.scrollWidth:0)>window.innerWidth+1,y=Math.max(d.scrollHeight,b?b.scrollHeight:0)>window.innerHeight+1,o=x+","+y;if(o===last)return;last=o;window.ReactNativeWebView.postMessage(JSON.stringify({type:${JSON.stringify(OVERFLOW_MESSAGE_TYPE)},x:x,y:y}));}report();if(window.ResizeObserver){var r=new ResizeObserver(report);r.observe(document.documentElement);if(document.body)r.observe(document.body);}window.addEventListener("resize",report);})();true;`;

function readOverflowMessage(data: string) {
type Overflow = { readonly x: boolean; readonly y: boolean };

const NO_OVERFLOW: Overflow = { x: false, y: false };

function readOverflowMessage(data: string): Overflow | null {
try {
const message: unknown = JSON.parse(data);
return typeof message === "object" &&
message !== null &&
"type" in message &&
message.type === OVERFLOW_MESSAGE_TYPE &&
"overflow" in message &&
typeof message.overflow === "boolean"
? message.overflow
"x" in message &&
typeof message.x === "boolean" &&
"y" in message &&
typeof message.y === "boolean"
? { x: message.x, y: message.y }
: null;
} catch {
return null;
Expand All @@ -88,7 +94,7 @@ export function HtmlRenderWebView(props: {
const [initialTheme] = useState(theme);
const [generation, setGeneration] = useState(0);
const [loaded, setLoaded] = useState(false);
const [overflows, setOverflows] = useState(false);
const [overflow, setOverflow] = useState(NO_OVERFLOW);
const webView = useRef<WebView<object>>(null);
const crashes = useRef(0);
// The theme the loaded document shows; null until it loads.
Expand All @@ -111,10 +117,10 @@ export function HtmlRenderWebView(props: {
}
shownTheme.current = null;
setLoaded(false);
setOverflows(false);
setOverflow(NO_OVERFLOW);
setGeneration((value) => value + 1);
};
const scrollable = !props.nested || overflows;
const scrollable = !props.nested || overflow.x || overflow.y;
// Pages have no horizontal padding of their own, so full screen adds the
// feed's gutter in the page's background color.
return (
Expand All @@ -141,7 +147,10 @@ export function HtmlRenderWebView(props: {
showsVerticalScrollIndicator={!props.nested}
showsHorizontalScrollIndicator={!props.nested}
scrollEnabled={scrollable}
nestedScrollEnabled={props.nested && overflows}
// Android only: the page holds vertical drags until it reaches an edge,
// then hands them to the feed (patches/react-native-webview). A page
// that only overflows sideways leaves vertical drags to the feed.
nestedScrollEnabled={props.nested && overflow.y}
overScrollMode={props.nested ? "never" : "always"}
// Only the page itself loads here; other top-frame navigations are
// dropped. A link the reader taps opens as a new window, which the
Expand All @@ -167,8 +176,8 @@ export function HtmlRenderWebView(props: {
? {
injectedJavaScript: OVERFLOW_SCRIPT,
onMessage: (event: WebViewMessageEvent) => {
const overflow = readOverflowMessage(event.nativeEvent.data);
if (overflow !== null) setOverflows(overflow);
const next = readOverflowMessage(event.nativeEvent.data);
if (next !== null) setOverflow(next);
},
}
: {})}
Expand Down
56 changes: 56 additions & 0 deletions patches/react-native-webview@14.0.1.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCWebView.java b/android/src/main/java/com/reactnativecommunity/webview/RNCWebView.java
index 80c6803..374f824 100644
--- a/android/src/main/java/com/reactnativecommunity/webview/RNCWebView.java
+++ b/android/src/main/java/com/reactnativecommunity/webview/RNCWebView.java
@@ -122,14 +122,50 @@ public class RNCWebView extends WebView implements LifecycleEventListener {
cleanupCallbacksAndDestroy();
}

+ private float touchDownX;
+ private float touchDownY;
+ private float lastTouchY;
+ private boolean multiTouch;
+
@Override
public boolean onTouchEvent(MotionEvent event) {
if (this.nestedScrollEnabled) {
- requestDisallowInterceptTouchEvent(true);
+ requestDisallowInterceptTouchEvent(!shouldYieldToParent(event));
}
return super.onTouchEvent(event);
}

+ // A page that scrolls vertically hands a mostly vertical drag to the scrolling
+ // parent once it cannot scroll further that way, as nested scroll views do on iOS
+ // and the web. A page that never scrolls vertically keeps every gesture, since it
+ // may scroll inside an iframe this view cannot measure. A gesture that ever has
+ // a second finger, such as a pinch, stays with the page until every finger lifts.
+ private boolean shouldYieldToParent(MotionEvent event) {
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ touchDownX = event.getX();
+ touchDownY = event.getY();
+ lastTouchY = touchDownY;
+ multiTouch = false;
+ return false;
+ case MotionEvent.ACTION_POINTER_DOWN:
+ multiTouch = true;
+ return false;
+ case MotionEvent.ACTION_MOVE:
+ if (multiTouch) return false;
+ float dy = event.getY() - lastTouchY;
+ lastTouchY = event.getY();
+ if (dy == 0) return false;
+ if (Math.abs(event.getY() - touchDownY) < Math.abs(event.getX() - touchDownX)) {
+ return false;
+ }
+ if (!canScrollVertically(1) && !canScrollVertically(-1)) return false;
+ return !canScrollVertically(dy > 0 ? -1 : 1);
+ default:
+ return false;
+ }
+ }
+
@Override
protected void onSizeChanged(int w, int h, int ow, int oh) {
super.onSizeChanged(w, h, ow, oh);
Loading
Loading