Skip to content

feat(mobile): load earlier chat messages on scroll-up, keeping the reader's place - #31339

Merged
AmoabaKelvin merged 5 commits into
mainfrom
mobile-loading-more-messages
Oct 21, 2026
Merged

AmoabaKelvin merged 5 commits into
mainfrom
mobile-loading-more-messages

Conversation

@AmoabaKelvin

@AmoabaKelvin AmoabaKelvin commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator
Files Added Deleted Net
Test 6 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​317 $\color{#cf222e}{\Huge{\mathbf{−}}}$​381 $\color{#cf222e}{\Huge{\mathbf{−}}}$​64
Prod 30 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​936 $\color{#cf222e}{\Huge{\mathbf{−}}}$​1616 $\color{#cf222e}{\Huge{\mathbf{−}}}$​680

ELI5

On the phone, reading back through a long chat meant scrolling to the top, tapping "Load earlier messages", then scrolling and tapping again. Now older messages load by themselves while you scroll up, even in the middle of a flick, and the text you are reading stays still.

What Changed

Before, older history on the phone page loaded only from the "Load earlier messages" tap. Even after a tap, scrolling up stopped at a spinner every few rows, because the page added rows only while the scroll was completely still.

Now:

  • Older history loads on its own a few screens before the top and appears mid-flick. A spinner shows only if you outrun it, and a "Try Again" pill only after a failure.
  • The chat list measures its scroll position from the newest message (flex-direction: column-reverse). Rows added at the top move nothing, so landing older history needs no scroll correction, and following a streaming reply needs no pinning.
  • Changes below a reader who has scrolled up (a reply streaming in, a new tool call joining a run, text size, opening a tool row) follow one rule: once the scroll is at rest, the line being read goes back before the frame paints. While the reader's finger is down or the list is coasting, those changes are held back and land when they stop. That covers the rows as they were drawn and the paced reveal of a streaming reply, so the text does not shake.
  • The list draws the newest rows and adds more as the reader nears the top of what is drawn. Adding at the top costs nothing, so no row heights are cached or estimated.

This removes the page list's windowing, height cache and measuring rounds, the place-hold component, and the step where both chat lanes waited for the reader to stop before landing an older page. The scroll boxes inside rows (tool output, diffs, reasoning) keep their behaviour behind a small commit boundary.

Why

On iPhone the page cannot correct the scroll position mid-flick. iOS 26 and earlier stop the flick on the write, and iOS 27 overrides it on the next frame; I measured both in the simulators. Safari 27's own scroll anchoring has the same problem mid-flick. That is why the page waited for rest before adding older rows, and why loading needed a tap.

Chat list libraries hit the same limit. Legend List's web build corrects with scrollBy, and its open issue #488 reports that this kills flicks in WKWebView. TanStack Virtual and virtua hold their corrections until the flick ends, which brings back the stop at the top. Anchoring the list at its end removes the correction for the common case, reading back through history, on every engine. What remains is a change below a parked reader, which the at-rest rule handles.

Linked Issue

Fixes #31177

Visual Proof

A fake 400-message chat in iOS 27 simulator Safari, with the same scripted flicks in both.

Before (main): the flicks reach the top and nothing loads until the tap. After the tap, one page lands and the next flicks stop at the top again.

before-main-small.mp4

After: the same flicks never reach the top while older pages load mid-flick.

after-branch-small.mp4

Testing

I tested in the iOS 26 and iOS 27 simulators (Safari) and Android Chrome on the emulator, with a harness that renders the real chat view over fake paging. The cases were non-stop flicks, flicks with reading pauses, a reply streaming while scrolled up, opening a tool row and its output box, jump to latest, and a failed load with retry. Frame-by-frame recordings showed no jumps when pages landed and no snap-backs while a reply streamed.

I also compared main with this branch on a fake 2,000-message agent chat, with the same harness bundled from each tree, in Chromium at 4× CPU throttling (median of three runs):

  • Opening is faster: 248 ms to the first rows against 314 ms.
  • Streaming at the end, or one screen up, is as smooth as on main.
  • Scrolling to the top of all 2,000 takes 6 swipes and 11 s with no taps, against 72 swipes, 19 taps and 95 s on main. While pages land there are 34 frames over 34 ms, against 4 on main.
  • Every row the reader passes stays drawn: 500 at the top of 2,000 messages, where main draws about 10. So streaming while parked up there has 14 frames over 34 ms in 8 s, against none on main.

The rows are laid out in block flow, not React Native's column flexbox, which cut that last case's layout time from 2.3 s to 0.3 s. Unthrottled WebKit is smooth on both.

The page list tests are rewritten for the new behaviour, and the reading-place tests now run against the new list.

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

AI Disclosure

Review

MobileNativeChatPageList.tsx holds the new rules: drawing upward, asking for older pages, keeping the reader's line at rest, and holding back changes below a moving reader. The native FlatList branch is unchanged; since #29969 only tests reach it. The scroll boxes inside rows still decide by browser name whether to correct themselves, and their lists are short.

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

Phone page only. No wire change, so older hosts and the desktop are unaffected. The same page code runs for SSH and local hosts.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: a102e987-915d-4dd0-ab0a-90800b825f45
📥 Commits

Reviewing files that changed from the base of the PR and between 8c299fc and dd10762.

📒 Files selected for processing: 4
  • mobile/src/session/MobileNativeChatPageList.tsx
  • mobile/src/session/MobileNativeChatRowList.tsx
  • mobile/src/session/MobileNativeChatView.tsx
  • mobile/src/session/mobile-native-chat-reading-place.test.tsx

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 6 remain after this review.

📝 Walkthrough

The mobile chat list now uses incremental keyed rendering, reading-position restoration, scroll-root preservation, and automatic older-page loading near the top. Tail following and scroll ownership use scroll-specific contracts. History loading no longer waits for scroll settlement. The history header renders animated loading, unavailable, and retry states. Growth holds pause paced text reveal during transcript growth. Tests and fixtures cover scrolling, paging, restoration, and growth behavior.

Priority: ➖ Normal

Merge Risk: 🟡 Moderate · up to dd107

The automatic web scrolling path looks sound. Two edge cases remain: an empty or cursorless older-page response can wipe or overwrite the live transcript, and on native Android a short chat can show a spinner with no tap fallback to load older history. These are worth fixing before merge, or explicitly accepting.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 59.46% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 37 functions across 27 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly summarizes the main change: automatic loading of earlier mobile chat messages while preserving the reader’s position during upward scrolling.
Description check Passed The description includes all required sections, explains the user-facing change and implementation, links issue #31177, provides visual proof, documents testing and performance results, and addresses …
Linked Issues check Passed Issue #31177 requires automatic loading of older mobile chat messages while scrolling upward and a fallback after load failure. MobileNativeChatPageList requests older pages before the drawn history…
Out of Scope Changes check Passed The changes stay within issue #31177. The reversed page list, automatic paging, retry state, reader-position preservation, deferred below-reader updates, and related tests support mobile history revie…
Full details: Description check

Explanation

The description includes all required sections, explains the user-facing change and implementation, links issue #31177, provides visual proof, documents testing and performance results, and addresses platform scope and compatibility. The unchecked small-and-focused checklist item is non-critical.

Full details: Linked Issues check

Explanation

Issue #31177 requires automatic loading of older mobile chat messages while scrolling upward and a fallback after load failure. MobileNativeChatPageList requests older pages before the drawn history ends through onNearTop. MobileNativeChatView connects this callback to guarded loading. mobile-native-chat-list-header renders retry UI after failure. Updated history-paging tests cover near-top loading, short initial content, and retry behavior.

Full details: Out of Scope Changes check

Explanation

The changes stay within issue #31177. The reversed page list, automatic paging, retry state, reader-position preservation, deferred below-reader updates, and related tests support mobile history review. The changes also remove obsolete windowing and measurement code used by the replaced behavior. The native FlatList path, desktop behavior, wire protocol, and unrelated contextual issues remain outside the change.

✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: e334f23c-aa2f-449c-8e43-b7f15a6c5697
📥 Commits

Reviewing files that changed from the base of the PR and between 5999642 and 17d0925.

📒 Files selected for processing: 33
  • mobile/src/session/MobileNativeChatMessage.tsx
  • mobile/src/session/MobileNativeChatPageList.tsx
  • mobile/src/session/MobileNativeChatPageScroll.tsx
  • mobile/src/session/MobileNativeChatReasoningRow.tsx
  • mobile/src/session/MobileNativeChatRowList.tsx
  • mobile/src/session/MobileNativeChatScrollHold.tsx
  • mobile/src/session/MobileNativeChatScrollRootsBoundary.tsx
  • mobile/src/session/MobileNativeChatToolLine.tsx
  • mobile/src/session/MobileNativeChatToolRun.tsx
  • mobile/src/session/MobileNativeChatView.harness.test-fixture.ts
  • mobile/src/session/MobileNativeChatView.history-paging.test.ts
  • mobile/src/session/MobileNativeChatView.render-stubs.test-fixture.ts
  • mobile/src/session/MobileNativeChatView.tsx
  • mobile/src/session/mobile-native-chat-list-header.tsx
  • mobile/src/session/mobile-native-chat-page-heights.test.ts
  • mobile/src/session/mobile-native-chat-page-heights.ts
  • mobile/src/session/mobile-native-chat-page-rows.ts
  • mobile/src/session/mobile-native-chat-page-window-coverage.ts
  • mobile/src/session/mobile-native-chat-page-window-state.ts
  • mobile/src/session/mobile-native-chat-reading-place.test.tsx
  • mobile/src/session/mobile-native-chat-reading-place.ts
  • mobile/src/session/mobile-native-chat-scroll-anchoring.ts
  • mobile/src/session/mobile-native-chat-scroll-motion.ts
  • mobile/src/session/mobile-native-chat-scroll-ownership.test.tsx
  • mobile/src/session/mobile-native-chat-scroll-root-handle.ts
  • mobile/src/session/mobile-native-chat-scroll-roots.ts
  • mobile/src/session/mobile-native-chat-view-styles.ts
  • mobile/src/session/use-mobile-native-chat-page-render.ts
  • mobile/src/session/use-mobile-native-chat-page-window.ts
  • mobile/src/session/use-mobile-native-chat-session.test.ts
  • mobile/src/session/use-mobile-native-chat-session.ts
  • mobile/src/session/use-mobile-native-chat-tail-follow.ts
  • mobile/src/session/use-mobile-structured-agent-state.ts
💤 Files with no reviewable changes: 9
  • mobile/src/session/mobile-native-chat-page-heights.test.ts
  • mobile/src/session/use-mobile-native-chat-page-window.ts
  • mobile/src/session/use-mobile-native-chat-page-render.ts
  • mobile/src/session/use-mobile-native-chat-session.test.ts
  • mobile/src/session/mobile-native-chat-page-window-coverage.ts
  • mobile/src/session/mobile-native-chat-page-heights.ts
  • mobile/src/session/MobileNativeChatScrollHold.tsx
  • mobile/src/session/mobile-native-chat-page-rows.ts
  • mobile/src/session/mobile-native-chat-page-window-state.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

Comment on lines +218 to +234
const drawAndFetch = useCallback(
(s: Scroller) => {
const { first: at, keys: drawnKeys, n: total, rowKeys: current } = latest.current
const screens = (s.scrollHeight - s.clientHeight + s.scrollTop) / s.clientHeight
if (at > 0 && screens < DRAW_AHEAD_SCREENS) {
const next = Math.max(0, at - DRAW_BATCH)
setDrawn({ scope: current.scope, firstKey: drawnKeys[next]!, count: total - next })
} else if (isFollowing() && at < total - FIRST_ROWS && screens > 3 * DRAW_AHEAD_SCREENS) {
// Back at the end, rows far above go: taking them away moves nothing.
setDrawn({ scope: current.scope, firstKey: null, count: 0 })
}
if (at < FETCH_AHEAD_ROWS && screens < FETCH_REACH_SCREENS) {
ask.current?.()
}
},
[isFollowing]
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

The draw/trim rule in drawAndFetch can loop with no end and crash the page list.

drawAndFetch runs in the useLayoutEffect after every commit (Line 257-263), and each setDrawn starts a new commit. The draw branch runs when screens < DRAW_AHEAD_SCREENS (3). The trim branch runs when the reader is following and screens > 3 * DRAW_AHEAD_SCREENS (9). One DRAW_BATCH of 12 rows can add more than 6 screens of content, for example with Tools expanded and several Edit diff cards. Under that condition the cycle is:

  1. The trim keeps 24 rows, and screens drops below 3.
  2. The next commit draws 12 more rows, and screens rises above 9.
  3. The next commit trims again.

Every step is a setState inside a layout effect. React stops after its nested-update limit with "Maximum update depth exceeded", so the chat view fails. The ResizeObserver callback (Line 272-276) can also drive the same cycle.

Do not trim on commit or resize. Trim only when the reader returns to the end in a scroll event. The draw-ahead rule then stops at 3 screens, and a commit cannot restart the trim.

🐛 Proposed fix: trim only from a scroll event
   const drawAndFetch = useCallback(
-    (s: Scroller) => {
+    (s: Scroller, mayTrim = false) => {
       const { first: at, keys: drawnKeys, n: total, rowKeys: current } = latest.current
       const screens = (s.scrollHeight - s.clientHeight + s.scrollTop) / s.clientHeight
       if (at > 0 && screens < DRAW_AHEAD_SCREENS) {
         const next = Math.max(0, at - DRAW_BATCH)
         setDrawn({ scope: current.scope, firstKey: drawnKeys[next]!, count: total - next })
-      } else if (isFollowing() && at < total - FIRST_ROWS && screens > 3 * DRAW_AHEAD_SCREENS) {
+      } else if (
+        mayTrim &&
+        isFollowing() &&
+        at < total - FIRST_ROWS &&
+        screens > 3 * DRAW_AHEAD_SCREENS
+      ) {

Then, in scrolled:

-      drawAndFetch(s)
+      drawAndFetch(s, true)

Comment on lines +298 to +303
// Older runtimes ignore the cursor and return the growing tail; rows that streamed in while
// it waited stay.
const live = mergerRef.current.list
const after = live.findIndex((message) => message.id === result.messages.at(-1)?.id)
setList(after === -1 ? result.messages : [...result.messages, ...live.slice(after + 1)])
more = result.messages.length >= nextLimit

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

On older hosts, a cursorless page keeps its own stale copy of the joined row.

The page from an older runtime can end at a row that is still streaming. At Line 302, the page's copy of that row (result.messages.at(-1)) replaces the live copy. Only rows after it come from live. Live frames can update that row while the page read is in flight. The newer text is then replaced by the older snapshot. If the turn has already ended, no later frame corrects it, and the message stays truncated until the chat reloads. The removed mergeNativeChatMessages reconciliation prevented this.

There is a second problem on the same line. If result.messages is empty, after is -1 and setList([]) clears every row the reader holds.

From the joined row on, keep the live copies. If the page is empty, keep the live list.

🐛 Proposed fix
         const live = mergerRef.current.list
         const after = live.findIndex((message) => message.id === result.messages.at(-1)?.id)
-        setList(after === -1 ? result.messages : [...result.messages, ...live.slice(after + 1)])
+        if (result.messages.length > 0) {
+          setList(
+            after === -1
+              ? result.messages
+              : [...result.messages.slice(0, -1), ...live.slice(after)]
+          )
+        }
         more = result.messages.length >= nextLimit
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Older runtimes ignore the cursor and return the growing tail; rows that streamed in while
// it waited stay.
const live = mergerRef.current.list
const after = live.findIndex((message) => message.id === result.messages.at(-1)?.id)
setList(after === -1 ? result.messages : [...result.messages, ...live.slice(after + 1)])
more = result.messages.length >= nextLimit
// Older runtimes ignore the cursor and return the growing tail; rows that streamed in while
// it waited stay.
const live = mergerRef.current.list
const after = live.findIndex((message) => message.id === result.messages.at(-1)?.id)
if (result.messages.length > 0) {
setList(
after === -1
? result.messages
: [...result.messages.slice(0, -1), ...live.slice(after)]
)
}
more = result.messages.length >= nextLimit

@AmoabaKelvin
AmoabaKelvin force-pushed the mobile-loading-more-messages branch from 9bb0866 to 8c299fc Compare October 21, 2026 16:23

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 47da29da-2d5e-47e9-b904-5d12cf54cf03
📥 Commits

Reviewing files that changed from the base of the PR and between 9bb0866 and 8c299fc.

📒 Files selected for processing: 8
  • mobile/src/session/MobileNativeChatPageList.tsx
  • mobile/src/session/MobileNativeChatToolLine.tsx
  • mobile/src/session/MobileNativeChatToolRun.tsx
  • mobile/src/session/MobileNativeChatView.tsx
  • mobile/src/session/mobile-native-chat-list-header.tsx
  • mobile/src/session/mobile-native-chat-reading-place.test.tsx
  • mobile/src/session/mobile-native-chat-scroll-ownership.test.tsx
  • mobile/src/session/mobile-native-chat-view-styles.ts
🚧 Files skipped from review as they are similar to previous changes: 2
  • mobile/src/session/MobileNativeChatToolLine.tsx
  • mobile/src/session/MobileNativeChatToolRun.tsx

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

Comment on lines 72 to +81
return (
<Pressable style={styles.loadEarlier} onPress={loadEarlier} disabled={loadingEarlier}>
{loadingEarlier ? (
<ActivityIndicator size="small" color={colors.textMuted} />
) : (
<Text style={styles.loadEarlierText}>Load earlier messages</Text>
)}
{olderPages === 'failed' ? (
<Text style={styles.loadEarlierFailedText}>{LOAD_EARLIER_FAILED}</Text>
) : null}
</Pressable>
<View style={styles.loadEarlier}>
<FadeIn key="loading" delay={SPINNER_DELAY_MS}>
<ActivityIndicator
size="small"
color={colors.textMuted}
accessibilityLabel={LOADING_EARLIER}
/>
</FadeIn>
</View>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/bash
rg -nP -C4 'scrollMayLoadEarlier|onLoadEarlier|loadEarlier\b' mobile/src/session --type=ts --type=tsx -g '!**/*.test.*' 2>/dev/null || rg -nP -C4 'scrollMayLoadEarlier|onLoadEarlier|loadEarlier\b' mobile/src/session -g '!**/*.test.*'
rg -nP -C3 'onContentSizeChange|pinToTailAfterContentResize' mobile/src/session/use-mobile-native-chat-tail-follow.ts

Repository: stablyai/orca

Length of output: 22841


Add a native fallback for short lists.

When hasMore is true and the native list content does not fill the viewport, the idle header only renders an ActivityIndicator. The native branch starts loading only from onScroll when contentOffset.y < 60 and scrollMayLoadEarlier() is true. A short Android list may emit no scroll event, so the reader cannot load earlier messages or retry from the idle state.

Suggested fix: add a native idle fallback
+  if (Platform.OS !== 'web' && olderPages === 'idle') {
+    return (
+      <View style={styles.loadEarlier}>
+        <Pressable accessibilityRole="button" onPress={loadEarlier}>
+          <Text style={styles.loadEarlierRetryLabel}>Load earlier messages</Text>
+        </Pressable>
+      </View>
+    )
+  }
   return (
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return (
<Pressable style={styles.loadEarlier} onPress={loadEarlier} disabled={loadingEarlier}>
{loadingEarlier ? (
<ActivityIndicator size="small" color={colors.textMuted} />
) : (
<Text style={styles.loadEarlierText}>Load earlier messages</Text>
)}
{olderPages === 'failed' ? (
<Text style={styles.loadEarlierFailedText}>{LOAD_EARLIER_FAILED}</Text>
) : null}
</Pressable>
<View style={styles.loadEarlier}>
<FadeIn key="loading" delay={SPINNER_DELAY_MS}>
<ActivityIndicator
size="small"
color={colors.textMuted}
accessibilityLabel={LOADING_EARLIER}
/>
</FadeIn>
</View>
if (Platform.OS !== 'web' && olderPages === 'idle') {
return (
<View style={styles.loadEarlier}>
<Pressable accessibilityRole="button" onPress={loadEarlier}>
<Text style={styles.loadEarlierRetryLabel}>Load earlier messages</Text>
</Pressable>
</View>
)
}
return (
<View style={styles.loadEarlier}>
<FadeIn key="loading" delay={SPINNER_DELAY_MS}>
<ActivityIndicator
size="small"
color={colors.textMuted}
accessibilityLabel={LOADING_EARLIER}
/>
</FadeIn>
</View>

The phone page's chat list is anchored at the newest message, so older
history lands above the reader without moving them, mid-fling included.

Fixes #31177
…der moves

The reveal grows its row on its own frames, outside the list's hold, so each new
line pushed the reader mid-fling and through the long iOS coast after it.
…n rows

A held row drawn through the live renderer still took a run's new members, and
trimming back to the newest rows could undo a draw on every commit.
…art mid-scroll

The held rows began inside a run whose head the older page drew live, so the
held head drew the run's members a second time until the reader rested.
In RN's column flexbox every streamed token re-laid each drawn row; with 500
rows drawn deep in history that was a 20 ms layout per frame on a throttled
phone-class CPU. Block flow draws the same geometry for a fraction of it.
@AmoabaKelvin
AmoabaKelvin force-pushed the mobile-loading-more-messages branch from 8c299fc to dd10762 Compare October 21, 2026 19:00
@AmoabaKelvin
AmoabaKelvin merged commit 54ef452 into main Oct 21, 2026
23 checks passed
@AmoabaKelvin
AmoabaKelvin deleted the mobile-loading-more-messages branch October 21, 2026 23:49
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.

[Feature]: Mobile chat: load earlier messages automatically when scrolling up

1 participant