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