diff --git a/Cargo.lock b/Cargo.lock index a545a03e..e5f784aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1856,7 +1856,7 @@ dependencies = [ [[package]] name = "makepad-derive-live" version = "0.4.0" -source = "git+https://github.com/makepad/makepad?branch=rik#9cceaf72acb1304ae13e82cc3775d15925636efc" +source = "git+https://github.com/kevinaboos/makepad?branch=extend_portal_list_item_api#6f0a560e9958d514b69fd9d83510d80a8ed1ea0a" dependencies = [ "makepad-live-id", "makepad-micro-proc-macro", @@ -1865,7 +1865,7 @@ dependencies = [ [[package]] name = "makepad-derive-wasm-bridge" version = "0.4.0" -source = "git+https://github.com/makepad/makepad?branch=rik#9cceaf72acb1304ae13e82cc3775d15925636efc" +source = "git+https://github.com/kevinaboos/makepad?branch=extend_portal_list_item_api#6f0a560e9958d514b69fd9d83510d80a8ed1ea0a" dependencies = [ "makepad-micro-proc-macro", ] @@ -1873,7 +1873,7 @@ dependencies = [ [[package]] name = "makepad-derive-widget" version = "0.4.0" -source = "git+https://github.com/makepad/makepad?branch=rik#9cceaf72acb1304ae13e82cc3775d15925636efc" +source = "git+https://github.com/kevinaboos/makepad?branch=extend_portal_list_item_api#6f0a560e9958d514b69fd9d83510d80a8ed1ea0a" dependencies = [ "makepad-live-id", "makepad-micro-proc-macro", @@ -1882,7 +1882,7 @@ dependencies = [ [[package]] name = "makepad-draw" version = "0.6.0" -source = "git+https://github.com/makepad/makepad?branch=rik#9cceaf72acb1304ae13e82cc3775d15925636efc" +source = "git+https://github.com/kevinaboos/makepad?branch=extend_portal_list_item_api#6f0a560e9958d514b69fd9d83510d80a8ed1ea0a" dependencies = [ "ab_glyph_rasterizer", "makepad-platform", @@ -1895,17 +1895,17 @@ dependencies = [ [[package]] name = "makepad-futures" version = "0.4.0" -source = "git+https://github.com/makepad/makepad?branch=rik#9cceaf72acb1304ae13e82cc3775d15925636efc" +source = "git+https://github.com/kevinaboos/makepad?branch=extend_portal_list_item_api#6f0a560e9958d514b69fd9d83510d80a8ed1ea0a" [[package]] name = "makepad-futures-legacy" version = "0.7.0" -source = "git+https://github.com/makepad/makepad?branch=rik#9cceaf72acb1304ae13e82cc3775d15925636efc" +source = "git+https://github.com/kevinaboos/makepad?branch=extend_portal_list_item_api#6f0a560e9958d514b69fd9d83510d80a8ed1ea0a" [[package]] name = "makepad-html" version = "0.4.0" -source = "git+https://github.com/makepad/makepad?branch=rik#9cceaf72acb1304ae13e82cc3775d15925636efc" +source = "git+https://github.com/kevinaboos/makepad?branch=extend_portal_list_item_api#6f0a560e9958d514b69fd9d83510d80a8ed1ea0a" dependencies = [ "makepad-live-id", ] @@ -1913,12 +1913,12 @@ dependencies = [ [[package]] name = "makepad-http" version = "0.4.0" -source = "git+https://github.com/makepad/makepad?branch=rik#9cceaf72acb1304ae13e82cc3775d15925636efc" +source = "git+https://github.com/kevinaboos/makepad?branch=extend_portal_list_item_api#6f0a560e9958d514b69fd9d83510d80a8ed1ea0a" [[package]] name = "makepad-live-compiler" version = "0.5.0" -source = "git+https://github.com/makepad/makepad?branch=rik#9cceaf72acb1304ae13e82cc3775d15925636efc" +source = "git+https://github.com/kevinaboos/makepad?branch=extend_portal_list_item_api#6f0a560e9958d514b69fd9d83510d80a8ed1ea0a" dependencies = [ "makepad-derive-live", "makepad-live-tokenizer", @@ -1928,7 +1928,7 @@ dependencies = [ [[package]] name = "makepad-live-id" version = "0.4.0" -source = "git+https://github.com/makepad/makepad?branch=rik#9cceaf72acb1304ae13e82cc3775d15925636efc" +source = "git+https://github.com/kevinaboos/makepad?branch=extend_portal_list_item_api#6f0a560e9958d514b69fd9d83510d80a8ed1ea0a" dependencies = [ "makepad-live-id-macros", ] @@ -1936,7 +1936,7 @@ dependencies = [ [[package]] name = "makepad-live-id-macros" version = "0.4.0" -source = "git+https://github.com/makepad/makepad?branch=rik#9cceaf72acb1304ae13e82cc3775d15925636efc" +source = "git+https://github.com/kevinaboos/makepad?branch=extend_portal_list_item_api#6f0a560e9958d514b69fd9d83510d80a8ed1ea0a" dependencies = [ "makepad-micro-proc-macro", ] @@ -1944,7 +1944,7 @@ dependencies = [ [[package]] name = "makepad-live-tokenizer" version = "0.4.0" -source = "git+https://github.com/makepad/makepad?branch=rik#9cceaf72acb1304ae13e82cc3775d15925636efc" +source = "git+https://github.com/kevinaboos/makepad?branch=extend_portal_list_item_api#6f0a560e9958d514b69fd9d83510d80a8ed1ea0a" dependencies = [ "makepad-live-id", "makepad-math", @@ -1954,17 +1954,17 @@ dependencies = [ [[package]] name = "makepad-math" version = "0.4.0" -source = "git+https://github.com/makepad/makepad?branch=rik#9cceaf72acb1304ae13e82cc3775d15925636efc" +source = "git+https://github.com/kevinaboos/makepad?branch=extend_portal_list_item_api#6f0a560e9958d514b69fd9d83510d80a8ed1ea0a" [[package]] name = "makepad-micro-proc-macro" version = "0.4.0" -source = "git+https://github.com/makepad/makepad?branch=rik#9cceaf72acb1304ae13e82cc3775d15925636efc" +source = "git+https://github.com/kevinaboos/makepad?branch=extend_portal_list_item_api#6f0a560e9958d514b69fd9d83510d80a8ed1ea0a" [[package]] name = "makepad-micro-serde" version = "0.4.0" -source = "git+https://github.com/makepad/makepad?branch=rik#9cceaf72acb1304ae13e82cc3775d15925636efc" +source = "git+https://github.com/kevinaboos/makepad?branch=extend_portal_list_item_api#6f0a560e9958d514b69fd9d83510d80a8ed1ea0a" dependencies = [ "makepad-micro-serde-derive", ] @@ -1972,7 +1972,7 @@ dependencies = [ [[package]] name = "makepad-micro-serde-derive" version = "0.4.0" -source = "git+https://github.com/makepad/makepad?branch=rik#9cceaf72acb1304ae13e82cc3775d15925636efc" +source = "git+https://github.com/kevinaboos/makepad?branch=extend_portal_list_item_api#6f0a560e9958d514b69fd9d83510d80a8ed1ea0a" dependencies = [ "makepad-micro-proc-macro", ] @@ -1980,12 +1980,12 @@ dependencies = [ [[package]] name = "makepad-objc-sys" version = "0.4.0" -source = "git+https://github.com/makepad/makepad?branch=rik#9cceaf72acb1304ae13e82cc3775d15925636efc" +source = "git+https://github.com/kevinaboos/makepad?branch=extend_portal_list_item_api#6f0a560e9958d514b69fd9d83510d80a8ed1ea0a" [[package]] name = "makepad-platform" version = "0.6.0" -source = "git+https://github.com/makepad/makepad?branch=rik#9cceaf72acb1304ae13e82cc3775d15925636efc" +source = "git+https://github.com/kevinaboos/makepad?branch=extend_portal_list_item_api#6f0a560e9958d514b69fd9d83510d80a8ed1ea0a" dependencies = [ "makepad-futures", "makepad-futures-legacy", @@ -2000,7 +2000,7 @@ dependencies = [ [[package]] name = "makepad-shader-compiler" version = "0.5.0" -source = "git+https://github.com/makepad/makepad?branch=rik#9cceaf72acb1304ae13e82cc3775d15925636efc" +source = "git+https://github.com/kevinaboos/makepad?branch=extend_portal_list_item_api#6f0a560e9958d514b69fd9d83510d80a8ed1ea0a" dependencies = [ "makepad-live-compiler", ] @@ -2008,7 +2008,7 @@ dependencies = [ [[package]] name = "makepad-vector" version = "0.4.0" -source = "git+https://github.com/makepad/makepad?branch=rik#9cceaf72acb1304ae13e82cc3775d15925636efc" +source = "git+https://github.com/kevinaboos/makepad?branch=extend_portal_list_item_api#6f0a560e9958d514b69fd9d83510d80a8ed1ea0a" dependencies = [ "ttf-parser", ] @@ -2016,7 +2016,7 @@ dependencies = [ [[package]] name = "makepad-wasm-bridge" version = "0.4.0" -source = "git+https://github.com/makepad/makepad?branch=rik#9cceaf72acb1304ae13e82cc3775d15925636efc" +source = "git+https://github.com/kevinaboos/makepad?branch=extend_portal_list_item_api#6f0a560e9958d514b69fd9d83510d80a8ed1ea0a" dependencies = [ "makepad-derive-wasm-bridge", "makepad-live-id", @@ -2025,7 +2025,7 @@ dependencies = [ [[package]] name = "makepad-widgets" version = "0.6.0" -source = "git+https://github.com/makepad/makepad?branch=rik#9cceaf72acb1304ae13e82cc3775d15925636efc" +source = "git+https://github.com/kevinaboos/makepad?branch=extend_portal_list_item_api#6f0a560e9958d514b69fd9d83510d80a8ed1ea0a" dependencies = [ "makepad-derive-widget", "makepad-draw", @@ -2037,16 +2037,16 @@ dependencies = [ [[package]] name = "makepad-windows" version = "0.51.1" -source = "git+https://github.com/makepad/makepad?branch=rik#9cceaf72acb1304ae13e82cc3775d15925636efc" +source = "git+https://github.com/kevinaboos/makepad?branch=extend_portal_list_item_api#6f0a560e9958d514b69fd9d83510d80a8ed1ea0a" dependencies = [ - "windows-core 0.51.1 (git+https://github.com/makepad/makepad?branch=rik)", + "windows-core 0.51.1 (git+https://github.com/kevinaboos/makepad?branch=extend_portal_list_item_api)", "windows-targets", ] [[package]] name = "makepad-zune-core" version = "0.2.14" -source = "git+https://github.com/makepad/makepad?branch=rik#9cceaf72acb1304ae13e82cc3775d15925636efc" +source = "git+https://github.com/kevinaboos/makepad?branch=extend_portal_list_item_api#6f0a560e9958d514b69fd9d83510d80a8ed1ea0a" dependencies = [ "bitflags 2.4.1", ] @@ -2054,7 +2054,7 @@ dependencies = [ [[package]] name = "makepad-zune-inflate" version = "0.2.54" -source = "git+https://github.com/makepad/makepad?branch=rik#9cceaf72acb1304ae13e82cc3775d15925636efc" +source = "git+https://github.com/kevinaboos/makepad?branch=extend_portal_list_item_api#6f0a560e9958d514b69fd9d83510d80a8ed1ea0a" dependencies = [ "simd-adler32", ] @@ -2062,7 +2062,7 @@ dependencies = [ [[package]] name = "makepad-zune-jpeg" version = "0.3.17" -source = "git+https://github.com/makepad/makepad?branch=rik#9cceaf72acb1304ae13e82cc3775d15925636efc" +source = "git+https://github.com/kevinaboos/makepad?branch=extend_portal_list_item_api#6f0a560e9958d514b69fd9d83510d80a8ed1ea0a" dependencies = [ "makepad-zune-core", ] @@ -2070,7 +2070,7 @@ dependencies = [ [[package]] name = "makepad-zune-png" version = "0.2.1" -source = "git+https://github.com/makepad/makepad?branch=rik#9cceaf72acb1304ae13e82cc3775d15925636efc" +source = "git+https://github.com/kevinaboos/makepad?branch=extend_portal_list_item_api#6f0a560e9958d514b69fd9d83510d80a8ed1ea0a" dependencies = [ "makepad-zune-core", "makepad-zune-inflate", @@ -4831,7 +4831,7 @@ dependencies = [ [[package]] name = "windows-core" version = "0.51.1" -source = "git+https://github.com/makepad/makepad?branch=rik#9cceaf72acb1304ae13e82cc3775d15925636efc" +source = "git+https://github.com/kevinaboos/makepad?branch=extend_portal_list_item_api#6f0a560e9958d514b69fd9d83510d80a8ed1ea0a" dependencies = [ "windows-targets", ] diff --git a/Cargo.toml b/Cargo.toml index 904e4865..422ba33a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,8 @@ metadata.makepad-auto-version = "zqpv-Yj-K7WNVK2I8h5Okhho46Q=" [dependencies] -makepad-widgets = { git = "https://github.com/makepad/makepad", branch = "rik" } +# makepad-widgets = { git = "https://github.com/makepad/makepad", branch = "rik" } +makepad-widgets = { git = "https://github.com/kevinaboos/makepad", branch = "extend_portal_list_item_api" } anyhow = "1.0" diff --git a/README.md b/README.md index 617285b7..196dc47c 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ These are generally sorted in order of priority. If you're interested in helping - [x] Backwards pagination (upon viewing a room timeline) - [ ] Dynamic backwards pagination based on scroll position/movement - [ ] Loading animation while waiting for pagination request -- [ ] Stable positioning of events view during timeline update +- [x] Stable positioning of events view during timeline update - [x] Display simple text-only messages - [x] Display image messages (PNG, JPEG) - [ ] Rich text formatting for message bodies diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 794c577e..2c61705a 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -1,7 +1,7 @@ //! A room screen is the UI page that displays a single Room's timeline of events/messages //! along with a message input bar at the bottom. -use std::{ops::DerefMut, sync::{Arc, Mutex}, collections::BTreeMap}; +use std::{collections::BTreeMap, ops::{DerefMut, RangeInclusive}, sync::{Arc, Mutex}}; use imbl::Vector; use makepad_widgets::*; @@ -602,9 +602,15 @@ 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 { - /// A update containing the entire list of timeline items for a room, - /// indicating that it has been updated in the background. - NewItems(Vector>), + /// The content of a room's timeline was updated in the background. + NewItems { + /// The entire list of timeline items (events) for a room. + items: Vector>, + /// The index of the first item in the `items` list that has changed. + /// Any items before this index are assumed to be unchanged and need not be redrawn, + /// while any items after this index are assumed to be changed and must be redrawn. + index_of_first_change: usize, + }, /// A notice that the start of the timeline has been reached, meaning that /// there is no need to send further backwards pagination requests. TimelineStartReached, @@ -644,6 +650,11 @@ struct TimelineUiState { /// The list of items (events) in this room's timeline that our client currently knows about. items: Vector>, + /// The range of items that have been updated since the last time the timeline was drawn. + /// This range is set on each background update to ensure that no changes items are missed; + /// thus, it is a conservative estimate that may include more items than necessary. + _updated_items: RangeInclusive, + /// The channel receiver for timeline updates for this room. /// /// Here we use a synchronous (non-async) channel because the receiver runs @@ -714,6 +725,7 @@ impl TimelineRef { // We assume timelines being viewed for the first time haven't been fully paginated. fully_paginated: false, items: Vector::new(), + _updated_items: usize::MIN ..= usize::MAX, update_receiver, media_cache: MediaCache::new(MediaFormatConst::File), saved_state: SavedState::default(), @@ -750,12 +762,27 @@ impl Widget for Timeline { // 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 { + let portal_list = self.portal_list(id!(list)); let Some(tl) = self.tl_state.as_mut() else { return }; let mut done_loading = false; while let Ok(update) = tl.update_receiver.try_recv() { match update { - TimelineUpdate::NewItems(items) => { + TimelineUpdate::NewItems { items, index_of_first_change } => { + // Determine which item is currently visible the top of the screen + // so that we can jump back to that position instantly after applying this update. + let first_id = portal_list.first_id(); + if let Some(top_event_id) = tl.items.get(first_id).map(|item| item.unique_id()) { + for (idx, item) in items.iter().enumerate() { + if item.unique_id() == top_event_id { + log!("Timeline::handle_event(): jumping from top event index {first_id} to index {idx}"); + portal_list.set_first_id(idx); + break; + } + } + } tl.items = items; + + // TODO: use index_of_first_change } TimelineUpdate::TimelineStartReached => { log!("Timeline::handle_event(): timeline start reached for room {}", tl.room_id); @@ -958,7 +985,7 @@ fn populate_message_view( } else { live_id!(Message) }; - let item = list.item(cx, item_id, template).unwrap(); + let (item, _existed) = list.item_with_existed(cx, item_id, template).unwrap(); item.label(id!(content.message)).set_text(&text.body); item } @@ -984,7 +1011,7 @@ fn populate_message_view( } else { live_id!(ImageMessage) }; - let item = list.item(cx, item_id, template).unwrap(); + let (item, _existed) = list.item_with_existed(cx, item_id, template).unwrap(); let img_ref = item.image(id!(body.content.message)); if let Some(data) = media_cache.try_get_media_or_fetch(mxc_uri, None) { @@ -1166,7 +1193,8 @@ fn populate_membership_change_view( format!("denied {}'s request to join this room.", change_user_id), }; - let item = list.item(cx, item_id, live_id!(SmallStateEvent)).unwrap(); + let (item, _existed) = list.item_with_existed(cx, item_id, live_id!(SmallStateEvent)).unwrap(); + set_timestamp(&item, id!(left_container.timestamp), event_tl_item.timestamp()); let username = set_avatar_and_get_username( cx, @@ -1192,7 +1220,7 @@ fn populate_profile_change_view( event_tl_item: &EventTimelineItem, change: &MemberProfileChange, ) -> WidgetRef { - let item = list.item(cx, item_id, live_id!(SmallStateEvent)).unwrap(); + let (item, _existed) = list.item_with_existed(cx, item_id, live_id!(SmallStateEvent)).unwrap(); let username = set_avatar_and_get_username( cx, item.avatar(id!(avatar)), diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index d0bb2457..fb4ddf54 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -1,5 +1,6 @@ use anyhow::{Result, bail}; use clap::Parser; +use eyeball_im::VectorDiff; use futures_util::{StreamExt, pin_mut}; use makepad_widgets::{SignalToUI, error, log}; use matrix_sdk::{ @@ -25,7 +26,7 @@ use tokio::{ sync::mpsc::{UnboundedSender, UnboundedReceiver}, task::JoinHandle, }; use unicode_segmentation::UnicodeSegmentation; -use std::{sync::{OnceLock, Mutex, Arc}, collections::BTreeMap}; +use std::{cmp::min, collections::BTreeMap, sync::{Arc, Mutex, OnceLock}}; use url::Url; use crate::{home::{rooms_list::{self, RoomPreviewEntry, RoomsListUpdate, RoomPreviewAvatar, enqueue_rooms_list_update}, room_screen::TimelineUpdate}, media_cache::MediaCacheEntry, utils::MEDIA_THUMBNAIL_FORMAT}; @@ -685,22 +686,86 @@ async fn timeline_subscriber_handler( log!("Starting timeline subscriber for room {room_id}..."); let (mut timeline_items, mut subscriber) = timeline.subscribe_batched().await; - sender.send(TimelineUpdate::NewItems(timeline_items.clone())) - .expect("Error: timeline update sender couldn't send update with initial items!"); + sender.send(TimelineUpdate::NewItems { + items: timeline_items.clone(), + index_of_first_change: 0, + }).expect("Error: timeline update sender couldn't send update with initial items!"); + + const LOG_DIFFS: bool = false; while let Some(batch) = subscriber.next().await { let num_updates = batch.len(); + let mut index_of_first_change = usize::MAX; for diff in batch { - diff.apply(&mut timeline_items); + match diff { + VectorDiff::Append { values } => { + index_of_first_change = min(index_of_first_change, timeline_items.len()); + if LOG_DIFFS { log!("timeline_subscriber: diff Append {}, index_of_first_change: {index_of_first_change}", values.len()); } + timeline_items.extend(values); + } + VectorDiff::Clear => { + if LOG_DIFFS { log!("timeline_subscriber: diff Clear"); } + index_of_first_change = 0; + timeline_items.clear(); + } + VectorDiff::PushFront { value } => { + if LOG_DIFFS { log!("timeline_subscriber: diff PushFront"); } + index_of_first_change = 0; + timeline_items.push_front(value); + } + VectorDiff::PushBack { value } => { + if LOG_DIFFS { log!("timeline_subscriber: diff PushBack"); } + index_of_first_change = min(index_of_first_change, timeline_items.len()); + timeline_items.push_back(value); + } + VectorDiff::PopFront => { + if LOG_DIFFS { log!("timeline_subscriber: diff PopFront"); } + index_of_first_change = 0; + timeline_items.pop_front(); + } + VectorDiff::PopBack => { + if LOG_DIFFS { log!("timeline_subscriber: diff PopBack"); } + timeline_items.pop_back(); + index_of_first_change = min(index_of_first_change, timeline_items.len().saturating_sub(1)); + } + VectorDiff::Insert { index, value } => { + if LOG_DIFFS { log!("timeline_subscriber: diff Insert at {index}"); } + index_of_first_change = min(index_of_first_change, index); + timeline_items.insert(index, value); + } + VectorDiff::Set { index, value } => { + if LOG_DIFFS { log!("timeline_subscriber: diff Set at {index}"); } + index_of_first_change = min(index_of_first_change, index); + timeline_items.set(index, value); + } + VectorDiff::Remove { index } => { + if LOG_DIFFS { log!("timeline_subscriber: diff Remove at {index}"); } + index_of_first_change = min(index_of_first_change, index.saturating_sub(1)); + timeline_items.remove(index); + } + VectorDiff::Truncate { length } => { + if LOG_DIFFS { log!("timeline_subscriber: diff Truncate to length {length}"); } + index_of_first_change = min(index_of_first_change, length.saturating_sub(1)); + timeline_items.truncate(length); + } + VectorDiff::Reset { values } => { + if LOG_DIFFS { log!("timeline_subscriber: diff Reset, new length {}", values.len()); } + index_of_first_change = 0; // we must assume that all items have changed. + timeline_items = values; + } + } } if num_updates > 0 { - log!("Timeline::handle_event(): applied {num_updates} updates for room {room_id}"); + log!("timeline_subscriber: applied {num_updates} updates for room {room_id}; first change at index {index_of_first_change}."); + + sender.send(TimelineUpdate::NewItems { + items: timeline_items.clone(), + index_of_first_change, + }).expect("Error: timeline update sender couldn't send update with new items!"); + + // Send a Makepad-level signal to update this room's timeline UI view. + SignalToUI::set_ui_signal(); } - sender.send(TimelineUpdate::NewItems(timeline_items.clone())) - .expect("Error: timeline update sender couldn't send update with new items!"); - - // Send a Makepad-level signal to update this room's timeline UI view. - SignalToUI::set_ui_signal(); } error!("Error: unexpectedly ended timeline subscriber for room {room_id}.");