diff --git a/Cargo.lock b/Cargo.lock
index 19d146a5..8a108a70 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1621,7 +1621,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
 dependencies = [
  "cfg-if",
- "windows-targets 0.48.5",
+ "windows-targets 0.52.6",
 ]
 
 [[package]]
@@ -1741,7 +1741,7 @@ dependencies = [
 [[package]]
 name = "makepad-derive-live"
 version = "0.4.0"
-source = "git+https://github.com/kevinaboos/makepad?branch=apple_bundle_resource_path#7e70055ab2fda14746374d02ca7ae70f804c40de"
+source = "git+https://github.com/kevinaboos/makepad?branch=portal_list_ref_immut_self#a18fde6784e71987852abaa8278d5b9e1bf9d610"
 dependencies = [
  "makepad-live-id",
  "makepad-micro-proc-macro",
@@ -1750,7 +1750,7 @@ dependencies = [
 [[package]]
 name = "makepad-derive-wasm-bridge"
 version = "0.4.0"
-source = "git+https://github.com/kevinaboos/makepad?branch=apple_bundle_resource_path#7e70055ab2fda14746374d02ca7ae70f804c40de"
+source = "git+https://github.com/kevinaboos/makepad?branch=portal_list_ref_immut_self#a18fde6784e71987852abaa8278d5b9e1bf9d610"
 dependencies = [
  "makepad-micro-proc-macro",
 ]
@@ -1758,7 +1758,7 @@ dependencies = [
 [[package]]
 name = "makepad-derive-widget"
 version = "0.4.0"
-source = "git+https://github.com/kevinaboos/makepad?branch=apple_bundle_resource_path#7e70055ab2fda14746374d02ca7ae70f804c40de"
+source = "git+https://github.com/kevinaboos/makepad?branch=portal_list_ref_immut_self#a18fde6784e71987852abaa8278d5b9e1bf9d610"
 dependencies = [
  "makepad-live-id",
  "makepad-micro-proc-macro",
@@ -1767,7 +1767,7 @@ dependencies = [
 [[package]]
 name = "makepad-draw"
 version = "0.6.0"
-source = "git+https://github.com/kevinaboos/makepad?branch=apple_bundle_resource_path#7e70055ab2fda14746374d02ca7ae70f804c40de"
+source = "git+https://github.com/kevinaboos/makepad?branch=portal_list_ref_immut_self#a18fde6784e71987852abaa8278d5b9e1bf9d610"
 dependencies = [
  "ab_glyph_rasterizer",
  "fxhash",
@@ -1784,17 +1784,17 @@ dependencies = [
 [[package]]
 name = "makepad-futures"
 version = "0.4.0"
-source = "git+https://github.com/kevinaboos/makepad?branch=apple_bundle_resource_path#7e70055ab2fda14746374d02ca7ae70f804c40de"
+source = "git+https://github.com/kevinaboos/makepad?branch=portal_list_ref_immut_self#a18fde6784e71987852abaa8278d5b9e1bf9d610"
 
 [[package]]
 name = "makepad-futures-legacy"
 version = "0.7.0"
-source = "git+https://github.com/kevinaboos/makepad?branch=apple_bundle_resource_path#7e70055ab2fda14746374d02ca7ae70f804c40de"
+source = "git+https://github.com/kevinaboos/makepad?branch=portal_list_ref_immut_self#a18fde6784e71987852abaa8278d5b9e1bf9d610"
 
 [[package]]
 name = "makepad-html"
 version = "0.4.0"
-source = "git+https://github.com/kevinaboos/makepad?branch=apple_bundle_resource_path#7e70055ab2fda14746374d02ca7ae70f804c40de"
+source = "git+https://github.com/kevinaboos/makepad?branch=portal_list_ref_immut_self#a18fde6784e71987852abaa8278d5b9e1bf9d610"
 dependencies = [
  "makepad-live-id",
 ]
@@ -1802,7 +1802,7 @@ dependencies = [
 [[package]]
 name = "makepad-http"
 version = "0.4.0"
-source = "git+https://github.com/kevinaboos/makepad?branch=apple_bundle_resource_path#7e70055ab2fda14746374d02ca7ae70f804c40de"
+source = "git+https://github.com/kevinaboos/makepad?branch=portal_list_ref_immut_self#a18fde6784e71987852abaa8278d5b9e1bf9d610"
 
 [[package]]
 name = "makepad-jni-sys"
@@ -1813,7 +1813,7 @@ checksum = "9775cbec5fa0647500c3e5de7c850280a88335d1d2d770e5aa2332b801ba7064"
 [[package]]
 name = "makepad-live-compiler"
 version = "0.5.0"
-source = "git+https://github.com/kevinaboos/makepad?branch=apple_bundle_resource_path#7e70055ab2fda14746374d02ca7ae70f804c40de"
+source = "git+https://github.com/kevinaboos/makepad?branch=portal_list_ref_immut_self#a18fde6784e71987852abaa8278d5b9e1bf9d610"
 dependencies = [
  "makepad-derive-live",
  "makepad-live-tokenizer",
@@ -1823,7 +1823,7 @@ dependencies = [
 [[package]]
 name = "makepad-live-id"
 version = "0.4.0"
-source = "git+https://github.com/kevinaboos/makepad?branch=apple_bundle_resource_path#7e70055ab2fda14746374d02ca7ae70f804c40de"
+source = "git+https://github.com/kevinaboos/makepad?branch=portal_list_ref_immut_self#a18fde6784e71987852abaa8278d5b9e1bf9d610"
 dependencies = [
  "makepad-live-id-macros",
 ]
@@ -1831,7 +1831,7 @@ dependencies = [
 [[package]]
 name = "makepad-live-id-macros"
 version = "0.4.0"
-source = "git+https://github.com/kevinaboos/makepad?branch=apple_bundle_resource_path#7e70055ab2fda14746374d02ca7ae70f804c40de"
+source = "git+https://github.com/kevinaboos/makepad?branch=portal_list_ref_immut_self#a18fde6784e71987852abaa8278d5b9e1bf9d610"
 dependencies = [
  "makepad-micro-proc-macro",
 ]
@@ -1839,7 +1839,7 @@ dependencies = [
 [[package]]
 name = "makepad-live-tokenizer"
 version = "0.4.0"
-source = "git+https://github.com/kevinaboos/makepad?branch=apple_bundle_resource_path#7e70055ab2fda14746374d02ca7ae70f804c40de"
+source = "git+https://github.com/kevinaboos/makepad?branch=portal_list_ref_immut_self#a18fde6784e71987852abaa8278d5b9e1bf9d610"
 dependencies = [
  "makepad-live-id",
  "makepad-math",
@@ -1849,7 +1849,7 @@ dependencies = [
 [[package]]
 name = "makepad-markdown"
 version = "0.4.0"
-source = "git+https://github.com/kevinaboos/makepad?branch=apple_bundle_resource_path#7e70055ab2fda14746374d02ca7ae70f804c40de"
+source = "git+https://github.com/kevinaboos/makepad?branch=portal_list_ref_immut_self#a18fde6784e71987852abaa8278d5b9e1bf9d610"
 dependencies = [
  "makepad-live-id",
 ]
@@ -1857,17 +1857,17 @@ dependencies = [
 [[package]]
 name = "makepad-math"
 version = "0.4.0"
-source = "git+https://github.com/kevinaboos/makepad?branch=apple_bundle_resource_path#7e70055ab2fda14746374d02ca7ae70f804c40de"
+source = "git+https://github.com/kevinaboos/makepad?branch=portal_list_ref_immut_self#a18fde6784e71987852abaa8278d5b9e1bf9d610"
 
 [[package]]
 name = "makepad-micro-proc-macro"
 version = "0.4.0"
-source = "git+https://github.com/kevinaboos/makepad?branch=apple_bundle_resource_path#7e70055ab2fda14746374d02ca7ae70f804c40de"
+source = "git+https://github.com/kevinaboos/makepad?branch=portal_list_ref_immut_self#a18fde6784e71987852abaa8278d5b9e1bf9d610"
 
 [[package]]
 name = "makepad-micro-serde"
 version = "0.4.0"
-source = "git+https://github.com/kevinaboos/makepad?branch=apple_bundle_resource_path#7e70055ab2fda14746374d02ca7ae70f804c40de"
+source = "git+https://github.com/kevinaboos/makepad?branch=portal_list_ref_immut_self#a18fde6784e71987852abaa8278d5b9e1bf9d610"
 dependencies = [
  "makepad-live-id",
  "makepad-micro-serde-derive",
@@ -1876,7 +1876,7 @@ dependencies = [
 [[package]]
 name = "makepad-micro-serde-derive"
 version = "0.4.0"
-source = "git+https://github.com/kevinaboos/makepad?branch=apple_bundle_resource_path#7e70055ab2fda14746374d02ca7ae70f804c40de"
+source = "git+https://github.com/kevinaboos/makepad?branch=portal_list_ref_immut_self#a18fde6784e71987852abaa8278d5b9e1bf9d610"
 dependencies = [
  "makepad-micro-proc-macro",
 ]
@@ -1884,12 +1884,12 @@ dependencies = [
 [[package]]
 name = "makepad-objc-sys"
 version = "0.4.0"
-source = "git+https://github.com/kevinaboos/makepad?branch=apple_bundle_resource_path#7e70055ab2fda14746374d02ca7ae70f804c40de"
+source = "git+https://github.com/kevinaboos/makepad?branch=portal_list_ref_immut_self#a18fde6784e71987852abaa8278d5b9e1bf9d610"
 
 [[package]]
 name = "makepad-platform"
 version = "0.6.0"
-source = "git+https://github.com/kevinaboos/makepad?branch=apple_bundle_resource_path#7e70055ab2fda14746374d02ca7ae70f804c40de"
+source = "git+https://github.com/kevinaboos/makepad?branch=portal_list_ref_immut_self#a18fde6784e71987852abaa8278d5b9e1bf9d610"
 dependencies = [
  "hilog-sys",
  "makepad-android-state",
@@ -1911,7 +1911,7 @@ dependencies = [
 [[package]]
 name = "makepad-rustybuzz"
 version = "0.8.0"
-source = "git+https://github.com/kevinaboos/makepad?branch=apple_bundle_resource_path#7e70055ab2fda14746374d02ca7ae70f804c40de"
+source = "git+https://github.com/kevinaboos/makepad?branch=portal_list_ref_immut_self#a18fde6784e71987852abaa8278d5b9e1bf9d610"
 dependencies = [
  "bitflags 1.3.2",
  "bytemuck",
@@ -1926,7 +1926,7 @@ dependencies = [
 [[package]]
 name = "makepad-shader-compiler"
 version = "0.5.0"
-source = "git+https://github.com/kevinaboos/makepad?branch=apple_bundle_resource_path#7e70055ab2fda14746374d02ca7ae70f804c40de"
+source = "git+https://github.com/kevinaboos/makepad?branch=portal_list_ref_immut_self#a18fde6784e71987852abaa8278d5b9e1bf9d610"
 dependencies = [
  "makepad-live-compiler",
 ]
@@ -1934,7 +1934,7 @@ dependencies = [
 [[package]]
 name = "makepad-vector"
 version = "0.4.0"
-source = "git+https://github.com/kevinaboos/makepad?branch=apple_bundle_resource_path#7e70055ab2fda14746374d02ca7ae70f804c40de"
+source = "git+https://github.com/kevinaboos/makepad?branch=portal_list_ref_immut_self#a18fde6784e71987852abaa8278d5b9e1bf9d610"
 dependencies = [
  "resvg",
  "ttf-parser",
@@ -1943,7 +1943,7 @@ dependencies = [
 [[package]]
 name = "makepad-wasm-bridge"
 version = "0.4.0"
-source = "git+https://github.com/kevinaboos/makepad?branch=apple_bundle_resource_path#7e70055ab2fda14746374d02ca7ae70f804c40de"
+source = "git+https://github.com/kevinaboos/makepad?branch=portal_list_ref_immut_self#a18fde6784e71987852abaa8278d5b9e1bf9d610"
 dependencies = [
  "makepad-derive-wasm-bridge",
  "makepad-live-id",
@@ -1952,7 +1952,7 @@ dependencies = [
 [[package]]
 name = "makepad-widgets"
 version = "0.6.0"
-source = "git+https://github.com/kevinaboos/makepad?branch=apple_bundle_resource_path#7e70055ab2fda14746374d02ca7ae70f804c40de"
+source = "git+https://github.com/kevinaboos/makepad?branch=portal_list_ref_immut_self#a18fde6784e71987852abaa8278d5b9e1bf9d610"
 dependencies = [
  "makepad-derive-widget",
  "makepad-draw",
@@ -1966,16 +1966,16 @@ dependencies = [
 [[package]]
 name = "makepad-windows"
 version = "0.51.1"
-source = "git+https://github.com/kevinaboos/makepad?branch=apple_bundle_resource_path#7e70055ab2fda14746374d02ca7ae70f804c40de"
+source = "git+https://github.com/kevinaboos/makepad?branch=portal_list_ref_immut_self#a18fde6784e71987852abaa8278d5b9e1bf9d610"
 dependencies = [
- "windows-core 0.51.1 (git+https://github.com/kevinaboos/makepad?branch=apple_bundle_resource_path)",
+ "windows-core 0.51.1 (git+https://github.com/kevinaboos/makepad?branch=portal_list_ref_immut_self)",
  "windows-targets 0.48.5",
 ]
 
 [[package]]
 name = "makepad-zune-core"
 version = "0.2.14"
-source = "git+https://github.com/kevinaboos/makepad?branch=apple_bundle_resource_path#7e70055ab2fda14746374d02ca7ae70f804c40de"
+source = "git+https://github.com/kevinaboos/makepad?branch=portal_list_ref_immut_self#a18fde6784e71987852abaa8278d5b9e1bf9d610"
 dependencies = [
  "bitflags 2.6.0",
 ]
@@ -1983,7 +1983,7 @@ dependencies = [
 [[package]]
 name = "makepad-zune-inflate"
 version = "0.2.54"
-source = "git+https://github.com/kevinaboos/makepad?branch=apple_bundle_resource_path#7e70055ab2fda14746374d02ca7ae70f804c40de"
+source = "git+https://github.com/kevinaboos/makepad?branch=portal_list_ref_immut_self#a18fde6784e71987852abaa8278d5b9e1bf9d610"
 dependencies = [
  "simd-adler32",
 ]
@@ -1991,7 +1991,7 @@ dependencies = [
 [[package]]
 name = "makepad-zune-jpeg"
 version = "0.3.17"
-source = "git+https://github.com/kevinaboos/makepad?branch=apple_bundle_resource_path#7e70055ab2fda14746374d02ca7ae70f804c40de"
+source = "git+https://github.com/kevinaboos/makepad?branch=portal_list_ref_immut_self#a18fde6784e71987852abaa8278d5b9e1bf9d610"
 dependencies = [
  "makepad-zune-core",
 ]
@@ -1999,7 +1999,7 @@ dependencies = [
 [[package]]
 name = "makepad-zune-png"
 version = "0.2.1"
-source = "git+https://github.com/kevinaboos/makepad?branch=apple_bundle_resource_path#7e70055ab2fda14746374d02ca7ae70f804c40de"
+source = "git+https://github.com/kevinaboos/makepad?branch=portal_list_ref_immut_self#a18fde6784e71987852abaa8278d5b9e1bf9d610"
 dependencies = [
  "makepad-zune-core",
  "makepad-zune-inflate",
@@ -3105,20 +3105,21 @@ dependencies = [
  "objc2-core-location",
  "objc2-foundation",
  "robius-android-env",
- "windows",
+ "windows 0.57.0",
 ]
 
 [[package]]
 name = "robius-open"
-version = "0.1.0"
+version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b4c6634e8febd0be2e37aefd55fd256f9f39eb5583db30c7163132aa625bfda"
+checksum = "563849989e82ed61036c169afe32f95d6683b944cb667986bc6735103c69a64a"
 dependencies = [
  "cfg-if",
  "icrate",
  "jni",
  "objc2",
  "robius-android-env",
+ "windows 0.54.0",
 ]
 
 [[package]]
@@ -4036,7 +4037,7 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
 [[package]]
 name = "ttf-parser"
 version = "0.21.1"
-source = "git+https://github.com/kevinaboos/makepad?branch=apple_bundle_resource_path#7e70055ab2fda14746374d02ca7ae70f804c40de"
+source = "git+https://github.com/kevinaboos/makepad?branch=portal_list_ref_immut_self#a18fde6784e71987852abaa8278d5b9e1bf9d610"
 
 [[package]]
 name = "typenum"
@@ -4430,6 +4431,16 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 
+[[package]]
+name = "windows"
+version = "0.54.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49"
+dependencies = [
+ "windows-core 0.54.0",
+ "windows-targets 0.52.6",
+]
+
 [[package]]
 name = "windows"
 version = "0.57.0"
@@ -4452,11 +4463,21 @@ dependencies = [
 [[package]]
 name = "windows-core"
 version = "0.51.1"
-source = "git+https://github.com/kevinaboos/makepad?branch=apple_bundle_resource_path#7e70055ab2fda14746374d02ca7ae70f804c40de"
+source = "git+https://github.com/kevinaboos/makepad?branch=portal_list_ref_immut_self#a18fde6784e71987852abaa8278d5b9e1bf9d610"
 dependencies = [
  "windows-targets 0.48.5",
 ]
 
+[[package]]
+name = "windows-core"
+version = "0.54.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65"
+dependencies = [
+ "windows-result 0.1.2",
+ "windows-targets 0.52.6",
+]
+
 [[package]]
 name = "windows-core"
 version = "0.57.0"
diff --git a/Cargo.toml b/Cargo.toml
index ed016d84..9f6d6396 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -17,12 +17,12 @@ metadata.makepad-auto-version = "zqpv-Yj-K7WNVK2I8h5Okhho46Q="
 
 [dependencies]
 # makepad-widgets = { git = "https://github.com/makepad/makepad", branch = "rik" }
-makepad-widgets = { git = "https://github.com/kevinaboos/makepad", branch = "apple_bundle_resource_path" }
+makepad-widgets = { git = "https://github.com/kevinaboos/makepad", branch = "portal_list_ref_immut_self" }
 
 
 ## Including this crate automatically configures all `robius-*` crates to work with Makepad.
 robius-use-makepad = "0.1.0"
-robius-open = "0.1.0"
+robius-open = "0.1.1"
 ## A fork of the `directories` crate that adds support for Android by using our `robius-android-env` crate.
 robius-directories = { git = "https://github.com/project-robius/robius-directories", branch = "robius"}
 robius-location = { git = "https://github.com/project-robius/robius-location" }
diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs
index 179fd9b0..360b528c 100644
--- a/src/home/room_screen.rs
+++ b/src/home/room_screen.rs
@@ -33,7 +33,7 @@ use crate::{
         html_or_plaintext::{HtmlOrPlaintextRef, HtmlOrPlaintextWidgetRefExt},
         text_or_image::{TextOrImageRef, TextOrImageWidgetRefExt},
         typing_animation::TypingAnimationWidgetExt,
-    }, sliding_sync::{get_client, submit_async_request, take_timeline_update_receiver, MatrixRequest}, utils::{self, unix_time_millis_to_datetime, MediaFormatConst}
+    }, sliding_sync::{get_client, submit_async_request, take_timeline_update_receiver, MatrixRequest, PaginationDirection}, utils::{self, unix_time_millis_to_datetime, MediaFormatConst}
 };
 use rangemap::RangeSet;
 
@@ -597,25 +597,27 @@ live_design! {
     }
 
 
-
-    // The top space is used to display a loading animation while the room is being paginated.
+    // The top space is used to display a loading message while the room is being paginated.
     TopSpace = <View> {
         visible: false,
         width: Fill,
         height: Fit,
-        align: {x: 0.5, y: 0.5}
+        align: {x: 0.5, y: 0}
         show_bg: true,
         draw_bg: {
-            color: #ebfcf2,
+            color: #xDAF5E5F0, // mostly opaque light green
         }
 
         label = <Label> {
-            padding: { top: 10.0, bottom: 8.0, left: 0.0, right: 0.0 }
+            width: Fill,
+            height: Fit,
+            align: {x: 0.5, y: 0.5},
+            padding: { top: 10.0, bottom: 7.0, left: 15.0, right: 15.0 }
             draw_text: {
                 text_style: <MESSAGE_TEXT_STYLE> { font_size: 10 },
                 color: (TIMESTAMP_TEXT_COLOR)
             }
-            text: "Loading more messages..."
+            text: "Loading earlier messages..."
         }
     }
 
@@ -780,8 +782,6 @@ live_design! {
                 width: Fill, height: Fill,
                 flow: Down,
 
-                top_space = <TopSpace> { }
-
                 // First, display the timeline of all messages/events.
                 timeline = <Timeline> {}
 
@@ -969,6 +969,10 @@ live_design! {
                 }
             }
 
+            // The top space should be displayed on top of the timeline
+            top_space = <TopSpace> { }
+
+            // The user profile sliding pane should be displayed on top of all other subviews.
             <View> {
                 width: Fill,
                 height: Fill,
@@ -1007,86 +1011,17 @@ impl Drop for RoomScreen {
     }
 }
 
-impl RoomScreen{
-    fn send_user_read_receipts_based_on_scroll_pos(
-        &mut self,
-        cx: &mut Cx,
-        actions: &ActionsBuf,
-    ) {
-        let portal_list = self.portal_list(id!(list));
-        //stopped scrolling
-        if portal_list.scrolled(actions) {
-            return;
-        }
-        let first_index = portal_list.first_id();
-
-        let Some(tl_state) = self.tl_state.as_mut() else { return };
-        let Some(room_id) = self.room_id.as_ref() else { return };
-        if let Some(ref mut index) = tl_state.prev_first_index {
-            // to detect change of scroll when scroll ends
-            if *index != first_index {
-                // scroll changed
-                self.fully_read_timer = cx.start_interval(5.0);
-                let time_now = std::time::Instant::now();
-                if first_index > *index {
-                    // Store visible event messages with current time into a hashmap
-                    let mut read_receipt_event = None;
-                    for r in first_index .. (first_index + portal_list.visible_items() + 1) {
-                        if let Some(v) = tl_state.items.get(r) {
-                            if let Some(e) = v.as_event().and_then(|f| f.event_id()) {
-                                read_receipt_event = Some(e.to_owned());
-                                if !tl_state.read_event_hashmap.contains_key(&e.to_string()) {
-                                    tl_state.read_event_hashmap.insert(
-                                        e.to_string(),
-                                        (room_id.clone(), e.to_owned(), time_now, false),
-                                    );
-                                }
-                            }
-                        }
-                    }
-                    if let Some(event_id) = read_receipt_event {
-                        submit_async_request(MatrixRequest::ReadReceipt { room_id: room_id.clone(), event_id });
-                    }
-                    let mut fully_read_receipt_event = None;
-                    // Implements sending fully read receipts when message is scrolled out of first row
-                    for r in *index..first_index {
-                        if let Some(v) = tl_state.items.get(r).clone() {
-                            if let Some(e) = v.as_event().and_then(|f| f.event_id()) {
-                                let mut to_remove = vec![];
-                                for (event_id_string, (_, event_id)) in &tl_state.marked_fully_read_queue {
-                                    if e == event_id {
-                                        fully_read_receipt_event = Some(event_id.clone());
-                                        to_remove.push(event_id_string.clone());
-                                    }
-                                }
-                                for r in to_remove {
-                                    tl_state.marked_fully_read_queue.remove(&r);
-                                }
-                            }
-                        }
-                    }
-                    if let Some(event_id) = fully_read_receipt_event {
-                        submit_async_request(MatrixRequest::FullyReadReceipt { room_id: room_id.clone(), event_id: event_id.clone()});
-                    }
-                }
-                *index = first_index;
-            }
-        } else {
-            tl_state.prev_first_index = Some(first_index);
-        }
-    }
-}
-
 impl Widget for RoomScreen {
     // Handle events and actions for the RoomScreen widget and its inner Timeline view.
     fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
         let widget_uid = self.widget_uid();
+        let portal_list = self.portal_list(id!(timeline.list));
         let pane = self.user_profile_sliding_pane(id!(user_profile_sliding_pane));
 
         // Currently, a Signal event is only used to tell this widget
         // that its timeline events have been updated in the background.
         if let Event::Signal = event {
-            self.process_timeline_updates(cx);
+            self.process_timeline_updates(cx, &portal_list);
         }
 
         if let Event::Actions(actions) = event {
@@ -1108,7 +1043,6 @@ impl Widget for RoomScreen {
                         }
                     }
                     MessageAction::ReplyPreviewClicked { reply_message_item_id, replied_to_event } => {
-                        let mut portal_list = self.portal_list(id!(list));
                         let Some(tl) = self.tl_state.as_mut() else {
                             continue;
                         };
@@ -1149,7 +1083,6 @@ impl Widget for RoomScreen {
                 }
 
                 // Handle the highlight animation.
-                let portal_list = self.portal_list(id!(list));
                 let Some(tl) = self.tl_state.as_mut() else { return };
                 if let MessageHighlightAnimationState::Pending { item_id } = tl.message_highlight_animation_state {
                     if portal_list.smooth_scroll_reached(actions) {
@@ -1260,9 +1193,9 @@ impl Widget for RoomScreen {
             }
 
             // Set visibility of loading message banner based of pagination logic
-            self.send_pagination_request_based_on_scroll_pos(cx, actions);
+            self.send_pagination_request_based_on_scroll_pos(cx, actions, &portal_list);
             // Handle sending any read receipts for the current logged-in user.
-            self.send_user_read_receipts_based_on_scroll_pos(cx, actions);
+            self.send_user_read_receipts_based_on_scroll_pos(cx, actions, &portal_list);
 
             // Handle the cancel reply button being clicked.
             if self.button(id!(cancel_reply_button)).clicked(&actions) {
@@ -1334,7 +1267,6 @@ impl Widget for RoomScreen {
 
             // Handle the jump to bottom button: update its visibility, and handle clicks.
             {
-                let mut portal_list = self.portal_list(id!(timeline.list));
                 let jump_to_bottom_view = self.view(id!(jump_to_bottom_view));
                 if portal_list.scrolled(&actions) {
                     // TODO: is_at_end() isn't perfect, see: <https://github.com/makepad/makepad/issues/517>
@@ -1520,8 +1452,7 @@ impl RoomScreen {
     /// Processes all pending background updates to the currently-shown timeline.
     ///
     /// Redraws this RoomScreen view if any updates were applied.
-    fn process_timeline_updates(&mut self, cx: &mut Cx) {
-        let portal_list = self.portal_list(id!(list));
+    fn process_timeline_updates(&mut self, cx: &mut Cx, portal_list: &PortalListRef) {
         let top_space = self.view(id!(top_space));
         let curr_first_id = portal_list.first_id();
         let Some(tl) = self.tl_state.as_mut() else { return };
@@ -1609,12 +1540,24 @@ impl RoomScreen {
                     tl.items = new_items;
                     done_loading = true;
                 }
-                TimelineUpdate::TimelineStartReached => {
-                    log!("Timeline::handle_event(): timeline start reached for room {}", tl.room_id);
-                    tl.fully_paginated = true;
+                TimelineUpdate::PaginationRunning(direction) => {
+                    if direction == PaginationDirection::Backwards {
+                        top_space.set_visible(true);
+                    } else {
+                        error!("Unexpected PaginationRunning update in the Forwards direction");
+                    }
+                }
+                TimelineUpdate::PaginationError { error, direction } => {
+                    error!("Pagination error ({direction}) in room {}: {error:?}", tl.room_id);
+                    done_loading = true;
                 }
-                TimelineUpdate::PaginationIdle => {
-                    top_space.set_visible(true);
+                TimelineUpdate::PaginationIdle { fully_paginated, direction } => {
+                    if direction == PaginationDirection::Backwards {
+                        done_loading = true;
+                        tl.fully_paginated = fully_paginated;
+                    } else {
+                        error!("Unexpected PaginationIdle update in the Forwards direction");
+                    }
                 }
                 TimelineUpdate::EventDetailsFetched {event_id, result } => {
                     if let Err(_e) = result {
@@ -1624,7 +1567,7 @@ impl RoomScreen {
                     // but for now we just fall through and let the final `redraw()` call re-draw the whole timeline view.
                 }
                 TimelineUpdate::RoomMembersFetched => {
-                    log!("Timeline::handle_event(): room members fetched for room {}", tl.room_id);
+                    // log!("Timeline::handle_event(): room members fetched for room {}", tl.room_id);
                     // Here, to be most efficient, we could redraw only the user avatars and names in the timeline,
                     // but for now we just fall through and let the final `redraw()` call re-draw the whole timeline view.
                 }
@@ -1751,7 +1694,6 @@ impl RoomScreen {
             Did you forget to save the timeline state back to the global map of states?",
         );
 
-
         let (mut tl_state, first_time_showing_room) = if let Some(existing) = TIMELINE_STATES.lock().unwrap().remove(&room_id) {
             (existing, false)
         } else {
@@ -1769,6 +1711,7 @@ impl RoomScreen {
                 replying_to: None,
                 saved_state: SavedState::default(),
                 message_highlight_animation_state: MessageHighlightAnimationState::default(),
+                last_scrolled_index: usize::MAX,
                 prev_first_index: None,
                 read_event_hashmap: HashMap::new(),
                 marked_fully_read_queue: HashMap::new(),
@@ -1785,36 +1728,31 @@ impl RoomScreen {
             }
         );
 
-        // kick off a back pagination request for this room
-        if !tl_state.fully_paginated {
+        // Kick off a back pagination request for this room. This is "urgent",
+        // because we want to show the user some messages as soon as possible
+        // when they first open the room, and there might not be any messages yet.
+        if first_time_showing_room && !tl_state.fully_paginated {
+            log!("Sending a first-time backwards pagination request for room {}", room_id);
             submit_async_request(MatrixRequest::PaginateRoomTimeline {
                 room_id: room_id.clone(),
                 num_events: 50,
-                forwards: false,
-            })
-        } else {
-            // log!("Note: skipping pagination request for room {} because it is already fully paginated.", room_id);
-        }
-
-        // Even though we specify that room member profiles should be lazy-loaded,
-        // the matrix server still doesn't consistently send them to our client properly.
-        // So we kick off a request to fetch the room members here upon first viewing the room.
-        if first_time_showing_room {
-            submit_async_request(MatrixRequest::FetchRoomMembers { room_id });
-            // TODO: in the future, move the back pagination request to here,
-            //       once back pagination is done dynamically based on timeline scroll position.
+                direction: PaginationDirection::Backwards,
+            });
         }
 
         // Now, restore the visual state of this timeline from its previously-saved state.
         self.restore_state(cx, &mut tl_state);
 
-        // As the final step, store the tl_state for this room into the Timeline widget,
+        // As the final step, store the tl_state for this room into this RoomScreen widget,
         // such that it can be accessed in future event/draw handlers.
         self.tl_state = Some(tl_state);
 
-        // Now we can process any background updates and redraw the timeline.
+        // Now that we have restored the TimelineUiState into this RoomScreen widget,
+        // we can proceed to processing pending background updates, and if any were processed,
+        // the timeline will also be redrawn.
         if first_time_showing_room {
-            self.process_timeline_updates(cx);
+            let portal_list = self.portal_list(id!(list));
+            self.process_timeline_updates(cx, &portal_list);
         }
 
         self.redraw(cx);
@@ -1906,24 +1844,100 @@ impl RoomScreen {
         self.show_timeline(cx);
         self.label(id!(room_name)).set_text(&self.room_name);
     }
-    
-    /// Send Pagination Request when the scroll position is at the top 
-    fn send_pagination_request_based_on_scroll_pos(
+
+    /// Sends read receipts based on the current scroll position of the timeline.
+    fn send_user_read_receipts_based_on_scroll_pos(
         &mut self,
         cx: &mut Cx,
         actions: &ActionsBuf,
+        portal_list: &PortalListRef,
     ) {
-        let portal_list = self.portal_list(id!(list));
-        //stopped scrolling and when scroll position is at top
+        //stopped scrolling
         if portal_list.scrolled(actions) {
             return;
         }
-        
+        let first_index = portal_list.first_id();
+
+        let Some(tl_state) = self.tl_state.as_mut() else { return };
         let Some(room_id) = self.room_id.as_ref() else { return };
-        if  portal_list.scroll_position() == 0.0 {
-            submit_async_request(MatrixRequest::PaginateRoomTimeline { room_id: room_id.clone(), num_events: 50, forwards: false});
+        if let Some(ref mut index) = tl_state.prev_first_index {
+            // to detect change of scroll when scroll ends
+            if *index != first_index {
+                // scroll changed
+                self.fully_read_timer = cx.start_interval(5.0);
+                let time_now = std::time::Instant::now();
+                if first_index > *index {
+                    // Store visible event messages with current time into a hashmap
+                    let mut read_receipt_event = None;
+                    for r in first_index .. (first_index + portal_list.visible_items() + 1) {
+                        if let Some(v) = tl_state.items.get(r) {
+                            if let Some(e) = v.as_event().and_then(|f| f.event_id()) {
+                                read_receipt_event = Some(e.to_owned());
+                                if !tl_state.read_event_hashmap.contains_key(&e.to_string()) {
+                                    tl_state.read_event_hashmap.insert(
+                                        e.to_string(),
+                                        (room_id.clone(), e.to_owned(), time_now, false),
+                                    );
+                                }
+                            }
+                        }
+                    }
+                    if let Some(event_id) = read_receipt_event {
+                        submit_async_request(MatrixRequest::ReadReceipt { room_id: room_id.clone(), event_id });
+                    }
+                    let mut fully_read_receipt_event = None;
+                    // Implements sending fully read receipts when message is scrolled out of first row
+                    for r in *index..first_index {
+                        if let Some(v) = tl_state.items.get(r).clone() {
+                            if let Some(e) = v.as_event().and_then(|f| f.event_id()) {
+                                let mut to_remove = vec![];
+                                for (event_id_string, (_, event_id)) in &tl_state.marked_fully_read_queue {
+                                    if e == event_id {
+                                        fully_read_receipt_event = Some(event_id.clone());
+                                        to_remove.push(event_id_string.clone());
+                                    }
+                                }
+                                for r in to_remove {
+                                    tl_state.marked_fully_read_queue.remove(&r);
+                                }
+                            }
+                        }
+                    }
+                    if let Some(event_id) = fully_read_receipt_event {
+                        submit_async_request(MatrixRequest::FullyReadReceipt { room_id: room_id.clone(), event_id: event_id.clone()});
+                    }
+                }
+                *index = first_index;
+            }
+        } else {
+            tl_state.prev_first_index = Some(first_index);
+        }
+    }
+
+    /// Sends a backwards pagination request if the user is scrolling up
+    /// and is approaching the top of the timeline.
+    fn send_pagination_request_based_on_scroll_pos(
+        &mut self,
+        _cx: &mut Cx,
+        actions: &ActionsBuf,
+        portal_list: &PortalListRef,
+    ) {
+        let Some(tl) = self.tl_state.as_mut() else { return };
+        if tl.fully_paginated { return };
+        if !portal_list.scrolled(actions) { return };
+
+        let first_index = portal_list.first_id();
+        if first_index == 0 && tl.last_scrolled_index > 0 {
+            log!("Scrolled up from item {} --> 0, sending back pagination request for room {}",
+                tl.last_scrolled_index, tl.room_id,
+            );
+            submit_async_request(MatrixRequest::PaginateRoomTimeline {
+                room_id: tl.room_id.clone(),
+                num_events: 50,
+                direction: PaginationDirection::Backwards,
+            });
         }
-        
+        tl.last_scrolled_index = first_index;
     }
 }
 
@@ -1935,7 +1949,6 @@ impl RoomScreenRef {
     }
 }
 
-
 /// A message that is sent from a background async task to a room's timeline view
 /// for the purpose of update the Timeline UI contents or metadata.
 pub enum TimelineUpdate {
@@ -1951,13 +1964,22 @@ pub enum TimelineUpdate {
         /// This supercedes `index_of_first_change` and is used when the entire timeline is being redrawn.
         clear_cache: bool,
     },
-    /// A notice that the start of the timeline has been reached, meaning that
-    /// there is no need to send further backwards pagination requests.
-    TimelineStartReached,
+    /// A notice that the background task doing pagination for this room is currently running
+    /// a pagination request in the given direction, and is waiting for that request to complete.
+    PaginationRunning(PaginationDirection),
+    /// An error occurred while paginating the timeline for this room.
+    PaginationError {
+        error: timeline::Error,
+        direction: PaginationDirection,
+    },
     /// A notice that the background task doing pagination for this room has become idle,
-    /// meaning that it has completed its recent pagination request(s) and is now waiting
-    /// for more requests, but that the start of the timeline has not yet been reached.
-    PaginationIdle,
+    /// meaning that it has completed its recent pagination request(s).
+    PaginationIdle {
+        /// If `true`, the start of the timeline has been reached, meaning that
+        /// there is no need to send further pagination requests.
+        fully_paginated: bool,
+        direction: PaginationDirection,
+    },
     /// A notice that event details have been fetched from the server,
     /// including a `result` that indicates whether the request was successful.
     EventDetailsFetched {
@@ -2042,6 +2064,12 @@ struct TimelineUiState {
     /// If the animation was trigged, the state goes back to Off.
     message_highlight_animation_state: MessageHighlightAnimationState,
 
+    /// The index of the timeline item that was most recently scrolled up past it.
+    /// This is used to detect when the user has scrolled up past the second visible item (index 1)
+    /// upwards to the first visible item (index 0), which is the top of the timeline,
+    /// at which point we submit a backwards pagination request to fetch more events.
+    last_scrolled_index: usize,
+
     prev_first_index: Option<usize>,
     read_event_hashmap: HashMap<String, (OwnedRoomId, OwnedEventId, Instant, bool)>,
     marked_fully_read_queue: HashMap<String, (OwnedRoomId, OwnedEventId)>,
diff --git a/src/home/rooms_list.rs b/src/home/rooms_list.rs
index 54797d0c..8d75b67a 100644
--- a/src/home/rooms_list.rs
+++ b/src/home/rooms_list.rs
@@ -3,10 +3,15 @@ use crossbeam_queue::SegQueue;
 use makepad_widgets::*;
 use matrix_sdk::ruma::{MilliSecondsSinceUnixEpoch, OwnedRoomId};
 
-use crate::{app::AppState, sliding_sync::{submit_async_request, MatrixRequest}};
+use crate::{app::AppState, sliding_sync::{submit_async_request, MatrixRequest, PaginationDirection}};
 
 use super::room_preview::RoomPreviewAction;
 
+/// Whether to pre-paginate visible rooms at least once in order to
+/// be able to display the latest message in the room preview,
+/// and to have something to immediately show when a user first opens a room.
+const PREPAGINATE_VISIBLE_ROOMS: bool = true;
+
 live_design! {
     import makepad_draw::shader::std::*;
     import makepad_widgets::view::*;
@@ -313,12 +318,12 @@ impl Widget for RoomsList {
                     room_info.is_selected = self.current_active_room_index == Some(item_id);
 
                     // Paginate the room if it hasn't been paginated yet.
-                    if !room_info.has_been_paginated {
+                    if PREPAGINATE_VISIBLE_ROOMS && !room_info.has_been_paginated {
                         room_info.has_been_paginated = true;
                         submit_async_request(MatrixRequest::PaginateRoomTimeline {
                             room_id: room_info.room_id.clone(),
                             num_events: 50,
-                            forwards: false,
+                            direction: PaginationDirection::Backwards,
                         });
                     }
 
diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs
index 32d5b064..d60e70a7 100644
--- a/src/sliding_sync.rs
+++ b/src/sliding_sync.rs
@@ -26,13 +26,12 @@ use matrix_sdk::{
 use matrix_sdk_ui::{
     room_list_service::{self, RoomListLoadingState},
     sync_service::{self, SyncService},
-    timeline::{AnyOtherFullStateEventContent, EventTimelineItem, LiveBackPaginationStatus, RepliedToInfo, TimelineDetails, TimelineItemContent},
+    timeline::{AnyOtherFullStateEventContent, EventTimelineItem, RepliedToInfo, TimelineDetails, TimelineItemContent},
     Timeline,
 };
 use tokio::{
     runtime::Handle,
     sync::mpsc::{UnboundedSender, UnboundedReceiver},
-    task::JoinHandle,
 };
 use unicode_segmentation::UnicodeSegmentation;
 use std::{cmp::{max, min}, collections::{BTreeMap, BTreeSet}, path:: Path, sync::{Arc, Mutex, OnceLock}};
@@ -166,6 +165,26 @@ async fn login(cli: Cli) -> Result<(Client, Option<String>)> {
 }
 
 
+/// Which direction to paginate in.
+/// 
+/// * `Forwards` will retrieve later events (towards the end of the timeline),
+///    which only works if the timeline is *focused* on a specific event.
+/// * `Backwards`: the more typical choice, in which earlier events are retrieved
+///    (towards the start of the timeline), which works in  both live mode and focused mode.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum PaginationDirection {
+    Forwards,
+    Backwards,
+}
+impl std::fmt::Display for PaginationDirection {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::Forwards => write!(f, "forwards"),
+            Self::Backwards => write!(f, "backwards"),
+        }
+    }
+}
+
 
 /// The set of requests for async work that can be made to the worker thread.
 pub enum MatrixRequest {
@@ -174,12 +193,7 @@ pub enum MatrixRequest {
         room_id: OwnedRoomId,
         /// The maximum number of timeline events to fetch in each pagination batch.
         num_events: u16,
-        /// Which "direction" to paginate in:
-        /// * `true`: paginate forwards to retrieve later events (towards the end of the timeline),
-        ///    which only works if the timeline is *focused* on a specific event.
-        /// * `false`: (default) paginate backwards to fill in earlier events (towards the start of the timeline),
-        ///    which works if the timeline is in either live mode and focused mode.
-        forwards: bool,
+        direction: PaginationDirection,
     },
     /// Request to fetch the full details of the given event in the given room's timeline.
     FetchDetailsForEvent {
@@ -285,69 +299,51 @@ async fn async_worker(mut receiver: UnboundedReceiver<MatrixRequest>) -> Result<
 
     while let Some(request) = receiver.recv().await {
         match request {
-            MatrixRequest::PaginateRoomTimeline { room_id, num_events, forwards } => {
-                let timeline = {
+            MatrixRequest::PaginateRoomTimeline { room_id, num_events, direction } => {
+                let (timeline, sender) = {
                     let mut all_room_info = ALL_ROOM_INFO.lock().unwrap();
                     let Some(room_info) = all_room_info.get_mut(&room_id) else {
                         log!("Skipping pagination request for not-yet-known room {room_id}");
                         continue;
                     };
 
-                    let room_id2 = room_id.clone();
                     let timeline_ref = room_info.timeline.clone();
-                    let timeline_ref2 = timeline_ref.clone();
                     let sender = room_info.timeline_update_sender.clone();
-
-                    // If the back-pagination status task is finished or doesn't exist, spawn a new one,
-                    // but only if the timeline is not already fully paginated.
-                    let should_spawn_pagination_status_task = match room_info.pagination_status_task.as_ref() {
-                        Some(t) => t.is_finished(),
-                        None => true,
-                    };
-                    if should_spawn_pagination_status_task {
-                        room_info.pagination_status_task = Some(Handle::current().spawn( async move {
-                            if let Some((pagination_status, mut pagination_stream)) = timeline_ref2.live_back_pagination_status().await {
-                                if !matches!(pagination_status, LiveBackPaginationStatus::Idle { hit_start_of_timeline: true }) {
-                                    while let Some(status) = pagination_stream.next().await {
-                                        log!("### Timeline {room_id2} back pagination status: {:?}", status);
-                                        match status {
-                                            LiveBackPaginationStatus::Idle { hit_start_of_timeline: false } => {
-                                                sender.send(TimelineUpdate::PaginationIdle).unwrap();
-                                                SignalToUI::set_ui_signal();
-                                            }
-                                            LiveBackPaginationStatus::Idle { hit_start_of_timeline: true } => {
-                                                sender.send(TimelineUpdate::TimelineStartReached).unwrap();
-                                                SignalToUI::set_ui_signal();
-                                                break;
-                                            }
-                                            _ => { }
-                                        }
-                                    }
-                                }
-                            }
-                        }));
-                    }
-
-                    // drop the lock on ALL_ROOM_INFO before spawning the actual pagination task.
-                    timeline_ref
+                    (timeline_ref, sender)
                 };
 
                 // Spawn a new async task that will make the actual pagination request.
                 let _paginate_task = Handle::current().spawn(async move {
-                    let direction = if forwards { "forwards" } else { "backwards" };
-                    log!("Sending {direction} pagination request for room {room_id}...");
-                    let res = if forwards {
+                    log!("Starting {direction} pagination request for room {room_id}...");
+                    sender.send(TimelineUpdate::PaginationRunning(direction)).unwrap();
+                    SignalToUI::set_ui_signal();
+
+                    let res = if direction == PaginationDirection::Forwards {
                         timeline.focused_paginate_forwards(num_events).await
                     } else {
                         timeline.paginate_backwards(num_events).await
                     };
+
                     match res {
-                        Ok(_hit_start_or_end) => log!(
-                            "Completed {direction} pagination request for room {room_id}, hit {} of timeline? {}",
-                            if forwards { "end" } else { "start" },
-                            if _hit_start_or_end { "yes" } else { "no" },
-                        ),
-                        Err(e) => error!("Error sending {direction} pagination request for room {room_id}: {e:?}"),
+                        Ok(fully_paginated) => {
+                            log!("Completed {direction} pagination request for room {room_id}, hit {} of timeline? {}",
+                                if direction == PaginationDirection::Forwards { "end" } else { "start" },
+                                if fully_paginated { "yes" } else { "no" },
+                            );
+                            sender.send(TimelineUpdate::PaginationIdle {
+                                fully_paginated,
+                                direction,
+                            }).unwrap();
+                            SignalToUI::set_ui_signal();
+                        }
+                        Err(error) => {
+                            error!("Error sending {direction} pagination request for room {room_id}: {error:?}");
+                            sender.send(TimelineUpdate::PaginationError {
+                                error,
+                                direction,
+                            }).unwrap();
+                            SignalToUI::set_ui_signal();
+                        }
                     }
                 });
             }
@@ -507,7 +503,7 @@ async fn async_worker(mut receiver: UnboundedReceiver<MatrixRequest>) -> Result<
                     submit_async_request(MatrixRequest::PaginateRoomTimeline {
                         room_id,
                         num_events: 50,
-                        forwards: false,
+                        direction: PaginationDirection::Backwards,
                     });
                 });
             }
@@ -765,8 +761,6 @@ struct RoomInfo {
     /// The UI thread can take ownership of these items  receiver in order for a specific
     /// timeline view (currently room_sccren) to receive and display updates to this room's timeline.
     timeline_update_receiver: Option<crossbeam_channel::Receiver<TimelineUpdate>>,
-    /// The async task that is subscribed to the timeline's back-pagination status.
-    pagination_status_task: Option<JoinHandle<()>>,
     /// A drop guard for the event handler that represents a subscription to typing notices for this room.
     typing_notice_subscriber: Option<EventHandlerDropGuard>,
 }
@@ -1108,7 +1102,6 @@ async fn add_new_room(room: &room_list_service::Room) -> Result<()> {
             timeline,
             timeline_update_receiver: Some(timeline_update_receiver),
             timeline_update_sender,
-            pagination_status_task: None,
             typing_notice_subscriber: None,
         },
     );
@@ -1158,7 +1151,7 @@ fn handle_ignore_user_list_subscriber(client: Client) {
                     submit_async_request(MatrixRequest::PaginateRoomTimeline {
                         room_id: joined_room.room_id().to_owned(),
                         num_events: 50,
-                        forwards: false,
+                        direction: PaginationDirection::Backwards,
                     });
                 }
             }