1use std::{cell::RefCell, collections::HashSet};
2
3use futures_util::StreamExt;
4use gettextrs::gettext;
5use gtk::{
6 glib,
7 glib::{clone, closure_local},
8 prelude::*,
9 subclass::prelude::*,
10};
11use matrix_sdk::{
12 deserialized_responses::AmbiguityChange, event_handler::EventHandlerDropGuard,
13 room::Room as MatrixRoom, send_queue::RoomSendQueueUpdate, Result as MatrixResult,
14 RoomDisplayName, RoomInfo, RoomMemberships, RoomState,
15};
16use ruma::{
17 api::client::{
18 error::{ErrorKind, RetryAfter},
19 receipt::create_receipt::v3::ReceiptType as ApiReceiptType,
20 },
21 events::{
22 receipt::ReceiptThread,
23 room::{
24 guest_access::GuestAccess, history_visibility::HistoryVisibility,
25 member::SyncRoomMemberEvent,
26 },
27 },
28 EventId, MatrixToUri, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId,
29};
30use tokio_stream::wrappers::BroadcastStream;
31use tracing::{debug, error, warn};
32
33mod aliases;
34mod category;
35mod highlight_flags;
36mod join_rule;
37mod member;
38mod member_list;
39mod permissions;
40mod timeline;
41mod typing_list;
42
43pub(crate) use self::{
44 aliases::{AddAltAliasError, RegisterLocalAliasError, RoomAliases},
45 category::{RoomCategory, TargetRoomCategory},
46 highlight_flags::HighlightFlags,
47 join_rule::{JoinRule, JoinRuleValue},
48 member::{Member, Membership},
49 member_list::MemberList,
50 permissions::*,
51 timeline::*,
52 typing_list::TypingList,
53};
54use super::{
55 notifications::NotificationsRoomSetting, room_list::RoomMetainfo, IdentityVerification,
56 Session, User,
57};
58use crate::{
59 components::{AtRoom, AvatarImage, AvatarUriSource, PillSource},
60 gettext_f,
61 prelude::*,
62 spawn, spawn_tokio,
63 utils::{string::linkify, BoundObjectWeakRef},
64};
65
66const DEFAULT_RETRY_AFTER: u32 = 30;
69
70mod imp {
71 use std::{
72 cell::{Cell, OnceCell},
73 marker::PhantomData,
74 sync::LazyLock,
75 time::SystemTime,
76 };
77
78 use glib::subclass::Signal;
79
80 use super::*;
81
82 #[derive(Default, glib::Properties)]
83 #[properties(wrapper_type = super::Room)]
84 pub struct Room {
85 matrix_room: OnceCell<MatrixRoom>,
87 #[property(get, set = Self::set_session, construct_only)]
89 session: glib::WeakRef<Session>,
90 #[property(get = Self::room_id_string)]
92 room_id_string: PhantomData<String>,
93 #[property(get)]
95 aliases: RoomAliases,
96 #[property(get)]
101 name: RefCell<Option<String>>,
102 #[property(get)]
107 has_avatar: Cell<bool>,
108 #[property(get)]
110 topic: RefCell<Option<String>>,
111 #[property(get)]
116 topic_linkified: RefCell<Option<String>>,
117 #[property(get, builder(RoomCategory::default()))]
119 category: Cell<RoomCategory>,
120 #[property(get)]
122 is_direct: Cell<bool>,
123 #[property(get)]
125 is_tombstoned: Cell<bool>,
126 pub(super) predecessor_id: OnceCell<OwnedRoomId>,
128 #[property(get = Self::predecessor_id_string)]
131 predecessor_id_string: PhantomData<Option<String>>,
132 pub(super) successor_id: OnceCell<OwnedRoomId>,
134 #[property(get = Self::successor_id_string)]
137 successor_id_string: PhantomData<Option<String>>,
138 #[property(get)]
141 successor: glib::WeakRef<super::Room>,
142 #[property(get)]
144 pub(super) members: glib::WeakRef<MemberList>,
145 members_drop_guard: OnceCell<EventHandlerDropGuard>,
146 #[property(get)]
149 joined_members_count: Cell<u64>,
150 #[property(get)]
152 own_member: OnceCell<Member>,
153 #[property(get)]
157 inviter: RefCell<Option<Member>>,
158 #[property(get)]
161 direct_member: RefCell<Option<Member>>,
162 #[property(get)]
164 live_timeline: OnceCell<Timeline>,
165 #[property(get)]
172 latest_activity: Cell<u64>,
173 #[property(get)]
175 is_read: Cell<bool>,
176 #[property(get)]
178 notification_count: Cell<u64>,
179 #[property(get)]
181 has_notifications: Cell<bool>,
182 #[property(get)]
184 highlight: Cell<HighlightFlags>,
185 #[property(get)]
187 is_encrypted: Cell<bool>,
188 #[property(get)]
190 join_rule: JoinRule,
191 #[property(get)]
193 guests_allowed: Cell<bool>,
194 #[property(get, builder(HistoryVisibilityValue::default()))]
196 history_visibility: Cell<HistoryVisibilityValue>,
197 #[property(get = Self::version)]
199 version: PhantomData<String>,
200 #[property(get = Self::federated)]
202 federated: PhantomData<bool>,
203 #[property(get)]
205 typing_list: TypingList,
206 typing_drop_guard: OnceCell<EventHandlerDropGuard>,
207 #[property(get, set = Self::set_notifications_setting, explicit_notify, builder(NotificationsRoomSetting::default()))]
209 notifications_setting: Cell<NotificationsRoomSetting>,
210 #[property(get)]
212 permissions: Permissions,
213 #[property(get, set = Self::set_verification, nullable, explicit_notify)]
215 verification: BoundObjectWeakRef<IdentityVerification>,
216 is_room_info_initialized: Cell<bool>,
220 }
221
222 #[glib::object_subclass]
223 impl ObjectSubclass for Room {
224 const NAME: &'static str = "Room";
225 type Type = super::Room;
226 type ParentType = PillSource;
227 }
228
229 #[glib::derived_properties]
230 impl ObjectImpl for Room {
231 fn signals() -> &'static [Signal] {
232 static SIGNALS: LazyLock<Vec<Signal>> =
233 LazyLock::new(|| vec![Signal::builder("room-forgotten").build()]);
234 SIGNALS.as_ref()
235 }
236 }
237
238 impl PillSourceImpl for Room {
239 fn identifier(&self) -> String {
240 self.aliases
241 .alias_string()
242 .unwrap_or_else(|| self.room_id_string())
243 }
244 }
245
246 impl Room {
247 pub(super) fn init(&self, matrix_room: MatrixRoom, metainfo: Option<RoomMetainfo>) {
249 let obj = self.obj();
250
251 self.matrix_room
252 .set(matrix_room)
253 .expect("matrix room is uninitialized");
254
255 self.init_live_timeline();
256 self.aliases.init(&obj);
257 self.load_predecessor();
258 self.watch_members();
259 self.join_rule.init(&obj);
260 self.set_up_typing();
261 self.watch_send_queue();
262
263 spawn!(
264 glib::Priority::DEFAULT_IDLE,
265 clone!(
266 #[weak(rename_to = imp)]
267 self,
268 async move {
269 imp.update_with_room_info(imp.matrix_room().clone_info())
270 .await;
271 imp.watch_room_info();
272 imp.is_room_info_initialized.set(true);
273
274 let preload = matches!(
280 imp.category.get(),
281 RoomCategory::Favorite
282 | RoomCategory::Normal
283 | RoomCategory::LowPriority
284 );
285 imp.live_timeline().set_preload(preload);
286
287 imp.permissions.init(&imp.obj()).await;
288 }
289 )
290 );
291
292 spawn!(
293 glib::Priority::DEFAULT_IDLE,
294 clone!(
295 #[weak(rename_to = imp)]
296 self,
297 async move {
298 imp.load_own_member().await;
299 }
300 )
301 );
302
303 if let Some(RoomMetainfo {
304 latest_activity,
305 is_read,
306 }) = metainfo
307 {
308 self.set_latest_activity(latest_activity);
309 self.set_is_read(is_read);
310
311 self.update_highlight();
312 }
313 }
314
315 pub(super) fn matrix_room(&self) -> &MatrixRoom {
317 self.matrix_room.get().expect("matrix room was initialized")
318 }
319
320 fn set_session(&self, session: &Session) {
322 self.session.set(Some(session));
323
324 let own_member = Member::new(&self.obj(), session.user_id().clone());
325 self.own_member
326 .set(own_member)
327 .expect("own member was uninitialized");
328 }
329
330 pub(super) fn room_id(&self) -> &RoomId {
332 self.matrix_room().room_id()
333 }
334
335 fn room_id_string(&self) -> String {
337 self.matrix_room().room_id().to_string()
338 }
339
340 fn update_name(&self) {
342 let name = self
343 .matrix_room()
344 .name()
345 .map(|mut s| {
346 s.truncate_end_whitespaces();
347 s
348 })
349 .filter(|s| !s.is_empty());
350
351 if *self.name.borrow() == name {
352 return;
353 }
354
355 self.name.replace(name);
356 self.obj().notify_name();
357 }
358
359 async fn update_display_name(&self) {
361 let matrix_room = self.matrix_room().clone();
362 let handle = spawn_tokio!(async move { matrix_room.display_name().await });
363
364 let sdk_display_name = handle
365 .await
366 .expect("task was not aborted")
367 .inspect_err(|error| {
368 error!("Could not compute display name: {error}");
369 })
370 .ok();
371
372 let mut display_name = if let Some(sdk_display_name) = sdk_display_name {
373 match sdk_display_name {
374 RoomDisplayName::Named(s)
375 | RoomDisplayName::Calculated(s)
376 | RoomDisplayName::Aliased(s) => s,
377 RoomDisplayName::EmptyWas(s) => {
378 gettext_f("Empty Room (was {user})", &[("user", &s)])
382 }
383 RoomDisplayName::Empty => gettext("Empty Room"),
385 }
386 } else {
387 Default::default()
388 };
389
390 display_name.truncate_end_whitespaces();
391
392 if display_name.is_empty() {
393 display_name = gettext("Unknown");
395 }
396
397 self.obj().set_display_name(display_name);
398 }
399
400 fn set_has_avatar(&self, has_avatar: bool) {
402 if self.has_avatar.get() == has_avatar {
403 return;
404 }
405
406 self.has_avatar.set(has_avatar);
407 self.obj().notify_has_avatar();
408 }
409
410 fn update_avatar(&self) {
412 let Some(session) = self.session.upgrade() else {
413 return;
414 };
415
416 let obj = self.obj();
417 let avatar_data = obj.avatar_data();
418 let matrix_room = self.matrix_room();
419
420 let prev_avatar_url = avatar_data.image().and_then(|i| i.uri());
421 let room_avatar_url = matrix_room.avatar_url();
422
423 if prev_avatar_url.is_some() && prev_avatar_url == room_avatar_url {
424 return;
426 }
427
428 if let Some(avatar_url) = room_avatar_url {
429 let avatar_info = matrix_room.avatar_info();
431
432 if let Some(avatar_image) = avatar_data
433 .image()
434 .filter(|i| i.uri_source() == AvatarUriSource::Room)
435 {
436 avatar_image.set_uri_and_info(Some(avatar_url), avatar_info);
437 } else {
438 let avatar_image = AvatarImage::new(
439 &session,
440 AvatarUriSource::Room,
441 Some(avatar_url),
442 avatar_info,
443 );
444
445 avatar_data.set_image(Some(avatar_image.clone()));
446 }
447
448 self.set_has_avatar(true);
449 return;
450 }
451
452 self.set_has_avatar(false);
453
454 if let Some(direct_member) = self.direct_member.borrow().as_ref() {
456 avatar_data.set_image(direct_member.avatar_data().image());
457 }
458
459 let avatar_image = avatar_data.image();
460
461 if let Some(avatar_image) = avatar_image
462 .as_ref()
463 .filter(|i| i.uri_source() == AvatarUriSource::Room)
464 {
465 avatar_image.set_uri_and_info(None, None);
467 } else if avatar_image.is_none() {
468 avatar_data.set_image(Some(AvatarImage::new(
470 &session,
471 AvatarUriSource::Room,
472 None,
473 None,
474 )));
475 }
476 }
477
478 fn update_topic(&self) {
480 let topic = self
481 .matrix_room()
482 .topic()
483 .map(|mut s| {
484 s.truncate_end_whitespaces();
485 s
486 })
487 .filter(|topic| !topic.is_empty());
488
489 if *self.topic.borrow() == topic {
490 return;
491 }
492
493 let topic_linkified = topic.as_ref().map(|t| {
494 let mut s = linkify(t);
496 s.truncate_end_whitespaces();
498 s
499 });
500
501 self.topic.replace(topic);
502 self.topic_linkified.replace(topic_linkified);
503
504 let obj = self.obj();
505 obj.notify_topic();
506 obj.notify_topic_linkified();
507 }
508
509 pub(super) fn set_category(&self, category: RoomCategory) {
511 let old_category = self.category.get();
512
513 if old_category == RoomCategory::Outdated || old_category == category {
514 return;
515 }
516
517 self.category.set(category);
518 self.obj().notify_category();
519
520 let room_state = self.matrix_room().state();
522 if !old_category.is_state(room_state) {
523 if self.is_room_info_initialized.get() {
524 debug!(room_id = %self.room_id(), ?room_state, "The state of the room changed");
525 }
526
527 match room_state {
528 RoomState::Joined => {
529 if let Some(members) = self.members.upgrade() {
530 members.reload();
533 }
534
535 self.set_up_typing();
536 }
537 RoomState::Left
538 | RoomState::Knocked
539 | RoomState::Banned
540 | RoomState::Invited => {}
541 }
542 }
543 }
544
545 pub(super) async fn update_category(&self) {
547 if self.category.get() == RoomCategory::Outdated {
549 return;
550 }
551
552 let matrix_room = self.matrix_room();
553 let category = match matrix_room.state() {
554 RoomState::Joined => {
555 if matrix_room.is_space() {
556 RoomCategory::Space
557 } else if matrix_room.is_favourite() {
558 RoomCategory::Favorite
559 } else if matrix_room.is_low_priority() {
560 RoomCategory::LowPriority
561 } else {
562 RoomCategory::Normal
563 }
564 }
565 RoomState::Invited => {
566 self.load_inviter().await;
567
568 if self
569 .inviter
570 .borrow()
571 .as_ref()
572 .is_some_and(Member::is_ignored)
573 {
574 RoomCategory::Ignored
575 } else {
576 RoomCategory::Invited
577 }
578 }
579 RoomState::Left | RoomState::Knocked | RoomState::Banned => RoomCategory::Left,
580 };
581
582 self.set_category(category);
583 }
584
585 async fn set_is_direct(&self, is_direct: bool) {
587 if self.is_direct.get() == is_direct {
588 return;
589 }
590
591 self.is_direct.set(is_direct);
592 self.obj().notify_is_direct();
593
594 self.update_direct_member().await;
595 }
596
597 pub(super) async fn update_is_direct(&self) {
599 let matrix_room = self.matrix_room().clone();
600 let handle = spawn_tokio!(async move { matrix_room.is_direct().await });
601
602 match handle.await.expect("task was not aborted") {
603 Ok(is_direct) => self.set_is_direct(is_direct).await,
604 Err(error) => {
605 error!(room_id = %self.room_id(), "Could not load whether room is direct: {error}");
606 }
607 }
608 }
609
610 fn update_tombstone(&self) {
612 let matrix_room = self.matrix_room();
613
614 if !matrix_room.is_tombstoned() || self.successor_id.get().is_some() {
615 return;
616 }
617 let obj = self.obj();
618
619 if let Some(room_tombstone) = matrix_room.tombstone() {
620 self.successor_id
621 .set(room_tombstone.replacement_room)
622 .expect("successor ID is uninitialized");
623 obj.notify_successor_id_string();
624 }
625
626 self.update_successor();
628
629 if self.successor.upgrade().is_none() {
631 if let Some(session) = self.session.upgrade() {
632 session
633 .room_list()
634 .add_tombstoned_room(self.room_id().to_owned());
635 }
636 }
637
638 if !self.is_tombstoned.get() {
639 self.is_tombstoned.set(true);
640 obj.notify_is_tombstoned();
641 }
642 }
643
644 pub(super) fn update_successor(&self) {
646 if self.category.get() == RoomCategory::Outdated {
647 return;
648 }
649
650 let Some(session) = self.session.upgrade() else {
651 return;
652 };
653 let room_list = session.room_list();
654
655 if let Some(successor) = self
656 .successor_id
657 .get()
658 .and_then(|successor_id| room_list.get(successor_id))
659 {
660 if let Some(predecessor_id) = successor.predecessor_id() {
664 if predecessor_id == self.room_id() {
665 self.set_successor(&successor);
666 return;
667 }
668 }
669 }
670
671 for room in room_list.iter::<super::Room>() {
674 let Ok(room) = room else {
675 break;
676 };
677
678 if let Some(predecessor_id) = room.predecessor_id() {
679 if predecessor_id == self.room_id() {
680 self.set_successor(&room);
681 return;
682 }
683 }
684 }
685 }
686
687 fn predecessor_id_string(&self) -> Option<String> {
690 self.predecessor_id.get().map(ToString::to_string)
691 }
692
693 fn load_predecessor(&self) {
695 let Some(event) = self.matrix_room().create_content() else {
696 return;
697 };
698 let Some(predecessor) = event.predecessor else {
699 return;
700 };
701
702 self.predecessor_id
703 .set(predecessor.room_id)
704 .expect("predecessor ID is uninitialized");
705 self.obj().notify_predecessor_id_string();
706 }
707
708 fn successor_id_string(&self) -> Option<String> {
710 self.successor_id.get().map(ToString::to_string)
711 }
712
713 fn set_successor(&self, successor: &super::Room) {
715 self.successor.set(Some(successor));
716 self.obj().notify_successor();
717
718 self.set_category(RoomCategory::Outdated);
719 }
720
721 fn watch_members(&self) {
723 let matrix_room = self.matrix_room();
724
725 let obj_weak = glib::SendWeakRef::from(self.obj().downgrade());
726 let handle = matrix_room.add_event_handler(move |event: SyncRoomMemberEvent| {
727 let obj_weak = obj_weak.clone();
728 async move {
729 let ctx = glib::MainContext::default();
730 ctx.spawn(async move {
731 spawn!(async move {
732 if let Some(obj) = obj_weak.upgrade() {
733 obj.imp().handle_member_event(&event);
734 }
735 });
736 });
737 }
738 });
739
740 let drop_guard = matrix_room.client().event_handler_drop_guard(handle);
741 self.members_drop_guard.set(drop_guard).unwrap();
742 }
743
744 fn handle_member_event(&self, event: &SyncRoomMemberEvent) {
746 let user_id = event.state_key();
747
748 if let Some(members) = self.members.upgrade() {
749 members.update_member(user_id.clone());
750 } else if user_id == self.own_member().user_id() {
751 self.own_member().update();
752 } else if self
753 .direct_member
754 .borrow()
755 .as_ref()
756 .is_some_and(|member| member.user_id() == user_id)
757 {
758 if let Some(member) = self.direct_member.borrow().as_ref() {
759 member.update();
760 }
761 }
762
763 spawn!(clone!(
765 #[weak(rename_to = imp)]
766 self,
767 async move {
768 imp.update_direct_member().await;
769 }
770 ));
771 }
772
773 fn set_joined_members_count(&self, count: u64) {
776 if self.joined_members_count.get() == count {
777 return;
778 }
779
780 self.joined_members_count.set(count);
781 self.obj().notify_joined_members_count();
782 }
783
784 pub(super) fn own_member(&self) -> &Member {
786 self.own_member.get().expect("Own member was initialized")
787 }
788
789 async fn load_own_member(&self) {
791 let own_member = self.own_member();
792 let user_id = own_member.user_id().clone();
793 let matrix_room = self.matrix_room().clone();
794
795 let handle =
796 spawn_tokio!(async move { matrix_room.get_member_no_sync(&user_id).await });
797
798 match handle.await.expect("task was not aborted") {
799 Ok(Some(matrix_member)) => own_member.update_from_room_member(&matrix_member),
800 Ok(None) => {}
801 Err(error) => error!(
802 "Could not load own member for room {}: {error}",
803 self.room_id()
804 ),
805 }
806 }
807
808 async fn load_inviter(&self) {
810 let matrix_room = self.matrix_room();
811
812 if matrix_room.state() != RoomState::Invited {
813 return;
815 }
816
817 let matrix_room_clone = matrix_room.clone();
818 let handle = spawn_tokio!(async move { matrix_room_clone.invite_details().await });
819
820 let invite = match handle.await.expect("task was not aborted") {
821 Ok(invite) => invite,
822 Err(error) => {
823 error!("Could not get invite: {error}");
824 return;
825 }
826 };
827
828 let Some(inviter_member) = invite.inviter else {
829 return;
830 };
831
832 if let Some(inviter) = self
833 .inviter
834 .borrow()
835 .as_ref()
836 .filter(|inviter| inviter.user_id() == inviter_member.user_id())
837 {
838 inviter.update_from_room_member(&inviter_member);
840
841 return;
842 }
843
844 let inviter = Member::new(&self.obj(), inviter_member.user_id().to_owned());
845 inviter.update_from_room_member(&inviter_member);
846
847 inviter
848 .upcast_ref::<User>()
849 .connect_is_ignored_notify(clone!(
850 #[weak(rename_to = imp)]
851 self,
852 move |_| {
853 spawn!(async move {
854 imp.update_category().await;
856 });
857 }
858 ));
859
860 self.inviter.replace(Some(inviter));
861
862 self.obj().notify_inviter();
863 }
864
865 fn set_direct_member(&self, member: Option<Member>) {
868 if *self.direct_member.borrow() == member {
869 return;
870 }
871
872 self.direct_member.replace(member);
873 self.obj().notify_direct_member();
874 self.update_avatar();
875 }
876
877 async fn direct_user_id(&self) -> Option<OwnedUserId> {
880 let matrix_room = self.matrix_room();
881
882 let mut direct_targets = matrix_room
884 .direct_targets()
885 .into_iter()
886 .filter_map(|id| OwnedUserId::try_from(id).ok());
887
888 let Some(direct_target_user_id) = direct_targets.next() else {
889 return None;
891 };
892
893 if direct_targets.next().is_some() {
894 return None;
896 }
897
898 let members_count = matrix_room.active_members_count();
900
901 if members_count > 2 {
902 return None;
905 }
906
907 let matrix_room_clone = matrix_room.clone();
910 let handle =
911 spawn_tokio!(
912 async move { matrix_room_clone.members(RoomMemberships::ACTIVE).await }
913 );
914
915 let members = match handle.await.expect("task was not aborted") {
916 Ok(m) => m,
917 Err(error) => {
918 error!("Could not load room members: {error}");
919 vec![]
920 }
921 };
922
923 let members_count = members_count.max(members.len() as u64);
924 if members_count > 2 {
925 return None;
927 }
928
929 let own_user_id = matrix_room.own_user_id();
930 for member in members {
932 let user_id = member.user_id();
933
934 if user_id != direct_target_user_id && user_id != own_user_id {
935 return None;
937 }
938 }
939
940 Some(direct_target_user_id)
941 }
942
943 async fn update_direct_member(&self) {
946 let Some(direct_user_id) = self.direct_user_id().await else {
947 self.set_direct_member(None);
948 return;
949 };
950
951 if self
952 .direct_member
953 .borrow()
954 .as_ref()
955 .is_some_and(|m| *m.user_id() == direct_user_id)
956 {
957 return;
959 }
960
961 let direct_member = if let Some(members) = self.members.upgrade() {
962 members.get_or_create(direct_user_id.clone())
963 } else {
964 Member::new(&self.obj(), direct_user_id.clone())
965 };
966
967 let matrix_room = self.matrix_room().clone();
968 let handle =
969 spawn_tokio!(async move { matrix_room.get_member_no_sync(&direct_user_id).await });
970
971 match handle.await.expect("task was not aborted") {
972 Ok(Some(matrix_member)) => {
973 direct_member.update_from_room_member(&matrix_member);
974 }
975 Ok(None) => {}
976 Err(error) => {
977 error!("Could not get direct member: {error}");
978 }
979 }
980
981 self.set_direct_member(Some(direct_member));
982 }
983
984 fn init_live_timeline(&self) {
986 let timeline = self
987 .live_timeline
988 .get_or_init(|| Timeline::new(&self.obj()));
989
990 timeline.connect_read_change_trigger(clone!(
991 #[weak(rename_to = imp)]
992 self,
993 move |_| {
994 spawn!(glib::Priority::DEFAULT_IDLE, async move {
995 imp.handle_read_change_trigger().await;
996 });
997 }
998 ));
999 }
1000
1001 fn live_timeline(&self) -> &Timeline {
1003 self.live_timeline
1004 .get()
1005 .expect("live timeline is initialized")
1006 }
1007
1008 pub(super) fn set_latest_activity(&self, latest_activity: u64) {
1010 if self.latest_activity.get() == latest_activity {
1011 return;
1012 }
1013
1014 self.latest_activity.set(latest_activity);
1015 self.obj().notify_latest_activity();
1016 }
1017
1018 fn set_is_read(&self, is_read: bool) {
1020 if self.is_read.get() == is_read {
1021 return;
1022 }
1023
1024 self.is_read.set(is_read);
1025 self.obj().notify_is_read();
1026 }
1027
1028 async fn handle_read_change_trigger(&self) {
1030 let timeline = self.live_timeline();
1031
1032 if let Some(has_unread) = timeline.has_unread_messages().await {
1033 self.set_is_read(!has_unread);
1034 }
1035
1036 self.update_highlight();
1037 }
1038
1039 fn set_highlight(&self, highlight: HighlightFlags) {
1041 if self.highlight.get() == highlight {
1042 return;
1043 }
1044
1045 self.highlight.set(highlight);
1046 self.obj().notify_highlight();
1047 }
1048
1049 fn update_highlight(&self) {
1051 let mut highlight = HighlightFlags::empty();
1052
1053 if matches!(self.category.get(), RoomCategory::Left) {
1054 self.set_highlight(highlight);
1056 self.set_notification_count(0);
1057 return;
1058 }
1059
1060 if self.is_read.get() {
1061 self.set_notification_count(0);
1062 } else {
1063 let counts = self.matrix_room().unread_notification_counts();
1064
1065 if counts.highlight_count > 0 {
1066 highlight = HighlightFlags::all();
1067 } else {
1068 highlight = HighlightFlags::BOLD;
1069 }
1070 self.set_notification_count(counts.notification_count);
1071 }
1072
1073 self.set_highlight(highlight);
1074 }
1075
1076 fn set_notification_count(&self, count: u64) {
1078 if self.notification_count.get() == count {
1079 return;
1080 }
1081
1082 self.notification_count.set(count);
1083 self.set_has_notifications(count > 0);
1084 self.obj().notify_notification_count();
1085 }
1086
1087 fn set_has_notifications(&self, has_notifications: bool) {
1089 if self.has_notifications.get() == has_notifications {
1090 return;
1091 }
1092
1093 self.has_notifications.set(has_notifications);
1094 self.obj().notify_has_notifications();
1095 }
1096
1097 async fn update_is_encrypted(&self) {
1099 let matrix_room = self.matrix_room();
1100 let matrix_room_clone = matrix_room.clone();
1101 let handle =
1102 spawn_tokio!(async move { matrix_room_clone.latest_encryption_state().await });
1103
1104 match handle.await.expect("task was not aborted") {
1105 Ok(state) => {
1106 if state.is_encrypted() {
1107 self.is_encrypted.set(true);
1108 self.obj().notify_is_encrypted();
1109 }
1110 }
1111 Err(error) => {
1112 if matches!(matrix_room.state(), RoomState::Invited | RoomState::Knocked)
1115 && error
1116 .as_client_api_error()
1117 .is_some_and(|e| e.status_code.is_client_error())
1118 {
1119 debug!("Could not load room encryption state: {error}");
1120 } else {
1121 error!("Could not load room encryption state: {error}");
1122 }
1123 }
1124 }
1125 }
1126
1127 fn update_guests_allowed(&self) {
1129 let matrix_room = self.matrix_room();
1130 let guests_allowed = matrix_room.guest_access() == GuestAccess::CanJoin;
1131
1132 if self.guests_allowed.get() == guests_allowed {
1133 return;
1134 }
1135
1136 self.guests_allowed.set(guests_allowed);
1137 self.obj().notify_guests_allowed();
1138 }
1139
1140 fn update_history_visibility(&self) {
1142 let matrix_room = self.matrix_room();
1143 let visibility = matrix_room.history_visibility_or_default().into();
1144
1145 if self.history_visibility.get() == visibility {
1146 return;
1147 }
1148
1149 self.history_visibility.set(visibility);
1150 self.obj().notify_history_visibility();
1151 }
1152
1153 fn version(&self) -> String {
1155 self.matrix_room()
1156 .create_content()
1157 .map(|c| c.room_version.to_string())
1158 .unwrap_or_default()
1159 }
1160
1161 fn federated(&self) -> bool {
1163 self.matrix_room()
1164 .create_content()
1165 .is_some_and(|c| c.federate)
1166 }
1167
1168 fn set_up_typing(&self) {
1170 if self.typing_drop_guard.get().is_some() {
1171 return;
1173 }
1174
1175 let matrix_room = self.matrix_room();
1176 if matrix_room.state() != RoomState::Joined {
1177 return;
1178 }
1179
1180 let (typing_drop_guard, receiver) = matrix_room.subscribe_to_typing_notifications();
1181 let stream = BroadcastStream::new(receiver);
1182
1183 let obj_weak = glib::SendWeakRef::from(self.obj().downgrade());
1184 let fut = stream.for_each(move |typing_user_ids| {
1185 let obj_weak = obj_weak.clone();
1186 async move {
1187 let Ok(typing_user_ids) = typing_user_ids else {
1188 return;
1189 };
1190
1191 let ctx = glib::MainContext::default();
1192 ctx.spawn(async move {
1193 spawn!(async move {
1194 if let Some(obj) = obj_weak.upgrade() {
1195 obj.imp().update_typing_list(typing_user_ids);
1196 }
1197 });
1198 });
1199 }
1200 });
1201 spawn_tokio!(fut);
1202
1203 self.typing_drop_guard
1204 .set(typing_drop_guard)
1205 .expect("typing drop guard is uninitialized");
1206 }
1207
1208 fn update_typing_list(&self, typing_user_ids: Vec<OwnedUserId>) {
1210 let Some(session) = self.session.upgrade() else {
1211 return;
1212 };
1213
1214 let Some(members) = self.members.upgrade() else {
1215 self.typing_list.update(vec![]);
1218 return;
1219 };
1220
1221 let own_user_id = session.user_id();
1222
1223 let members = typing_user_ids
1224 .into_iter()
1225 .filter(|user_id| user_id != own_user_id)
1226 .map(|user_id| members.get_or_create(user_id))
1227 .collect();
1228
1229 self.typing_list.update(members);
1230 }
1231
1232 fn set_notifications_setting(&self, setting: NotificationsRoomSetting) {
1234 if self.notifications_setting.get() == setting {
1235 return;
1236 }
1237
1238 self.notifications_setting.set(setting);
1239 self.obj().notify_notifications_setting();
1240 }
1241
1242 fn set_verification(&self, verification: Option<IdentityVerification>) {
1244 if self.verification.obj().is_some() && verification.is_some() {
1245 return;
1248 }
1249
1250 self.verification.disconnect_signals();
1251
1252 let verification = verification.or_else(|| {
1253 let room_id = self.matrix_room().room_id();
1255 self.session
1256 .upgrade()
1257 .map(|s| s.verification_list())
1258 .and_then(|list| list.ongoing_room_verification(room_id))
1259 });
1260
1261 if let Some(verification) = &verification {
1262 let state_handler = verification.connect_is_finished_notify(clone!(
1263 #[weak(rename_to = imp)]
1264 self,
1265 move |_| {
1266 imp.set_verification(None);
1267 }
1268 ));
1269
1270 let dismiss_handler = verification.connect_dismiss(clone!(
1271 #[weak(rename_to = imp)]
1272 self,
1273 move |_| {
1274 imp.set_verification(None);
1275 }
1276 ));
1277
1278 self.verification
1279 .set(verification, vec![state_handler, dismiss_handler]);
1280 }
1281
1282 self.obj().notify_verification();
1283 }
1284
1285 fn watch_room_info(&self) {
1287 let matrix_room = self.matrix_room();
1288 let subscriber = matrix_room.subscribe_info();
1289
1290 let obj_weak = glib::SendWeakRef::from(self.obj().downgrade());
1291 let fut = subscriber.for_each(move |room_info| {
1292 let obj_weak = obj_weak.clone();
1293 async move {
1294 let ctx = glib::MainContext::default();
1295 ctx.spawn(async move {
1296 spawn!(async move {
1297 if let Some(obj) = obj_weak.upgrade() {
1298 obj.imp().update_with_room_info(room_info).await;
1299 }
1300 });
1301 });
1302 }
1303 });
1304 spawn_tokio!(fut);
1305 }
1306
1307 async fn update_with_room_info(&self, room_info: RoomInfo) {
1309 self.aliases.update();
1310 self.update_name();
1311 self.update_display_name().await;
1312 self.update_avatar();
1313 self.update_topic();
1314 self.update_category().await;
1315 self.update_is_direct().await;
1316 self.update_tombstone();
1317 self.set_joined_members_count(room_info.joined_members_count());
1318 self.update_is_encrypted().await;
1319 self.join_rule.update(room_info.join_rule());
1320 self.update_guests_allowed();
1321 self.update_history_visibility();
1322 }
1323
1324 pub(super) fn handle_ambiguity_changes<'a>(
1326 &self,
1327 changes: impl Iterator<Item = &'a AmbiguityChange>,
1328 ) {
1329 let user_ids = changes
1331 .flat_map(AmbiguityChange::user_ids)
1332 .collect::<HashSet<_>>();
1333
1334 if let Some(members) = self.members.upgrade() {
1335 for user_id in user_ids {
1336 members.update_member(user_id.to_owned());
1337 }
1338 } else {
1339 let own_member = self.own_member();
1340 let own_user_id = own_member.user_id();
1341
1342 if user_ids.contains(&**own_user_id) {
1343 own_member.update();
1344 }
1345 }
1346 }
1347
1348 fn watch_send_queue(&self) {
1350 let matrix_room = self.matrix_room().clone();
1351
1352 let room_weak = glib::SendWeakRef::from(self.obj().downgrade());
1353 spawn_tokio!(async move {
1354 let send_queue = matrix_room.send_queue();
1355 let subscriber = match send_queue.subscribe().await {
1356 Ok((_, subscriber)) => BroadcastStream::new(subscriber),
1357 Err(error) => {
1358 warn!("Failed to listen to room send queue: {error}");
1359 return;
1360 }
1361 };
1362
1363 subscriber
1364 .for_each(move |update| {
1365 let room_weak = room_weak.clone();
1366 async move {
1367 let Ok(RoomSendQueueUpdate::SendError {
1368 error,
1369 is_recoverable: true,
1370 ..
1371 }) = update
1372 else {
1373 return;
1374 };
1375
1376 let ctx = glib::MainContext::default();
1377 ctx.spawn(async move {
1378 spawn!(async move {
1379 let Some(obj) = room_weak.upgrade() else {
1380 return;
1381 };
1382 let Some(session) = obj.session() else {
1383 return;
1384 };
1385
1386 if session.is_offline() {
1387 return;
1390 }
1391
1392 let duration = match error.client_api_error_kind() {
1393 Some(ErrorKind::LimitExceeded {
1394 retry_after: Some(retry_after),
1395 }) => match retry_after {
1396 RetryAfter::Delay(duration) => Some(*duration),
1397 RetryAfter::DateTime(time) => {
1398 time.duration_since(SystemTime::now()).ok()
1399 }
1400 },
1401 _ => None,
1402 };
1403 let retry_after = duration
1404 .and_then(|d| d.as_secs().try_into().ok())
1405 .unwrap_or(DEFAULT_RETRY_AFTER);
1406
1407 glib::timeout_add_seconds_local_once(retry_after, move || {
1408 let matrix_room = obj.matrix_room().clone();
1409 spawn_tokio!(async move {
1411 matrix_room.send_queue().set_enabled(true);
1412 });
1413 });
1414 });
1415 });
1416 }
1417 })
1418 .await;
1419 });
1420 }
1421 }
1422}
1423
1424glib::wrapper! {
1425 pub struct Room(ObjectSubclass<imp::Room>) @extends PillSource;
1429}
1430
1431impl Room {
1432 pub fn new(session: &Session, matrix_room: MatrixRoom, metainfo: Option<RoomMetainfo>) -> Self {
1434 let this = glib::Object::builder::<Self>()
1435 .property("session", session)
1436 .build();
1437
1438 this.imp().init(matrix_room, metainfo);
1439 this
1440 }
1441
1442 pub(crate) fn matrix_room(&self) -> &MatrixRoom {
1444 self.imp().matrix_room()
1445 }
1446
1447 pub(crate) fn room_id(&self) -> &RoomId {
1449 self.imp().room_id()
1450 }
1451
1452 pub fn human_readable_id(&self) -> String {
1457 format!("{} ({})", self.display_name(), self.room_id())
1458 }
1459
1460 pub(crate) fn is_joined(&self) -> bool {
1462 self.own_member().membership() == Membership::Join
1463 }
1464
1465 pub(crate) fn predecessor_id(&self) -> Option<&OwnedRoomId> {
1468 self.imp().predecessor_id.get()
1469 }
1470
1471 pub(crate) fn successor_id(&self) -> Option<&RoomId> {
1473 self.imp().successor_id.get().map(std::ops::Deref::deref)
1474 }
1475
1476 pub(crate) async fn matrix_to_uri(&self) -> MatrixToUri {
1478 let matrix_room = self.matrix_room().clone();
1479
1480 let handle = spawn_tokio!(async move { matrix_room.matrix_to_permalink().await });
1481 match handle.await.expect("task was not aborted") {
1482 Ok(permalink) => {
1483 return permalink;
1484 }
1485 Err(error) => {
1486 error!("Could not get room event permalink: {error}");
1487 }
1488 }
1489
1490 self.room_id().matrix_to_uri()
1492 }
1493
1494 pub(crate) async fn matrix_to_event_uri(&self, event_id: OwnedEventId) -> MatrixToUri {
1496 let matrix_room = self.matrix_room().clone();
1497
1498 let event_id_clone = event_id.clone();
1499 let handle =
1500 spawn_tokio!(
1501 async move { matrix_room.matrix_to_event_permalink(event_id_clone).await }
1502 );
1503 match handle.await.expect("task was not aborted") {
1504 Ok(permalink) => {
1505 return permalink;
1506 }
1507 Err(error) => {
1508 error!("Could not get room event permalink: {error}");
1509 }
1510 }
1511
1512 self.room_id().matrix_to_event_uri(event_id)
1514 }
1515
1516 pub(crate) fn at_room(&self) -> AtRoom {
1518 let at_room = AtRoom::new(self.room_id().to_owned());
1519
1520 self.avatar_data()
1522 .bind_property("image", &at_room.avatar_data(), "image")
1523 .sync_create()
1524 .build();
1525
1526 at_room
1527 }
1528
1529 pub(crate) fn get_or_create_members(&self) -> MemberList {
1533 let members = &self.imp().members;
1534 if let Some(list) = members.upgrade() {
1535 list
1536 } else {
1537 let list = MemberList::new(self);
1538 members.set(Some(&list));
1539 self.notify_members();
1540 list
1541 }
1542 }
1543
1544 pub(crate) async fn change_category(&self, category: TargetRoomCategory) -> MatrixResult<()> {
1553 let previous_category = self.category();
1554
1555 if previous_category == category {
1556 return Ok(());
1557 }
1558
1559 if previous_category == RoomCategory::Outdated {
1560 warn!("Cannot change the category of an upgraded room");
1561 return Ok(());
1562 }
1563
1564 self.imp().set_category(category.into());
1565
1566 let matrix_room = self.matrix_room().clone();
1567 let handle = spawn_tokio!(async move {
1568 let room_state = matrix_room.state();
1569
1570 match category {
1571 TargetRoomCategory::Favorite => {
1572 if !matrix_room.is_favourite() {
1573 matrix_room.set_is_favourite(true, None).await?;
1575 } else if matrix_room.is_low_priority() {
1576 matrix_room.set_is_low_priority(false, None).await?;
1577 }
1578
1579 if matches!(room_state, RoomState::Invited | RoomState::Left) {
1580 matrix_room.join().await?;
1581 }
1582 }
1583 TargetRoomCategory::Normal => {
1584 if matrix_room.is_favourite() {
1585 matrix_room.set_is_favourite(false, None).await?;
1586 }
1587 if matrix_room.is_low_priority() {
1588 matrix_room.set_is_low_priority(false, None).await?;
1589 }
1590
1591 if matches!(room_state, RoomState::Invited | RoomState::Left) {
1592 matrix_room.join().await?;
1593 }
1594 }
1595 TargetRoomCategory::LowPriority => {
1596 if !matrix_room.is_low_priority() {
1597 matrix_room.set_is_low_priority(true, None).await?;
1599 } else if matrix_room.is_favourite() {
1600 matrix_room.set_is_favourite(false, None).await?;
1601 }
1602
1603 if matches!(room_state, RoomState::Invited | RoomState::Left) {
1604 matrix_room.join().await?;
1605 }
1606 }
1607 TargetRoomCategory::Left => {
1608 if matches!(room_state, RoomState::Invited | RoomState::Joined) {
1609 matrix_room.leave().await?;
1610 }
1611 }
1612 }
1613
1614 Result::<_, matrix_sdk::Error>::Ok(())
1615 });
1616
1617 match handle.await.expect("task was not aborted") {
1618 Ok(()) => Ok(()),
1619 Err(error) => {
1620 error!("Could not set the room category: {error}");
1621
1622 self.imp().update_category().await;
1624
1625 Err(error)
1626 }
1627 }
1628 }
1629
1630 pub(crate) async fn toggle_reaction(&self, key: String, event: &Event) -> Result<(), ()> {
1632 let matrix_timeline = self.live_timeline().matrix_timeline();
1633 let identifier = event.identifier();
1634
1635 let handle =
1636 spawn_tokio!(async move { matrix_timeline.toggle_reaction(&identifier, &key).await });
1637
1638 if let Err(error) = handle.await.expect("task was not aborted") {
1639 error!("Could not toggle reaction: {error}");
1640 return Err(());
1641 }
1642
1643 Ok(())
1644 }
1645
1646 pub(crate) async fn send_receipt(
1648 &self,
1649 receipt_type: ApiReceiptType,
1650 position: ReceiptPosition,
1651 ) {
1652 let Some(session) = self.session() else {
1653 return;
1654 };
1655 let send_public_receipt = session.settings().public_read_receipts_enabled();
1656
1657 let receipt_type = match receipt_type {
1658 ApiReceiptType::Read if !send_public_receipt => ApiReceiptType::ReadPrivate,
1659 t => t,
1660 };
1661
1662 let matrix_timeline = self.live_timeline().matrix_timeline();
1663 let handle = spawn_tokio!(async move {
1664 match position {
1665 ReceiptPosition::End => matrix_timeline.mark_as_read(receipt_type).await,
1666 ReceiptPosition::Event(event_id) => {
1667 matrix_timeline
1668 .send_single_receipt(receipt_type, ReceiptThread::Unthreaded, event_id)
1669 .await
1670 }
1671 }
1672 });
1673
1674 if let Err(error) = handle.await.expect("task was not aborted") {
1675 error!("Could not send read receipt: {error}");
1676 }
1677 }
1678
1679 pub(crate) fn send_typing_notification(&self, is_typing: bool) {
1681 let matrix_room = self.matrix_room();
1682 if matrix_room.state() != RoomState::Joined {
1683 return;
1684 }
1685
1686 let matrix_room = matrix_room.clone();
1687 let handle = spawn_tokio!(async move { matrix_room.typing_notice(is_typing).await });
1688
1689 spawn!(glib::Priority::DEFAULT_IDLE, async move {
1690 match handle.await.expect("task was not aborted") {
1691 Ok(()) => {}
1692 Err(error) => error!("Could not send typing notification: {error}"),
1693 }
1694 });
1695 }
1696
1697 pub(crate) async fn redact<'a>(
1702 &self,
1703 events: &'a [OwnedEventId],
1704 reason: Option<String>,
1705 ) -> Result<(), Vec<&'a EventId>> {
1706 let matrix_room = self.matrix_room();
1707 if matrix_room.state() != RoomState::Joined {
1708 return Ok(());
1709 }
1710
1711 let events_clone = events.to_owned();
1712 let matrix_room = matrix_room.clone();
1713 let handle = spawn_tokio!(async move {
1714 let mut failed_redactions = Vec::new();
1715
1716 for (i, event_id) in events_clone.iter().enumerate() {
1717 match matrix_room.redact(event_id, reason.as_deref(), None).await {
1718 Ok(_) => {}
1719 Err(error) => {
1720 error!("Could not redact event with ID {event_id}: {error}");
1721 failed_redactions.push(i);
1722 }
1723 }
1724 }
1725
1726 failed_redactions
1727 });
1728
1729 let failed_redactions = handle.await.expect("task was not aborted");
1730 let failed_redactions = failed_redactions
1731 .into_iter()
1732 .map(|i| &*events[i])
1733 .collect::<Vec<_>>();
1734
1735 if failed_redactions.is_empty() {
1736 Ok(())
1737 } else {
1738 Err(failed_redactions)
1739 }
1740 }
1741
1742 pub(crate) async fn report_events<'a>(
1749 &self,
1750 events: &'a [(OwnedEventId, Option<String>)],
1751 ) -> Result<(), Vec<&'a EventId>> {
1752 let events_clone = events.to_owned();
1753 let matrix_room = self.matrix_room().clone();
1754 let handle = spawn_tokio!(async move {
1755 let futures = events_clone
1756 .into_iter()
1757 .map(|(event_id, reason)| matrix_room.report_content(event_id, None, reason));
1758 futures_util::future::join_all(futures).await
1759 });
1760
1761 let mut failed = Vec::new();
1762 for (index, result) in handle
1763 .await
1764 .expect("task was not aborted")
1765 .iter()
1766 .enumerate()
1767 {
1768 match result {
1769 Ok(_) => {}
1770 Err(error) => {
1771 error!(
1772 "Could not report content with event ID {}: {error}",
1773 events[index].0,
1774 );
1775 failed.push(&*events[index].0);
1776 }
1777 }
1778 }
1779
1780 if failed.is_empty() {
1781 Ok(())
1782 } else {
1783 Err(failed)
1784 }
1785 }
1786
1787 pub(crate) async fn invite<'a>(
1792 &self,
1793 user_ids: &'a [OwnedUserId],
1794 ) -> Result<(), Vec<&'a UserId>> {
1795 let matrix_room = self.matrix_room();
1796 if matrix_room.state() != RoomState::Joined {
1797 error!("Can’t invite users, because this room isn’t a joined room");
1798 return Ok(());
1799 }
1800
1801 let user_ids_clone = user_ids.to_owned();
1802 let matrix_room = matrix_room.clone();
1803 let handle = spawn_tokio!(async move {
1804 let invitations = user_ids_clone
1805 .iter()
1806 .map(|user_id| matrix_room.invite_user_by_id(user_id));
1807 futures_util::future::join_all(invitations).await
1808 });
1809
1810 let mut failed_invites = Vec::new();
1811 for (index, result) in handle
1812 .await
1813 .expect("task was not aborted")
1814 .iter()
1815 .enumerate()
1816 {
1817 match result {
1818 Ok(()) => {}
1819 Err(error) => {
1820 error!("Could not invite user with ID {}: {error}", user_ids[index],);
1821 failed_invites.push(&*user_ids[index]);
1822 }
1823 }
1824 }
1825
1826 if failed_invites.is_empty() {
1827 Ok(())
1828 } else {
1829 Err(failed_invites)
1830 }
1831 }
1832
1833 pub(crate) async fn kick<'a>(
1840 &self,
1841 users: &'a [(OwnedUserId, Option<String>)],
1842 ) -> Result<(), Vec<&'a UserId>> {
1843 let users_clone = users.to_owned();
1844 let matrix_room = self.matrix_room().clone();
1845 let handle = spawn_tokio!(async move {
1846 let futures = users_clone
1847 .iter()
1848 .map(|(user_id, reason)| matrix_room.kick_user(user_id, reason.as_deref()));
1849 futures_util::future::join_all(futures).await
1850 });
1851
1852 let mut failed_kicks = Vec::new();
1853 for (index, result) in handle
1854 .await
1855 .expect("task was not aborted")
1856 .iter()
1857 .enumerate()
1858 {
1859 match result {
1860 Ok(()) => {}
1861 Err(error) => {
1862 error!("Could not kick user with ID {}: {error}", users[index].0);
1863 failed_kicks.push(&*users[index].0);
1864 }
1865 }
1866 }
1867
1868 if failed_kicks.is_empty() {
1869 Ok(())
1870 } else {
1871 Err(failed_kicks)
1872 }
1873 }
1874
1875 pub(crate) async fn ban<'a>(
1882 &self,
1883 users: &'a [(OwnedUserId, Option<String>)],
1884 ) -> Result<(), Vec<&'a UserId>> {
1885 let users_clone = users.to_owned();
1886 let matrix_room = self.matrix_room().clone();
1887 let handle = spawn_tokio!(async move {
1888 let futures = users_clone
1889 .iter()
1890 .map(|(user_id, reason)| matrix_room.ban_user(user_id, reason.as_deref()));
1891 futures_util::future::join_all(futures).await
1892 });
1893
1894 let mut failed_bans = Vec::new();
1895 for (index, result) in handle
1896 .await
1897 .expect("task was not aborted")
1898 .iter()
1899 .enumerate()
1900 {
1901 match result {
1902 Ok(()) => {}
1903 Err(error) => {
1904 error!("Could not ban user with ID {}: {error}", users[index].0);
1905 failed_bans.push(&*users[index].0);
1906 }
1907 }
1908 }
1909
1910 if failed_bans.is_empty() {
1911 Ok(())
1912 } else {
1913 Err(failed_bans)
1914 }
1915 }
1916
1917 pub(crate) async fn unban<'a>(
1924 &self,
1925 users: &'a [(OwnedUserId, Option<String>)],
1926 ) -> Result<(), Vec<&'a UserId>> {
1927 let users_clone = users.to_owned();
1928 let matrix_room = self.matrix_room().clone();
1929 let handle = spawn_tokio!(async move {
1930 let futures = users_clone
1931 .iter()
1932 .map(|(user_id, reason)| matrix_room.unban_user(user_id, reason.as_deref()));
1933 futures_util::future::join_all(futures).await
1934 });
1935
1936 let mut failed_unbans = Vec::new();
1937 for (index, result) in handle
1938 .await
1939 .expect("task was not aborted")
1940 .iter()
1941 .enumerate()
1942 {
1943 match result {
1944 Ok(()) => {}
1945 Err(error) => {
1946 error!("Could not unban user with ID {}: {error}", users[index].0);
1947 failed_unbans.push(&*users[index].0);
1948 }
1949 }
1950 }
1951
1952 if failed_unbans.is_empty() {
1953 Ok(())
1954 } else {
1955 Err(failed_unbans)
1956 }
1957 }
1958
1959 pub(crate) async fn enable_encryption(&self) -> Result<(), ()> {
1961 if self.is_encrypted() {
1962 return Ok(());
1964 }
1965
1966 let matrix_room = self.matrix_room().clone();
1967 let handle = spawn_tokio!(async move { matrix_room.enable_encryption().await });
1968
1969 match handle.await.expect("task was not aborted") {
1970 Ok(()) => Ok(()),
1971 Err(error) => {
1972 error!("Could not enable room encryption: {error}");
1973 Err(())
1974 }
1975 }
1976 }
1977
1978 pub(crate) async fn forget(&self) -> MatrixResult<()> {
1980 if self.category() != RoomCategory::Left {
1981 warn!("Cannot forget a room that is not left");
1982 return Ok(());
1983 }
1984
1985 let matrix_room = self.matrix_room().clone();
1986 let handle = spawn_tokio!(async move { matrix_room.forget().await });
1987
1988 match handle.await.expect("task was not aborted") {
1989 Ok(()) => {
1990 self.emit_by_name::<()>("room-forgotten", &[]);
1991 Ok(())
1992 }
1993 Err(error) => {
1994 error!("Could not forget the room: {error}");
1995 Err(error)
1996 }
1997 }
1998 }
1999
2000 pub(crate) fn handle_ambiguity_changes<'a>(
2002 &self,
2003 changes: impl Iterator<Item = &'a AmbiguityChange>,
2004 ) {
2005 self.imp().handle_ambiguity_changes(changes);
2006 }
2007
2008 fn update_latest_activity<'a>(&self, events: impl Iterator<Item = &'a Event>) {
2012 let own_user_id = self.imp().own_member().user_id();
2013 let mut latest_activity = self.latest_activity();
2014
2015 for event in events {
2016 if event.counts_as_activity(own_user_id) {
2017 latest_activity = latest_activity.max(event.origin_server_ts().get().into());
2018 break;
2019 }
2020 }
2021
2022 self.imp().set_latest_activity(latest_activity);
2023 }
2024
2025 pub(crate) fn update_successor(&self) {
2027 self.imp().update_successor();
2028 }
2029
2030 pub(crate) fn connect_room_forgotten<F: Fn(&Self) + 'static>(
2032 &self,
2033 f: F,
2034 ) -> glib::SignalHandlerId {
2035 self.connect_closure(
2036 "room-forgotten",
2037 true,
2038 closure_local!(move |obj: Self| {
2039 f(&obj);
2040 }),
2041 )
2042 }
2043}
2044
2045#[derive(Debug, Default, Hash, Eq, PartialEq, Clone, Copy, glib::Enum)]
2047#[enum_type(name = "HistoryVisibilityValue")]
2048pub enum HistoryVisibilityValue {
2049 WorldReadable,
2051 #[default]
2053 Shared,
2054 Invited,
2056 Joined,
2058 Unsupported,
2060}
2061
2062impl From<HistoryVisibility> for HistoryVisibilityValue {
2063 fn from(value: HistoryVisibility) -> Self {
2064 match value {
2065 HistoryVisibility::Invited => Self::Invited,
2066 HistoryVisibility::Joined => Self::Joined,
2067 HistoryVisibility::Shared => Self::Shared,
2068 HistoryVisibility::WorldReadable => Self::WorldReadable,
2069 _ => Self::Unsupported,
2070 }
2071 }
2072}
2073
2074impl From<HistoryVisibilityValue> for HistoryVisibility {
2075 fn from(value: HistoryVisibilityValue) -> Self {
2076 match value {
2077 HistoryVisibilityValue::Invited => Self::Invited,
2078 HistoryVisibilityValue::Joined => Self::Joined,
2079 HistoryVisibilityValue::Shared => Self::Shared,
2080 HistoryVisibilityValue::WorldReadable => Self::WorldReadable,
2081 HistoryVisibilityValue::Unsupported => unimplemented!(),
2082 }
2083 }
2084}
2085
2086#[derive(Debug, Clone)]
2088pub(crate) enum ReceiptPosition {
2089 End,
2091 Event(OwnedEventId),
2093}