1use std::sync::Arc;
16
17use eyeball::{ObservableWriteGuard, SharedObservable, Subscriber};
18use eyeball_im::{ObservableVector, VectorDiff, VectorSubscriberBatchedStream};
19use futures_util::future::join_all;
20use imbl::Vector;
21use matrix_sdk::{
22 Result, Room,
23 deserialized_responses::TimelineEvent,
24 event_cache::{RoomEventCacheUpdate, Subscriber as EventCacheSubscriber},
25 locks::Mutex,
26 paginators::PaginationToken,
27 room::ListThreadsOptions,
28 task_monitor::BackgroundTaskHandle,
29};
30use matrix_sdk_common::serde_helpers::extract_thread_root;
31use ruma::{MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId};
32use tokio::sync::Mutex as AsyncMutex;
33use tracing::{error, trace, warn};
34
35use crate::timeline::{Profile, TimelineDetails, TimelineItemContent, traits::RoomDataProvider};
36
37#[derive(Clone, Debug)]
47pub struct ThreadListItem {
48 pub root_event: ThreadListItemEvent,
50
51 pub latest_event: Option<ThreadListItemEvent>,
57
58 pub num_replies: u32,
63}
64
65#[derive(Clone, Debug)]
68pub struct ThreadListItemEvent {
69 pub event_id: OwnedEventId,
71
72 pub timestamp: MilliSecondsSinceUnixEpoch,
74
75 pub sender: OwnedUserId,
77
78 pub is_own: bool,
80
81 pub sender_profile: TimelineDetails<Profile>,
83
84 pub content: Option<TimelineItemContent>,
90}
91
92#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
94#[derive(Clone, Debug, Eq, PartialEq)]
95pub enum ThreadListPaginationState {
96 Idle {
98 end_reached: bool,
101 },
102 Loading,
104}
105
106#[derive(Debug, thiserror::Error)]
108pub enum ThreadListServiceError {
109 #[error(transparent)]
111 Sdk(#[from] matrix_sdk::Error),
112}
113
114pub struct ThreadListService {
149 room: Room,
151
152 token: AsyncMutex<PaginationToken>,
154
155 pagination_state: SharedObservable<ThreadListPaginationState>,
157
158 items: Arc<Mutex<ObservableVector<ThreadListItem>>>,
160
161 _event_cache_task: BackgroundTaskHandle,
164}
165
166impl ThreadListService {
167 pub fn new(room: Room) -> Self {
173 let items: Arc<Mutex<ObservableVector<ThreadListItem>>> =
174 Arc::new(Mutex::new(ObservableVector::new()));
175
176 let client = room.client();
177
178 let event_cache_task = client
179 .task_monitor()
180 .spawn_infinite_task("thread_list_service::event_cache_listener", {
181 let room = room.clone();
182 let items = items.clone();
183 let client = client.clone();
184
185 async move {
186 if let Some(event_cache) = client.event_cache()
188 && let Ok((room_event_cache, _event_cache_drop_handles)) =
189 event_cache.room(room.room_id()).await
190 && let Ok((_initial_events, mut subscriber)) =
191 room_event_cache.subscribe().await
192 {
193 trace!("ThreadListService: event cache listener started");
194
195 Self::event_cache_listener_loop(&room, &mut subscriber, items).await;
196 } else {
197 error!(
198 "ThreadListService: failed to subscribe to room event cache, \
199 live updates will not work"
200 );
201 }
202 }
203 })
204 .abort_on_drop();
205
206 Self {
207 room,
208 token: AsyncMutex::new(PaginationToken::None),
209 pagination_state: SharedObservable::new(ThreadListPaginationState::Idle {
210 end_reached: false,
211 }),
212 items,
213 _event_cache_task: event_cache_task,
214 }
215 }
216
217 pub fn pagination_state(&self) -> ThreadListPaginationState {
219 self.pagination_state.get()
220 }
221
222 pub fn subscribe_to_pagination_state_updates(&self) -> Subscriber<ThreadListPaginationState> {
227 self.pagination_state.subscribe()
228 }
229
230 pub fn items(&self) -> Vec<ThreadListItem> {
232 self.items.lock().iter().cloned().collect()
233 }
234
235 pub fn subscribe_to_items_updates(
240 &self,
241 ) -> (Vector<ThreadListItem>, VectorSubscriberBatchedStream<ThreadListItem>) {
242 self.items.lock().subscribe().into_values_and_batched_stream()
243 }
244
245 pub async fn paginate(&self) -> Result<(), ThreadListServiceError> {
253 {
255 let mut pagination_state = self.pagination_state.write();
256
257 match *pagination_state {
258 ThreadListPaginationState::Idle { end_reached: true }
259 | ThreadListPaginationState::Loading => return Ok(()),
260 _ => {}
261 }
262
263 ObservableWriteGuard::set(&mut pagination_state, ThreadListPaginationState::Loading);
264 }
265
266 let mut pagination_token = self.token.lock().await;
267
268 let from = match &*pagination_token {
271 PaginationToken::HasMore(token) => Some(token.clone()),
272 _ => None,
273 };
274
275 let opts = ListThreadsOptions { from, ..Default::default() };
276
277 match self.load_thread_list(opts).await {
278 Ok(thread_list) => {
279 *pagination_token = match &thread_list.prev_batch_token {
282 Some(token) => PaginationToken::HasMore(token.clone()),
283 None => PaginationToken::HitEnd,
284 };
285
286 let end_reached = thread_list.prev_batch_token.is_none();
287
288 self.items.lock().append(thread_list.items.into());
290
291 self.pagination_state.set(ThreadListPaginationState::Idle { end_reached });
292
293 Ok(())
294 }
295 Err(err) => {
296 self.pagination_state.set(ThreadListPaginationState::Idle { end_reached: false });
297 Err(ThreadListServiceError::Sdk(err))
298 }
299 }
300 }
301
302 pub async fn reset(&self) {
309 let mut pagination_token = self.token.lock().await;
310 *pagination_token = PaginationToken::None;
311
312 self.items.lock().clear();
313
314 self.pagination_state.set(ThreadListPaginationState::Idle { end_reached: false });
315 }
316
317 async fn load_thread_list(&self, opts: ListThreadsOptions) -> Result<ThreadList> {
318 let thread_roots = self.room.list_threads(opts).await?;
319
320 let list_items = join_all(
321 thread_roots
322 .chunk
323 .into_iter()
324 .map(|timeline_event| Self::build_thread_list_item(&self.room, timeline_event))
325 .collect::<Vec<_>>(),
326 )
327 .await
328 .into_iter()
329 .flatten()
330 .collect();
331
332 Ok(ThreadList { items: list_items, prev_batch_token: thread_roots.prev_batch_token })
333 }
334
335 async fn build_thread_list_item(
336 room: &Room,
337 timeline_event: TimelineEvent,
338 ) -> Option<ThreadListItem> {
339 let thread_summary_with_latest_event = timeline_event.thread_summary_with_latest_event();
341
342 let root_event = Self::build_event(room, timeline_event).await?;
344
345 let mut num_replies = 0;
347 let mut latest_event = None;
348
349 if let Some((thread_summary, latest_timeline_event)) = thread_summary_with_latest_event {
350 num_replies = thread_summary.num_replies;
351 latest_event = Self::build_event(room, latest_timeline_event).await
352 }
353
354 Some(ThreadListItem { root_event, latest_event, num_replies })
355 }
356
357 async fn build_event(
359 room: &Room,
360 timeline_event: TimelineEvent,
361 ) -> Option<ThreadListItemEvent> {
362 let event_id = timeline_event.event_id()?.to_owned();
363 let timestamp = timeline_event.timestamp()?;
364 let sender = timeline_event.sender()?;
365 let is_own = room.own_user_id() == sender;
366 let sender_profile =
367 TimelineDetails::from_initial_value(Profile::load(room, &sender).await);
368 let content = TimelineItemContent::from_event(room, timeline_event).await;
369 Some(ThreadListItemEvent { event_id, timestamp, sender, is_own, sender_profile, content })
370 }
371
372 async fn event_cache_listener_loop(
378 room: &Room,
379 subscriber: &mut EventCacheSubscriber<RoomEventCacheUpdate>,
380 items: Arc<Mutex<ObservableVector<ThreadListItem>>>,
381 ) {
382 use tokio::sync::broadcast::error::RecvError;
383
384 loop {
385 let update = match subscriber.recv().await {
386 Ok(update) => update,
387 Err(RecvError::Closed) => {
388 error!("ThreadListService: event cache channel closed, stopping listener");
389 break;
390 }
391 Err(RecvError::Lagged(n)) => {
392 warn!("ThreadListService: lagged behind {n} event cache updates");
393 continue;
394 }
395 };
396
397 if let RoomEventCacheUpdate::UpdateTimelineEvents(timeline_diffs) = update {
398 let new_events = Self::collect_events_from_diffs(timeline_diffs.diffs);
399
400 for event in new_events {
401 let Some(thread_root) = extract_thread_root(event.raw()) else { continue };
404
405 let position = {
407 let guard = items.lock();
408 guard.iter().position(|item| item.root_event.event_id == thread_root)
409 };
410
411 if let Some(index) = position {
412 if let Some(latest_event) = Self::build_event(room, event).await {
415 let mut guard = items.lock();
416
417 if index < guard.len()
421 && guard[index].root_event.event_id == thread_root
422 {
423 let mut updated = guard[index].clone();
424 updated.latest_event = Some(latest_event);
425 updated.num_replies = updated.num_replies.saturating_add(1);
426 guard.set(index, updated);
427 }
428 }
429 }
430 }
431 }
432 }
433 }
434
435 fn collect_events_from_diffs(
437 diffs: Vec<VectorDiff<matrix_sdk_base::event_cache::Event>>,
438 ) -> Vec<matrix_sdk_base::event_cache::Event> {
439 let mut events = Vec::new();
440
441 for diff in diffs {
442 match diff {
443 VectorDiff::Append { values } => events.extend(values),
444 VectorDiff::PushBack { value }
445 | VectorDiff::PushFront { value }
446 | VectorDiff::Insert { value, .. }
447 | VectorDiff::Set { value, .. } => events.push(value),
448 VectorDiff::Reset { values } => events.extend(values),
449 VectorDiff::Clear
451 | VectorDiff::PopBack
452 | VectorDiff::PopFront
453 | VectorDiff::Remove { .. }
454 | VectorDiff::Truncate { .. } => {}
455 }
456 }
457
458 events
459 }
460}
461
462#[derive(Clone, Debug)]
465struct ThreadList {
466 pub items: Vec<ThreadListItem>,
468
469 pub prev_batch_token: Option<String>,
471}
472
473#[cfg(test)]
474mod tests {
475 use std::{pin::pin, time::Duration};
476
477 use assert_matches::assert_matches;
478 use matrix_sdk::test_utils::mocks::MatrixMockServer;
479 use matrix_sdk_test::{async_test, event_factory::EventFactory};
480 use ruma::{
481 event_id,
482 events::{
483 AnyTimelineEvent,
484 room::{
485 encrypted::{
486 EncryptedEventScheme, MegolmV1AesSha2ContentInit, RoomEncryptedEventContent,
487 },
488 message::RedactedRoomMessageEventContent,
489 },
490 },
491 room_id,
492 serde::Raw,
493 user_id,
494 };
495 use stream_assert::{assert_next_matches, assert_pending};
496
497 use super::{ThreadListPaginationState, ThreadListService};
498 use crate::timeline::{MsgLikeContent, MsgLikeKind, TimelineItemContent};
499
500 #[async_test]
501 async fn test_initial_state() {
502 let server = MatrixMockServer::new().await;
503 let service = make_service(&server).await;
504
505 assert_eq!(
506 service.pagination_state(),
507 ThreadListPaginationState::Idle { end_reached: false }
508 );
509 assert!(service.items().is_empty());
510 }
511
512 #[async_test]
513 async fn test_pagination() {
514 let server = MatrixMockServer::new().await;
515 let client = server.client_builder().build().await;
516 let room_id = room_id!("!a:b.c");
517 let sender_id = user_id!("@alice:b.c");
518
519 let f = EventFactory::new().room(room_id).sender(sender_id);
520
521 let eid1 = event_id!("$1");
522 let eid2 = event_id!("$2");
523
524 server
525 .mock_room_threads()
526 .ok(
527 vec![f.text_msg("Thread root 1").event_id(eid1).into_raw()],
528 Some("next_page_token".to_owned()),
529 )
530 .mock_once()
531 .mount()
532 .await;
533
534 server
535 .mock_room_threads()
536 .match_from("next_page_token")
537 .ok(vec![f.text_msg("Thread root 2").event_id(eid2).into_raw()], None)
538 .mock_once()
539 .mount()
540 .await;
541
542 let room = server.sync_joined_room(&client, room_id).await;
543 let service = ThreadListService::new(room);
544
545 service.paginate().await.expect("first paginate failed");
546
547 assert_eq!(
548 service.pagination_state(),
549 ThreadListPaginationState::Idle { end_reached: false }
550 );
551 assert_eq!(service.items().len(), 1);
552 assert_eq!(service.items()[0].root_event.event_id, eid1);
553
554 service.paginate().await.expect("second paginate failed");
555
556 assert_eq!(
557 service.pagination_state(),
558 ThreadListPaginationState::Idle { end_reached: true }
559 );
560 assert_eq!(service.items().len(), 2);
561 assert_eq!(service.items()[1].root_event.event_id, eid2);
562 }
563
564 #[async_test]
565 async fn test_pagination_end_reached() {
566 let server = MatrixMockServer::new().await;
567 let client = server.client_builder().build().await;
568 let room_id = room_id!("!a:b.c");
569 let sender_id = user_id!("@alice:b.c");
570 let f = EventFactory::new().room(room_id).sender(sender_id);
571 let eid1 = event_id!("$1");
572
573 server
574 .mock_room_threads()
575 .ok(vec![f.text_msg("Thread root").event_id(eid1).into_raw()], None)
576 .mock_once()
577 .mount()
578 .await;
579
580 let room = server.sync_joined_room(&client, room_id).await;
581 let service = ThreadListService::new(room);
582
583 service.paginate().await.expect("paginate failed");
584 assert_eq!(
585 service.pagination_state(),
586 ThreadListPaginationState::Idle { end_reached: true }
587 );
588 assert_eq!(service.items().len(), 1);
589
590 service.paginate().await.expect("second paginate should be a no-op");
591 assert_eq!(service.items().len(), 1);
592 assert_eq!(
593 service.pagination_state(),
594 ThreadListPaginationState::Idle { end_reached: true }
595 );
596 }
597
598 #[async_test]
603 async fn test_concurrent_pagination_is_not_possible() {
604 let server = MatrixMockServer::new().await;
605 let client = server.client_builder().build().await;
606 let room_id = room_id!("!a:b.c");
607 let sender_id = user_id!("@alice:b.c");
608 let f = EventFactory::new().room(room_id).sender(sender_id);
609 let eid1 = event_id!("$1");
610
611 let chunk: Vec<Raw<AnyTimelineEvent>> =
615 vec![f.text_msg("Thread root").event_id(eid1).into_raw()];
616 server
617 .mock_room_threads()
618 .with_delay(Duration::from_millis(100))
619 .ok(chunk, None)
620 .expect(1)
621 .mount()
622 .await;
623
624 let room = server.sync_joined_room(&client, room_id).await;
625 let service = ThreadListService::new(room);
626
627 let (first, second) = tokio::join!(service.paginate(), service.paginate());
629
630 first.expect("first paginate should succeed");
631 second.expect("second (concurrent) paginate should succeed as a no-op");
632
633 assert_eq!(service.items().len(), 1);
635 assert_eq!(service.items()[0].root_event.event_id, eid1);
636 assert_eq!(
637 service.pagination_state(),
638 ThreadListPaginationState::Idle { end_reached: true }
639 );
640 }
641
642 #[async_test]
646 async fn test_pagination_error() {
647 let server = MatrixMockServer::new().await;
648 let client = server.client_builder().build().await;
649 let room_id = room_id!("!a:b.c");
650
651 server.mock_room_threads().error500().mock_once().mount().await;
652
653 let room = server.sync_joined_room(&client, room_id).await;
654 let service = ThreadListService::new(room);
655
656 service.paginate().await.expect_err("paginate should fail on a 500 response");
658
659 assert_eq!(
662 service.pagination_state(),
663 ThreadListPaginationState::Idle { end_reached: false }
664 );
665
666 assert!(service.items().is_empty());
668 }
669
670 #[async_test]
671 async fn test_reset() {
672 let server = MatrixMockServer::new().await;
673 let client = server.client_builder().build().await;
674 let room_id = room_id!("!a:b.c");
675 let sender_id = user_id!("@alice:b.c");
676 let f = EventFactory::new().room(room_id).sender(sender_id);
677 let eid1 = event_id!("$1");
678
679 server
680 .mock_room_threads()
681 .ok(vec![f.text_msg("Thread root").event_id(eid1).into_raw()], None)
682 .expect(2)
683 .mount()
684 .await;
685
686 let room = server.sync_joined_room(&client, room_id).await;
687 let service = ThreadListService::new(room);
688
689 service.paginate().await.expect("first paginate failed");
690 assert_eq!(service.items().len(), 1);
691 assert_eq!(
692 service.pagination_state(),
693 ThreadListPaginationState::Idle { end_reached: true }
694 );
695
696 service.reset().await;
697 assert!(service.items().is_empty());
698 assert_eq!(
699 service.pagination_state(),
700 ThreadListPaginationState::Idle { end_reached: false }
701 );
702
703 service.paginate().await.expect("paginate after reset failed");
704 assert_eq!(service.items().len(), 1);
705 }
706
707 #[async_test]
708 async fn test_pagination_state_subscriber() {
709 let server = MatrixMockServer::new().await;
710 let client = server.client_builder().build().await;
711 let room_id = room_id!("!a:b.c");
712 let sender_id = user_id!("@alice:b.c");
713 let f = EventFactory::new().room(room_id).sender(sender_id);
714 let eid1 = event_id!("$1");
715
716 server
717 .mock_room_threads()
718 .ok(
719 vec![f.text_msg("Thread root").event_id(eid1).into_raw()],
720 Some("next_token".to_owned()),
721 )
722 .mock_once()
723 .mount()
724 .await;
725
726 let room = server.sync_joined_room(&client, room_id).await;
727 let service = ThreadListService::new(room);
728
729 let subscriber = service.subscribe_to_pagination_state_updates();
730 let mut subscriber = pin!(subscriber);
731
732 assert_pending!(subscriber);
733
734 service.paginate().await.expect("paginate failed");
735
736 assert_next_matches!(subscriber, ThreadListPaginationState::Idle { end_reached: false });
737 }
738
739 #[async_test]
740 async fn test_paginated_items_have_num_replies_zero_without_summary() {
741 let server = MatrixMockServer::new().await;
742 let client = server.client_builder().build().await;
743 let room_id = room_id!("!a:b.c");
744 let sender_id = user_id!("@alice:b.c");
745 let f = EventFactory::new().room(room_id).sender(sender_id);
746 let eid1 = event_id!("$1");
747
748 server
750 .mock_room_threads()
751 .ok(vec![f.text_msg("Thread root").event_id(eid1).into_raw()], None)
752 .mock_once()
753 .mount()
754 .await;
755
756 let room = server.sync_joined_room(&client, room_id).await;
757 let service = ThreadListService::new(room);
758
759 service.paginate().await.expect("paginate failed");
760
761 let items = service.items();
762 assert_eq!(items.len(), 1);
763 assert_eq!(items[0].num_replies, 0);
764 assert!(items[0].latest_event.is_none());
765 }
766
767 #[async_test]
768 async fn test_paginated_items_have_num_replies_from_bundled_summary() {
769 let server = MatrixMockServer::new().await;
770 let client = server.client_builder().build().await;
771 let room_id = room_id!("!a:b.c");
772 let sender_id = user_id!("@alice:b.c");
773 let f = EventFactory::new().room(room_id).sender(sender_id);
774 let root_id = event_id!("$root");
775 let reply_id = event_id!("$reply");
776
777 let reply_event =
781 f.text_msg("Reply in thread").event_id(reply_id).into_raw_sync().cast_unchecked();
782
783 let thread_root = f
785 .text_msg("Thread root")
786 .event_id(root_id)
787 .with_bundled_thread_summary(reply_event, 3, false)
788 .into_raw();
789
790 server.mock_room_threads().ok(vec![thread_root], None).mock_once().mount().await;
791
792 let room = server.sync_joined_room(&client, room_id).await;
793 let service = ThreadListService::new(room);
794
795 service.paginate().await.expect("paginate failed");
796
797 let items = service.items();
798 assert_eq!(items.len(), 1);
799 assert_eq!(items[0].root_event.event_id, root_id);
800 assert_eq!(items[0].num_replies, 3);
801
802 let latest = items[0].latest_event.as_ref().expect("should have latest_event");
804 assert_eq!(latest.event_id, reply_id);
805 assert_eq!(latest.sender.as_str(), sender_id.as_str());
806 }
807
808 #[async_test]
809 async fn test_redacted_root_with_encrypted_latest_event() {
810 let server = MatrixMockServer::new().await;
811 let client = server.client_builder().build().await;
812 let room_id = room_id!("!a:b.c");
813 let sender_id = user_id!("@alice:b.c");
814 let f = EventFactory::new().room(room_id).sender(sender_id);
815 let root_id = event_id!("$root");
816 let latest_id = event_id!("$latest");
817
818 let encrypted_latest = f
820 .event(RoomEncryptedEventContent::new(
821 EncryptedEventScheme::MegolmV1AesSha2(
822 MegolmV1AesSha2ContentInit {
823 ciphertext: "ciphertext".to_owned(),
824 sender_key: "sender-key".to_owned(),
825 device_id: "device-id".to_owned().into(),
826 session_id: "session-id".to_owned(),
827 }
828 .into(),
829 ),
830 None,
831 ))
832 .event_id(latest_id)
833 .into_raw_sync()
834 .cast_unchecked();
835
836 let thread_root = f
839 .redacted(sender_id, RedactedRoomMessageEventContent::new())
840 .event_id(root_id)
841 .with_bundled_thread_summary(encrypted_latest, 3, false)
842 .into_raw();
843
844 server.mock_room_threads().ok(vec![thread_root], None).mock_once().mount().await;
845
846 let room = server.sync_joined_room(&client, room_id).await;
847 let service = ThreadListService::new(room);
848
849 service.paginate().await.expect("paginate failed");
850
851 let items = service.items();
852 assert_eq!(items.len(), 1);
853
854 let latest = items[0].latest_event.as_ref().expect("should have latest_event");
857 assert_matches!(
858 latest.content,
859 Some(TimelineItemContent::MsgLike(MsgLikeContent {
860 kind: MsgLikeKind::UnableToDecrypt(_),
861 ..
862 }))
863 );
864 }
865
866 #[async_test]
867 async fn test_redacted_root_still_listed_with_summary() {
868 let server = MatrixMockServer::new().await;
869 let client = server.client_builder().build().await;
870 let room_id = room_id!("!a:b.c");
871 let sender_id = user_id!("@alice:b.c");
872 let f = EventFactory::new().room(room_id).sender(sender_id);
873 let root_id = event_id!("$root");
874 let reply_id = event_id!("$reply");
875
876 let reply_event =
877 f.text_msg("Reply in thread").event_id(reply_id).into_raw_sync().cast_unchecked();
878
879 let thread_root = f
881 .redacted(sender_id, RedactedRoomMessageEventContent::new())
882 .event_id(root_id)
883 .with_bundled_thread_summary(reply_event, 3, false)
884 .into_raw();
885
886 server.mock_room_threads().ok(vec![thread_root], None).mock_once().mount().await;
887
888 let room = server.sync_joined_room(&client, room_id).await;
889 let service = ThreadListService::new(room);
890
891 service.paginate().await.expect("paginate failed");
892
893 let items = service.items();
894 assert_eq!(items.len(), 1);
895 assert_eq!(items[0].root_event.event_id, root_id);
896 assert_eq!(items[0].num_replies, 3);
897
898 assert!(matches!(
900 items[0].root_event.content,
901 Some(TimelineItemContent::MsgLike(MsgLikeContent { kind: MsgLikeKind::Redacted, .. }))
902 ));
903
904 let latest = items[0].latest_event.as_ref().expect("should have latest_event");
906 assert_eq!(latest.event_id, reply_id);
907 assert!(matches!(
908 latest.content,
909 Some(TimelineItemContent::MsgLike(MsgLikeContent {
910 kind: MsgLikeKind::Message(_),
911 ..
912 }))
913 ));
914 }
915
916 async fn make_service(server: &MatrixMockServer) -> ThreadListService {
919 let client = server.client_builder().build().await;
920 let room_id = room_id!("!a:b.c");
921 let room = server.sync_joined_room(&client, room_id).await;
922 ThreadListService::new(room)
923 }
924}