matrix_sdk_ui/timeline/builder.rs
1// Copyright 2023 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::sync::Arc;
16
17use matrix_sdk::Room;
18use matrix_sdk_base::{SendOutsideWasm, SyncOutsideWasm};
19use ruma::{events::AnySyncTimelineEvent, room_version_rules::RoomVersionRules};
20use tracing::{Instrument, Span, info_span};
21
22use super::{
23 DateDividerMode, Error, Timeline, TimelineDropHandle, TimelineFocus,
24 controller::{TimelineController, TimelineSettings},
25};
26use crate::{
27 timeline::{
28 TimelineReadReceiptTracking,
29 controller::{InitFocusResult, spawn_crypto_tasks},
30 tasks::{room_event_cache_updates_task, room_send_queue_update_task},
31 },
32 unable_to_decrypt_hook::UtdHookManager,
33};
34
35/// Builder that allows creating and configuring various parts of a
36/// [`Timeline`].
37#[must_use]
38#[derive(Debug)]
39pub struct TimelineBuilder {
40 room: Room,
41 settings: TimelineSettings,
42 focus: TimelineFocus,
43
44 /// An optional hook to call whenever we run into an unable-to-decrypt or a
45 /// late-decryption event.
46 unable_to_decrypt_hook: Option<Arc<UtdHookManager>>,
47
48 /// An optional prefix for internal IDs.
49 internal_id_prefix: Option<String>,
50}
51
52impl TimelineBuilder {
53 pub fn new(room: &Room) -> Self {
54 Self {
55 room: room.clone(),
56 settings: TimelineSettings::default(),
57 unable_to_decrypt_hook: None,
58 focus: TimelineFocus::Live { hide_threaded_events: false },
59 internal_id_prefix: None,
60 }
61 }
62
63 /// Sets up the initial focus for this timeline.
64 ///
65 /// By default, the focus for a timeline is to be "live" (i.e. it will
66 /// listen to sync and append this room's events in real-time, and it'll be
67 /// able to back-paginate older events), and show all events (including
68 /// events in threads). Look at [`TimelineFocus`] for other options.
69 pub fn with_focus(mut self, focus: TimelineFocus) -> Self {
70 self.focus = focus;
71 self
72 }
73
74 /// Sets up a hook to catch unable-to-decrypt (UTD) events for the timeline
75 /// we're building.
76 ///
77 /// If it was previously set before, will overwrite the previous one.
78 pub fn with_unable_to_decrypt_hook(mut self, hook: Arc<UtdHookManager>) -> Self {
79 self.unable_to_decrypt_hook = Some(hook);
80 self
81 }
82
83 /// Sets the internal id prefix for this timeline.
84 ///
85 /// The prefix will be prepended to any internal ID using when generating
86 /// timeline IDs for this timeline.
87 pub fn with_internal_id_prefix(mut self, prefix: String) -> Self {
88 self.internal_id_prefix = Some(prefix);
89 self
90 }
91
92 /// Choose when to insert the date separators, either in between each day
93 /// or each month.
94 pub fn with_date_divider_mode(mut self, mode: DateDividerMode) -> Self {
95 self.settings.date_divider_mode = mode;
96 self
97 }
98
99 /// Choose whether to enable tracking of the fully-read marker and the read
100 /// receipts and on which event types.
101 pub fn track_read_marker_and_receipts(mut self, tracking: TimelineReadReceiptTracking) -> Self {
102 self.settings.track_read_receipts = tracking;
103 self
104 }
105
106 /// Use the given filter to choose whether to add events to the timeline.
107 ///
108 /// # Arguments
109 ///
110 /// * `filter` - A function that takes a deserialized event, and should
111 /// return `true` if the event should be added to the `Timeline`.
112 ///
113 /// If this is not overridden, the timeline uses the default filter that
114 /// only allows events that are materialized into a `Timeline` item. For
115 /// instance, reactions and edits don't get their own timeline item (as
116 /// they affect another existing one), so they're "filtered out" to
117 /// reflect that.
118 ///
119 /// You can use the default event filter with
120 /// [`crate::timeline::default_event_filter`] so as to chain it with
121 /// your own event filter, if you want to avoid situations where a read
122 /// receipt would be attached to an event that doesn't get its own
123 /// timeline item.
124 ///
125 /// Note that currently:
126 ///
127 /// - Not all event types have a representation as a `TimelineItem` so these
128 /// are not added no matter what the filter returns.
129 /// - It is not possible to filter out `m.room.encrypted` events (otherwise
130 /// they couldn't be decrypted when the appropriate room key arrives).
131 pub fn event_filter<F>(mut self, filter: F) -> Self
132 where
133 F: Fn(&AnySyncTimelineEvent, &RoomVersionRules) -> bool
134 + SendOutsideWasm
135 + SyncOutsideWasm
136 + 'static,
137 {
138 self.settings.event_filter = Arc::new(filter);
139 self
140 }
141
142 /// Whether to add events that failed to deserialize to the timeline.
143 ///
144 /// Defaults to `true`.
145 pub fn add_failed_to_parse(mut self, add: bool) -> Self {
146 self.settings.add_failed_to_parse = add;
147 self
148 }
149
150 /// Create a [`Timeline`] with the options set on this builder.
151 #[tracing::instrument(
152 skip(self),
153 fields(
154 room_id = ?self.room.room_id(),
155 track_read_receipts = ?self.settings.track_read_receipts,
156 )
157 )]
158 pub async fn build(self) -> Result<Timeline, Error> {
159 let Self { room, settings, unable_to_decrypt_hook, focus, internal_id_prefix } = self;
160
161 // Subscribe the event cache to sync responses, in case we hadn't done it yet.
162 let client = room.client();
163 let event_cache = client.event_cache();
164 event_cache.subscribe()?;
165
166 let room_id = room.room_id();
167 let (room_event_cache, event_cache_drop) = event_cache.room(room_id).await?;
168 let (_, event_subscriber) = room_event_cache.subscribe().await?;
169
170 let is_room_encrypted = room
171 .latest_encryption_state()
172 .await
173 .map(|state| state.is_encrypted())
174 .ok()
175 .unwrap_or_default();
176
177 let controller = TimelineController::new(
178 room.clone(),
179 &focus,
180 event_cache,
181 internal_id_prefix.clone(),
182 unable_to_decrypt_hook,
183 is_room_encrypted,
184 settings,
185 )
186 .await?;
187
188 let InitFocusResult { focus_task, has_events } = controller.init_focus().await?;
189
190 let room_update_join_handle = room
191 .client()
192 .task_monitor()
193 .spawn_infinite_task("timeline::room_event_cache_updates", {
194 let span = info_span!(
195 parent: Span::none(),
196 "live_update_handler",
197 room_id = ?room.room_id(),
198 focus = focus.debug_string(),
199 prefix = internal_id_prefix
200 );
201 span.follows_from(Span::current());
202
203 room_event_cache_updates_task(
204 room_event_cache.clone(),
205 controller.clone(),
206 event_subscriber,
207 focus.clone(),
208 )
209 .instrument(span)
210 })
211 .abort_on_drop();
212
213 let local_echo_listener_handle = {
214 let timeline_controller = controller.clone();
215 let (local_echoes, send_queue_stream) = room.send_queue().subscribe().await?;
216
217 room.client()
218 .task_monitor()
219 .spawn_infinite_task("timeline::local_echo_listener", {
220 // Handles existing local echoes first.
221 for echo in local_echoes {
222 timeline_controller.handle_local_echo(echo).await;
223 }
224
225 let span = info_span!(
226 parent: Span::none(),
227 "local_echo_handler",
228 room_id = ?room.room_id(),
229 focus = focus.debug_string(),
230 prefix = internal_id_prefix
231 );
232 span.follows_from(Span::current());
233
234 room_send_queue_update_task(send_queue_stream, timeline_controller)
235 .instrument(span)
236 })
237 .abort_on_drop()
238 };
239
240 let crypto_drop_handles = spawn_crypto_tasks(controller.clone()).await;
241
242 let timeline = Timeline {
243 controller,
244 drop_handle: Arc::new(TimelineDropHandle {
245 _crypto_drop_handles: crypto_drop_handles,
246 _room_update_join_handle: room_update_join_handle,
247 _local_echo_listener_handle: local_echo_listener_handle,
248 _focus_drop_handle: focus_task,
249 _event_cache_drop_handle: event_cache_drop,
250 }),
251 };
252
253 if has_events {
254 // The events we're injecting might be encrypted events, but we might
255 // have received the room key to decrypt them while nobody was listening to the
256 // `m.room_key` event, let's retry now.
257 timeline.retry_decryption_for_all_events().await;
258 }
259
260 Ok(timeline)
261 }
262}