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 Result as MatrixResult, RoomDisplayName, RoomInfo, RoomMemberships, RoomState,
13 deserialized_responses::{AmbiguityChange, RawSyncOrStrippedState},
14 event_handler::EventHandlerDropGuard,
15 room::Room as MatrixRoom,
16 send_queue::RoomSendQueueUpdate,
17};
18use ruma::{
19 EventId, MatrixToUri, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId,
20 api::client::{
21 error::{ErrorKind, RetryAfter},
22 receipt::create_receipt::v3::ReceiptType as ApiReceiptType,
23 },
24 events::room::{
25 guest_access::GuestAccess,
26 history_visibility::HistoryVisibility,
27 member::{MembershipState, RoomMemberEventContent, SyncRoomMemberEvent},
28 },
29 room_version_rules::RoomVersionRules,
30};
31use serde::Deserialize;
32use tokio_stream::wrappers::BroadcastStream;
33use tracing::{debug, error, warn};
34
35mod aliases;
36mod category;
37mod highlight_flags;
38mod join_rule;
39mod member;
40mod member_list;
41mod permissions;
42mod timeline;
43mod typing_list;
44
45pub(crate) use self::{
46 aliases::{AddAltAliasError, RegisterLocalAliasError, RoomAliases},
47 category::{RoomCategory, TargetRoomCategory},
48 highlight_flags::HighlightFlags,
49 join_rule::{JoinRule, JoinRuleValue},
50 member::{Member, Membership},
51 member_list::*,
52 permissions::*,
53 timeline::*,
54 typing_list::TypingList,
55};
56use super::{
57 IdentityVerification, Session, User, notifications::NotificationsRoomSetting,
58 room_list::RoomMetainfo,
59};
60use crate::{
61 components::{AtRoom, AvatarImage, AvatarUriSource, PillSource},
62 gettext_f,
63 prelude::*,
64 spawn, spawn_tokio,
65 utils::{BoundObjectWeakRef, string::linkify},
66};
67
68const DEFAULT_RETRY_AFTER: u32 = 30;
71
72mod imp {
73 use std::{
74 cell::{Cell, OnceCell},
75 marker::PhantomData,
76 sync::LazyLock,
77 time::SystemTime,
78 };
79
80 use glib::subclass::Signal;
81
82 use super::*;
83
84 #[derive(Default, glib::Properties)]
85 #[properties(wrapper_type = super::Room)]
86 pub struct Room {
87 matrix_room: OnceCell<MatrixRoom>,
89 #[property(get, set = Self::set_session, construct_only)]
91 session: glib::WeakRef<Session>,
92 #[property(get = Self::room_id_string)]
94 room_id_string: PhantomData<String>,
95 #[property(get)]
97 aliases: RoomAliases,
98 #[property(get)]
103 name: RefCell<Option<String>>,
104 #[property(get)]
109 has_avatar: Cell<bool>,
110 #[property(get)]
112 topic: RefCell<Option<String>>,
113 #[property(get)]
118 topic_linkified: RefCell<Option<String>>,
119 #[property(get, builder(RoomCategory::default()))]
121 category: Cell<RoomCategory>,
122 #[property(get)]
124 is_direct: Cell<bool>,
125 #[property(get)]
127 is_tombstoned: Cell<bool>,
128 pub(super) predecessor_id: OnceCell<OwnedRoomId>,
130 #[property(get = Self::predecessor_id_string)]
133 predecessor_id_string: PhantomData<Option<String>>,
134 pub(super) successor_id: OnceCell<OwnedRoomId>,
136 #[property(get = Self::successor_id_string)]
139 successor_id_string: PhantomData<Option<String>>,
140 #[property(get)]
143 successor: glib::WeakRef<super::Room>,
144 #[property(get)]
146 pub(super) members: glib::WeakRef<MemberList>,
147 members_drop_guard: OnceCell<EventHandlerDropGuard>,
148 #[property(get)]
151 joined_members_count: Cell<u64>,
152 #[property(get)]
154 own_member: OnceCell<Member>,
155 #[property(get)]
158 is_invite: Cell<bool>,
159 #[property(get)]
163 inviter: RefCell<Option<Member>>,
164 #[property(get)]
167 direct_member: RefCell<Option<Member>>,
168 #[property(get)]
170 live_timeline: OnceCell<Timeline>,
171 #[property(get)]
178 latest_activity: Cell<u64>,
179 #[property(get)]
181 is_marked_unread: Cell<bool>,
182 #[property(get)]
184 is_read: Cell<bool>,
185 #[property(get)]
187 notification_count: Cell<u64>,
188 #[property(get)]
190 has_notifications: Cell<bool>,
191 #[property(get)]
193 highlight: Cell<HighlightFlags>,
194 #[property(get)]
196 is_encrypted: Cell<bool>,
197 #[property(get)]
199 join_rule: JoinRule,
200 #[property(get)]
202 guests_allowed: Cell<bool>,
203 #[property(get, builder(HistoryVisibilityValue::default()))]
205 history_visibility: Cell<HistoryVisibilityValue>,
206 #[property(get = Self::version)]
208 version: PhantomData<String>,
209 #[property(get = Self::federated)]
211 federated: PhantomData<bool>,
212 #[property(get)]
214 typing_list: TypingList,
215 typing_drop_guard: OnceCell<EventHandlerDropGuard>,
216 #[property(get, set = Self::set_notifications_setting, explicit_notify, builder(NotificationsRoomSetting::default()))]
218 notifications_setting: Cell<NotificationsRoomSetting>,
219 #[property(get)]
221 permissions: Permissions,
222 #[property(get, set = Self::set_verification, nullable, explicit_notify)]
224 verification: BoundObjectWeakRef<IdentityVerification>,
225 #[property(get)]
229 is_room_info_initialized: Cell<bool>,
230 #[property(get)]
232 attempted_auto_join: Cell<bool>,
233 }
234
235 #[glib::object_subclass]
236 impl ObjectSubclass for Room {
237 const NAME: &'static str = "Room";
238 type Type = super::Room;
239 type ParentType = PillSource;
240 }
241
242 #[glib::derived_properties]
243 impl ObjectImpl for Room {
244 fn signals() -> &'static [Signal] {
245 static SIGNALS: LazyLock<Vec<Signal>> =
246 LazyLock::new(|| vec![Signal::builder("room-forgotten").build()]);
247 SIGNALS.as_ref()
248 }
249 }
250
251 impl PillSourceImpl for Room {
252 fn identifier(&self) -> String {
253 self.aliases
254 .alias_string()
255 .unwrap_or_else(|| self.room_id_string())
256 }
257 }
258
259 impl Room {
260 pub(super) fn init(&self, matrix_room: MatrixRoom, metainfo: Option<RoomMetainfo>) {
262 let obj = self.obj();
263
264 self.matrix_room
265 .set(matrix_room)
266 .expect("matrix room is uninitialized");
267
268 self.init_live_timeline();
269 self.aliases.init(&obj);
270 self.load_predecessor();
271 self.watch_members();
272 self.join_rule.init(&obj);
273 self.set_up_typing();
274 self.watch_send_queue();
275
276 spawn!(
277 glib::Priority::DEFAULT_IDLE,
278 clone!(
279 #[weak(rename_to = imp)]
280 self,
281 async move {
282 imp.update_with_room_info(imp.matrix_room().clone_info())
283 .await;
284 imp.watch_room_info();
285
286 imp.is_room_info_initialized.set(true);
287 imp.obj().notify_is_room_info_initialized();
288
289 let preload = matches!(
295 imp.category.get(),
296 RoomCategory::Favorite
297 | RoomCategory::Normal
298 | RoomCategory::LowPriority
299 );
300 imp.live_timeline().set_preload(preload);
301
302 imp.permissions.init(&imp.obj()).await;
303 }
304 )
305 );
306
307 spawn!(
308 glib::Priority::DEFAULT_IDLE,
309 clone!(
310 #[weak(rename_to = imp)]
311 self,
312 async move {
313 imp.load_own_member().await;
314 }
315 )
316 );
317
318 if let Some(RoomMetainfo {
319 latest_activity,
320 is_read,
321 }) = metainfo
322 {
323 self.set_latest_activity(latest_activity);
324 self.set_is_read(is_read);
325
326 self.update_highlight();
327 }
328 }
329
330 pub(super) fn matrix_room(&self) -> &MatrixRoom {
332 self.matrix_room.get().expect("matrix room was initialized")
333 }
334
335 fn set_session(&self, session: &Session) {
337 self.session.set(Some(session));
338
339 let own_member = Member::new(&self.obj(), session.user_id().clone());
340 self.own_member
341 .set(own_member)
342 .expect("own member was uninitialized");
343 }
344
345 pub(super) fn room_id(&self) -> &RoomId {
347 self.matrix_room().room_id()
348 }
349
350 fn room_id_string(&self) -> String {
352 self.matrix_room().room_id().to_string()
353 }
354
355 fn update_name(&self) {
357 let name = self.matrix_room().name().into_clean_string();
358
359 if *self.name.borrow() == name {
360 return;
361 }
362
363 self.name.replace(name);
364 self.obj().notify_name();
365 }
366
367 async fn update_display_name(&self) {
369 let matrix_room = self.matrix_room().clone();
370 let handle = spawn_tokio!(async move { matrix_room.display_name().await });
371
372 let sdk_display_name = handle
373 .await
374 .expect("task was not aborted")
375 .inspect_err(|error| {
376 error!("Could not compute display name: {error}");
377 })
378 .ok();
379
380 let mut display_name = if let Some(sdk_display_name) = sdk_display_name {
381 match sdk_display_name {
382 RoomDisplayName::Named(s)
383 | RoomDisplayName::Calculated(s)
384 | RoomDisplayName::Aliased(s) => s,
385 RoomDisplayName::EmptyWas(s) => {
386 gettext_f("Empty Room (was {user})", &[("user", &s)])
390 }
391 RoomDisplayName::Empty => gettext("Empty Room"),
393 }
394 } else {
395 Default::default()
396 };
397
398 display_name.clean_string();
399
400 if display_name.is_empty() {
401 display_name = gettext("Unknown");
403 }
404
405 self.obj().set_display_name(display_name);
406 }
407
408 fn set_has_avatar(&self, has_avatar: bool) {
410 if self.has_avatar.get() == has_avatar {
411 return;
412 }
413
414 self.has_avatar.set(has_avatar);
415 self.obj().notify_has_avatar();
416 }
417
418 fn update_avatar(&self) {
420 let Some(session) = self.session.upgrade() else {
421 return;
422 };
423
424 let obj = self.obj();
425 let avatar_data = obj.avatar_data();
426 let matrix_room = self.matrix_room();
427
428 let prev_avatar_url = avatar_data.image().and_then(|i| i.uri());
429 let room_avatar_url = matrix_room.avatar_url();
430
431 if prev_avatar_url.is_some() && prev_avatar_url == room_avatar_url {
432 return;
434 }
435
436 if let Some(avatar_url) = room_avatar_url {
437 let avatar_info = matrix_room.avatar_info();
439
440 if let Some(avatar_image) = avatar_data
441 .image()
442 .filter(|i| i.uri_source() == AvatarUriSource::Room)
443 {
444 avatar_image.set_uri_and_info(Some(avatar_url), avatar_info);
445 } else {
446 let avatar_image = AvatarImage::new(
447 &session,
448 AvatarUriSource::Room,
449 Some(avatar_url),
450 avatar_info,
451 );
452
453 avatar_data.set_image(Some(avatar_image.clone()));
454 }
455
456 self.set_has_avatar(true);
457 return;
458 }
459
460 self.set_has_avatar(false);
461
462 if let Some(direct_member) = self.direct_member.borrow().as_ref() {
464 avatar_data.set_image(direct_member.avatar_data().image());
465 }
466
467 let avatar_image = avatar_data.image();
468
469 if let Some(avatar_image) = avatar_image
470 .as_ref()
471 .filter(|i| i.uri_source() == AvatarUriSource::Room)
472 {
473 avatar_image.set_uri_and_info(None, None);
475 } else if avatar_image.is_none() {
476 avatar_data.set_image(Some(AvatarImage::new(
478 &session,
479 AvatarUriSource::Room,
480 None,
481 None,
482 )));
483 }
484 }
485
486 fn update_topic(&self) {
488 let topic = self
489 .matrix_room()
490 .topic()
491 .map(|mut s| {
492 s.strip_nul();
493 s.truncate_end_whitespaces();
494 s
495 })
496 .filter(|topic| !topic.is_empty());
497
498 if *self.topic.borrow() == topic {
499 return;
500 }
501
502 let topic_linkified = topic.as_ref().map(|t| {
503 let mut s = linkify(t);
505 s.truncate_end_whitespaces();
507 s
508 });
509
510 self.topic.replace(topic);
511 self.topic_linkified.replace(topic_linkified);
512
513 let obj = self.obj();
514 obj.notify_topic();
515 obj.notify_topic_linkified();
516 }
517
518 fn set_category(&self, category: RoomCategory) {
520 let old_category = self.category.get();
521
522 if old_category == RoomCategory::Outdated || old_category == category {
523 return;
524 }
525
526 self.category.set(category);
527 self.obj().notify_category();
528
529 let room_state = self.matrix_room().state();
531 if !old_category.is_state(room_state) {
532 if self.is_room_info_initialized.get() {
533 debug!(room_id = %self.room_id(), ?room_state, "The state of the room changed");
534 }
535
536 match room_state {
537 RoomState::Joined => {
538 if let Some(members) = self.members.upgrade() {
539 members.reload();
542 }
543
544 self.set_up_typing();
545 }
546 RoomState::Left
547 | RoomState::Knocked
548 | RoomState::Banned
549 | RoomState::Invited => {}
550 }
551 }
552 }
553
554 pub(super) async fn update_category(&self) {
556 if self.category.get() == RoomCategory::Outdated {
558 return;
559 }
560
561 self.update_is_invite().await;
562 self.update_inviter().await;
563
564 let matrix_room = self.matrix_room();
565 let state = matrix_room.state();
566
567 if state != RoomState::Invited {
569 self.attempted_auto_join.take();
570 }
571
572 let category = match state {
573 RoomState::Joined => {
574 if matrix_room.is_space() {
575 RoomCategory::Space
576 } else if matrix_room.is_favourite() {
577 RoomCategory::Favorite
578 } else if matrix_room.is_low_priority() {
579 RoomCategory::LowPriority
580 } else {
581 RoomCategory::Normal
582 }
583 }
584 RoomState::Invited => {
585 if !self.attempted_auto_join.get()
587 && self.was_membership(&MembershipState::Knock).await
588 {
589 self.attempted_auto_join.set(true);
590
591 if self
592 .change_category(TargetRoomCategory::Normal)
593 .await
594 .is_ok()
595 {
596 return;
599 }
600 }
601
602 if self
603 .inviter
604 .borrow()
605 .as_ref()
606 .is_some_and(Member::is_ignored)
607 {
608 RoomCategory::Ignored
609 } else {
610 RoomCategory::Invited
611 }
612 }
613 RoomState::Knocked => RoomCategory::Knocked,
614 RoomState::Left | RoomState::Banned => RoomCategory::Left,
615 };
616
617 self.set_category(category);
618 }
619
620 async fn set_is_direct(&self, is_direct: bool) {
622 if self.is_direct.get() == is_direct {
623 return;
624 }
625
626 self.is_direct.set(is_direct);
627 self.obj().notify_is_direct();
628
629 self.update_direct_member().await;
630 }
631
632 pub(super) async fn update_is_direct(&self) {
634 let matrix_room = self.matrix_room().clone();
635 let handle = spawn_tokio!(async move { matrix_room.is_direct().await });
636
637 match handle.await.expect("task was not aborted") {
638 Ok(is_direct) => self.set_is_direct(is_direct).await,
639 Err(error) => {
640 error!(room_id = %self.room_id(), "Could not load whether room is direct: {error}");
641 }
642 }
643 }
644
645 fn update_tombstone(&self) {
647 let matrix_room = self.matrix_room();
648
649 if !matrix_room.is_tombstoned() || self.successor_id.get().is_some() {
650 return;
651 }
652 let obj = self.obj();
653
654 if let Some(room_tombstone) = matrix_room.tombstone_content() {
655 self.successor_id
656 .set(room_tombstone.replacement_room)
657 .expect("successor ID should be uninitialized");
658 obj.notify_successor_id_string();
659 }
660
661 self.update_successor();
663
664 if self.successor.upgrade().is_none()
666 && let Some(session) = self.session.upgrade()
667 {
668 session
669 .room_list()
670 .add_tombstoned_room(self.room_id().to_owned());
671 }
672
673 if !self.is_tombstoned.get() {
674 self.is_tombstoned.set(true);
675 obj.notify_is_tombstoned();
676 }
677 }
678
679 pub(super) fn update_successor(&self) {
681 if self.category.get() == RoomCategory::Outdated {
682 return;
683 }
684
685 let Some(session) = self.session.upgrade() else {
686 return;
687 };
688 let room_list = session.room_list();
689
690 if let Some(successor) = self
691 .successor_id
692 .get()
693 .and_then(|successor_id| room_list.get(successor_id))
694 {
695 if successor
699 .predecessor_id()
700 .is_some_and(|predecessor_id| predecessor_id == self.room_id())
701 {
702 self.set_successor(&successor);
703 return;
704 }
705 }
706
707 for room in room_list.iter::<super::Room>() {
710 let Ok(room) = room else {
711 break;
712 };
713
714 if room
715 .predecessor_id()
716 .is_some_and(|predecessor_id| predecessor_id == self.room_id())
717 {
718 self.set_successor(&room);
719 return;
720 }
721 }
722 }
723
724 fn predecessor_id_string(&self) -> Option<String> {
727 self.predecessor_id.get().map(ToString::to_string)
728 }
729
730 fn load_predecessor(&self) {
732 let Some(event) = self.matrix_room().create_content() else {
733 return;
734 };
735 let Some(predecessor) = event.predecessor else {
736 return;
737 };
738
739 self.predecessor_id
740 .set(predecessor.room_id)
741 .expect("predecessor ID is uninitialized");
742 self.obj().notify_predecessor_id_string();
743 }
744
745 fn successor_id_string(&self) -> Option<String> {
747 self.successor_id.get().map(ToString::to_string)
748 }
749
750 fn set_successor(&self, successor: &super::Room) {
752 self.successor.set(Some(successor));
753 self.obj().notify_successor();
754
755 self.set_category(RoomCategory::Outdated);
756 }
757
758 fn watch_members(&self) {
760 let matrix_room = self.matrix_room();
761
762 let obj_weak = glib::SendWeakRef::from(self.obj().downgrade());
763 let handle = matrix_room.add_event_handler(move |event: SyncRoomMemberEvent| {
764 let obj_weak = obj_weak.clone();
765 async move {
766 let ctx = glib::MainContext::default();
767 ctx.spawn(async move {
768 spawn!(async move {
769 if let Some(obj) = obj_weak.upgrade() {
770 obj.imp().handle_member_event(&event);
771 }
772 });
773 });
774 }
775 });
776
777 let drop_guard = matrix_room.client().event_handler_drop_guard(handle);
778 self.members_drop_guard.set(drop_guard).unwrap();
779 }
780
781 fn handle_member_event(&self, event: &SyncRoomMemberEvent) {
783 let user_id = event.state_key();
784
785 if let Some(members) = self.members.upgrade() {
786 members.update_member(user_id.clone());
787 } else if user_id == self.own_member().user_id() {
788 self.own_member().update();
789 } else if let Some(member) = self
790 .direct_member
791 .borrow()
792 .as_ref()
793 .filter(|member| member.user_id() == user_id)
794 {
795 member.update();
796 }
797
798 spawn!(clone!(
800 #[weak(rename_to = imp)]
801 self,
802 async move {
803 imp.update_direct_member().await;
804 }
805 ));
806 }
807
808 fn set_joined_members_count(&self, count: u64) {
811 if self.joined_members_count.get() == count {
812 return;
813 }
814
815 self.joined_members_count.set(count);
816 self.obj().notify_joined_members_count();
817 }
818
819 pub(super) fn own_member(&self) -> &Member {
821 self.own_member.get().expect("Own member was initialized")
822 }
823
824 async fn load_own_member(&self) {
826 let own_member = self.own_member();
827 let user_id = own_member.user_id().clone();
828 let matrix_room = self.matrix_room().clone();
829
830 let handle =
831 spawn_tokio!(async move { matrix_room.get_member_no_sync(&user_id).await });
832
833 match handle.await.expect("task was not aborted") {
834 Ok(Some(matrix_member)) => own_member.update_from_room_member(&matrix_member),
835 Ok(None) => {}
836 Err(error) => error!(
837 "Could not load own member for room {}: {error}",
838 self.room_id()
839 ),
840 }
841 }
842
843 async fn update_is_invite(&self) {
846 let matrix_room = self.matrix_room();
847
848 let is_invite = match matrix_room.state() {
849 RoomState::Invited => true,
850 RoomState::Left | RoomState::Banned => {
851 self.was_membership(&MembershipState::Invite).await
852 }
853 _ => false,
854 };
855
856 if self.is_invite.get() == is_invite {
857 return;
858 }
859
860 self.is_invite.set(is_invite);
861 self.obj().notify_is_invite();
862 }
863
864 async fn was_membership(&self, membership: &MembershipState) -> bool {
867 let matrix_room = self.matrix_room();
868
869 let matrix_room_clone = matrix_room.clone();
873 let handle = spawn_tokio!(async move {
874 matrix_room_clone
875 .get_state_event_static_for_key::<RoomMemberEventContent, _>(
876 matrix_room_clone.own_user_id(),
877 )
878 .await
879 });
880
881 let raw_member_event = match handle.await.expect("task was not aborted") {
882 Ok(Some(raw_member_event)) => raw_member_event,
883 Ok(None) => {
884 return false;
885 }
886 Err(error) => {
887 error!("Could not get own member event: {error}");
888 return false;
889 }
890 };
891
892 let member_event = match raw_member_event {
893 RawSyncOrStrippedState::Sync(raw) => {
894 raw.deserialize_as_unchecked::<RoomMemberMembershipEvent>()
895 }
896 RawSyncOrStrippedState::Stripped(raw) => raw.deserialize_as_unchecked(),
897 };
898
899 let member_event = match member_event {
900 Ok(member_event) => member_event,
901 Err(error) => {
902 warn!("Could not deserialize room member event: {error}");
903 return false;
904 }
905 };
906
907 if member_event.content.membership == *membership {
910 return true;
911 }
912
913 if let Some(prev_content) = member_event
916 .unsigned
917 .as_ref()
918 .and_then(|unsigned| unsigned.prev_content.as_ref())
919 {
920 return prev_content.membership == *membership;
921 }
922
923 let Some(replaces_state) = member_event
926 .unsigned
927 .and_then(|unsigned| unsigned.replaces_state)
928 else {
929 return false;
930 };
931
932 let matrix_room = matrix_room.clone();
933 let handle = spawn_tokio!(async move {
934 matrix_room.load_or_fetch_event(&replaces_state, None).await
935 });
936
937 let raw_prev_member_event = match handle.await.expect("task was not aborted") {
938 Ok(event) => event,
939 Err(error) => {
940 warn!("Could not fetch previous member event: {error}");
941 return false;
942 }
943 };
944
945 match raw_prev_member_event
946 .kind
947 .raw()
948 .deserialize_as_unchecked::<RoomMemberMembershipEvent>()
949 {
950 Ok(prev_member_event) => prev_member_event.content.membership == *membership,
951 Err(error) => {
952 warn!("Could not deserialize previous member event: {error}");
953 false
954 }
955 }
956 }
957
958 async fn update_inviter(&self) {
960 let matrix_room = self.matrix_room();
961
962 if matrix_room.state() != RoomState::Invited {
964 if self.inviter.take().is_some() {
965 self.obj().notify_inviter();
966 }
967
968 return;
969 }
970
971 let matrix_room = matrix_room.clone();
972 let handle = spawn_tokio!(async move { matrix_room.invite_details().await });
973
974 let invite = match handle.await.expect("task was not aborted") {
975 Ok(invite) => invite,
976 Err(error) => {
977 error!("Could not get invite: {error}");
978 return;
979 }
980 };
981
982 let Some(inviter_member) = invite.inviter else {
983 if self.inviter.take().is_some() {
984 self.obj().notify_inviter();
985 }
986 return;
987 };
988
989 if let Some(inviter) = self
990 .inviter
991 .borrow()
992 .as_ref()
993 .filter(|inviter| inviter.user_id() == inviter_member.user_id())
994 {
995 inviter.update_from_room_member(&inviter_member);
997
998 return;
999 }
1000
1001 let inviter = Member::new(&self.obj(), inviter_member.user_id().to_owned());
1002 inviter.update_from_room_member(&inviter_member);
1003
1004 inviter
1005 .upcast_ref::<User>()
1006 .connect_is_ignored_notify(clone!(
1007 #[weak(rename_to = imp)]
1008 self,
1009 move |_| {
1010 spawn!(async move {
1011 imp.update_category().await;
1013 });
1014 }
1015 ));
1016
1017 self.inviter.replace(Some(inviter));
1018
1019 self.obj().notify_inviter();
1020 }
1021
1022 fn set_direct_member(&self, member: Option<Member>) {
1025 if *self.direct_member.borrow() == member {
1026 return;
1027 }
1028
1029 self.direct_member.replace(member);
1030 self.obj().notify_direct_member();
1031 self.update_avatar();
1032 }
1033
1034 async fn direct_user_id(&self) -> Option<OwnedUserId> {
1037 let matrix_room = self.matrix_room();
1038
1039 let mut direct_targets = matrix_room
1041 .direct_targets()
1042 .into_iter()
1043 .filter_map(|id| OwnedUserId::try_from(id).ok());
1044
1045 let Some(direct_target_user_id) = direct_targets.next() else {
1046 return None;
1048 };
1049
1050 if direct_targets.next().is_some() {
1051 return None;
1053 }
1054
1055 let members_count = matrix_room.active_members_count();
1057
1058 if members_count > 2 {
1059 return None;
1062 }
1063
1064 let matrix_room_clone = matrix_room.clone();
1067 let handle =
1068 spawn_tokio!(
1069 async move { matrix_room_clone.members(RoomMemberships::ACTIVE).await }
1070 );
1071
1072 let members = match handle.await.expect("task was not aborted") {
1073 Ok(m) => m,
1074 Err(error) => {
1075 error!("Could not load room members: {error}");
1076 vec![]
1077 }
1078 };
1079
1080 let members_count = members_count.max(members.len() as u64);
1081 if members_count > 2 {
1082 return None;
1084 }
1085
1086 let own_user_id = matrix_room.own_user_id();
1087 for member in members {
1089 let user_id = member.user_id();
1090
1091 if user_id != direct_target_user_id && user_id != own_user_id {
1092 return None;
1094 }
1095 }
1096
1097 Some(direct_target_user_id)
1098 }
1099
1100 async fn update_direct_member(&self) {
1103 let Some(direct_user_id) = self.direct_user_id().await else {
1104 self.set_direct_member(None);
1105 return;
1106 };
1107
1108 if self
1109 .direct_member
1110 .borrow()
1111 .as_ref()
1112 .is_some_and(|m| *m.user_id() == direct_user_id)
1113 {
1114 return;
1116 }
1117
1118 let direct_member = if let Some(members) = self.members.upgrade() {
1119 members.get_or_create(direct_user_id.clone())
1120 } else {
1121 Member::new(&self.obj(), direct_user_id.clone())
1122 };
1123
1124 let matrix_room = self.matrix_room().clone();
1125 let handle =
1126 spawn_tokio!(async move { matrix_room.get_member_no_sync(&direct_user_id).await });
1127
1128 match handle.await.expect("task was not aborted") {
1129 Ok(Some(matrix_member)) => {
1130 direct_member.update_from_room_member(&matrix_member);
1131 }
1132 Ok(None) => {}
1133 Err(error) => {
1134 error!("Could not get direct member: {error}");
1135 }
1136 }
1137
1138 self.set_direct_member(Some(direct_member));
1139 }
1140
1141 fn init_live_timeline(&self) {
1143 let timeline = self
1144 .live_timeline
1145 .get_or_init(|| Timeline::new(&self.obj()));
1146
1147 timeline.connect_read_change_trigger(clone!(
1148 #[weak(rename_to = imp)]
1149 self,
1150 move |_| {
1151 spawn!(glib::Priority::DEFAULT_IDLE, async move {
1152 imp.handle_read_change_trigger().await;
1153 });
1154 }
1155 ));
1156 }
1157
1158 fn live_timeline(&self) -> &Timeline {
1160 self.live_timeline
1161 .get()
1162 .expect("live timeline is initialized")
1163 }
1164
1165 pub(super) fn set_latest_activity(&self, latest_activity: u64) {
1167 if self.latest_activity.get() == latest_activity {
1168 return;
1169 }
1170
1171 self.latest_activity.set(latest_activity);
1172 self.obj().notify_latest_activity();
1173 }
1174
1175 async fn update_is_marked_unread(&self) {
1177 let is_marked_unread = self.matrix_room().is_marked_unread();
1178
1179 if self.is_marked_unread.get() == is_marked_unread {
1180 return;
1181 }
1182
1183 self.is_marked_unread.set(is_marked_unread);
1184 self.handle_read_change_trigger().await;
1185 self.obj().notify_is_marked_unread();
1186 }
1187
1188 fn set_is_read(&self, is_read: bool) {
1190 if self.is_read.get() == is_read {
1191 return;
1192 }
1193
1194 self.is_read.set(is_read);
1195 self.obj().notify_is_read();
1196 }
1197
1198 async fn handle_read_change_trigger(&self) {
1200 let timeline = self.live_timeline();
1201
1202 if self.is_marked_unread.get() {
1203 self.set_is_read(false);
1204 } else if let Some(has_unread) = timeline.has_unread_messages().await {
1205 self.set_is_read(!has_unread);
1206 }
1207
1208 self.update_highlight();
1209 }
1210
1211 fn set_highlight(&self, highlight: HighlightFlags) {
1213 if self.highlight.get() == highlight {
1214 return;
1215 }
1216
1217 self.highlight.set(highlight);
1218 self.obj().notify_highlight();
1219 }
1220
1221 fn update_highlight(&self) {
1223 let mut highlight = HighlightFlags::empty();
1224
1225 if matches!(self.category.get(), RoomCategory::Left) {
1226 self.set_highlight(highlight);
1228 self.set_notification_count(0);
1229 return;
1230 }
1231
1232 if self.is_read.get() {
1233 self.set_notification_count(0);
1234 } else {
1235 let counts = self.matrix_room().unread_notification_counts();
1236
1237 if counts.highlight_count > 0 {
1238 highlight = HighlightFlags::all();
1239 } else {
1240 highlight = HighlightFlags::BOLD;
1241 }
1242 self.set_notification_count(counts.notification_count);
1243 }
1244
1245 self.set_highlight(highlight);
1246 }
1247
1248 fn set_notification_count(&self, count: u64) {
1250 if self.notification_count.get() == count {
1251 return;
1252 }
1253
1254 self.notification_count.set(count);
1255 self.set_has_notifications(count > 0);
1256 self.obj().notify_notification_count();
1257 }
1258
1259 fn set_has_notifications(&self, has_notifications: bool) {
1261 if self.has_notifications.get() == has_notifications {
1262 return;
1263 }
1264
1265 self.has_notifications.set(has_notifications);
1266 self.obj().notify_has_notifications();
1267 }
1268
1269 async fn update_is_encrypted(&self) {
1271 let matrix_room = self.matrix_room();
1272 let matrix_room_clone = matrix_room.clone();
1273 let handle =
1274 spawn_tokio!(async move { matrix_room_clone.latest_encryption_state().await });
1275
1276 match handle.await.expect("task was not aborted") {
1277 Ok(state) => {
1278 if state.is_encrypted() {
1279 self.is_encrypted.set(true);
1280 self.obj().notify_is_encrypted();
1281 }
1282 }
1283 Err(error) => {
1284 if matches!(matrix_room.state(), RoomState::Invited | RoomState::Knocked)
1287 && error
1288 .as_client_api_error()
1289 .is_some_and(|e| e.status_code.is_client_error())
1290 {
1291 debug!("Could not load room encryption state: {error}");
1292 } else {
1293 error!("Could not load room encryption state: {error}");
1294 }
1295 }
1296 }
1297 }
1298
1299 fn update_guests_allowed(&self) {
1301 let matrix_room = self.matrix_room();
1302 let guests_allowed = matrix_room.guest_access() == GuestAccess::CanJoin;
1303
1304 if self.guests_allowed.get() == guests_allowed {
1305 return;
1306 }
1307
1308 self.guests_allowed.set(guests_allowed);
1309 self.obj().notify_guests_allowed();
1310 }
1311
1312 fn update_history_visibility(&self) {
1314 let matrix_room = self.matrix_room();
1315 let visibility = matrix_room.history_visibility_or_default().into();
1316
1317 if self.history_visibility.get() == visibility {
1318 return;
1319 }
1320
1321 self.history_visibility.set(visibility);
1322 self.obj().notify_history_visibility();
1323 }
1324
1325 fn version(&self) -> String {
1327 self.matrix_room()
1328 .create_content()
1329 .map(|c| c.room_version.to_string())
1330 .unwrap_or_default()
1331 }
1332
1333 pub(super) fn rules(&self) -> RoomVersionRules {
1335 self.matrix_room()
1336 .clone_info()
1337 .room_version_rules_or_default()
1338 }
1339
1340 fn federated(&self) -> bool {
1342 self.matrix_room()
1343 .create_content()
1344 .is_some_and(|c| c.federate)
1345 }
1346
1347 fn set_up_typing(&self) {
1349 if self.typing_drop_guard.get().is_some() {
1350 return;
1352 }
1353
1354 let matrix_room = self.matrix_room();
1355 if matrix_room.state() != RoomState::Joined {
1356 return;
1357 }
1358
1359 let (typing_drop_guard, receiver) = matrix_room.subscribe_to_typing_notifications();
1360 let stream = BroadcastStream::new(receiver);
1361
1362 let obj_weak = glib::SendWeakRef::from(self.obj().downgrade());
1363 let fut = stream.for_each(move |typing_user_ids| {
1364 let obj_weak = obj_weak.clone();
1365 async move {
1366 let Ok(typing_user_ids) = typing_user_ids else {
1367 return;
1368 };
1369
1370 let ctx = glib::MainContext::default();
1371 ctx.spawn(async move {
1372 spawn!(async move {
1373 if let Some(obj) = obj_weak.upgrade() {
1374 obj.imp().update_typing_list(typing_user_ids);
1375 }
1376 });
1377 });
1378 }
1379 });
1380 spawn_tokio!(fut);
1381
1382 self.typing_drop_guard
1383 .set(typing_drop_guard)
1384 .expect("typing drop guard is uninitialized");
1385 }
1386
1387 fn update_typing_list(&self, typing_user_ids: Vec<OwnedUserId>) {
1389 let Some(session) = self.session.upgrade() else {
1390 return;
1391 };
1392
1393 let Some(members) = self.members.upgrade() else {
1394 self.typing_list.update(vec![]);
1397 return;
1398 };
1399
1400 let own_user_id = session.user_id();
1401
1402 let members = typing_user_ids
1403 .into_iter()
1404 .filter(|user_id| user_id != own_user_id)
1405 .map(|user_id| members.get_or_create(user_id))
1406 .collect();
1407
1408 self.typing_list.update(members);
1409 }
1410
1411 fn set_notifications_setting(&self, setting: NotificationsRoomSetting) {
1413 if self.notifications_setting.get() == setting {
1414 return;
1415 }
1416
1417 self.notifications_setting.set(setting);
1418 self.obj().notify_notifications_setting();
1419 }
1420
1421 fn set_verification(&self, verification: Option<IdentityVerification>) {
1423 if self.verification.obj().is_some() && verification.is_some() {
1424 return;
1427 }
1428
1429 self.verification.disconnect_signals();
1430
1431 let verification = verification.or_else(|| {
1432 let room_id = self.matrix_room().room_id();
1434 self.session
1435 .upgrade()
1436 .map(|s| s.verification_list())
1437 .and_then(|list| list.ongoing_room_verification(room_id))
1438 });
1439
1440 if let Some(verification) = &verification {
1441 let state_handler = verification.connect_is_finished_notify(clone!(
1442 #[weak(rename_to = imp)]
1443 self,
1444 move |_| {
1445 imp.set_verification(None);
1446 }
1447 ));
1448
1449 let dismiss_handler = verification.connect_dismiss(clone!(
1450 #[weak(rename_to = imp)]
1451 self,
1452 move |_| {
1453 imp.set_verification(None);
1454 }
1455 ));
1456
1457 self.verification
1458 .set(verification, vec![state_handler, dismiss_handler]);
1459 }
1460
1461 self.obj().notify_verification();
1462 }
1463
1464 fn watch_room_info(&self) {
1466 let matrix_room = self.matrix_room();
1467 let subscriber = matrix_room.subscribe_info();
1468
1469 let obj_weak = glib::SendWeakRef::from(self.obj().downgrade());
1470 let fut = subscriber.for_each(move |room_info| {
1471 let obj_weak = obj_weak.clone();
1472 async move {
1473 let ctx = glib::MainContext::default();
1474 ctx.spawn(async move {
1475 spawn!(async move {
1476 if let Some(obj) = obj_weak.upgrade() {
1477 obj.imp().update_with_room_info(room_info).await;
1478 }
1479 });
1480 });
1481 }
1482 });
1483 spawn_tokio!(fut);
1484 }
1485
1486 async fn update_with_room_info(&self, room_info: RoomInfo) {
1488 self.aliases.update();
1489 self.update_name();
1490 self.update_display_name().await;
1491 self.update_avatar();
1492 self.update_topic();
1493 self.update_category().await;
1494 self.update_is_direct().await;
1495 self.update_is_marked_unread().await;
1496 self.update_tombstone();
1497 self.set_joined_members_count(room_info.joined_members_count());
1498 self.update_is_encrypted().await;
1499 self.join_rule.update(room_info.join_rule());
1500 self.update_guests_allowed();
1501 self.update_history_visibility();
1502 }
1503
1504 pub(super) fn handle_ambiguity_changes<'a>(
1506 &self,
1507 changes: impl Iterator<Item = &'a AmbiguityChange>,
1508 ) {
1509 let user_ids = changes
1511 .flat_map(AmbiguityChange::user_ids)
1512 .collect::<HashSet<_>>();
1513
1514 if let Some(members) = self.members.upgrade() {
1515 for user_id in user_ids {
1516 members.update_member(user_id.to_owned());
1517 }
1518 } else {
1519 let own_member = self.own_member();
1520 let own_user_id = own_member.user_id();
1521
1522 if user_ids.contains(&**own_user_id) {
1523 own_member.update();
1524 }
1525 }
1526 }
1527
1528 fn watch_send_queue(&self) {
1530 let matrix_room = self.matrix_room().clone();
1531
1532 let room_weak = glib::SendWeakRef::from(self.obj().downgrade());
1533 spawn_tokio!(async move {
1534 let send_queue = matrix_room.send_queue();
1535 let subscriber = match send_queue.subscribe().await {
1536 Ok((_, subscriber)) => BroadcastStream::new(subscriber),
1537 Err(error) => {
1538 warn!("Failed to listen to room send queue: {error}");
1539 return;
1540 }
1541 };
1542
1543 subscriber
1544 .for_each(move |update| {
1545 let room_weak = room_weak.clone();
1546 async move {
1547 let Ok(RoomSendQueueUpdate::SendError {
1548 error,
1549 is_recoverable: true,
1550 ..
1551 }) = update
1552 else {
1553 return;
1554 };
1555
1556 let ctx = glib::MainContext::default();
1557 ctx.spawn(async move {
1558 spawn!(async move {
1559 let Some(obj) = room_weak.upgrade() else {
1560 return;
1561 };
1562 let Some(session) = obj.session() else {
1563 return;
1564 };
1565
1566 if session.is_offline() {
1567 return;
1570 }
1571
1572 let duration = match error.client_api_error_kind() {
1573 Some(ErrorKind::LimitExceeded {
1574 retry_after: Some(retry_after),
1575 }) => match retry_after {
1576 RetryAfter::Delay(duration) => Some(*duration),
1577 RetryAfter::DateTime(time) => {
1578 time.duration_since(SystemTime::now()).ok()
1579 }
1580 },
1581 _ => None,
1582 };
1583 let retry_after = duration
1584 .and_then(|d| d.as_secs().try_into().ok())
1585 .unwrap_or(DEFAULT_RETRY_AFTER);
1586
1587 glib::timeout_add_seconds_local_once(retry_after, move || {
1588 let matrix_room = obj.matrix_room().clone();
1589 spawn_tokio!(async move {
1591 matrix_room.send_queue().set_enabled(true);
1592 });
1593 });
1594 });
1595 });
1596 }
1597 })
1598 .await;
1599 });
1600 }
1601
1602 pub(super) async fn change_category(
1612 &self,
1613 category: TargetRoomCategory,
1614 ) -> MatrixResult<()> {
1615 let previous_category = self.category.get();
1616
1617 if previous_category == category {
1618 return Ok(());
1619 }
1620
1621 if previous_category == RoomCategory::Outdated {
1622 warn!("Cannot change the category of an upgraded room");
1623 return Ok(());
1624 }
1625
1626 self.set_category(category.into());
1627
1628 let matrix_room = self.matrix_room().clone();
1629 let handle = spawn_tokio!(async move {
1630 let room_state = matrix_room.state();
1631
1632 match category {
1633 TargetRoomCategory::Favorite => {
1634 if !matrix_room.is_favourite() {
1635 matrix_room.set_is_favourite(true, None).await?;
1637 } else if matrix_room.is_low_priority() {
1638 matrix_room.set_is_low_priority(false, None).await?;
1639 }
1640
1641 if matches!(room_state, RoomState::Invited | RoomState::Left) {
1642 matrix_room.join().await?;
1643 }
1644 }
1645 TargetRoomCategory::Normal => {
1646 if matrix_room.is_favourite() {
1647 matrix_room.set_is_favourite(false, None).await?;
1648 }
1649 if matrix_room.is_low_priority() {
1650 matrix_room.set_is_low_priority(false, None).await?;
1651 }
1652
1653 if matches!(room_state, RoomState::Invited | RoomState::Left) {
1654 matrix_room.join().await?;
1655 }
1656 }
1657 TargetRoomCategory::LowPriority => {
1658 if !matrix_room.is_low_priority() {
1659 matrix_room.set_is_low_priority(true, None).await?;
1661 } else if matrix_room.is_favourite() {
1662 matrix_room.set_is_favourite(false, None).await?;
1663 }
1664
1665 if matches!(room_state, RoomState::Invited | RoomState::Left) {
1666 matrix_room.join().await?;
1667 }
1668 }
1669 TargetRoomCategory::Left => {
1670 if matches!(
1671 room_state,
1672 RoomState::Knocked | RoomState::Invited | RoomState::Joined
1673 ) {
1674 matrix_room.leave().await?;
1675 }
1676 }
1677 }
1678
1679 Result::<_, matrix_sdk::Error>::Ok(())
1680 });
1681
1682 match handle.await.expect("task was not aborted") {
1683 Ok(()) => Ok(()),
1684 Err(error) => {
1685 error!("Could not set the room category: {error}");
1686
1687 Box::pin(self.update_category()).await;
1689
1690 Err(error)
1691 }
1692 }
1693 }
1694 }
1695}
1696
1697glib::wrapper! {
1698 pub struct Room(ObjectSubclass<imp::Room>) @extends PillSource;
1702}
1703
1704impl Room {
1705 pub fn new(session: &Session, matrix_room: MatrixRoom, metainfo: Option<RoomMetainfo>) -> Self {
1707 let this = glib::Object::builder::<Self>()
1708 .property("session", session)
1709 .build();
1710
1711 this.imp().init(matrix_room, metainfo);
1712 this
1713 }
1714
1715 pub(crate) fn matrix_room(&self) -> &MatrixRoom {
1717 self.imp().matrix_room()
1718 }
1719
1720 pub(crate) fn room_id(&self) -> &RoomId {
1722 self.imp().room_id()
1723 }
1724
1725 pub fn human_readable_id(&self) -> String {
1730 format!("{} ({})", self.display_name(), self.room_id())
1731 }
1732
1733 pub(crate) fn rules(&self) -> RoomVersionRules {
1735 self.imp().rules()
1736 }
1737
1738 pub(crate) fn is_joined(&self) -> bool {
1740 self.own_member().membership() == Membership::Join
1741 }
1742
1743 pub(crate) fn predecessor_id(&self) -> Option<&OwnedRoomId> {
1746 self.imp().predecessor_id.get()
1747 }
1748
1749 pub(crate) fn successor_id(&self) -> Option<&OwnedRoomId> {
1751 self.imp().successor_id.get()
1752 }
1753
1754 pub(crate) async fn matrix_to_uri(&self) -> MatrixToUri {
1756 let matrix_room = self.matrix_room().clone();
1757
1758 let handle = spawn_tokio!(async move { matrix_room.matrix_to_permalink().await });
1759 match handle.await.expect("task was not aborted") {
1760 Ok(permalink) => {
1761 return permalink;
1762 }
1763 Err(error) => {
1764 error!("Could not get room event permalink: {error}");
1765 }
1766 }
1767
1768 self.room_id().matrix_to_uri()
1770 }
1771
1772 pub(crate) async fn matrix_to_event_uri(&self, event_id: OwnedEventId) -> MatrixToUri {
1774 let matrix_room = self.matrix_room().clone();
1775
1776 let event_id_clone = event_id.clone();
1777 let handle =
1778 spawn_tokio!(
1779 async move { matrix_room.matrix_to_event_permalink(event_id_clone).await }
1780 );
1781 match handle.await.expect("task was not aborted") {
1782 Ok(permalink) => {
1783 return permalink;
1784 }
1785 Err(error) => {
1786 error!("Could not get room event permalink: {error}");
1787 }
1788 }
1789
1790 self.room_id().matrix_to_event_uri(event_id)
1792 }
1793
1794 pub(crate) fn at_room(&self) -> AtRoom {
1796 AtRoom::new(self)
1797 }
1798
1799 pub(crate) fn get_or_create_members(&self) -> MemberList {
1803 let members = &self.imp().members;
1804 if let Some(list) = members.upgrade() {
1805 list
1806 } else {
1807 let list = MemberList::new(self);
1808 members.set(Some(&list));
1809 self.notify_members();
1810 list
1811 }
1812 }
1813
1814 pub(crate) async fn change_category(&self, category: TargetRoomCategory) -> MatrixResult<()> {
1823 self.imp().change_category(category).await
1824 }
1825
1826 pub(crate) async fn toggle_reaction(&self, key: String, event: &Event) -> Result<(), ()> {
1828 let matrix_timeline = self.live_timeline().matrix_timeline();
1829 let identifier = event.identifier();
1830
1831 let handle =
1832 spawn_tokio!(async move { matrix_timeline.toggle_reaction(&identifier, &key).await });
1833
1834 if let Err(error) = handle.await.expect("task was not aborted") {
1835 error!("Could not toggle reaction: {error}");
1836 return Err(());
1837 }
1838
1839 Ok(())
1840 }
1841
1842 pub(crate) async fn send_receipt(
1846 &self,
1847 receipt_type: ApiReceiptType,
1848 position: ReceiptPosition,
1849 ) {
1850 let Some(session) = self.session() else {
1851 return;
1852 };
1853 let send_public_receipt = session.settings().public_read_receipts_enabled();
1854
1855 let receipt_type = match receipt_type {
1856 ApiReceiptType::Read if !send_public_receipt => ApiReceiptType::ReadPrivate,
1857 t => t,
1858 };
1859
1860 let matrix_timeline = self.live_timeline().matrix_timeline();
1861 let handle = spawn_tokio!(async move {
1862 match position {
1863 ReceiptPosition::End => matrix_timeline.mark_as_read(receipt_type).await,
1864 ReceiptPosition::Event(event_id) => {
1865 matrix_timeline
1866 .send_single_receipt(receipt_type, event_id)
1867 .await
1868 }
1869 }
1870 });
1871
1872 if let Err(error) = handle.await.expect("task was not aborted") {
1873 error!("Could not send read receipt: {error}");
1874 }
1875 }
1876
1877 pub(crate) async fn mark_as_unread(&self) {
1879 let matrix_room = self.matrix_room().clone();
1880 let handle = spawn_tokio!(async move { matrix_room.set_unread_flag(true).await });
1881
1882 if let Err(error) = handle.await.expect("task was not aborted") {
1883 error!("Could not mark room as unread: {error}");
1884 }
1885 }
1886
1887 pub(crate) fn send_typing_notification(&self, is_typing: bool) {
1889 let matrix_room = self.matrix_room();
1890 if matrix_room.state() != RoomState::Joined {
1891 return;
1892 }
1893
1894 let matrix_room = matrix_room.clone();
1895 let handle = spawn_tokio!(async move { matrix_room.typing_notice(is_typing).await });
1896
1897 spawn!(glib::Priority::DEFAULT_IDLE, async move {
1898 match handle.await.expect("task was not aborted") {
1899 Ok(()) => {}
1900 Err(error) => error!("Could not send typing notification: {error}"),
1901 }
1902 });
1903 }
1904
1905 pub(crate) async fn redact<'a>(
1910 &self,
1911 events: &'a [OwnedEventId],
1912 reason: Option<String>,
1913 ) -> Result<(), Vec<&'a EventId>> {
1914 let matrix_room = self.matrix_room();
1915 if matrix_room.state() != RoomState::Joined {
1916 return Ok(());
1917 }
1918
1919 let events_clone = events.to_owned();
1920 let matrix_room = matrix_room.clone();
1921 let handle = spawn_tokio!(async move {
1922 let mut failed_redactions = Vec::new();
1923
1924 for (i, event_id) in events_clone.iter().enumerate() {
1925 match matrix_room.redact(event_id, reason.as_deref(), None).await {
1926 Ok(_) => {}
1927 Err(error) => {
1928 error!("Could not redact event with ID {event_id}: {error}");
1929 failed_redactions.push(i);
1930 }
1931 }
1932 }
1933
1934 failed_redactions
1935 });
1936
1937 let failed_redactions = handle.await.expect("task was not aborted");
1938 let failed_redactions = failed_redactions
1939 .into_iter()
1940 .map(|i| &*events[i])
1941 .collect::<Vec<_>>();
1942
1943 if failed_redactions.is_empty() {
1944 Ok(())
1945 } else {
1946 Err(failed_redactions)
1947 }
1948 }
1949
1950 pub(crate) async fn report_events<'a>(
1957 &self,
1958 events: &'a [(OwnedEventId, Option<String>)],
1959 ) -> Result<(), Vec<&'a EventId>> {
1960 let events_clone = events.to_owned();
1961 let matrix_room = self.matrix_room().clone();
1962 let handle = spawn_tokio!(async move {
1963 let futures = events_clone
1964 .into_iter()
1965 .map(|(event_id, reason)| matrix_room.report_content(event_id, None, reason));
1966 futures_util::future::join_all(futures).await
1967 });
1968
1969 let mut failed = Vec::new();
1970 for (index, result) in handle
1971 .await
1972 .expect("task was not aborted")
1973 .iter()
1974 .enumerate()
1975 {
1976 match result {
1977 Ok(_) => {}
1978 Err(error) => {
1979 error!(
1980 "Could not report content with event ID {}: {error}",
1981 events[index].0,
1982 );
1983 failed.push(&*events[index].0);
1984 }
1985 }
1986 }
1987
1988 if failed.is_empty() {
1989 Ok(())
1990 } else {
1991 Err(failed)
1992 }
1993 }
1994
1995 pub(crate) async fn invite<'a>(
2000 &self,
2001 user_ids: &'a [OwnedUserId],
2002 ) -> Result<(), Vec<&'a UserId>> {
2003 let matrix_room = self.matrix_room();
2004 if matrix_room.state() != RoomState::Joined {
2005 error!("Can’t invite users, because this room isn’t a joined room");
2006 return Ok(());
2007 }
2008
2009 let user_ids_clone = user_ids.to_owned();
2010 let matrix_room = matrix_room.clone();
2011 let handle = spawn_tokio!(async move {
2012 let invitations = user_ids_clone
2013 .iter()
2014 .map(|user_id| matrix_room.invite_user_by_id(user_id));
2015 futures_util::future::join_all(invitations).await
2016 });
2017
2018 let mut failed_invites = Vec::new();
2019 for (index, result) in handle
2020 .await
2021 .expect("task was not aborted")
2022 .iter()
2023 .enumerate()
2024 {
2025 match result {
2026 Ok(()) => {}
2027 Err(error) => {
2028 error!("Could not invite user with ID {}: {error}", user_ids[index],);
2029 failed_invites.push(&*user_ids[index]);
2030 }
2031 }
2032 }
2033
2034 if failed_invites.is_empty() {
2035 Ok(())
2036 } else {
2037 Err(failed_invites)
2038 }
2039 }
2040
2041 pub(crate) async fn kick<'a>(
2048 &self,
2049 users: &'a [(OwnedUserId, Option<String>)],
2050 ) -> Result<(), Vec<&'a UserId>> {
2051 let users_clone = users.to_owned();
2052 let matrix_room = self.matrix_room().clone();
2053 let handle = spawn_tokio!(async move {
2054 let futures = users_clone
2055 .iter()
2056 .map(|(user_id, reason)| matrix_room.kick_user(user_id, reason.as_deref()));
2057 futures_util::future::join_all(futures).await
2058 });
2059
2060 let mut failed_kicks = Vec::new();
2061 for (index, result) in handle
2062 .await
2063 .expect("task was not aborted")
2064 .iter()
2065 .enumerate()
2066 {
2067 match result {
2068 Ok(()) => {}
2069 Err(error) => {
2070 error!("Could not kick user with ID {}: {error}", users[index].0);
2071 failed_kicks.push(&*users[index].0);
2072 }
2073 }
2074 }
2075
2076 if failed_kicks.is_empty() {
2077 Ok(())
2078 } else {
2079 Err(failed_kicks)
2080 }
2081 }
2082
2083 pub(crate) async fn ban<'a>(
2090 &self,
2091 users: &'a [(OwnedUserId, Option<String>)],
2092 ) -> Result<(), Vec<&'a UserId>> {
2093 let users_clone = users.to_owned();
2094 let matrix_room = self.matrix_room().clone();
2095 let handle = spawn_tokio!(async move {
2096 let futures = users_clone
2097 .iter()
2098 .map(|(user_id, reason)| matrix_room.ban_user(user_id, reason.as_deref()));
2099 futures_util::future::join_all(futures).await
2100 });
2101
2102 let mut failed_bans = Vec::new();
2103 for (index, result) in handle
2104 .await
2105 .expect("task was not aborted")
2106 .iter()
2107 .enumerate()
2108 {
2109 match result {
2110 Ok(()) => {}
2111 Err(error) => {
2112 error!("Could not ban user with ID {}: {error}", users[index].0);
2113 failed_bans.push(&*users[index].0);
2114 }
2115 }
2116 }
2117
2118 if failed_bans.is_empty() {
2119 Ok(())
2120 } else {
2121 Err(failed_bans)
2122 }
2123 }
2124
2125 pub(crate) async fn unban<'a>(
2132 &self,
2133 users: &'a [(OwnedUserId, Option<String>)],
2134 ) -> Result<(), Vec<&'a UserId>> {
2135 let users_clone = users.to_owned();
2136 let matrix_room = self.matrix_room().clone();
2137 let handle = spawn_tokio!(async move {
2138 let futures = users_clone
2139 .iter()
2140 .map(|(user_id, reason)| matrix_room.unban_user(user_id, reason.as_deref()));
2141 futures_util::future::join_all(futures).await
2142 });
2143
2144 let mut failed_unbans = Vec::new();
2145 for (index, result) in handle
2146 .await
2147 .expect("task was not aborted")
2148 .iter()
2149 .enumerate()
2150 {
2151 match result {
2152 Ok(()) => {}
2153 Err(error) => {
2154 error!("Could not unban user with ID {}: {error}", users[index].0);
2155 failed_unbans.push(&*users[index].0);
2156 }
2157 }
2158 }
2159
2160 if failed_unbans.is_empty() {
2161 Ok(())
2162 } else {
2163 Err(failed_unbans)
2164 }
2165 }
2166
2167 pub(crate) async fn enable_encryption(&self) -> Result<(), ()> {
2169 if self.is_encrypted() {
2170 return Ok(());
2172 }
2173
2174 let matrix_room = self.matrix_room().clone();
2175 let handle = spawn_tokio!(async move { matrix_room.enable_encryption().await });
2176
2177 match handle.await.expect("task was not aborted") {
2178 Ok(()) => Ok(()),
2179 Err(error) => {
2180 error!("Could not enable room encryption: {error}");
2181 Err(())
2182 }
2183 }
2184 }
2185
2186 pub(crate) async fn forget(&self) -> MatrixResult<()> {
2188 if self.category() != RoomCategory::Left {
2189 warn!("Cannot forget a room that is not left");
2190 return Ok(());
2191 }
2192
2193 let matrix_room = self.matrix_room().clone();
2194 let handle = spawn_tokio!(async move { matrix_room.forget().await });
2195
2196 match handle.await.expect("task was not aborted") {
2197 Ok(()) => {
2198 self.emit_by_name::<()>("room-forgotten", &[]);
2199 Ok(())
2200 }
2201 Err(error) => {
2202 error!("Could not forget the room: {error}");
2203 Err(error)
2204 }
2205 }
2206 }
2207
2208 pub(crate) fn handle_ambiguity_changes<'a>(
2210 &self,
2211 changes: impl Iterator<Item = &'a AmbiguityChange>,
2212 ) {
2213 self.imp().handle_ambiguity_changes(changes);
2214 }
2215
2216 fn update_latest_activity<'a>(&self, events: impl Iterator<Item = &'a Event>) {
2220 let own_user_id = self.imp().own_member().user_id();
2221 let mut latest_activity = self.latest_activity();
2222
2223 for event in events {
2224 if event.counts_as_activity(own_user_id) {
2225 latest_activity = latest_activity.max(event.origin_server_ts().get().into());
2226 break;
2227 }
2228 }
2229
2230 self.imp().set_latest_activity(latest_activity);
2231 }
2232
2233 pub(crate) fn update_successor(&self) {
2235 self.imp().update_successor();
2236 }
2237
2238 pub(crate) fn connect_room_forgotten<F: Fn(&Self) + 'static>(
2240 &self,
2241 f: F,
2242 ) -> glib::SignalHandlerId {
2243 self.connect_closure(
2244 "room-forgotten",
2245 true,
2246 closure_local!(move |obj: Self| {
2247 f(&obj);
2248 }),
2249 )
2250 }
2251}
2252
2253#[derive(Debug, Default, Hash, Eq, PartialEq, Clone, Copy, glib::Enum)]
2255#[enum_type(name = "HistoryVisibilityValue")]
2256pub enum HistoryVisibilityValue {
2257 WorldReadable,
2259 #[default]
2261 Shared,
2262 Invited,
2264 Joined,
2266 Unsupported,
2268}
2269
2270impl From<HistoryVisibility> for HistoryVisibilityValue {
2271 fn from(value: HistoryVisibility) -> Self {
2272 match value {
2273 HistoryVisibility::Invited => Self::Invited,
2274 HistoryVisibility::Joined => Self::Joined,
2275 HistoryVisibility::Shared => Self::Shared,
2276 HistoryVisibility::WorldReadable => Self::WorldReadable,
2277 _ => Self::Unsupported,
2278 }
2279 }
2280}
2281
2282impl From<HistoryVisibilityValue> for HistoryVisibility {
2283 fn from(value: HistoryVisibilityValue) -> Self {
2284 match value {
2285 HistoryVisibilityValue::Invited => Self::Invited,
2286 HistoryVisibilityValue::Joined => Self::Joined,
2287 HistoryVisibilityValue::Shared => Self::Shared,
2288 HistoryVisibilityValue::WorldReadable => Self::WorldReadable,
2289 HistoryVisibilityValue::Unsupported => unimplemented!(),
2290 }
2291 }
2292}
2293
2294#[derive(Debug, Clone)]
2296pub(crate) enum ReceiptPosition {
2297 End,
2299 Event(OwnedEventId),
2301}
2302
2303#[derive(Deserialize)]
2306struct RoomMemberMembershipEvent {
2307 content: RoomMemberMembershipContent,
2308 unsigned: Option<RoomMemberMembershipUnsigned>,
2309}
2310
2311#[derive(Deserialize)]
2314struct RoomMemberMembershipUnsigned {
2315 replaces_state: Option<OwnedEventId>,
2316 prev_content: Option<RoomMemberMembershipContent>,
2317}
2318
2319#[derive(Deserialize)]
2322struct RoomMemberMembershipContent {
2323 membership: MembershipState,
2324}