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, 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    pinned_events_loader::PinnedEventsRoom,
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
67impl RoomExt for Room {
68    async fn timeline(&self) -> Result<Timeline, timeline::Error> {
69        self.timeline_builder().build().await
70    }
71
72    fn timeline_builder(&self) -> TimelineBuilder {
73        TimelineBuilder::new(self)
74            .track_read_marker_and_receipts(TimelineReadReceiptTracking::AllEvents)
75    }
76
77    async fn latest_event(&self) -> LatestEventValue {
78        LatestEventValue::from_base_latest_event_value(
79            (**self).latest_event(),
80            self,
81            &self.client(),
82        )
83        .await
84    }
85}
86
87pub(super) trait RoomDataProvider:
88    Clone + PaginableRoom + PaginableThread + PinnedEventsRoom + 'static
89{
90    fn own_user_id(&self) -> &UserId;
91    fn room_version_rules(&self) -> RoomVersionRules;
92
93    fn crypto_context_info(&self)
94    -> impl Future<Output = CryptoContextInfo> + SendOutsideWasm + '_;
95
96    fn profile_from_user_id<'a>(
97        &'a self,
98        user_id: &'a UserId,
99    ) -> impl Future<Output = Option<Profile>> + SendOutsideWasm + 'a;
100
101    /// Loads a user receipt from the storage backend.
102    fn load_user_receipt<'a>(
103        &'a self,
104        receipt_type: ReceiptType,
105        thread: ReceiptThread,
106        user_id: &'a UserId,
107    ) -> impl Future<Output = Option<(OwnedEventId, Receipt)>> + SendOutsideWasm + 'a;
108
109    /// Loads read receipts for an event from the storage backend.
110    fn load_event_receipts<'a>(
111        &'a self,
112        event_id: &'a EventId,
113        receipt_thread: ReceiptThread,
114    ) -> impl Future<Output = IndexMap<OwnedUserId, Receipt>> + SendOutsideWasm + 'a;
115
116    /// Load the current fully-read event id, from storage.
117    fn load_fully_read_marker(&self) -> impl Future<Output = Option<OwnedEventId>> + '_;
118
119    /// Send an event to that room.
120    fn send(
121        &self,
122        content: AnyMessageLikeEventContent,
123    ) -> impl Future<Output = Result<(), super::Error>> + SendOutsideWasm + '_;
124
125    /// Redact an event from that room.
126    fn redact<'a>(
127        &'a self,
128        event_id: &'a EventId,
129        reason: Option<&'a str>,
130        transaction_id: Option<OwnedTransactionId>,
131    ) -> impl Future<Output = Result<(), super::Error>> + SendOutsideWasm + 'a;
132
133    fn room_info(&self) -> Subscriber<RoomInfo>;
134
135    /// Loads an event from the cache or network.
136    fn load_event<'a>(
137        &'a self,
138        event_id: &'a EventId,
139    ) -> impl Future<Output = Result<TimelineEvent>> + SendOutsideWasm + 'a;
140}
141
142impl RoomDataProvider for Room {
143    fn own_user_id(&self) -> &UserId {
144        (**self).own_user_id()
145    }
146
147    fn room_version_rules(&self) -> RoomVersionRules {
148        (**self).clone_info().room_version_rules_or_default()
149    }
150
151    async fn crypto_context_info(&self) -> CryptoContextInfo {
152        self.crypto_context_info().await
153    }
154
155    async fn profile_from_user_id<'a>(&'a self, user_id: &'a UserId) -> Option<Profile> {
156        match self.get_member_no_sync(user_id).await {
157            Ok(Some(member)) => Some(Profile {
158                display_name: member.display_name().map(ToOwned::to_owned),
159                display_name_ambiguous: member.name_ambiguous(),
160                avatar_url: member.avatar_url().map(ToOwned::to_owned),
161            }),
162            Ok(None) if self.are_members_synced() => Some(Profile::default()),
163            Ok(None) => None,
164            Err(e) => {
165                error!(%user_id, "Failed to fetch room member information: {e}");
166                None
167            }
168        }
169    }
170
171    async fn load_user_receipt<'a>(
172        &'a self,
173        receipt_type: ReceiptType,
174        thread: ReceiptThread,
175        user_id: &'a UserId,
176    ) -> Option<(OwnedEventId, Receipt)> {
177        match self.load_user_receipt(receipt_type.clone(), thread.clone(), user_id).await {
178            Ok(receipt) => receipt,
179            Err(e) => {
180                error!(
181                    ?receipt_type,
182                    ?thread,
183                    ?user_id,
184                    "Failed to get read receipt for user: {e}"
185                );
186                None
187            }
188        }
189    }
190
191    async fn load_event_receipts<'a>(
192        &'a self,
193        event_id: &'a EventId,
194        receipt_thread: ReceiptThread,
195    ) -> IndexMap<OwnedUserId, Receipt> {
196        match self.load_event_receipts(ReceiptType::Read, receipt_thread.clone(), event_id).await {
197            Ok(receipts) => receipts.into_iter().collect(),
198            Err(e) => {
199                error!(?event_id, ?receipt_thread, "Failed to get read receipts for event: {e}");
200                IndexMap::new()
201            }
202        }
203    }
204
205    async fn load_fully_read_marker(&self) -> Option<OwnedEventId> {
206        match self.account_data_static::<FullyReadEventContent>().await {
207            Ok(Some(fully_read)) => match fully_read.deserialize() {
208                Ok(fully_read) => Some(fully_read.content.event_id),
209                Err(e) => {
210                    error!("Failed to deserialize fully-read account data: {e}");
211                    None
212                }
213            },
214            Err(e) => {
215                error!("Failed to get fully-read account data from the store: {e}");
216                None
217            }
218            _ => None,
219        }
220    }
221
222    async fn send(&self, content: AnyMessageLikeEventContent) -> Result<(), super::Error> {
223        let _ = self.send_queue().send(content).await?;
224        Ok(())
225    }
226
227    async fn redact<'a>(
228        &'a self,
229        event_id: &'a EventId,
230        reason: Option<&'a str>,
231        transaction_id: Option<OwnedTransactionId>,
232    ) -> Result<(), super::Error> {
233        let _ = self
234            .redact(event_id, reason, transaction_id)
235            .await
236            .map_err(RedactError::HttpError)
237            .map_err(super::Error::RedactError)?;
238        Ok(())
239    }
240
241    fn room_info(&self) -> Subscriber<RoomInfo> {
242        self.subscribe_info()
243    }
244
245    async fn load_event<'a>(&'a self, event_id: &'a EventId) -> Result<TimelineEvent> {
246        self.load_or_fetch_event(event_id, None).await
247    }
248}