Skip to content

fix(ui): Introduce Timeline regions #5000

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
c012d12
fix(ui): Offset the timeline index in the presence of a `TimelineStart`.
Hywan May 5, 2025
b3ca0a4
test: Add assert messages in the `assert_timeline_stream` macro.
Hywan May 5, 2025
cfc50dc
test(ui): Support `index [$nth] --- date divider ---` in `assert_time…
Hywan May 6, 2025
3871ddf
test(ui): Add a regression test.
Hywan May 6, 2025
d30dfec
refactor(ui): Add `ObservableItemsTransaction::push_timeline_start_if…
Hywan May 6, 2025
0b73f52
refactor(ui): Add `ObservableItemsTransaction::push_local`.
Hywan May 6, 2025
a5d9eec
refactor(ui): Add `ObservableItemsTransaction::has_local`.
Hywan May 6, 2025
f8967b2
chore(base): Move the `bitflags` dependency in the workspace.
Hywan May 7, 2025
43dbce5
feat(ui): Define _regions_ in the `Timeline`.
Hywan May 7, 2025
af465fc
refactor(ui): `DateDividerAdjuster` works on _remotes_ and _locals_ r…
Hywan May 7, 2025
ec09b40
refactor(ui): `ReadReceiptTimelineUpdate` works on _remotes_ region.
Hywan May 7, 2025
9092f44
refactor(ui): `TimelineMetadata` works on the _remotes_ region.
Hywan May 7, 2025
52b8ac0
refactor(ui): `TimelineStateTransaction` works on _remotes_ and _all_…
Hywan May 7, 2025
0122d11
refactor(ui): `EventHandler` uses regions to improve the code and avo…
Hywan May 7, 2025
29ae0eb
chore(ui): Make Clippy happy.
Hywan May 12, 2025
d108829
doc(ui): Add #5000 in the `CHANGELOG.md`.
Hywan May 12, 2025
b7149b0
test(ui): Add tests for `push_local` and `push_date_divider`.
Hywan May 12, 2025
4b0a2e6
doc(ui): Fix a typo in a comment.
Hywan May 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ async-stream = "0.3.5"
async-trait = "0.1.85"
as_variant = "1.3.0"
base64 = "0.22.1"
bitflags = "2.8.0"
byteorder = "1.5.0"
chrono = "0.4.39"
eyeball = { version = "0.8.8", features = ["tracing"] }
Expand Down
2 changes: 1 addition & 1 deletion crates/matrix-sdk-base/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ as_variant = { workspace = true }
assert_matches = { workspace = true, optional = true }
assert_matches2 = { workspace = true, optional = true }
async-trait = { workspace = true }
bitflags = { version = "2.8.0", features = ["serde"] }
bitflags = { workspace = true, features = ["serde"] }
decancer = "3.2.8"
eyeball = { workspace = true, features = ["async-lock"] }
eyeball-im = { workspace = true }
Expand Down
13 changes: 10 additions & 3 deletions crates/matrix-sdk-ui/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,29 @@ All notable changes to this project will be documented in this file.

## [Unreleased] - ReleaseDate

### Bug Fixes

- Introduce `Timeline` regions, which helps to remove a class of bugs in the
`Timeline` where items could be inserted in the wrong _regions_, such as
a remote timeline item before the `TimelineStart` virtual timeline item.
([#5000](https://github.com/matrix-org/matrix-rust-sdk/pull/5000))

## [0.11.0] - 2025-04-11

### Bug Fixes

### Features

- [**breaking**] Optionally allow starting threads with `Timeline::send_reply`.
([4819](https://github.com/matrix-org/matrix-rust-sdk/pull/4819))
([#4819](https://github.com/matrix-org/matrix-rust-sdk/pull/4819))
- [**breaking**] Push `RepliedToInfo`, `ReplyContent`, `EnforceThread` and
`UnsupportedReplyItem` (becoming `ReplyError`) down into matrix_sdk.
[`Timeline::send_reply()`] now takes an event ID rather than a `RepliedToInfo`.
`Timeline::replied_to_info_from_event_id` has been made private in `matrix_sdk`.
([4842](https://github.com/matrix-org/matrix-rust-sdk/pull/4842))
([#4842](https://github.com/matrix-org/matrix-rust-sdk/pull/4842))
- Allow sending media as (thread) replies. The reply behaviour can be configured
through new fields on [`AttachmentConfig`].
([4852](https://github.com/matrix-org/matrix-rust-sdk/pull/4852))
([#4852](https://github.com/matrix-org/matrix-rust-sdk/pull/4852))

### Refactor

Expand Down
1 change: 1 addition & 0 deletions crates/matrix-sdk-ui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ async_cell = "0.2.2"
async-once-cell = "0.5.4"
async-rx = { workspace = true }
async-stream = { workspace = true }
bitflags = { workspace = true }
chrono = { workspace = true }
eyeball = { workspace = true }
eyeball-im = { workspace = true }
Expand Down
39 changes: 21 additions & 18 deletions crates/matrix-sdk-ui/src/timeline/controller/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,7 @@ use ruma::{EventId, OwnedEventId, OwnedUserId, RoomVersionId};
use tracing::trace;

use super::{
super::{
rfind_event_by_id, subscriber::skip::SkipCount, TimelineItem, TimelineItemKind,
TimelineUniqueId,
},
super::{subscriber::skip::SkipCount, TimelineItem, TimelineItemKind, TimelineUniqueId},
read_receipts::ReadReceipts,
Aggregations, AllRemoteEvents, ObservableItemsTransaction, PendingEdit,
};
Expand Down Expand Up @@ -196,37 +193,43 @@ impl TimelineMetadata {
let Some(fully_read_event) = &self.fully_read_event else { return };
trace!(?fully_read_event, "Updating read marker");

let read_marker_idx = items.iter().rposition(|item| item.is_read_marker());
let read_marker_idx = items
.iter_remotes_region()
.rev()
.find_map(|(idx, item)| item.is_read_marker().then_some(idx));

let mut fully_read_event_idx =
rfind_event_by_id(items, fully_read_event).map(|(idx, _)| idx);
let mut fully_read_event_idx = items.iter_remotes_region().rev().find_map(|(idx, item)| {
(item.as_event()?.event_id() == Some(fully_read_event)).then_some(idx)
});

if let Some(i) = &mut fully_read_event_idx {
if let Some(fully_read_event_idx) = &mut fully_read_event_idx {
// The item at position `i` is the first item that's fully read, we're about to
// insert a read marker just after it.
//
// Do another forward pass to skip all the events we've sent too.

// Find the position of the first element…
let next = items
.iter()
.enumerate()
.iter_remotes_region()
// …strictly *after* the fully read event…
.skip(*i + 1)
.skip_while(|(idx, _)| idx <= fully_read_event_idx)
// …that's not virtual and not sent by us…
.find(|(_, item)| {
item.as_event().is_some_and(|event| event.sender() != self.own_user_id)
})
.map(|(i, _)| i);
.find_map(|(idx, item)| {
(item.as_event()?.sender() != self.own_user_id).then_some(idx)
});

if let Some(next) = next {
// `next` point to the first item that's not sent by us, so the *previous* of
// next is the right place where to insert the fully read marker.
*i = next.wrapping_sub(1);
*fully_read_event_idx = next.wrapping_sub(1);
} else {
// There's no event after the read marker that's not sent by us, i.e. the full
// timeline has been read: the fully read marker goes to the end.
*i = items.len().wrapping_sub(1);
// timeline has been read: the fully read marker goes to the end, even after the
// local timeline items.
//
// TODO (@hywan): Should we introduce a `items.position_of_last_remote()` to
// insert before the local timeline items?
*fully_read_event_idx = items.len().wrapping_sub(1);
}
}

Expand Down
7 changes: 3 additions & 4 deletions crates/matrix-sdk-ui/src/timeline/controller/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1318,10 +1318,9 @@ impl<P: RoomDataProvider, D: Decryptor> TimelineController<P, D> {
pub async fn insert_timeline_start_if_missing(&self) {
let mut state = self.state.write().await;
let mut txn = state.transaction();
if txn.items.get(0).is_some_and(|item| item.is_timeline_start()) {
return;
}
txn.items.push_front(txn.meta.new_timeline_item(VirtualTimelineItem::TimelineStart), None);
txn.items.push_timeline_start_if_missing(
txn.meta.new_timeline_item(VirtualTimelineItem::TimelineStart),
);
txn.commit();
}
}
Expand Down
Loading
Loading