Skip to main content

matrix_sdk_ui/timeline/
traits.rs

1// Copyright 2025 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::future::Future;
16
17use eyeball::Subscriber;
18use indexmap::IndexMap;
19use matrix_sdk::{
20    Result, Room, SendOutsideWasm,
21    deserialized_responses::TimelineEvent,
22    paginators::{PaginableRoom, thread::PaginableThread},
23};
24use matrix_sdk_base::{RoomInfo, crypto::types::events::CryptoContextInfo};
25use ruma::{
26    EventId, OwnedEventId, OwnedTransactionId, OwnedUserId, RoomId, UserId,
27    events::{
28        AnyMessageLikeEventContent,
29        fully_read::FullyReadEventContent,
30        receipt::{Receipt, ReceiptThread, ReceiptType},
31    },
32    room_version_rules::RoomVersionRules,
33};
34use tracing::error;
35
36use super::{Profile, RedactError, TimelineBuilder};
37use crate::timeline::{
38    self, Timeline, TimelineReadReceiptTracking, latest_event::LatestEventValue,
39    thread_list_service::ThreadListService,
40};
41
42pub trait RoomExt {
43    /// Get a [`Timeline`] for this room.
44    ///
45    /// This offers a higher-level API than event handlers, in treating things
46    /// like edits and reactions as updates of existing items rather than new
47    /// independent events.
48    ///
49    /// This is the same as using `room.timeline_builder().build()`.
50    fn timeline(&self)
51    -> impl Future<Output = Result<Timeline, timeline::Error>> + SendOutsideWasm;
52
53    /// Get a [`TimelineBuilder`] for this room.
54    ///
55    /// [`Timeline`] offers a higher-level API than event handlers, in treating
56    /// things like edits and reactions as updates of existing items rather
57    /// than new independent events.
58    ///
59    /// This allows to customize settings of the [`Timeline`] before
60    /// constructing it.
61    fn timeline_builder(&self) -> TimelineBuilder;
62
63    /// Return a [`LatestEventValue`] corresponding to this room's latest event.
64    fn latest_event(&self) -> impl Future<Output = LatestEventValue>;
65
66    /// Create a [`ThreadListService`] for this room.
67    ///
68    /// The returned service provides a paginated, observable list of thread
69    /// roots for the room and can be used to page through threads and
70    /// subscribe to updates.
71    fn thread_list_service(&self) -> ThreadListService;
72}
73
74impl RoomExt for Room {
75    async fn timeline(&self) -> Result<Timeline, timeline::Error> {
76        self.timeline_builder().build().await
77    }
78
79    fn timeline_builder(&self) -> TimelineBuilder {
80        TimelineBuilder::new(self)
81            .track_read_marker_and_receipts(TimelineReadReceiptTracking::AllEvents)
82    }
83
84    async fn latest_event(&self) -> LatestEventValue {
85        LatestEventValue::from_base_latest_event_value(
86            (**self).latest_event(),
87            self,
88            &self.client(),
89        )
90        .await
91    }
92
93    fn thread_list_service(&self) -> ThreadListService {
94        ThreadListService::new(self.clone())
95    }
96}
97
98pub(super) trait RoomDataProvider:
99    Clone + PaginableRoom + PaginableThread + 'static
100{
101    fn room_id(&self) -> &RoomId;
102    fn own_user_id(&self) -> &UserId;
103    fn room_version_rules(&self) -> RoomVersionRules;
104
105    fn crypto_context_info(&self)
106    -> impl Future<Output = CryptoContextInfo> + SendOutsideWasm + '_;
107
108    fn profile_from_user_id<'a>(
109        &'a self,
110        user_id: &'a UserId,
111    ) -> impl Future<Output = Option<Profile>> + SendOutsideWasm + 'a;
112
113    /// Loads a user receipt from the storage backend.
114    fn load_user_receipt<'a>(
115        &'a self,
116        receipt_type: ReceiptType,
117        thread: &'a ReceiptThread,
118        user_id: &'a UserId,
119    ) -> impl Future<Output = Option<(OwnedEventId, Receipt)>> + SendOutsideWasm + 'a;
120
121    /// Loads read receipts for an event from the storage backend.
122    fn load_event_receipts<'a>(
123        &'a self,
124        event_id: &'a EventId,
125        receipt_thread: &'a ReceiptThread,
126    ) -> impl Future<Output = IndexMap<OwnedUserId, Receipt>> + SendOutsideWasm + 'a;
127
128    /// Load the current fully-read event id, from storage.
129    fn load_fully_read_marker(&self) -> impl Future<Output = Option<OwnedEventId>> + '_;
130
131    /// Send an event to that room, merging `extra_content`'s fields into the
132    /// outgoing event's content, if provided.
133    fn send(
134        &self,
135        content: AnyMessageLikeEventContent,
136        extra_content: Option<serde_json::Map<String, serde_json::Value>>,
137    ) -> impl Future<Output = Result<(), super::Error>> + SendOutsideWasm + '_;
138
139    /// Redact an event from that room.
140    fn redact<'a>(
141        &'a self,
142        event_id: &'a EventId,
143        reason: Option<&'a str>,
144        transaction_id: Option<OwnedTransactionId>,
145    ) -> impl Future<Output = Result<(), super::Error>> + SendOutsideWasm + 'a;
146
147    fn room_info(&self) -> Subscriber<RoomInfo>;
148
149    /// Loads an event from the cache or network.
150    fn load_event<'a>(
151        &'a self,
152        event_id: &'a EventId,
153    ) -> impl Future<Output = Result<TimelineEvent>> + SendOutsideWasm + 'a;
154}
155
156impl RoomDataProvider for Room {
157    fn room_id(&self) -> &RoomId {
158        (**self).room_id()
159    }
160
161    fn own_user_id(&self) -> &UserId {
162        (**self).own_user_id()
163    }
164
165    fn room_version_rules(&self) -> RoomVersionRules {
166        (**self).clone_info().room_version_rules_or_default()
167    }
168
169    async fn crypto_context_info(&self) -> CryptoContextInfo {
170        self.crypto_context_info().await
171    }
172
173    async fn profile_from_user_id<'a>(&'a self, user_id: &'a UserId) -> Option<Profile> {
174        Profile::load(self, user_id).await
175    }
176
177    async fn load_user_receipt<'a>(
178        &'a self,
179        receipt_type: ReceiptType,
180        receipt_thread: &'a ReceiptThread,
181        user_id: &'a UserId,
182    ) -> Option<(OwnedEventId, Receipt)> {
183        match self.load_user_receipt(receipt_type.clone(), receipt_thread, user_id).await {
184            Ok(receipt) => receipt,
185            Err(e) => {
186                error!(
187                    ?receipt_type,
188                    ?receipt_thread,
189                    ?user_id,
190                    "Failed to get read receipt for user: {e}"
191                );
192                None
193            }
194        }
195    }
196
197    async fn load_event_receipts<'a>(
198        &'a self,
199        event_id: &'a EventId,
200        receipt_thread: &'a ReceiptThread,
201    ) -> IndexMap<OwnedUserId, Receipt> {
202        match self.load_event_receipts(ReceiptType::Read, receipt_thread, event_id).await {
203            Ok(receipts) => receipts.into_iter().collect(),
204            Err(e) => {
205                error!(?event_id, ?receipt_thread, "Failed to get read receipts for event: {e}");
206                IndexMap::new()
207            }
208        }
209    }
210
211    async fn load_fully_read_marker(&self) -> Option<OwnedEventId> {
212        match self.account_data_static::<FullyReadEventContent>().await {
213            Ok(Some(fully_read)) => match fully_read.deserialize() {
214                Ok(fully_read) => Some(fully_read.content.event_id),
215                Err(e) => {
216                    error!("Failed to deserialize fully-read account data: {e}");
217                    None
218                }
219            },
220            Err(e) => {
221                error!("Failed to get fully-read account data from the store: {e}");
222                None
223            }
224            _ => None,
225        }
226    }
227
228    async fn send(
229        &self,
230        content: AnyMessageLikeEventContent,
231        extra_content: Option<serde_json::Map<String, serde_json::Value>>,
232    ) -> Result<(), super::Error> {
233        let queue = self.send_queue();
234        let send = queue.send(content);
235        let send = match extra_content {
236            Some(extra_content) => send.with_extra_content(extra_content),
237            None => send,
238        };
239        let _ = send.await?;
240        Ok(())
241    }
242
243    async fn redact<'a>(
244        &'a self,
245        event_id: &'a EventId,
246        reason: Option<&'a str>,
247        transaction_id: Option<OwnedTransactionId>,
248    ) -> Result<(), super::Error> {
249        let _ = self
250            .redact(event_id, reason, transaction_id)
251            .await
252            .map_err(RedactError::HttpError)
253            .map_err(super::Error::RedactError)?;
254        Ok(())
255    }
256
257    fn room_info(&self) -> Subscriber<RoomInfo> {
258        self.subscribe_info()
259    }
260
261    async fn load_event<'a>(&'a self, event_id: &'a EventId) -> Result<TimelineEvent> {
262        self.load_or_fetch_event(event_id, None).await
263    }
264}