Skip to content

PostStream loses scroll position during iOS Safari momentum scroll when loadPrevious() prepends posts #4587

@ekumanov

Description

@ekumanov

Summary

On iOS Safari, when a user flick-scrolls upward inside a long discussion and PostStream.loadPostsIfNeeded() triggers stream.loadPrevious() to prepend older posts, the viewport jumps — often all the way to the top of the discussion. The faster the flick, the more severe the jump; a very slow, deliberate scroll works correctly. Desktop browsers are unaffected.

Affected versions

Steps to reproduce

  1. Open a long discussion on an iPhone (Safari). The user should already have read through it so the "end" is the target post.
  2. Flick-scroll upward briskly to page back through older posts.
  3. Keep scrolling until the top of the currently-loaded window is about to leave the viewport and loadPrevious() fires.

Observed: the viewport jumps sharply — sometimes to the beginning of the discussion — as the prepended batch lands.

Expected: the viewport stays anchored on whichever posts the user was looking at (same behavior as desktop).

Why this only appears post-#4561

#4561 ("perf: frontend performance improvements") changed the viewport meta from

<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1">

to

<meta name="viewport" content="width=device-width, initial-scale=1">

which is a correct accessibility fix — users can now pinch-zoom on mobile. On beta.8 (with maximum-scale=1, minimum-scale=1), iOS Safari runs a simpler scroll pipeline in which programmatic scrollTop writes during momentum scroll tend to be honored; once user-zoom is enabled, iOS's zoom-and-scroll coordination layer kicks in and programmatic scrollTop writes during in-flight momentum are silently dropped. This is the same well-known WebKit behavior that has bitten many virtualized-list libraries; the viewport change in #4561 merely unmasked it in Flarum.

The accessibility win from #4561 should stay; the PostStream jump needs a client-side fix so momentum-scroll pagination works regardless of zoom configuration.

Root cause

PostStreamState.loadPage() wraps the prepend redraw in anchorScroll():

// framework/core/js/src/forum/states/PostStreamState.ts
const redraw = () => {
  if (start < this.visibleStart || end > this.visibleEnd) return;
  const anchorIndex = backwards ? this.visibleEnd - 1 : this.visibleStart;
  anchorScroll(`.PostStream-item[data-index=\"${anchorIndex}\"]`, m.redraw.sync);
};

anchorScroll() itself is:

// framework/core/js/src/common/utils/anchorScroll.js
export default function anchorScroll(element, callback) {
  const $window = $(window);
  const relativeScroll = $(element).offset().top - $window.scrollTop();
  callback();
  $window.scrollTop($(element).offset().top - relativeScroll);
}

On iOS Safari during momentum (inertial) scrolling with user-zoom enabled, programmatic writes to window.scrollTop are silently ignored — the in-flight momentum animation wins. As a result:

  1. The batch of placeholders is prepended to .PostStream.
  2. The final $window.scrollTop(...) call is dropped.
  3. The momentum animation continues from the old absolute scroll offset, which now points far earlier in the discussion because the content above the viewport has grown by ~20 items — producing the observed jump.

Related reports with the same pattern:

CSS scroll-anchoring (overflow-anchor) would normally be a safety net, but WebKit only shipped it in Safari 26 (WebKit tracker #171099, caniuse). A grep of the Flarum JS tree for overflow-anchor returns no matches, so there is no fallback today either.

Suggested fix

Route the restore through window.scrollTo(x, y) instead of jQuery's $window.scrollTop(y). window.scrollTo goes through a different WebKit code path that DOES halt the in-flight momentum and apply the new position, while scrollTop writes are silently dropped:

-  $window.scrollTop($(element).offset().top - relativeScroll);
+  window.scrollTo(window.scrollX, $(element).offset().top - relativeScroll);

This is a one-line surgical change with no UX or layout side effects on any platform — on non-iOS browsers it's equivalent behavior. PR to follow.

Environment

  • Flarum 2.x-dev (staging).
  • Affected device: iPhone, mobile Safari.
  • Desktop Chrome, Firefox, and Safari are all fine.
  • Custom extensions installed on the reporting forum do not touch the post-stream scroll path and are not involved; reproduced with the symptom caused by core behavior alone.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions