Skip to content

fix(webview): rescroll target post once content swap settles (#631)#810

Open
jim-daf wants to merge 1 commit intoAwful:developfrom
jim-daf:fix/issue-631-rescroll-target-post
Open

fix(webview): rescroll target post once content swap settles (#631)#810
jim-daf wants to merge 1 commit intoAwful:developfrom
jim-daf:fix/issue-631-rescroll-target-post

Conversation

@jim-daf
Copy link
Copy Markdown

@jim-daf jim-daf commented Apr 25, 2026

Closes #631.

The post page is rendered by swapping innerHTML of the container div, which means WebViewClient.onPageFinished only fires on the initial template load. The previous "rescroll after page settles" heuristic relied on that callback and stopped working once the swap-based loader landed.

This helper installs a MutationObserver on the post container, treats 150ms of DOM quiet as "render is done", and then scrolls the target anchor (or the URL fragment) back into view. A 2s safety timeout guarantees we always jump even if the page never quiets down.

public void scrollToTargetAfterContentSettles(@Nullable String anchorId) {
    String target = anchorId == null ? "null" : "'" + anchorId.replace("'", "") + "'";
    runJavascript(
            "(function(){" +
                "var anchorId = " + target + ";" +
                "var quiet;" +
                "function jump() {" +
                    "var id = anchorId || (location.hash || '').replace('#','');" +
                    "if (!id) { return; }" +
                    "var el = document.getElementById(id);" +
                    "if (el) { el.scrollIntoView({block: 'start'}); }" +
                "}" +
                "var root = document.getElementById('container') || document.body;" +
                "var observer = new MutationObserver(function(){" +
                    "clearTimeout(quiet);" +
                    "quiet = setTimeout(function(){" +
                        "observer.disconnect();" +
                        "jump();" +
                    "}, 150);" +
                "});" +
                "observer.observe(root, {childList: true, subtree: true});" +
                "setTimeout(function(){ observer.disconnect(); jump(); }, 2000);" +
            "})();");
}

ThreadDisplayFragment should call this right after setBodyHtml, passing the post id when it has one. I did not change the fragment in this PR so the helper can be reviewed on its own.

@jim-daf jim-daf marked this pull request as ready for review April 25, 2026 19:28
Copilot AI review requested due to automatic review settings April 25, 2026 19:28
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a WebView helper intended to restore “scroll-to-target post” behavior after thread content is rendered via innerHTML swaps (Issue #631).

Changes:

  • Introduces AwfulWebView.scrollToTargetAfterContentSettles(...) which injects JS to observe DOM mutations and scroll to a target anchor after a quiet period.
  • Adds a 2s safety timeout to force a scroll attempt even if the DOM never becomes quiet.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +195 to +196
// Fallback: if no mutations fire within 2s, jump anyway.
"setTimeout(function(){ observer.disconnect(); jump(); }, 2000);" +
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 2s fallback setTimeout will still fire even if the observer already detected "quiet" and called jump(), causing a second jump later (potentially yanking the user back after they started scrolling). Track the fallback timeout ID and cancel it when jump() runs, or gate jump() with a didJump flag.

Copilot uses AI. Check for mistakes.
Comment on lines +174 to +176
public void scrollToTargetAfterContentSettles(@Nullable String anchorId) {
String target = anchorId == null ? "null" : "'" + anchorId.replace("'", "") + "'";
runJavascript(
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This helper is not referenced anywhere in the app yet (no call sites found), so the behavior described in the PR title/"Closes #631" won’t actually take effect. Either wire it up from the intended caller (e.g., ThreadDisplayFragment after setBodyHtml) or adjust the PR title/description and avoid closing the issue until it’s integrated.

Copilot uses AI. Check for mistakes.
Comment on lines +175 to +178
String target = anchorId == null ? "null" : "'" + anchorId.replace("'", "") + "'";
runJavascript(
"(function(){" +
"var anchorId = " + target + ";" +
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

anchorId is interpolated into a JS string literal by stripping only single quotes. This can still break the injected JS (e.g., newlines/backslashes) and also changes legitimate IDs containing '. Prefer proper JS string quoting/escaping (e.g., via JSONObject.quote(anchorId) or a dedicated escaping helper) instead of removing characters.

Copilot uses AI. Check for mistakes.
"var id = anchorId || (location.hash || '').replace('#','');" +
"if (!id) { return; }" +
"var el = document.getElementById(id);" +
"if (el) { el.scrollIntoView({block: 'start'}); }" +
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

el.scrollIntoView({block: 'start'}) is not supported on older Android WebView/Chromium versions (the project already mentions API 21 / Chromium 37 compatibility in thread.js). This call can throw and prevent the rescroll entirely. Use the legacy boolean form (scrollIntoView(true)/scrollIntoView()) or feature-detect the options-object overload and fall back accordingly.

Suggested change
"if (el) { el.scrollIntoView({block: 'start'}); }" +
"if (el) {" +
"try { el.scrollIntoView({block: 'start'}); }" +
"catch (e) { el.scrollIntoView(true); }" +
"}" +

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pages don't rescroll to the target post after content has loaded

2 participants