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::{
25    RoomInfo, crypto::types::events::CryptoContextInfo, latest_event::LatestEvent,
26};
27use ruma::{
28    EventId, OwnedEventId, OwnedTransactionId, OwnedUserId, UserId,
29    events::{
30        AnyMessageLikeEventContent,
31        fully_read::FullyReadEventContent,
32        receipt::{Receipt, ReceiptThread, ReceiptType},
33    },
34    room_version_rules::RoomVersionRules,
35};
36use tracing::error;
37
38use super::{EventTimelineItem, Profile, RedactError, TimelineBuilder};
39use crate::timeline::{
40    self, Timeline, latest_event::LatestEventValue, pinned_events_loader::PinnedEventsRoom,
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 an optional [`EventTimelineItem`] corresponding to this room's
65    /// latest event.
66    fn latest_event_item(
67        &self,
68    ) -> impl Future<Output = Option<EventTimelineItem>> + SendOutsideWasm;
69
70    /// Return a [`LatestEventValue`] corresponding to this room's latest event.
71    fn new_latest_event(&self) -> impl Future<Output = LatestEventValue>;
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).track_read_marker_and_receipts()
81    }
82
83    async fn latest_event_item(&self) -> Option<EventTimelineItem> {
84        if let Some(latest_event) = self.latest_event() {
85            EventTimelineItem::from_latest_event(self.client(), self.room_id(), latest_event).await
86        } else {
87            None
88        }
89    }
90
91    async fn new_latest_event(&self) -> LatestEventValue {
92        LatestEventValue::from_base_latest_event_value(
93            (**self).new_latest_event(),
94            self,
95            &self.client(),
96        )
97        .await
98    }
99}
100
101pub(super) trait RoomDataProvider:
102    Clone + PaginableRoom + PaginableThread + PinnedEventsRoom + 'static
103{
104    fn own_user_id(&self) -> &UserId;
105    fn room_version_rules(&self) -> RoomVersionRules;
106
107    fn crypto_context_info(&self)
108    -> impl Future<Output = CryptoContextInfo> + SendOutsideWasm + '_;
109
110    fn profile_from_user_id<'a>(
111        &'a self,
112        user_id: &'a UserId,
113    ) -> impl Future<Output = Option<Profile>> + SendOutsideWasm + 'a;
114    fn profile_from_latest_event(&self, latest_event: &LatestEvent) -> Option<Profile>;
115
116    /// Loads a user receipt from the storage backend.
117    fn load_user_receipt<'a>(
118        &'a self,
119        receipt_type: ReceiptType,
120        thread: ReceiptThread,
121        user_id: &'a UserId,
122    ) -> impl Future<Output = Option<(OwnedEventId, Receipt)>> + SendOutsideWasm + 'a;
123
124    /// Loads read receipts for an event from the storage backend.
125    fn load_event_receipts<'a>(
126        &'a self,
127        event_id: &'a EventId,
128        receipt_thread: ReceiptThread,
129    ) -> impl Future<Output = IndexMap<OwnedUserId, Receipt>> + SendOutsideWasm + 'a;
130
131    /// Load the current fully-read event id, from storage.
132    fn load_fully_read_marker(&self) -> impl Future<Output = Option<OwnedEventId>> + '_;
133
134    /// Send an event to that room.
135    fn send(
136        &self,
137        content: AnyMessageLikeEventContent,
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
157impl RoomDataProvider for Room {
158    fn own_user_id(&self) -> &UserId {
159        (**self).own_user_id()
160    }
161
162    fn room_version_rules(&self) -> RoomVersionRules {
163        (**self).clone_info().room_version_rules_or_default()
164    }
165
166    async fn crypto_context_info(&self) -> CryptoContextInfo {
167        self.crypto_context_info().await
168    }
169
170    async fn profile_from_user_id<'a>(&'a self, user_id: &'a UserId) -> Option<Profile> {
171        match self.get_member_no_sync(user_id).await {
172            Ok(Some(member)) => Some(Profile {
173                display_name: member.display_name().map(ToOwned::to_owned),
174                display_name_ambiguous: member.name_ambiguous(),
175                avatar_url: member.avatar_url().map(ToOwned::to_owned),
176            }),
177            Ok(None) if self.are_members_synced() => Some(Profile::default()),
178            Ok(None) => None,
179            Err(e) => {
180                error!(%user_id, "Failed to fetch room member information: {e}");
181                None
182            }
183        }
184    }
185
186    fn profile_from_latest_event(&self, latest_event: &LatestEvent) -> Option<Profile> {
187        if !latest_event.has_sender_profile() {
188            return None;
189        }
190
191        Some(Profile {
192            display_name: latest_event.sender_display_name().map(ToOwned::to_owned),
193            display_name_ambiguous: latest_event.sender_name_ambiguous().unwrap_or(false),
194            avatar_url: latest_event.sender_avatar_url().map(ToOwned::to_owned),
195        })
196    }
197
198    async fn load_user_receipt<'a>(
199        &'a self,
200        receipt_type: ReceiptType,
201        thread: ReceiptThread,
202        user_id: &'a UserId,
203    ) -> Option<(OwnedEventId, Receipt)> {
204        match self.load_user_receipt(receipt_type.clone(), thread.clone(), user_id).await {
205            Ok(receipt) => receipt,
206            Err(e) => {
207                error!(
208                    ?receipt_type,
209                    ?thread,
210                    ?user_id,
211                    "Failed to get read receipt for user: {e}"
212                );
213                None
214            }
215        }
216    }
217
218    async fn load_event_receipts<'a>(
219        &'a self,
220        event_id: &'a EventId,
221        receipt_thread: ReceiptThread,
222    ) -> IndexMap<OwnedUserId, Receipt> {
223        match self.load_event_receipts(ReceiptType::Read, receipt_thread.clone(), event_id).await {
224            Ok(receipts) => receipts.into_iter().collect(),
225            Err(e) => {
226                error!(?event_id, ?receipt_thread, "Failed to get read receipts for event: {e}");
227                IndexMap::new()
228            }
229        }
230    }
231
232    async fn load_fully_read_marker(&self) -> Option<OwnedEventId> {
233        match self.account_data_static::<FullyReadEventContent>().await {
234            Ok(Some(fully_read)) => match fully_read.deserialize() {
235                Ok(fully_read) => Some(fully_read.content.event_id),
236                Err(e) => {
237                    error!("Failed to deserialize fully-read account data: {e}");
238                    None
239                }
240            },
241            Err(e) => {
242                error!("Failed to get fully-read account data from the store: {e}");
243                None
244            }
245            _ => None,
246        }
247    }
248
249    async fn send(&self, content: AnyMessageLikeEventContent) -> Result<(), super::Error> {
250        let _ = self.send_queue().send(content).await?;
251        Ok(())
252    }
253
254    async fn redact<'a>(
255        &'a self,
256        event_id: &'a EventId,
257        reason: Option<&'a str>,
258        transaction_id: Option<OwnedTransactionId>,
259    ) -> Result<(), super::Error> {
260        let _ = self
261            .redact(event_id, reason, transaction_id)
262            .await
263            .map_err(RedactError::HttpError)
264            .map_err(super::Error::RedactError)?;
265        Ok(())
266    }
267
268    fn room_info(&self) -> Subscriber<RoomInfo> {
269        self.subscribe_info()
270    }
271
272    async fn load_event<'a>(&'a self, event_id: &'a EventId) -> Result<TimelineEvent> {
273        self.load_or_fetch_event(event_id, None).await
274    }
275}