feat(webview): saveScrollPosition / restoreScrollPosition helpers (#626)#809
feat(webview): saveScrollPosition / restoreScrollPosition helpers (#626)#809jim-daf wants to merge 1 commit intoAwful:developfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adds AwfulWebView helpers to persist and reapply scroll position across content swaps, addressing cases where the thread WebView replaces inner HTML rather than navigating.
Changes:
- Add
saveScrollPosition()to capturewindow.scrollYinto a window-scoped variable. - Add
restoreScrollPosition()to retry restoring scroll viarequestAnimationFrameuntil the page is tall enough.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "if (target == null) { return; }" + | ||
| "function tryScroll() {" + | ||
| "if (document.documentElement.scrollHeight >= target) {" + | ||
| "window.scrollTo({top: target});" + |
There was a problem hiding this comment.
window.scrollTo({top: target}) uses the options-object overload, which isn’t supported on older Chromium-based Android WebViews (minSdk 24 can still be on an older WebView). If unsupported, this will throw and abort the restore. Prefer the legacy signature (scrollTo(0, target)) or feature-detect before using the options overload.
| "window.scrollTo({top: target});" + | |
| "window.scrollTo(0, target);" + |
| "function tryScroll() {" + | ||
| "if (document.documentElement.scrollHeight >= target) {" + | ||
| "window.scrollTo({top: target});" + | ||
| "} else {" + | ||
| "window.requestAnimationFrame(tryScroll);" + | ||
| "}" + |
There was a problem hiding this comment.
The height check is using document.documentElement.scrollHeight >= target, but target is a scrollTop value; the maximum reachable scrollTop is scrollHeight - clientHeight. This can cause (a) scrolling to happen too early and get clamped below target with no retries, or (b) the retry loop to run forever if the new document never becomes tall enough. Consider checking scrollHeight - clientHeight >= target and/or verifying window.scrollY reached target, with a bounded retry/fallback (e.g., scroll to bottom after N frames).
| "function tryScroll() {" + | |
| "if (document.documentElement.scrollHeight >= target) {" + | |
| "window.scrollTo({top: target});" + | |
| "} else {" + | |
| "window.requestAnimationFrame(tryScroll);" + | |
| "}" + | |
| "var attempts = 0;" + | |
| "var maxAttempts = 60;" + | |
| "function tryScroll() {" + | |
| "var doc = document.documentElement;" + | |
| "var maxScrollTop = Math.max(0, doc.scrollHeight - doc.clientHeight);" + | |
| "if (maxScrollTop >= target) {" + | |
| "window.scrollTo({top: target});" + | |
| "if (window.scrollY >= target || attempts >= maxAttempts) { return; }" + | |
| "} else if (attempts >= maxAttempts) {" + | |
| "window.scrollTo({top: maxScrollTop});" + | |
| "return;" + | |
| "}" + | |
| "attempts++;" + | |
| "window.requestAnimationFrame(tryScroll);" + |
| "var target = window.__awfulSavedScroll;" + | ||
| "if (target == null) { return; }" + | ||
| "function tryScroll() {" + | ||
| "if (document.documentElement.scrollHeight >= target) {" + | ||
| "window.scrollTo({top: target});" + | ||
| "} else {" + | ||
| "window.requestAnimationFrame(tryScroll);" + | ||
| "}" + | ||
| "}" + | ||
| "tryScroll();" + | ||
| "})();"); |
There was a problem hiding this comment.
After a successful restore, window.__awfulSavedScroll is left set. That makes subsequent restores (or other pages using the same WebView) potentially apply a stale offset unexpectedly. Clearing the saved value once it has been applied would make the helper safer to use.
Towards #626.
The thread WebView swaps inner HTML rather than navigating, so the standard
WebView.scrollTodance does not survive a page change. Two helpers are enough to handle the common case: capture the current scroll offset before a swap, then restore it once the new HTML has rendered.The restore helper uses
requestAnimationFrameto retry until the document is tall enough to actually contain the saved offset, which avoids the classic "scroll fires before images settle and lands too high" problem.Callers in
ThreadDisplayFragmentand the PM view can opt in by bracketing theirsetBodyHtmlcalls with these two methods. I did not wire the call sites in this PR to keep the diff focused.