Skip to main content

matrix_sdk_ui/timeline/
pagination.rs

1// Copyright 2023 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::pin::pin;
16
17use async_rx::StreamExt as _;
18use async_stream::stream;
19use futures_core::Stream;
20use futures_util::StreamExt as _;
21use matrix_sdk::event_cache::{PaginationStatus, RoomPagination};
22use tracing::instrument;
23
24use super::Error;
25use crate::timeline::{PaginationError::NotSupported, controller::TimelineFocusKind};
26
27impl super::Timeline {
28    /// Add more events to the start of the timeline.
29    ///
30    /// Returns whether we hit the start of the timeline.
31    #[instrument(skip_all, fields(room_id = ?self.room().room_id()))]
32    pub async fn paginate_backwards(&self, mut num_events: u16) -> Result<bool, Error> {
33        match self.controller.focus() {
34            TimelineFocusKind::Live { event_cache, .. } => {
35                match self.controller.live_lazy_paginate_backwards(num_events).await {
36                    Some(needed_num_events) => {
37                        num_events = needed_num_events.try_into().expect(
38                            "failed to cast `needed_num_events` (`usize`) into `num_events` (`usize`)",
39                        );
40                    }
41                    None => {
42                        // We could adjust the skip count to a lower value,
43                        // while passing the requested number of events. We
44                        // _may_ have reached the start of the timeline, but
45                        // since we're fulfilling the caller's request, assume
46                        // it's not the case and return false here. A subsequent
47                        // call will go to the `Some()` arm of this match, and
48                        // cause a call to the event cache's pagination.
49                        return Ok(false);
50                    }
51                }
52
53                Ok(self.live_paginate_backwards(&event_cache.pagination(), num_events).await?)
54            }
55
56            TimelineFocusKind::Event { event_cache, .. } => {
57                Ok(event_cache.paginate_backwards(num_events).await?.hit_end_of_timeline)
58            }
59
60            TimelineFocusKind::Thread { event_cache, .. } => Ok(event_cache
61                .pagination()
62                .run_backwards_once(num_events)
63                .await
64                .map(|outcome| outcome.reached_start)?),
65
66            TimelineFocusKind::PinnedEvents { .. } => Err(Error::PaginationError(NotSupported)),
67        }
68    }
69
70    /// Add more events to the end of the timeline.
71    ///
72    /// Returns whether we hit the end of the timeline.
73    #[instrument(skip_all, fields(room_id = ?self.room().room_id()))]
74    pub async fn paginate_forwards(&self, num_events: u16) -> Result<bool, Error> {
75        match self.controller.focus() {
76            TimelineFocusKind::Live { .. } => Ok(true),
77
78            TimelineFocusKind::Event { event_cache, .. } => {
79                Ok(event_cache.paginate_forwards(num_events).await?.hit_end_of_timeline)
80            }
81
82            TimelineFocusKind::Thread { .. } | TimelineFocusKind::PinnedEvents { .. } => {
83                Err(Error::PaginationError(NotSupported))
84            }
85        }
86    }
87
88    /// Paginate backwards in live mode.
89    ///
90    /// This can only be called when the timeline is in live mode, not focused
91    /// on a specific event.
92    ///
93    /// Returns whether we hit the start of the timeline.
94    async fn live_paginate_backwards(
95        &self,
96        event_cache_pagination: &RoomPagination,
97        batch_size: u16,
98    ) -> Result<bool, Error> {
99        loop {
100            match event_cache_pagination.run_backwards_once(batch_size).await {
101                Ok(outcome) => {
102                    if outcome.reached_start {
103                        self.controller.insert_timeline_start_if_missing().await;
104                        return Ok(true);
105                    }
106
107                    if !outcome.events.is_empty() {
108                        return Ok(false);
109                    }
110
111                    // Fallthrough: as a special contract, restart pagination,
112                    // if it returned 0 events.
113                }
114
115                // Propagate errors as such.
116                Err(err) => return Err(err.into()),
117            }
118        }
119    }
120
121    /// Subscribe to the back-pagination status of a live timeline.
122    ///
123    /// This will return `None` if the timeline is in the focused mode.
124    ///
125    /// Note: this may send multiple Paginating/Idle sequences during a single
126    /// call to [`Self::paginate_backwards()`].
127    pub async fn live_back_pagination_status(
128        &self,
129    ) -> Option<(PaginationStatus, impl Stream<Item = PaginationStatus> + use<>)> {
130        let TimelineFocusKind::Live { event_cache, .. } = self.controller.focus() else {
131            return None;
132        };
133
134        let pagination = event_cache.pagination();
135
136        let mut status = pagination.status();
137
138        let current_value = self.controller.map_pagination_status(status.next_now()).await;
139
140        let controller = self.controller.clone();
141        let stream = Box::pin(stream! {
142            let status_stream = status.dedup();
143
144            let mut status_stream = pin!(status_stream);
145
146            while let Some(state) = status_stream.next().await {
147                let state = controller.map_pagination_status(state).await;
148
149                match state {
150                    PaginationStatus::Idle { hit_timeline_start } => {
151                        if hit_timeline_start {
152                            controller.insert_timeline_start_if_missing().await;
153                        }
154                    }
155                    PaginationStatus::Paginating => {}
156                }
157
158                yield state;
159            }
160        });
161
162        Some((current_value, stream))
163    }
164}