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