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