Skip to content

Commit 5eb10a3

Browse files
committed
timeline: reset pagination status if a live back-pagination is aborted
1 parent dc2b9ed commit 5eb10a3

File tree

3 files changed

+106
-19
lines changed

3 files changed

+106
-19
lines changed

crates/matrix-sdk-ui/src/timeline/pagination.rs

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,36 @@
1414

1515
use std::{fmt, ops::ControlFlow, sync::Arc, time::Duration};
1616

17-
use eyeball::Subscriber;
17+
use eyeball::{SharedObservable, Subscriber};
1818
use matrix_sdk::event_cache::{self, BackPaginationOutcome};
1919
use tracing::{instrument, trace, warn};
2020

2121
use super::Error;
2222
use crate::timeline::{event_item::RemoteEventOrigin, inner::TimelineEnd};
2323

24+
struct ResetStatusGuard {
25+
status: SharedObservable<PaginationStatus>,
26+
target: Option<PaginationStatus>,
27+
}
28+
29+
impl ResetStatusGuard {
30+
fn new(status: SharedObservable<PaginationStatus>, target: PaginationStatus) -> Self {
31+
Self { status, target: Some(target) }
32+
}
33+
34+
fn disarm(mut self) {
35+
self.target = None;
36+
}
37+
}
38+
39+
impl Drop for ResetStatusGuard {
40+
fn drop(&mut self) {
41+
if let Some(target) = self.target.take() {
42+
self.status.set_if_not_eq(target);
43+
}
44+
}
45+
}
46+
2447
impl super::Timeline {
2548
/// Add more events to the start of the timeline.
2649
///
@@ -77,6 +100,9 @@ impl super::Timeline {
77100
return Ok(false);
78101
}
79102

103+
let reset_status_guard =
104+
ResetStatusGuard::new(back_pagination_status.clone(), PaginationStatus::Idle);
105+
80106
// The first time, we allow to wait a bit for *a* back-pagination token to come
81107
// over via sync.
82108
const WAIT_FOR_TOKEN_TIMEOUT: Duration = Duration::from_secs(3);
@@ -104,6 +130,9 @@ impl super::Timeline {
104130
.await;
105131

106132
if reached_start {
133+
// Don't reset the status to `Idle`…
134+
reset_status_guard.disarm();
135+
// …and set it to `TimelineEndReached` instead.
107136
back_pagination_status
108137
.set_if_not_eq(PaginationStatus::TimelineEndReached);
109138
return Ok(true);
@@ -142,7 +171,8 @@ impl super::Timeline {
142171
}
143172
}
144173

145-
back_pagination_status.set_if_not_eq(PaginationStatus::Idle);
174+
// The status is automatically reset to idle by `reset_status_guard`.
175+
146176
Ok(false)
147177
}
148178

crates/matrix-sdk-ui/tests/integration/timeline/pagination.rs

Lines changed: 71 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ use std::{sync::Arc, time::Duration};
1616

1717
use assert_matches2::assert_let;
1818
use eyeball_im::VectorDiff;
19-
use futures_util::future::{join, join3};
19+
use futures_util::{
20+
future::{join, join3},
21+
FutureExt,
22+
};
2023
use matrix_sdk::{config::SyncSettings, test_utils::logged_in_client_with_server};
2124
use matrix_sdk_test::{
2225
async_test, EventBuilder, JoinedRoomBuilder, StateTestEvent, SyncResponseBuilder, ALICE, BOB,
@@ -35,7 +38,10 @@ use ruma::{
3538
};
3639
use serde_json::{json, Value as JsonValue};
3740
use stream_assert::{assert_next_eq, assert_next_matches};
38-
use tokio::time::{sleep, timeout};
41+
use tokio::{
42+
spawn,
43+
time::{sleep, timeout},
44+
};
3945
use wiremock::{
4046
matchers::{header, method, path_regex, query_param, query_param_is_missing},
4147
Mock, ResponseTemplate,
@@ -49,10 +55,10 @@ async fn test_back_pagination() {
4955
let (client, server) = logged_in_client_with_server().await;
5056
let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000));
5157

52-
let mut ev_builder = SyncResponseBuilder::new();
53-
ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id));
58+
let mut sync_builder = SyncResponseBuilder::new();
59+
sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id));
5460

55-
mock_sync(&server, ev_builder.build_json_sync_response(), None).await;
61+
mock_sync(&server, sync_builder.build_json_sync_response(), None).await;
5662
let _response = client.sync_once(sync_settings.clone()).await.unwrap();
5763
server.reset().await;
5864

@@ -140,16 +146,16 @@ async fn test_back_pagination_highlighted() {
140146
let (client, server) = logged_in_client_with_server().await;
141147
let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000));
142148

143-
let mut ev_builder = SyncResponseBuilder::new();
144-
ev_builder
149+
let mut sync_builder = SyncResponseBuilder::new();
150+
sync_builder
145151
// We need the member event and power levels locally so the push rules processor works.
146152
.add_joined_room(
147153
JoinedRoomBuilder::new(room_id)
148154
.add_state_event(StateTestEvent::Member)
149155
.add_state_event(StateTestEvent::PowerLevels),
150156
);
151157

152-
mock_sync(&server, ev_builder.build_json_sync_response(), None).await;
158+
mock_sync(&server, sync_builder.build_json_sync_response(), None).await;
153159
let _response = client.sync_once(sync_settings.clone()).await.unwrap();
154160
server.reset().await;
155161

@@ -522,10 +528,10 @@ async fn test_empty_chunk() {
522528
let (client, server) = logged_in_client_with_server().await;
523529
let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000));
524530

525-
let mut ev_builder = SyncResponseBuilder::new();
526-
ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id));
531+
let mut sync_builder = SyncResponseBuilder::new();
532+
sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id));
527533

528-
mock_sync(&server, ev_builder.build_json_sync_response(), None).await;
534+
mock_sync(&server, sync_builder.build_json_sync_response(), None).await;
529535
let _response = client.sync_once(sync_settings.clone()).await.unwrap();
530536
server.reset().await;
531537

@@ -612,10 +618,10 @@ async fn test_until_num_items_with_empty_chunk() {
612618
let (client, server) = logged_in_client_with_server().await;
613619
let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000));
614620

615-
let mut ev_builder = SyncResponseBuilder::new();
616-
ev_builder.add_joined_room(JoinedRoomBuilder::new(room_id));
621+
let mut sync_builder = SyncResponseBuilder::new();
622+
sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id));
617623

618-
mock_sync(&server, ev_builder.build_json_sync_response(), None).await;
624+
mock_sync(&server, sync_builder.build_json_sync_response(), None).await;
619625
let _response = client.sync_once(sync_settings.clone()).await.unwrap();
620626
server.reset().await;
621627

@@ -719,3 +725,54 @@ async fn test_until_num_items_with_empty_chunk() {
719725
assert!(day_divider.is_day_divider());
720726
assert_next_matches!(timeline_stream, VectorDiff::Remove { index: 2 });
721727
}
728+
729+
#[async_test]
730+
async fn test_back_pagination_aborted() {
731+
let room_id = room_id!("!a98sd12bjh:example.org");
732+
let (client, server) = logged_in_client_with_server().await;
733+
let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000));
734+
735+
let mut sync_builder = SyncResponseBuilder::new();
736+
sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id));
737+
738+
mock_sync(&server, sync_builder.build_json_sync_response(), None).await;
739+
let _response = client.sync_once(sync_settings.clone()).await.unwrap();
740+
server.reset().await;
741+
742+
let room = client.get_room(room_id).unwrap();
743+
let timeline = Arc::new(room.timeline().await.unwrap());
744+
let mut back_pagination_status = timeline.back_pagination_status();
745+
746+
// Delay the server response, so we have time to abort the request.
747+
Mock::given(method("GET"))
748+
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/messages$"))
749+
.and(header("authorization", "Bearer 1234"))
750+
.respond_with(
751+
ResponseTemplate::new(200)
752+
.set_body_json(&*ROOM_MESSAGES_BATCH_1)
753+
.set_delay(Duration::from_secs(5)),
754+
)
755+
.mount(&server)
756+
.await;
757+
758+
let paginate = spawn({
759+
let timeline = timeline.clone();
760+
async move {
761+
timeline.live_paginate_backwards(PaginationOptions::simple_request(10)).await.unwrap();
762+
}
763+
});
764+
765+
assert_eq!(back_pagination_status.next().await, Some(PaginationStatus::Paginating));
766+
767+
// Abort the pagination!
768+
paginate.abort();
769+
770+
// The task should finish with a cancellation.
771+
assert!(paginate.await.unwrap_err().is_cancelled());
772+
773+
// The timeline should automatically reset to idle.
774+
assert_next_eq!(back_pagination_status, PaginationStatus::Idle);
775+
776+
// And there should be no other pending pagination status updates.
777+
assert!(back_pagination_status.next().now_or_never().is_none());
778+
}

crates/matrix-sdk/src/event_cache/paginator.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ struct ResetStateGuard {
144144

145145
impl ResetStateGuard {
146146
/// Create a new reset state guard.
147-
fn new(target: PaginatorState, state: SharedObservable<PaginatorState>) -> Self {
147+
fn new(state: SharedObservable<PaginatorState>, target: PaginatorState) -> Self {
148148
Self { target: Some(target), state }
149149
}
150150

@@ -210,7 +210,7 @@ impl Paginator {
210210
});
211211
}
212212

213-
let reset_state_guard = ResetStateGuard::new(PaginatorState::Initial, self.state.clone());
213+
let reset_state_guard = ResetStateGuard::new(self.state.clone(), PaginatorState::Initial);
214214

215215
// TODO: do we want to lazy load members?
216216
let lazy_load_members = true;
@@ -307,7 +307,7 @@ impl Paginator {
307307
});
308308
}
309309

310-
let reset_state_guard = ResetStateGuard::new(PaginatorState::Idle, self.state.clone());
310+
let reset_state_guard = ResetStateGuard::new(self.state.clone(), PaginatorState::Idle);
311311

312312
let mut options = MessagesOptions::new(dir).from(token.as_deref());
313313
options.limit = num_events;

0 commit comments

Comments
 (0)