From 15adbfecbe08e8bc726554bf307d91e729cfa3ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Tue, 12 Dec 2023 10:17:22 +0100 Subject: [PATCH] room: Port to glib::Properties macro --- src/session/model/room/event/mod.rs | 359 ++++++------ .../model/room/event/reaction_group.rs | 115 ++-- src/session/model/room/member.rs | 83 +-- src/session/model/room/member_list.rs | 98 ++-- src/session/model/room/mod.rs | 518 +++++++----------- src/session/model/room/power_levels.rs | 43 +- src/session/model/room/timeline/mod.rs | 165 +++--- .../model/room/timeline/timeline_item.rs | 123 +++-- .../model/room/timeline/virtual_item.rs | 63 +-- src/session/model/room/typing_list.rs | 71 +-- src/session/view/content/invite.rs | 4 +- .../content/room_details/general_page/mod.rs | 34 +- .../room_details/history_viewer/audio_row.rs | 9 +- .../room_details/history_viewer/event.rs | 12 +- .../room_details/history_viewer/media_item.rs | 10 +- .../room_details/history_viewer/timeline.rs | 2 +- .../invite_subpage/invitee_list.rs | 9 +- .../view/content/room_history/item_row.rs | 34 +- .../room_history/message_row/content.rs | 19 +- .../content/room_history/message_row/mod.rs | 7 +- .../room_history/message_row/reaction/mod.rs | 4 +- .../room_history/message_toolbar/mod.rs | 24 +- src/session/view/content/room_history/mod.rs | 16 +- .../content/room_history/state_row/mod.rs | 8 +- .../room_history/state_row/tombstone.rs | 4 +- src/session/view/media_viewer.rs | 10 +- src/session/view/session_view.rs | 5 +- src/utils/matrix.rs | 2 +- 28 files changed, 755 insertions(+), 1096 deletions(-) diff --git a/src/session/model/room/event/mod.rs b/src/session/model/room/event/mod.rs index 0fd6bfaa..da506555 100644 --- a/src/session/model/room/event/mod.rs +++ b/src/session/model/room/event/mod.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, fmt}; +use std::{borrow::Cow, fmt, ops::Deref}; use gtk::{gio, glib, prelude::*, subclass::prelude::*}; use indexmap::IndexMap; @@ -82,7 +82,15 @@ pub enum MessageState { #[derive(Clone, Debug, glib::Boxed)] #[boxed_type(name = "BoxedEventTimelineItem")] -pub struct BoxedEventTimelineItem(EventTimelineItem); +pub struct BoxedEventTimelineItem(pub EventTimelineItem); + +impl Deref for BoxedEventTimelineItem { + type Target = EventTimelineItem; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} /// A user's read receipt. #[derive(Clone, Debug)] @@ -92,29 +100,47 @@ pub struct UserReadReceipt { } mod imp { - use std::cell::{Cell, RefCell}; - - use glib::object::WeakRef; - use once_cell::sync::Lazy; + use std::{ + cell::{Cell, RefCell}, + marker::PhantomData, + }; use super::*; - #[derive(Debug)] + #[derive(Debug, glib::Properties)] + #[properties(wrapper_type = super::Event)] pub struct Event { /// The underlying SDK timeline item. - pub item: RefCell>, - + #[property(get = Self::item, set = Self::set_item, type = BoxedEventTimelineItem)] + pub item: RefCell>, /// The room containing this `Event`. - pub room: WeakRef, - + #[property(get, set = Self::set_room, construct_only)] + pub room: glib::WeakRef, /// The reactions on this event. + #[property(get)] pub reactions: ReactionList, - /// The read receipts on this event. + #[property(get)] pub read_receipts: gio::ListStore, - /// The state of this event. + #[property(get, builder(MessageState::default()))] pub state: Cell, + /// The pretty-formatted JSON source for this `Event`, if it has + /// been echoed back by the server. + #[property(get = Self::source)] + pub source: PhantomData>, + /// The timestamp of this `Event`. + #[property(get = Self::timestamp)] + pub timestamp: PhantomData, + /// Whether this `Event` was edited. + #[property(get = Self::is_edited)] + pub is_edited: PhantomData, + /// Whether this `Event` should be highlighted. + #[property(get = Self::is_highlighted)] + pub is_highlighted: PhantomData, + /// Whether this event has any read receipt. + #[property(get = Self::has_read_receipts)] + pub has_read_receipts: PhantomData, } impl Default for Event { @@ -125,6 +151,11 @@ mod imp { reactions: Default::default(), read_receipts: gio::ListStore::new::(), state: Default::default(), + source: Default::default(), + timestamp: Default::default(), + is_edited: Default::default(), + is_highlighted: Default::default(), + has_read_receipts: Default::default(), } } } @@ -136,76 +167,8 @@ mod imp { type ParentType = TimelineItem; } - impl ObjectImpl for Event { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![ - glib::ParamSpecBoxed::builder::("item") - .write_only() - .build(), - glib::ParamSpecString::builder("source").read_only().build(), - glib::ParamSpecObject::builder::("room") - .construct_only() - .build(), - glib::ParamSpecBoxed::builder::("timestamp") - .read_only() - .build(), - glib::ParamSpecObject::builder::("reactions") - .read_only() - .build(), - glib::ParamSpecBoolean::builder("is-edited") - .read_only() - .build(), - glib::ParamSpecBoolean::builder("is-highlighted") - .read_only() - .build(), - glib::ParamSpecObject::builder::("read-receipts") - .read_only() - .build(), - glib::ParamSpecBoolean::builder("has-read-receipts") - .read_only() - .build(), - glib::ParamSpecEnum::builder::("state") - .read_only() - .build(), - ] - }); - - PROPERTIES.as_ref() - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - let obj = self.obj(); - - match pspec.name() { - "item" => { - let item = value.get::().unwrap(); - obj.set_item(item.0); - } - "room" => { - obj.set_room(value.get().unwrap()); - } - _ => unimplemented!(), - } - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - let obj = self.obj(); - - match pspec.name() { - "source" => obj.source().to_value(), - "room" => obj.room().to_value(), - "timestamp" => obj.timestamp().to_value(), - "reactions" => obj.reactions().to_value(), - "is-edited" => obj.is_edited().to_value(), - "is-highlighted" => obj.is_highlighted().to_value(), - "read-receipts" => obj.read_receipts().to_value(), - "has-read-receipts" => obj.has_read_receipts().to_value(), - "state" => obj.state().to_value(), - _ => unimplemented!(), - } - } - } + #[glib::derived_properties] + impl ObjectImpl for Event {} impl TimelineItemImpl for Event { fn id(&self) -> String { @@ -239,6 +202,96 @@ mod imp { true } } + + impl Event { + /// The underlying SDK timeline item of this `Event`. + fn item(&self) -> BoxedEventTimelineItem { + self.item.borrow().clone().unwrap() + } + + /// Set the underlying SDK timeline item of this `Event`. + fn set_item(&self, item: BoxedEventTimelineItem) { + let obj = self.obj(); + + let was_edited = self.is_edited(); + let was_highlighted = self.is_highlighted(); + + self.reactions.update(item.reactions().clone()); + obj.update_read_receipts(item.read_receipts()); + self.item.replace(Some(item)); + + obj.notify_source(); + if self.is_edited() != was_edited { + obj.notify_is_edited(); + } + if self.is_highlighted() != was_highlighted { + obj.notify_is_highlighted(); + } + obj.update_state(); + } + + /// The pretty-formatted JSON source for this `Event`, if it has + /// been echoed back by the server. + fn source(&self) -> Option { + self.item + .borrow() + .as_ref() + .unwrap() + .original_json() + .map(|raw| { + // We have to convert it to a Value, because a RawValue cannot be + // pretty-printed. + let json = serde_json::to_value(raw).unwrap(); + + serde_json::to_string_pretty(&json).unwrap() + }) + } + + /// Set the room that contains this `Event`. + fn set_room(&self, room: Room) { + self.room.set(Some(&room)); + if let Some(session) = room.session() { + self.reactions.set_user(session.user().clone()); + } + } + + /// The timestamp of this `Event`. + fn timestamp(&self) -> glib::DateTime { + let ts = self.obj().origin_server_ts(); + + glib::DateTime::from_unix_utc(ts.as_secs().into()) + .and_then(|t| t.to_local()) + .unwrap() + } + + /// Whether this `Event` was edited. + fn is_edited(&self) -> bool { + let item_ref = self.item.borrow(); + let Some(item) = item_ref.as_ref() else { + return false; + }; + + match item.content() { + TimelineItemContent::Message(msg) => msg.is_edited(), + _ => false, + } + } + + /// Whether this `Event` should be highlighted. + fn is_highlighted(&self) -> bool { + let item_ref = self.item.borrow(); + let Some(item) = item_ref.as_ref() else { + return false; + }; + + item.is_highlighted() + } + + /// Whether this event has any read receipt. + fn has_read_receipts(&self) -> bool { + self.read_receipts.n_items() > 0 + } + } } glib::wrapper! { @@ -264,13 +317,13 @@ impl Event { EventKey::TransactionId(txn_id) if item.is_local_echo() && item.transaction_id() == Some(txn_id) => { - self.set_item(item.clone()); + self.set_item(BoxedEventTimelineItem(item.clone())); return true; } EventKey::EventId(event_id) if !item.is_local_echo() && item.event_id() == Some(event_id) => { - self.set_item(item.clone()); + self.set_item(BoxedEventTimelineItem(item.clone())); return true; } _ => {} @@ -279,43 +332,6 @@ impl Event { false } - /// The room that contains this `Event`. - pub fn room(&self) -> Room { - self.imp().room.upgrade().unwrap() - } - - /// Set the room that contains this `Event`. - fn set_room(&self, room: Room) { - let imp = self.imp(); - imp.room.set(Some(&room)); - imp.reactions.set_user(room.session().user()); - } - - /// The underlying SDK timeline item of this `Event`. - pub fn item(&self) -> EventTimelineItem { - self.imp().item.borrow().clone().unwrap() - } - - /// Set the underlying SDK timeline item of this `Event`. - pub fn set_item(&self, item: EventTimelineItem) { - let was_edited = self.is_edited(); - let was_highlighted = self.is_highlighted(); - let imp = self.imp(); - - imp.reactions.update(item.reactions().clone()); - self.update_read_receipts(item.read_receipts()); - imp.item.replace(Some(item)); - - self.notify("source"); - if self.is_edited() != was_edited { - self.notify("is-edited"); - } - if self.is_highlighted() != was_highlighted { - self.notify("is-highlighted"); - } - self.update_state(); - } - /// The raw JSON source for this `Event`, if it has been echoed back /// by the server. pub fn raw(&self) -> Option> { @@ -328,24 +344,6 @@ impl Event { .cloned() } - /// The pretty-formatted JSON source for this `Event`, if it has - /// been echoed back by the server. - pub fn source(&self) -> Option { - self.imp() - .item - .borrow() - .as_ref() - .unwrap() - .original_json() - .map(|raw| { - // We have to convert it to a Value, because a RawValue cannot be - // pretty-printed. - let json = serde_json::to_value(raw).unwrap(); - - serde_json::to_string_pretty(&json).unwrap() - }) - } - /// The unique of this `Event` in the timeline. pub fn key(&self) -> EventKey { let item_ref = self.imp().item.borrow(); @@ -390,6 +388,7 @@ impl Event { /// available, otherwise it will be created on every call. pub fn sender(&self) -> Member { self.room() + .unwrap() .get_or_create_members() .get_or_create(self.sender_id()) } @@ -410,15 +409,6 @@ impl Event { self.origin_server_ts().get().into() } - /// The timestamp of this `Event`. - pub fn timestamp(&self) -> glib::DateTime { - let ts = self.origin_server_ts(); - - glib::DateTime::from_unix_utc(ts.as_secs().into()) - .and_then(|t| t.to_local()) - .unwrap() - } - /// Whether this `Event` is redacted. pub fn is_redacted(&self) -> bool { matches!( @@ -440,24 +430,6 @@ impl Event { } } - /// Whether this `Event` was edited. - pub fn is_edited(&self) -> bool { - let item_ref = self.imp().item.borrow(); - let Some(item) = item_ref.as_ref() else { - return false; - }; - - match item.content() { - TimelineItemContent::Message(msg) => msg.is_edited(), - _ => false, - } - } - - /// The state of this `Event`. - pub fn state(&self) -> MessageState { - self.imp().state.get() - } - /// Compute the current state of this `Event`. fn compute_state(&self) -> MessageState { let item_ref = self.imp().item.borrow(); @@ -495,32 +467,7 @@ impl Event { } self.imp().state.set(state); - self.notify("state"); - } - - /// Whether this `Event` should be highlighted. - pub fn is_highlighted(&self) -> bool { - let item_ref = self.imp().item.borrow(); - let Some(item) = item_ref.as_ref() else { - return false; - }; - - item.is_highlighted() - } - - /// The reactions to this event. - pub fn reactions(&self) -> &ReactionList { - &self.imp().reactions - } - - /// The read receipts on this event. - pub fn read_receipts(&self) -> &gio::ListStore { - &self.imp().read_receipts - } - - /// Whether this event has any read receipt. - pub fn has_read_receipts(&self) -> bool { - self.imp().read_receipts.n_items() > 0 + self.notify_state(); } /// Update the read receipts list with the given receipts. @@ -566,7 +513,7 @@ impl Event { let has_read_receipts = new_count > 0; if had_read_receipts != has_read_receipts { - self.notify("has-read-receipts"); + self.notify_has_read_receipts(); } } @@ -599,11 +546,14 @@ impl Event { /// /// This is a no-op if called for a local event. pub async fn fetch_missing_details(&self) -> Result<(), TimelineError> { + let Some(room) = self.room() else { + return Ok(()); + }; let Some(event_id) = self.event_id() else { return Ok(()); }; - let timeline = self.room().timeline().matrix_timeline(); + let timeline = room.timeline().matrix_timeline(); spawn_tokio!(async move { timeline.fetch_details_for_event(&event_id).await }) .await .unwrap() @@ -623,11 +573,21 @@ impl Event { /// Returns `Err` if an error occurred while fetching the content. Panics on /// an incompatible event. pub async fn get_media_content(&self) -> Result<(String, Vec), matrix_sdk::Error> { + let Some(room) = self.room() else { + return Err(matrix_sdk::Error::UnknownError( + "Failed to upgrade Room".into(), + )); + }; + let Some(session) = room.session() else { + return Err(matrix_sdk::Error::UnknownError( + "Failed to upgrade Session".into(), + )); + }; let TimelineItemContent::Message(message) = self.content() else { panic!("Trying to get the media content of an event of incompatible type"); }; - let client = self.room().session().client(); + let client = session.client(); get_media_content(client, message.msgtype().clone()).await } @@ -648,13 +608,6 @@ impl Event { pub fn counts_as_unread(&self) -> bool { count_as_unread(self.imp().item.borrow().as_ref().unwrap().content()) } - - /// Listen to changes of the source of this `TimelineEvent`. - pub fn connect_source_notify(&self, f: F) -> glib::SignalHandlerId { - self.connect_notify_local(Some("source"), move |this, _| { - f(this); - }) - } } /// Whether the given event can count as an unread message. diff --git a/src/session/model/room/event/reaction_group.rs b/src/session/model/room/event/reaction_group.rs index da35573f..bc62f4bd 100644 --- a/src/session/model/room/event/reaction_group.rs +++ b/src/session/model/room/event/reaction_group.rs @@ -5,22 +5,30 @@ use super::EventKey; use crate::{prelude::*, session::model::User}; mod imp { - use std::cell::RefCell; - - use once_cell::{sync::Lazy, unsync::OnceCell}; + use std::{ + cell::{OnceCell, RefCell}, + marker::PhantomData, + }; use super::*; - #[derive(Debug, Default)] + #[derive(Debug, Default, glib::Properties)] + #[properties(wrapper_type = super::ReactionGroup)] pub struct ReactionGroup { /// The user of the parent session. + #[property(get, construct_only)] pub user: OnceCell, - /// The key of the group. + #[property(get, construct_only)] pub key: OnceCell, - /// The reactions in the group. pub reactions: RefCell>, + /// The number of reactions in this group. + #[property(get = Self::count)] + pub count: PhantomData, + /// Whether this group has a reaction from our own user. + #[property(get = Self::has_user)] + pub has_user: PhantomData, } #[glib::object_subclass] @@ -30,50 +38,8 @@ mod imp { type Interfaces = (gio::ListModel,); } - impl ObjectImpl for ReactionGroup { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![ - glib::ParamSpecObject::builder::("user") - .construct_only() - .build(), - glib::ParamSpecString::builder("key") - .construct_only() - .build(), - glib::ParamSpecUInt::builder("count").read_only().build(), - glib::ParamSpecBoolean::builder("has-user") - .read_only() - .build(), - ] - }); - - PROPERTIES.as_ref() - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - match pspec.name() { - "user" => { - self.user.set(value.get().unwrap()).unwrap(); - } - "key" => { - self.key.set(value.get().unwrap()).unwrap(); - } - _ => unimplemented!(), - } - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - let obj = self.obj(); - - match pspec.name() { - "user" => obj.user().to_value(), - "key" => obj.key().to_value(), - "count" => obj.count().to_value(), - "has-user" => obj.has_user().to_value(), - _ => unimplemented!(), - } - } - } + #[glib::derived_properties] + impl ObjectImpl for ReactionGroup {} impl ListModelImpl for ReactionGroup { fn item_type(&self) -> glib::Type { @@ -97,6 +63,23 @@ mod imp { }) } } + + impl ReactionGroup { + /// The number of reactions in this group. + fn count(&self) -> u32 { + self.n_items() + } + + /// Whether this group has a reaction from our own user. + fn has_user(&self) -> bool { + let user_id = UserExt::user_id(self.user.get().unwrap()); + self.reactions + .borrow() + .as_ref() + .filter(|reactions| reactions.by_sender(&user_id).next().is_some()) + .is_some() + } + } } glib::wrapper! { @@ -113,25 +96,10 @@ impl ReactionGroup { .build() } - /// The user of the parent session. - pub fn user(&self) -> &User { - self.imp().user.get().unwrap() - } - - /// The key of the group. - pub fn key(&self) -> &str { - self.imp().key.get().unwrap() - } - - /// The number of reactions in this group - pub fn count(&self) -> u32 { - self.imp().n_items() - } - /// The event ID of the reaction in this group sent by the logged-in user, /// if any. pub fn user_reaction_event_key(&self) -> Option { - let user_id = UserExt::user_id(self.user()); + let user_id = UserExt::user_id(&self.user()); self.imp() .reactions .borrow() @@ -148,17 +116,6 @@ impl ReactionGroup { }) } - /// Whether this group has a reaction from the logged-in user. - pub fn has_user(&self) -> bool { - let user_id = UserExt::user_id(self.user()); - self.imp() - .reactions - .borrow() - .as_ref() - .filter(|reactions| reactions.by_sender(&user_id).next().is_some()) - .is_some() - } - /// Update this group with the given reactions. pub fn update(&self, new_reactions: SdkReactionGroup) { let prev_has_user = self.has_user(); @@ -188,11 +145,11 @@ impl ReactionGroup { self.items_changed(0, prev_count, new_count); if self.count() != prev_count { - self.notify("count"); + self.notify_count(); } if self.has_user() != prev_has_user { - self.notify("has-user"); + self.notify_has_user(); } } } diff --git a/src/session/model/room/member.rs b/src/session/model/room/member.rs index e17bc832..183a52e4 100644 --- a/src/session/model/room/member.rs +++ b/src/session/model/room/member.rs @@ -53,15 +53,19 @@ impl From for Membership { mod imp { use std::cell::Cell; - use once_cell::sync::Lazy; - use super::*; - #[derive(Debug, Default)] + #[derive(Debug, Default, glib::Properties)] + #[properties(wrapper_type = super::Member)] pub struct Member { + /// The power level of the member. + #[property(get, minimum = POWER_LEVEL_MIN, maximum = POWER_LEVEL_MAX)] pub power_level: Cell, + /// This member's membership state. + #[property(get, builder(Membership::default()))] pub membership: Cell, /// The timestamp of the latest activity of this member. + #[property(get, set = Self::set_latest_activity, explicit_notify)] pub latest_activity: Cell, } @@ -72,43 +76,18 @@ mod imp { type ParentType = User; } - impl ObjectImpl for Member { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![ - glib::ParamSpecInt64::builder("power-level") - .minimum(POWER_LEVEL_MIN) - .maximum(POWER_LEVEL_MAX) - .read_only() - .build(), - glib::ParamSpecEnum::builder::("membership") - .read_only() - .build(), - glib::ParamSpecUInt64::builder("latest-activity") - .explicit_notify() - .build(), - ] - }); - - PROPERTIES.as_ref() - } + #[glib::derived_properties] + impl ObjectImpl for Member {} - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - match pspec.name() { - "latest-activity" => self.obj().set_latest_activity(value.get().unwrap()), - _ => unimplemented!(), + impl Member { + /// Set the timestamp of the latest activity of this member. + fn set_latest_activity(&self, activity: u64) { + if self.latest_activity.get() >= activity { + return; } - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - let obj = self.obj(); - match pspec.name() { - "power-level" => obj.power_level().to_value(), - "membership" => obj.membership().to_value(), - "latest-activity" => obj.latest_activity().to_value(), - _ => unimplemented!(), - } + self.latest_activity.set(activity); + self.obj().notify_latest_activity(); } } } @@ -127,18 +106,13 @@ impl Member { .build() } - /// The power level of the member. - pub fn power_level(&self) -> PowerLevel { - self.imp().power_level.get() - } - /// Set the power level of the member. fn set_power_level(&self, power_level: PowerLevel) { if self.power_level() == power_level { return; } self.imp().power_level.replace(power_level); - self.notify("power-level"); + self.notify_power_level(); } pub fn role(&self) -> MemberRole { @@ -157,12 +131,6 @@ impl Member { self.role().is_peasant() } - /// This member's membership state. - pub fn membership(&self) -> Membership { - let imp = self.imp(); - imp.membership.get() - } - /// Set this member's membership state. fn set_membership(&self, membership: Membership) { if self.membership() == membership { @@ -170,22 +138,7 @@ impl Member { } let imp = self.imp(); imp.membership.replace(membership); - self.notify("membership"); - } - - /// The timestamp of the latest activity of this member. - pub fn latest_activity(&self) -> u64 { - self.imp().latest_activity.get() - } - - /// Set the timestamp of the latest activity of this member. - pub fn set_latest_activity(&self, activity: u64) { - if self.latest_activity() >= activity { - return; - } - - self.imp().latest_activity.set(activity); - self.notify("latest-activity"); + self.notify_membership(); } /// Update the user based on the room member. diff --git a/src/session/model/room/member_list.rs b/src/session/model/room/member_list.rs index 67750c2c..eaeb3002 100644 --- a/src/session/model/room/member_list.rs +++ b/src/session/model/room/member_list.rs @@ -20,18 +20,18 @@ use crate::{spawn, spawn_tokio, utils::LoadingState}; mod imp { use std::cell::{Cell, RefCell}; - use glib::object::WeakRef; - use once_cell::sync::Lazy; - use super::*; - #[derive(Debug, Default)] + #[derive(Debug, Default, glib::Properties)] + #[properties(wrapper_type = super::MemberList)] pub struct MemberList { /// The list of known members. pub members: RefCell>, /// The room these members belong to. - pub room: WeakRef, + #[property(get, set = Self::set_room, construct_only)] + pub room: glib::WeakRef, /// The loading state of the list. + #[property(get, builder(LoadingState::default()))] pub state: Cell, } @@ -42,39 +42,8 @@ mod imp { type Interfaces = (gio::ListModel,); } - impl ObjectImpl for MemberList { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![ - glib::ParamSpecObject::builder::("room") - .construct_only() - .build(), - glib::ParamSpecEnum::builder::("state") - .read_only() - .build(), - ] - }); - - PROPERTIES.as_ref() - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - match pspec.name() { - "room" => self.obj().set_room(&value.get().ok().unwrap()), - _ => unimplemented!(), - } - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - let obj = self.obj(); - - match pspec.name() { - "room" => obj.room().to_value(), - "state" => obj.state().to_value(), - _ => unimplemented!(), - } - } - } + #[glib::derived_properties] + impl ObjectImpl for MemberList {} impl ListModelImpl for MemberList { fn item_type(&self) -> glib::Type { @@ -93,6 +62,22 @@ mod imp { .map(|(_user_id, member)| member.clone().upcast()) } } + + impl MemberList { + /// Set the room these members belong to. + fn set_room(&self, room: Room) { + let obj = self.obj(); + self.room.set(Some(&room)); + obj.notify_room(); + + spawn!( + glib::Priority::LOW, + clone!(@weak obj => async move { + obj.load().await; + }) + ); + } + } } glib::wrapper! { @@ -108,28 +93,6 @@ impl MemberList { glib::Object::builder().property("room", room).build() } - /// The room containing these members. - pub fn room(&self) -> Room { - self.imp().room.upgrade().unwrap() - } - - fn set_room(&self, room: &Room) { - self.imp().room.set(Some(room)); - self.notify("room"); - - spawn!( - glib::Priority::LOW, - clone!(@weak self as obj => async move { - obj.load().await; - }) - ); - } - - /// The state of this list. - pub fn state(&self) -> LoadingState { - self.imp().state.get() - } - /// Set whether this list is being loaded. fn set_state(&self, state: LoadingState) { if self.state() == state { @@ -137,7 +100,7 @@ impl MemberList { } self.imp().state.set(state); - self.notify("state"); + self.notify_state(); } pub fn reload(&self) { @@ -150,13 +113,15 @@ impl MemberList { /// Load this list. async fn load(&self) { + let Some(room) = self.room() else { + return; + }; if matches!(self.state(), LoadingState::Loading | LoadingState::Ready) { return; } self.set_state(LoadingState::Loading); - let room = self.room(); let matrix_room = room.matrix_room(); // First load what we have locally. @@ -210,12 +175,15 @@ impl MemberList { /// If some of the values do not correspond to existing members, new members /// are created. fn update_from_room_members(&self, new_members: &[matrix_sdk::room::RoomMember]) { + let Some(room) = self.room() else { + return; + }; let imp = self.imp(); let mut members = imp.members.borrow_mut(); let prev_len = members.len(); for member in new_members { if let Entry::Vacant(entry) = members.entry(member.user_id().into()) { - entry.insert(Member::new(&self.room(), member.user_id())); + entry.insert(Member::new(&room, member.user_id())); } } let num_members_added = members.len().saturating_sub(prev_len); @@ -234,7 +202,7 @@ impl MemberList { } // Restore the members activity according to the known timeline events. - for item in self.room().timeline().items().iter::().rev() { + for item in room.timeline().items().iter::().rev() { let Ok(item) = item else { // The iterator is broken, stop. break; @@ -270,7 +238,7 @@ impl MemberList { .entry(user_id) .or_insert_with_key(|user_id| { was_member_added = true; - Member::new(&self.room(), user_id) + Member::new(&self.room().unwrap(), user_id) }) .clone(); diff --git a/src/session/model/room/mod.rs b/src/session/model/room/mod.rs index d719a482..3aae2b88 100644 --- a/src/session/model/room/mod.rs +++ b/src/session/model/room/mod.rs @@ -56,28 +56,55 @@ use super::{ use crate::{components::Pill, gettext_f, prelude::*, spawn, spawn_tokio}; mod imp { - use std::cell::Cell; + use std::{ + cell::{Cell, OnceCell}, + marker::PhantomData, + }; - use glib::{object::WeakRef, subclass::Signal}; - use once_cell::{sync::Lazy, unsync::OnceCell}; + use glib::subclass::Signal; + use once_cell::sync::Lazy; use super::*; - #[derive(Default)] + #[derive(Default, glib::Properties)] + #[properties(wrapper_type = super::Room)] pub struct Room { + /// The ID of this room. + #[property(set = Self::set_room_id, construct_only, type = String)] pub room_id: OnceCell, pub matrix_room: RefCell>, - pub session: WeakRef, - pub name: RefCell>, + /// The current session. + #[property(get, construct_only)] + pub session: glib::WeakRef, + /// The name that is set for this room. + /// + /// This can be empty, the display name should be used instead in the + /// interface. + #[property(get = Self::name)] + pub name: PhantomData>, + /// The display name of this room. + #[property(get = Self::display_name, type = String)] + pub display_name: RefCell>, + /// The Avatar data of this room. + #[property(get)] pub avatar_data: OnceCell, + /// The category of this room. + #[property(get, builder(RoomType::default()))] pub category: Cell, + /// The timeline of this room. + #[property(get)] pub timeline: OnceCell, - pub members: WeakRef, + /// The members of this room. + #[property(get)] + pub members: glib::WeakRef, /// The number of joined members in the room, according to the /// homeserver. + #[property(get)] pub joined_members_count: Cell, - /// The user who sent the invite to this room. This is only set when - /// this room is an invitation. + /// The user who sent the invite to this room. + /// + /// This is only set when this room is an invitation. + #[property(get)] pub inviter: RefCell>, pub power_levels: RefCell, /// The timestamp of the room's latest activity. @@ -86,27 +113,46 @@ mod imp { /// unread. /// /// If it is not known, it will return `0`. + #[property(get)] pub latest_activity: Cell, /// Whether all messages of this room are read. + #[property(get)] pub is_read: Cell, - /// The highlight state of the room, + /// The highlight state of the room. + #[property(get)] pub highlight: Cell, /// The ID of the room that was upgraded and that this one replaces. pub predecessor_id: OnceCell, /// The ID of the successor of this Room, if this room was upgraded. pub successor_id: OnceCell, - /// The successor of this Room, if this room was upgraded. - pub successor: WeakRef, + /// The successor of this Room, if this room was upgraded and the + /// successor was joined. + #[property(get)] + pub successor: glib::WeakRef, /// The most recent verification request event. + #[property(get, set)] pub verification: RefCell>, - /// Whether this room is encrypted - pub is_encrypted: Cell, + /// Whether this room is encrypted. + #[property(get)] + pub encrypted: Cell, /// The list of members currently typing in this room. + #[property(get)] pub typing_list: TypingList, /// Whether anyone can join this room. + #[property(get)] pub is_join_rule_public: Cell, - /// Whether this room is a DM. + /// Whether this room is a direct chat. + #[property(get)] pub is_direct: Cell, + /// The number of unread notifications of this room. + #[property(get = Self::notification_count)] + pub notification_count: PhantomData, + /// The topic of this room. + #[property(get = Self::topic)] + pub topic: PhantomData>, + /// Whether this room has been upgraded. + #[property(get = Self::is_tombstoned)] + pub is_tombstoned: PhantomData, } #[glib::object_subclass] @@ -116,131 +162,8 @@ mod imp { type ParentType = SidebarItem; } + #[glib::derived_properties] impl ObjectImpl for Room { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![ - glib::ParamSpecString::builder("room-id") - .construct_only() - .build(), - glib::ParamSpecObject::builder::("session") - .construct_only() - .build(), - glib::ParamSpecString::builder("name").read_only().build(), - glib::ParamSpecString::builder("display-name") - .read_only() - .build(), - glib::ParamSpecObject::builder::("inviter") - .read_only() - .build(), - glib::ParamSpecObject::builder::("avatar-data") - .read_only() - .build(), - glib::ParamSpecObject::builder::("timeline") - .read_only() - .build(), - glib::ParamSpecFlags::builder::("highlight") - .read_only() - .build(), - glib::ParamSpecUInt64::builder("notification-count") - .read_only() - .build(), - glib::ParamSpecEnum::builder::("category") - .read_only() - .build(), - glib::ParamSpecString::builder("topic").read_only().build(), - glib::ParamSpecUInt64::builder("latest-activity") - .read_only() - .build(), - glib::ParamSpecBoolean::builder("is-read") - .read_only() - .build(), - glib::ParamSpecObject::builder::("members") - .read_only() - .build(), - glib::ParamSpecUInt64::builder("joined-members-count") - .read_only() - .build(), - glib::ParamSpecString::builder("predecessor-id") - .read_only() - .build(), - glib::ParamSpecBoolean::builder("is-tombstoned") - .read_only() - .build(), - glib::ParamSpecString::builder("successor-id") - .read_only() - .build(), - glib::ParamSpecObject::builder::("successor") - .read_only() - .build(), - glib::ParamSpecObject::builder::("verification") - .explicit_notify() - .build(), - glib::ParamSpecBoolean::builder("encrypted") - .explicit_notify() - .build(), - glib::ParamSpecObject::builder::("typing-list") - .read_only() - .build(), - glib::ParamSpecBoolean::builder("is-join-rule-public") - .read_only() - .build(), - glib::ParamSpecBoolean::builder("is-direct") - .read_only() - .build(), - ] - }); - - PROPERTIES.as_ref() - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - let obj = self.obj(); - - match pspec.name() { - "session" => self.session.set(value.get().ok().as_ref()), - "room-id" => self - .room_id - .set(RoomId::parse(value.get::<&str>().unwrap()).unwrap()) - .unwrap(), - "verification" => obj.set_verification(value.get().unwrap()), - "encrypted" => obj.set_is_encrypted(value.get().unwrap()), - _ => unimplemented!(), - } - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - let obj = self.obj(); - - match pspec.name() { - "room-id" => obj.room_id().as_str().to_value(), - "session" => obj.session().to_value(), - "inviter" => obj.inviter().to_value(), - "name" => obj.name().to_value(), - "display-name" => obj.display_name().to_value(), - "avatar-data" => obj.avatar_data().to_value(), - "timeline" => self.timeline.get().unwrap().to_value(), - "category" => obj.category().to_value(), - "highlight" => obj.highlight().to_value(), - "topic" => obj.topic().to_value(), - "members" => obj.members().to_value(), - "joined-members-count" => obj.joined_members_count().to_value(), - "notification-count" => obj.notification_count().to_value(), - "latest-activity" => obj.latest_activity().to_value(), - "is-read" => obj.is_read().to_value(), - "predecessor-id" => obj.predecessor_id().map(|id| id.as_str()).to_value(), - "is-tombstoned" => obj.is_tombstoned().to_value(), - "successor-id" => obj.successor_id().map(|id| id.as_str()).to_value(), - "successor" => obj.successor().to_value(), - "verification" => obj.verification().to_value(), - "encrypted" => obj.is_encrypted().to_value(), - "typing-list" => obj.typing_list().to_value(), - "is-join-rule-public" => obj.is_join_rule_public().to_value(), - "is-direct" => obj.is_direct().to_value(), - _ => unimplemented!(), - } - } - fn signals() -> &'static [Signal] { static SIGNALS: Lazy> = Lazy::new(|| vec![Signal::builder("room-forgotten").build()]); @@ -250,8 +173,11 @@ mod imp { fn constructed(&self) { self.parent_constructed(); let obj = self.obj(); + let Some(session) = obj.session() else { + return; + }; - obj.set_matrix_room(obj.session().client().get_room(obj.room_id()).unwrap()); + obj.set_matrix_room(session.client().get_room(obj.room_id()).unwrap()); self.timeline.set(Timeline::new(&obj)).unwrap(); self.timeline @@ -265,7 +191,7 @@ mod imp { // Initialize the avatar first since loading is async. self.avatar_data .set(AvatarData::with_image(AvatarImage::new( - &obj.session(), + &session, obj.matrix_room().avatar_url().as_deref(), AvatarUriSource::Room, ))) @@ -280,7 +206,7 @@ mod imp { obj.setup_is_encrypted().await; })); - obj.bind_property("display-name", obj.avatar_data(), "display-name") + obj.bind_property("display-name", &obj.avatar_data(), "display-name") .sync_create() .build(); @@ -297,6 +223,55 @@ mod imp { } impl SidebarItemImpl for Room {} + + impl Room { + /// Set the ID of this room. + fn set_room_id(&self, room_id: String) { + self.room_id.set(RoomId::parse(room_id).unwrap()).unwrap(); + } + + /// The name of this room. + /// + /// This can be empty, the display name should be used instead in the + /// interface. + fn name(&self) -> Option { + self.matrix_room.borrow().as_ref().unwrap().name() + } + + /// The display name of this room. + pub fn display_name(&self) -> String { + let display_name = self.display_name.borrow().clone(); + // Translators: This is displayed when the room name is unknown yet. + display_name.unwrap_or_else(|| gettext("Unknown")) + } + + /// The number of unread notifications of this room. + fn notification_count(&self) -> u64 { + self.matrix_room + .borrow() + .as_ref() + .unwrap() + .unread_notification_counts() + .notification_count + } + + /// The topic of this room. + fn topic(&self) -> Option { + self.matrix_room + .borrow() + .as_ref() + .unwrap() + .topic() + .filter(|topic| { + !topic.is_empty() && topic.find(|c: char| !c.is_whitespace()).is_some() + }) + } + + /// Whether this room was tombstoned. + pub fn is_tombstoned(&self) -> bool { + self.matrix_room.borrow().as_ref().unwrap().is_tombstoned() + } + } } glib::wrapper! { @@ -327,11 +302,6 @@ impl Room { this } - /// The current session. - pub fn session(&self) -> Session { - self.imp().session.upgrade().unwrap() - } - /// The ID of this room. pub fn room_id(&self) -> &RoomId { self.imp().room_id.get().unwrap() @@ -375,11 +345,6 @@ impl Room { self.matrix_room().state() } - /// Whether this room is direct or not. - pub fn is_direct(&self) -> bool { - self.imp().is_direct.get() - } - /// Set whether this room is direct. fn set_is_direct(&self, is_direct: bool) { if self.is_direct() == is_direct { @@ -387,7 +352,7 @@ impl Room { } self.imp().is_direct.set(is_direct); - self.notify("is-direct"); + self.notify_is_direct(); } pub async fn load_is_direct(&self) { @@ -440,10 +405,6 @@ impl Room { ) } - pub fn category(&self) -> RoomType { - self.imp().category.get() - } - fn set_category_internal(&self, category: RoomType) { let old_category = self.category(); @@ -452,7 +413,7 @@ impl Room { } self.imp().category.set(category); - self.notify("category"); + self.notify_category(); } /// Set the category of this room. @@ -699,10 +660,6 @@ impl Room { self.set_joined_members_count(room_info.joined_members_count()); } - pub fn typing_list(&self) -> &TypingList { - &self.imp().typing_list - } - fn setup_typing(&self) { let matrix_room = self.matrix_room(); if matrix_room.state() != RoomState::Joined { @@ -746,7 +703,9 @@ impl Room { } fn handle_receipt_event(&self, content: ReceiptEventContent) { - let session = self.session(); + let Some(session) = self.session() else { + return; + }; let own_user_id = session.user_id(); for (_event_id, receipts) in content.iter() { @@ -759,6 +718,9 @@ impl Room { } fn handle_typing_event(&self, content: TypingEventContent) { + let Some(session) = self.session() else { + return; + }; let typing_list = &self.imp().typing_list; let Some(members) = self.members() else { @@ -768,7 +730,6 @@ impl Room { return; }; - let session = self.session(); let own_user_id = session.user_id(); let members = content @@ -780,11 +741,6 @@ impl Room { typing_list.update(members); } - /// The timeline of this room. - pub fn timeline(&self) -> &Timeline { - self.imp().timeline.get().unwrap() - } - /// The members of this room. /// /// This creates the [`MemberList`] if no strong reference to it exists. @@ -795,21 +751,11 @@ impl Room { } else { let list = MemberList::new(self); members.set(Some(&list)); - self.notify("members"); + self.notify_members(); list } } - /// The members of this room, if a strong reference to the list exists. - pub fn members(&self) -> Option { - self.imp().members.upgrade() - } - - /// The number of joined members in the room, according to the homeserver. - pub fn joined_members_count(&self) -> u64 { - self.imp().joined_members_count.get() - } - /// Set the number of joined members in the room, according to the /// homeserver. fn set_joined_members_count(&self, count: u64) { @@ -818,11 +764,7 @@ impl Room { } self.imp().joined_members_count.set(count); - self.notify("joined-members-count"); - } - - fn notify_notification_count(&self) { - self.notify("notification-count"); + self.notify_joined_members_count(); } fn update_highlight(&self) { @@ -851,11 +793,6 @@ impl Room { self.set_highlight(highlight); } - /// How this room is highlighted. - pub fn highlight(&self) -> HighlightFlags { - self.imp().highlight.get() - } - /// Set how this room is highlighted. fn set_highlight(&self, highlight: HighlightFlags) { if self.highlight() == highlight { @@ -863,7 +800,7 @@ impl Room { } self.imp().highlight.set(highlight); - self.notify("highlight"); + self.notify_highlight(); } fn update_is_read(&self) { @@ -876,11 +813,6 @@ impl Room { })); } - /// Whether all messages of this room are read. - pub fn is_read(&self) -> bool { - self.imp().is_read.get() - } - /// Set whether all messages of this room are read. pub fn set_is_read(&self, is_read: bool) { if is_read == self.is_read() { @@ -888,22 +820,7 @@ impl Room { } self.imp().is_read.set(is_read); - self.notify("is-read"); - } - - /// The name of this room. - /// - /// This can be empty, the display name should be used instead in the - /// interface. - pub fn name(&self) -> Option { - self.matrix_room().name() - } - - /// The display name of this room. - pub fn display_name(&self) -> String { - let display_name = self.imp().name.borrow().clone(); - // Translators: This is displayed when the room name is unknown yet. - display_name.unwrap_or_else(|| gettext("Unknown")) + self.notify_is_read(); } /// Set the display name of this room. @@ -912,8 +829,8 @@ impl Room { return; } - self.imp().name.replace(display_name); - self.notify("display-name"); + self.imp().display_name.replace(display_name); + self.notify_display_name(); } fn load_display_name(&self) { @@ -943,48 +860,23 @@ impl Room { ); } - /// The number of unread notifications of this room. - pub fn notification_count(&self) -> u64 { - let matrix_room = self.imp().matrix_room.borrow(); - matrix_room - .as_ref() - .unwrap() - .unread_notification_counts() - .notification_count - } - - /// The Avatar of this room. - pub fn avatar_data(&self) -> &AvatarData { - self.imp().avatar_data.get().unwrap() - } - - /// The topic of this room. - pub fn topic(&self) -> Option { - self.matrix_room() - .topic() - .filter(|topic| !topic.is_empty() && topic.find(|c: char| !c.is_whitespace()).is_some()) - } - pub fn power_levels(&self) -> PowerLevels { self.imp().power_levels.borrow().clone() } - /// The user who sent the invite to this room. - /// - /// This is only set when this room represents an invite. - pub fn inviter(&self) -> Option { - self.imp().inviter.borrow().clone() - } - /// Load the member that invited us to this room, when applicable. async fn load_inviter(&self) { + let Some(session) = self.session() else { + return; + }; + let matrix_room = self.matrix_room(); if matrix_room.state() != RoomState::Invited { return; } - let own_user_id = self.session().user_id().to_owned(); + let own_user_id = session.user_id().to_owned(); let matrix_room_clone = matrix_room.clone(); let handle = spawn_tokio!(async move { matrix_room_clone.get_member_no_sync(&own_user_id).await }); @@ -1020,7 +912,7 @@ impl Room { inviter.update_from_room_member(&inviter_member); self.imp().inviter.replace(Some(inviter)); - self.notify("inviter"); + self.notify_inviter(); } /// Update the room state based on the new sync response @@ -1074,19 +966,12 @@ impl Room { } } } - self.session() - .verification_list() - .handle_response_room(self.clone(), events); - } - /// The timestamp of the room's latest activity. - /// - /// This is the timestamp of the latest event that counts as possibly - /// unread. - /// - /// If it is not known, it will return `0`. - pub fn latest_activity(&self) -> u64 { - self.imp().latest_activity.get() + if let Some(session) = self.session() { + session + .verification_list() + .handle_response_room(self.clone(), events); + } } /// Set the timestamp of the room's latest possibly unread event. @@ -1096,7 +981,7 @@ impl Room { } self.imp().latest_activity.set(latest_activity); - self.notify("latest-activity"); + self.notify_latest_activity(); } fn load_power_levels(&self) { @@ -1209,7 +1094,7 @@ impl Room { &self, room_action: PowerLevelAction, ) -> gtk::ClosureExpression { - let session = self.session(); + let session = self.session().unwrap(); let user_id = session.user_id().to_owned(); self.power_levels() .member_is_allowed_to_expr(user_id, room_action) @@ -1324,12 +1209,6 @@ impl Room { }; self.imp().predecessor_id.set(predecessor.room_id).unwrap(); - self.notify("predecessor-id"); - } - - /// Whether this room was tombstoned. - pub fn is_tombstoned(&self) -> bool { - self.matrix_room().is_tombstoned() } /// The ID of the successor of this Room, if this room was upgraded. @@ -1337,16 +1216,10 @@ impl Room { self.imp().successor_id.get().map(std::ops::Deref::deref) } - /// The successor of this Room, if this room was upgraded and the successor - /// was joined. - pub fn successor(&self) -> Option { - self.imp().successor.upgrade() - } - /// Set the successor of this Room. fn set_successor(&self, successor: &Room) { self.imp().successor.set(Some(successor)); - self.notify("successor") + self.notify_successor(); } /// Load the tombstone for this room. @@ -1361,16 +1234,17 @@ impl Room { imp.successor_id .set(room_tombstone.replacement_room) .unwrap(); - self.notify("successor-id"); }; if !self.update_outdated() { - self.session() - .room_list() - .add_tombstoned_room(self.room_id().to_owned()); + if let Some(session) = self.session() { + session + .room_list() + .add_tombstoned_room(self.room_id().to_owned()); + } } - self.notify("is-tombstoned"); + self.notify_is_tombstoned(); } /// Update whether this `Room` is outdated. @@ -1383,7 +1257,9 @@ impl Room { return true; } - let session = self.session(); + let Some(session) = self.session() else { + return false; + }; let room_list = session.room_list(); if let Some(successor_id) = self.successor_id() { @@ -1507,17 +1383,6 @@ impl Room { } } - /// Set the most recent active verification for a user in this room. - pub fn set_verification(&self, verification: IdentityVerification) { - self.imp().verification.replace(Some(verification)); - self.notify("verification"); - } - - /// The most recent active verification for a user in this room. - pub fn verification(&self) -> Option { - self.imp().verification.borrow().clone() - } - /// Update the latest activity of the room with the given events. /// /// The events must be in reverse chronological order. @@ -1534,14 +1399,9 @@ impl Room { self.set_latest_activity(latest_activity); } - /// Whether this room is encrypted. - pub fn is_encrypted(&self) -> bool { - self.imp().is_encrypted.get() - } - /// Set whether this room is encrypted. pub fn set_is_encrypted(&self, is_encrypted: bool) { - let was_encrypted = self.is_encrypted(); + let was_encrypted = self.encrypted(); if was_encrypted == is_encrypted { return; } @@ -1574,8 +1434,8 @@ impl Room { return; } - self.imp().is_encrypted.set(true); - self.notify("encrypted"); + self.imp().encrypted.set(true); + self.notify_encrypted(); } /// Get a `Pill` representing this `Room`. @@ -1614,39 +1474,40 @@ impl Room { // Check if this is a 1-to-1 room to see if we can use a fallback. // We don't have the active member count for invited rooms so process them too. - if avatar_url.is_none() && members_count > 0 && members_count <= 2 { - let handle = - spawn_tokio!(async move { matrix_room.members(RoomMemberships::ACTIVE).await }); - let members = match handle.await.unwrap() { - Ok(m) => m, - Err(e) => { - error!("Failed to load room members: {e}"); - vec![] - } - }; + if let Some(session) = self.session() { + if avatar_url.is_none() && members_count > 0 && members_count <= 2 { + let handle = + spawn_tokio!(async move { matrix_room.members(RoomMemberships::ACTIVE).await }); + let members = match handle.await.unwrap() { + Ok(m) => m, + Err(e) => { + error!("Failed to load room members: {e}"); + vec![] + } + }; - let session = self.session(); - let own_user_id = session.user_id(); - let mut has_own_member = false; - let mut other_member = None; + let own_user_id = session.user_id(); + let mut has_own_member = false; + let mut other_member = None; - // Get the other member from the list. - for member in members { - if member.user_id() == own_user_id { - has_own_member = true; - } else { - other_member = Some(member); - } + // Get the other member from the list. + for member in members { + if member.user_id() == own_user_id { + has_own_member = true; + } else { + other_member = Some(member); + } - if has_own_member && other_member.is_some() { - break; + if has_own_member && other_member.is_some() { + break; + } } - } - // Fallback to other user's avatar if this is a 1-to-1 room. - if members_count == 1 || (members_count == 2 && has_own_member) { - if let Some(other_member) = other_member { - avatar_url = other_member.avatar_url().map(ToOwned::to_owned) + // Fallback to other user's avatar if this is a 1-to-1 room. + if members_count == 1 || (members_count == 2 && has_own_member) { + if let Some(other_member) = other_member { + avatar_url = other_member.avatar_url().map(ToOwned::to_owned) + } } } } @@ -1657,11 +1518,6 @@ impl Room { .set_uri(avatar_url.map(String::from)); } - /// Whether anyone can join this room. - pub fn is_join_rule_public(&self) -> bool { - self.imp().is_join_rule_public.get() - } - /// Set whether anyone can join this room. fn set_is_join_rule_public(&self, is_public: bool) { if self.is_join_rule_public() == is_public { @@ -1669,6 +1525,6 @@ impl Room { } self.imp().is_join_rule_public.set(is_public); - self.notify("is-join-rule-public"); + self.notify_is_join_rule_public(); } } diff --git a/src/session/model/room/power_levels.rs b/src/session/model/room/power_levels.rs index 5fa213cc..18332413 100644 --- a/src/session/model/room/power_levels.rs +++ b/src/session/model/room/power_levels.rs @@ -22,13 +22,14 @@ pub const POWER_LEVEL_MIN: i64 = -POWER_LEVEL_MAX; mod imp { use std::cell::RefCell; - use once_cell::sync::Lazy; - use super::*; - #[derive(Debug, Default)] + #[derive(Debug, Default, glib::Properties)] + #[properties(wrapper_type = super::PowerLevels)] pub struct PowerLevels { - pub content: RefCell, + /// The source of the power levels information. + #[property(get)] + pub power_levels: RefCell, } #[glib::object_subclass] @@ -37,29 +38,12 @@ mod imp { type Type = super::PowerLevels; } - impl ObjectImpl for PowerLevels { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![ - glib::ParamSpecBoxed::builder::("power-levels") - .read_only() - .build(), - ] - }); - - PROPERTIES.as_ref() - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - match pspec.name() { - "power-levels" => self.obj().power_levels().to_value(), - _ => unimplemented!(), - } - } - } + #[glib::derived_properties] + impl ObjectImpl for PowerLevels {} } glib::wrapper! { + /// The power levels of a room. pub struct PowerLevels(ObjectSubclass); } @@ -68,15 +52,10 @@ impl PowerLevels { glib::Object::new() } - /// The source of the power levels information. - pub fn power_levels(&self) -> BoxedPowerLevelsEventContent { - self.imp().content.borrow().clone() - } - /// Returns whether the member with the given user ID is allowed to do the /// given action. pub fn member_is_allowed_to(&self, user_id: &UserId, room_action: PowerLevelAction) -> bool { - let content = self.imp().content.borrow().0.clone(); + let content = self.imp().power_levels.borrow().0.clone(); RoomPowerLevels::from(content).user_can_do(user_id, room_action) } @@ -100,8 +79,8 @@ impl PowerLevels { /// Updates the power levels from the given event. pub fn update_from_event(&self, event: OriginalSyncStateEvent) { let content = BoxedPowerLevelsEventContent(event.content); - self.imp().content.replace(content); - self.notify("power-levels"); + self.imp().power_levels.replace(content); + self.notify_power_levels(); } } diff --git a/src/session/model/room/timeline/mod.rs b/src/session/model/room/timeline/mod.rs index 8d11d18a..b4b5a367 100644 --- a/src/session/model/room/timeline/mod.rs +++ b/src/session/model/room/timeline/mod.rs @@ -53,16 +53,19 @@ impl From for TimelineState { const MAX_BATCH_SIZE: u16 = 20; mod imp { - use std::cell::{Cell, RefCell}; - - use glib::object::WeakRef; - use once_cell::{sync::Lazy, unsync::OnceCell}; + use std::{ + cell::{Cell, OnceCell, RefCell}, + marker::PhantomData, + }; use super::*; - #[derive(Debug)] + #[derive(Debug, glib::Properties)] + #[properties(wrapper_type = super::Timeline)] pub struct Timeline { - pub room: WeakRef, + /// The room containing this timeline. + #[property(get, set = Self::set_room, construct_only)] + pub room: glib::WeakRef, /// The underlying SDK timeline. pub timeline: OnceCell>, /// Items added at the start of the timeline. @@ -72,14 +75,20 @@ mod imp { /// Items added at the end of the timeline. pub end_items: gio::ListStore, /// The `GListModel` containing all the timeline items. + #[property(get)] pub items: gtk::FlattenListModel, /// A Hashmap linking `EventKey` to corresponding `Event` pub event_map: RefCell>, + /// The state of the timeline. + #[property(get, builder(TimelineState::default()))] pub state: Cell, /// Whether this timeline has a typing row. pub has_typing: Cell, pub diff_handle: OnceCell, pub back_pagination_status_handle: OnceCell, + /// Whether the timeline is empty. + #[property(get = Self::is_empty)] + pub empty: PhantomData, } impl Default for Timeline { @@ -105,6 +114,7 @@ mod imp { has_typing: Default::default(), diff_handle: Default::default(), back_pagination_status_handle: Default::default(), + empty: Default::default(), } } } @@ -115,52 +125,42 @@ mod imp { type Type = super::Timeline; } + #[glib::derived_properties] impl ObjectImpl for Timeline { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![ - glib::ParamSpecObject::builder::("room") - .construct_only() - .build(), - glib::ParamSpecObject::builder::("items") - .read_only() - .build(), - glib::ParamSpecBoolean::builder("empty").read_only().build(), - glib::ParamSpecEnum::builder::("state") - .read_only() - .build(), - ] - }); - - PROPERTIES.as_ref() - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - match pspec.name() { - "room" => self.obj().set_room(value.get().unwrap()), - _ => unimplemented!(), + fn dispose(&self) { + if let Some(handle) = self.diff_handle.get() { + handle.abort(); + } + if let Some(handle) = self.back_pagination_status_handle.get() { + handle.abort(); } } + } - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { + impl Timeline { + /// Set the room containing this timeline. + fn set_room(&self, room: Option) { let obj = self.obj(); + self.room.set(room.as_ref()); - match pspec.name() { - "room" => obj.room().to_value(), - "items" => obj.items().to_value(), - "empty" => obj.is_empty().to_value(), - "state" => obj.state().to_value(), - _ => unimplemented!(), + if let Some(room) = room { + room.typing_list().connect_items_changed( + clone!(@weak obj => move |list, _, _, _| { + if !list.is_empty() { + obj.add_typing_row(); + } + }), + ); } + + spawn!(clone!(@weak obj => async move { + obj.setup_timeline().await; + })); } - fn dispose(&self) { - if let Some(handle) = self.diff_handle.get() { - handle.abort(); - } - if let Some(handle) = self.back_pagination_status_handle.get() { - handle.abort(); - } + /// Whether the timeline is empty. + fn is_empty(&self) -> bool { + self.sdk_items.n_items() == 0 } } } @@ -179,11 +179,6 @@ impl Timeline { glib::Object::builder().property("room", room).build() } - /// The `GListModel` containing the timeline items. - pub fn items(&self) -> &gio::ListModel { - self.imp().items.upcast_ref() - } - /// The `GListModel` containing only the items provided by the SDK. pub fn sdk_items(&self) -> &gio::ListModel { self.imp().sdk_items.upcast_ref() @@ -191,10 +186,12 @@ impl Timeline { /// Update this `Timeline` with the given diff. fn update(&self, diff: VectorDiff>) { + let Some(room) = self.room() else { + return; + }; let imp = self.imp(); let sdk_items = &imp.sdk_items; - let room = self.room(); - let was_empty = self.is_empty(); + let was_empty = self.empty(); match diff { VectorDiff::Append { values } => { @@ -326,8 +323,8 @@ impl Timeline { } } - if self.is_empty() != was_empty { - self.notify("empty"); + if self.empty() != was_empty { + self.notify_empty(); } } @@ -368,7 +365,8 @@ impl Timeline { /// Create a `TimelineItem` in this `Timeline` from the given SDK timeline /// item. fn create_item(&self, item: &SdkTimelineItem) -> TimelineItem { - let item = TimelineItem::new(item, &self.room()); + let room = self.room().unwrap(); + let item = TimelineItem::new(item, &room); if let Some(event) = item.downcast_ref::() { self.imp() @@ -378,7 +376,7 @@ impl Timeline { // Keep track of the activity of the sender. if event.counts_as_unread() { - if let Some(members) = self.room().members() { + if let Some(members) = room.members() { let member = members.get_or_create(event.sender_id()); member.set_latest_activity(event.origin_server_ts_u64()); } @@ -482,7 +480,9 @@ impl Timeline { if let Some(event) = self.event_by_key(&EventKey::EventId(event_id.clone())) { event.raw().unwrap().deserialize().map_err(Into::into) } else { - let room = self.room(); + let Some(room) = self.room() else { + return Err(MatrixError::UnknownError("Failed to upgrade Room".into())); + }; let matrix_room = room.matrix_room(); let event_id_clone = event_id.clone(); let handle = @@ -498,29 +498,12 @@ impl Timeline { } } - /// Set the room containing this timeline. - fn set_room(&self, room: Option) { - self.imp().room.set(room.as_ref()); - - if let Some(room) = room { - room.typing_list().connect_items_changed( - clone!(@weak self as obj => move |list, _, _, _| { - if !list.is_empty() { - obj.add_typing_row(); - } - }), - ); - } - - spawn!(clone!(@weak self as obj => async move { - obj.setup_timeline().await; - })); - } - /// Setup the underlying SDK timeline. async fn setup_timeline(&self) { + let Some(room) = self.room() else { + return; + }; let imp = self.imp(); - let room = self.room(); let room_id = room.room_id().to_owned(); let matrix_room = room.matrix_room(); @@ -614,7 +597,10 @@ impl Timeline { /// Setup the back-pagination status. async fn setup_back_pagination_status(&self) { - let room_id = self.room().room_id().to_owned(); + let Some(room) = self.room() else { + return; + }; + let room_id = room.room_id().to_owned(); let matrix_timeline = self.matrix_timeline(); let mut subscriber = matrix_timeline.back_pagination_status(); @@ -646,11 +632,6 @@ impl Timeline { .unwrap(); } - /// The room containing this timeline. - pub fn room(&self) -> Room { - self.imp().room.upgrade().unwrap() - } - /// The underlying SDK timeline. pub fn matrix_timeline(&self) -> Arc { self.imp().timeline.get().unwrap().clone() @@ -677,17 +658,7 @@ impl Timeline { _ => start_items.remove_all(), } - self.notify("state"); - } - - /// The state of the timeline. - pub fn state(&self) -> TimelineState { - self.imp().state.get() - } - - /// Whether the timeline is empty. - pub fn is_empty(&self) -> bool { - self.imp().sdk_items.n_items() == 0 + self.notify_state(); } fn has_typing_row(&self) -> bool { @@ -703,7 +674,7 @@ impl Timeline { } pub fn remove_empty_typing_row(&self) { - if !self.has_typing_row() || !self.room().typing_list().is_empty() { + if !self.has_typing_row() || !self.room().is_some_and(|r| r.typing_list().is_empty()) { return; } @@ -715,7 +686,13 @@ impl Timeline { /// Returns `None` if it is not possible to know, for example if there are /// no events in the Timeline. pub async fn has_unread_messages(&self) -> Option { - let own_user_id = self.room().session().user_id().to_owned(); + let Some(room) = self.room() else { + return None; + }; + let Some(session) = room.session() else { + return None; + }; + let own_user_id = session.user_id().to_owned(); let matrix_timeline = self.matrix_timeline(); let user_receipt_item = spawn_tokio!(async move { diff --git a/src/session/model/room/timeline/timeline_item.rs b/src/session/model/room/timeline/timeline_item.rs index 71cb358d..1aa0353a 100644 --- a/src/session/model/room/timeline/timeline_item.rs +++ b/src/session/model/room/timeline/timeline_item.rs @@ -6,9 +6,7 @@ use super::VirtualItem; use crate::session::model::{Event, Room}; mod imp { - use std::cell::Cell; - - use once_cell::sync::Lazy; + use std::{cell::Cell, marker::PhantomData}; use super::*; @@ -45,9 +43,34 @@ mod imp { (klass.as_ref().event_sender_id)(this) } - #[derive(Debug, Default)] + #[derive(Debug, Default, glib::Properties)] + #[properties(wrapper_type = super::TimelineItem)] pub struct TimelineItem { + /// A unique ID for this `TimelineItem`. + /// + /// For debugging purposes. + #[property(get = Self::id)] + pub id: PhantomData, + /// Whether this `TimelineItem` is selectable. + /// + /// Defaults to `false`. + #[property(get = Self::selectable)] + pub selectable: PhantomData, + /// Whether this `TimelineItem` should show its header. + /// + /// Defaults to `false`. + #[property(get, set = Self::set_show_header, explicit_notify)] pub show_header: Cell, + /// Whether this `TimelineItem` is allowed to hide its header. + /// + /// Defaults to `false`. + #[property(get = Self::can_hide_header)] + pub can_hide_header: PhantomData, + /// If this is a Matrix event, the sender of the event. + /// + /// Defaults to `None`. + #[property(get = Self::event_sender_id)] + pub event_sender_id: PhantomData>, } #[glib::object_subclass] @@ -58,51 +81,46 @@ mod imp { type Class = TimelineItemClass; } - impl ObjectImpl for TimelineItem { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![ - glib::ParamSpecString::builder("id").read_only().build(), - glib::ParamSpecBoolean::builder("selectable") - .read_only() - .build(), - glib::ParamSpecBoolean::builder("show-header") - .explicit_notify() - .build(), - glib::ParamSpecBoolean::builder("can-hide-header") - .read_only() - .build(), - glib::ParamSpecString::builder("event-sender-id") - .read_only() - .build(), - ] - }); - - PROPERTIES.as_ref() + #[glib::derived_properties] + impl ObjectImpl for TimelineItem {} + + impl TimelineItem { + /// A unique ID for this `TimelineItem`. + /// + /// For debugging purposes. + pub fn id(&self) -> String { + imp::timeline_item_id(&self.obj()) } - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - match pspec.name() { - "show-header" => self.obj().set_show_header(value.get().unwrap()), - _ => unimplemented!(), - } + /// Whether this `TimelineItem` is selectable. + /// + /// Defaults to `false`. + pub fn selectable(&self) -> bool { + imp::timeline_item_selectable(&self.obj()) } - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - let obj = self.obj(); - - match pspec.name() { - "id" => obj.id().to_value(), - "selectable" => obj.selectable().to_value(), - "show-header" => obj.show_header().to_value(), - "can-hide-header" => obj.can_hide_header().to_value(), - "event-sender-id" => obj - .event_sender_id() - .as_ref() - .map(|u| u.as_str()) - .to_value(), - _ => unimplemented!(), + /// Set whether this `TimelineItem` should show its header. + pub fn set_show_header(&self, show: bool) { + if self.show_header.get() == show { + return; } + + self.show_header.set(show); + self.obj().notify_show_header(); + } + + /// Whether this `TimelineItem` is allowed to hide its header. + /// + /// Defaults to `false`. + pub fn can_hide_header(&self) -> bool { + imp::timeline_item_can_hide_header(&self.obj()) + } + + /// If this is a Matrix event, the sender of the event. + /// + /// Defaults to `None`. + pub fn event_sender_id(&self) -> Option { + imp::timeline_item_event_sender_id(&self.obj()).map(Into::into) } } } @@ -180,30 +198,23 @@ pub trait TimelineItemExt: 'static { impl> TimelineItemExt for O { fn id(&self) -> String { - imp::timeline_item_id(self.upcast_ref()) + self.upcast_ref().id() } fn selectable(&self) -> bool { - imp::timeline_item_selectable(self.upcast_ref()) + self.upcast_ref().selectable() } fn show_header(&self) -> bool { - self.upcast_ref().imp().show_header.get() + self.upcast_ref().show_header() } fn set_show_header(&self, show: bool) { - let item = self.upcast_ref(); - - if item.show_header() == show { - return; - } - - item.imp().show_header.set(show); - item.notify("show-header"); + self.upcast_ref().set_show_header(show); } fn can_hide_header(&self) -> bool { - imp::timeline_item_can_hide_header(self.upcast_ref()) + self.upcast_ref().can_hide_header() } fn event_sender_id(&self) -> Option { diff --git a/src/session/model/room/timeline/virtual_item.rs b/src/session/model/room/timeline/virtual_item.rs index 387c41d6..b569138a 100644 --- a/src/session/model/room/timeline/virtual_item.rs +++ b/src/session/model/room/timeline/virtual_item.rs @@ -1,3 +1,5 @@ +use std::ops::Deref; + use gtk::{glib, prelude::*, subclass::prelude::*}; use matrix_sdk_ui::timeline::VirtualTimelineItem; use ruma::MilliSecondsSinceUnixEpoch; @@ -15,27 +17,35 @@ pub enum VirtualItemKind { } impl VirtualItemKind { - /// Convert this into a [`VirtualItemKindBoxed`]. - fn boxed(self) -> VirtualItemKindBoxed { - VirtualItemKindBoxed(self) + /// Convert this into a [`BoxedVirtualItemKind`]. + fn boxed(self) -> BoxedVirtualItemKind { + BoxedVirtualItemKind(self) } } -#[derive(Clone, Debug, PartialEq, Eq, glib::Boxed)] -#[boxed_type(name = "VirtualItemKindBoxed")] -struct VirtualItemKindBoxed(VirtualItemKind); +#[derive(Clone, Debug, Default, PartialEq, Eq, glib::Boxed)] +#[boxed_type(name = "BoxedVirtualItemKind")] +pub struct BoxedVirtualItemKind(VirtualItemKind); + +impl Deref for BoxedVirtualItemKind { + type Target = VirtualItemKind; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} mod imp { use std::cell::RefCell; - use once_cell::sync::Lazy; - use super::*; - #[derive(Debug, Default)] + #[derive(Debug, Default, glib::Properties)] + #[properties(wrapper_type = super::VirtualItem)] pub struct VirtualItem { /// The kind of virtual item. - pub kind: RefCell, + #[property(get, set, construct)] + pub kind: RefCell, } #[glib::object_subclass] @@ -45,34 +55,12 @@ mod imp { type ParentType = TimelineItem; } - impl ObjectImpl for VirtualItem { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![ - glib::ParamSpecBoxed::builder::("kind") - .construct() - .write_only() - .build(), - ] - }); - - PROPERTIES.as_ref() - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - match pspec.name() { - "kind" => { - let boxed = value.get::().unwrap(); - self.kind.replace(boxed.0); - } - _ => unimplemented!(), - } - } - } + #[glib::derived_properties] + impl ObjectImpl for VirtualItem {} impl TimelineItemImpl for VirtualItem { fn id(&self) -> String { - match self.obj().kind() { + match &**self.kind.borrow() { VirtualItemKind::Spinner => "VirtualItem::Spinner".to_owned(), VirtualItemKind::Typing => "VirtualItem::Typing".to_owned(), VirtualItemKind::TimelineStart => "VirtualItem::TimelineStart".to_owned(), @@ -145,9 +133,4 @@ impl VirtualItem { .property("kind", VirtualItemKind::DayDivider(date).boxed()) .build() } - - /// The kind of virtual item. - pub fn kind(&self) -> VirtualItemKind { - self.imp().kind.borrow().clone() - } } diff --git a/src/session/model/room/typing_list.rs b/src/session/model/room/typing_list.rs index 5416fdcb..93b8758b 100644 --- a/src/session/model/room/typing_list.rs +++ b/src/session/model/room/typing_list.rs @@ -5,28 +5,18 @@ use super::Member; mod imp { use std::cell::{Cell, RefCell}; - use once_cell::sync::Lazy; - use super::*; - #[derive(Debug)] + #[derive(Debug, Default, glib::Properties)] + #[properties(wrapper_type = super::TypingList)] pub struct TypingList { /// The list of members currently typing. pub members: RefCell>, - /// Whether this list is empty. + #[property(get, set = Self::set_is_empty, explicit_notify)] pub is_empty: Cell, } - impl Default for TypingList { - fn default() -> Self { - Self { - members: Default::default(), - is_empty: Cell::new(true), - } - } - } - #[glib::object_subclass] impl ObjectSubclass for TypingList { const NAME: &'static str = "TypingList"; @@ -34,25 +24,8 @@ mod imp { type Interfaces = (gio::ListModel,); } - impl ObjectImpl for TypingList { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![glib::ParamSpecBoolean::builder("is-empty") - .default_value(true) - .read_only() - .build()] - }); - - PROPERTIES.as_ref() - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - match pspec.name() { - "is-empty" => self.obj().is_empty().to_value(), - _ => unimplemented!(), - } - } - } + #[glib::derived_properties] + impl ObjectImpl for TypingList {} impl ListModelImpl for TypingList { fn item_type(&self) -> glib::Type { @@ -70,6 +43,18 @@ mod imp { .map(|member| member.clone().upcast()) } } + + impl TypingList { + /// Set whether the list is empty. + fn set_is_empty(&self, is_empty: bool) { + if self.is_empty.get() == is_empty { + return; + } + + self.is_empty.set(is_empty); + self.obj().notify_is_empty(); + } + } } glib::wrapper! { @@ -87,24 +72,9 @@ impl TypingList { self.imp().members.borrow().clone() } - /// Set whether the list is empty. - fn set_is_empty(&self, empty: bool) { - self.imp().is_empty.set(empty); - self.notify("is-empty"); - } - - /// Whether the list is empty. - pub fn is_empty(&self) -> bool { - self.imp().is_empty.get() - } - pub fn update(&self, new_members: Vec) { - let prev_is_empty = self.is_empty(); - if new_members.is_empty() { - if !prev_is_empty { - self.set_is_empty(true); - } + self.set_is_empty(true); return; } @@ -118,10 +88,7 @@ impl TypingList { }; self.items_changed(0, removed, added); - - if prev_is_empty { - self.set_is_empty(false); - } + self.set_is_empty(false); } } diff --git a/src/session/view/content/invite.rs b/src/session/view/content/invite.rs index 89f7c91b..8661d814 100644 --- a/src/session/view/content/invite.rs +++ b/src/session/view/content/invite.rs @@ -161,7 +161,9 @@ impl Invite { if category == RoomType::Left { // We declined the invite or the invite was retracted, we should close the room // if it is opened. - let session = room.session(); + let Some(session) = room.session() else { + return; + }; let selection = session.sidebar_list_model().selection_model(); if let Some(selected_room) = selection.selected_item().and_downcast::() { if selected_room == *room { diff --git a/src/session/view/content/room_details/general_page/mod.rs b/src/session/view/content/room_details/general_page/mod.rs index 63c86efb..e9053c7c 100644 --- a/src/session/view/content/room_details/general_page/mod.rs +++ b/src/session/view/content/room_details/general_page/mod.rs @@ -162,7 +162,7 @@ impl GeneralPage { let expr_watch = AvatarData::this_expression("image") .chain_property::("uri") .watch( - Some(avatar_data), + Some(&avatar_data), clone!(@weak self as obj, @weak avatar_data => move || { obj.avatar_changed(avatar_data.image().and_then(|i| i.uri())); }), @@ -170,24 +170,15 @@ impl GeneralPage { imp.expr_watches.borrow_mut().push(expr_watch); let room_handler_ids = vec![ - room.connect_notify_local( - Some("name"), - clone!(@weak self as obj => move |room, _| { - obj.name_changed(room.name()); - }), - ), - room.connect_notify_local( - Some("topic"), - clone!(@weak self as obj => move |room, _| { - obj.topic_changed(room.topic()); - }), - ), - room.connect_notify_local( - Some("joined-members-count"), - clone!(@weak self as obj => move |room, _| { - obj.member_count_changed(room.joined_members_count()); - }), - ), + room.connect_name_notify(clone!(@weak self as obj => move |room| { + obj.name_changed(room.name()); + })), + room.connect_topic_notify(clone!(@weak self as obj => move |room| { + obj.topic_changed(room.topic()); + })), + room.connect_joined_members_count_notify(clone!(@weak self as obj => move |room| { + obj.member_count_changed(room.joined_members_count()); + })), ]; self.member_count_changed(room.joined_members_count()); @@ -288,7 +279,10 @@ impl GeneralPage { mimetype: Some(info.mime.to_string()), }); - let client = room.session().client(); + let Some(session) = room.session() else { + return; + }; + let client = session.client(); let handle = spawn_tokio!(async move { client.media().upload(&info.mime, data).await }); let uri = match handle.await.unwrap() { diff --git a/src/session/view/content/room_details/history_viewer/audio_row.rs b/src/session/view/content/room_details/history_viewer/audio_row.rs index 74238d9c..c88f9902 100644 --- a/src/session/view/content/room_details/history_viewer/audio_row.rs +++ b/src/session/view/content/room_details/history_viewer/audio_row.rs @@ -121,10 +121,11 @@ impl AudioRow { imp.duration_label.set_label(&gettext("Unknown duration")); } - let session = event.room().unwrap().session(); - spawn!(clone!(@weak self as obj => async move { - obj.download_audio(audio, &session).await; - })); + if let Some(session) = event.room().and_then(|r| r.session()) { + spawn!(clone!(@weak self as obj, @weak session => async move { + obj.download_audio(audio, &session).await; + })); + } } } } diff --git a/src/session/view/content/room_details/history_viewer/event.rs b/src/session/view/content/room_details/history_viewer/event.rs index b9f25282..f34c3fe7 100644 --- a/src/session/view/content/room_details/history_viewer/event.rs +++ b/src/session/view/content/room_details/history_viewer/event.rs @@ -72,7 +72,17 @@ impl HistoryViewerEvent { pub async fn get_file_content(&self) -> Result<(String, Vec), matrix_sdk::Error> { if let AnyMessageLikeEventContent::RoomMessage(content) = self.original_content().unwrap() { - let media = self.room().unwrap().session().client().media(); + let Some(room) = self.room() else { + return Err(matrix_sdk::Error::UnknownError( + "Failed to upgrade Room".into(), + )); + }; + let Some(session) = room.session() else { + return Err(matrix_sdk::Error::UnknownError( + "Failed to upgrade Session".into(), + )); + }; + let media = session.client().media(); if let MessageType::File(content) = content.msgtype { let filename = content diff --git a/src/session/view/content/room_details/history_viewer/media_item.rs b/src/session/view/content/room_details/history_viewer/media_item.rs index ed10cf4e..c25f9541 100644 --- a/src/session/view/content/room_details/history_viewer/media_item.rs +++ b/src/session/view/content/room_details/history_viewer/media_item.rs @@ -116,13 +116,19 @@ impl MediaItem { } if let Some(ref event) = event { + let Some(room) = event.room() else { + return; + }; + let Some(session) = room.session() else { + return; + }; match event.original_content() { Some(AnyMessageLikeEventContent::RoomMessage(message)) => match message.msgtype { MessageType::Image(content) => { - self.show_image(content, &event.room().unwrap().session()); + self.show_image(content, &session); } MessageType::Video(content) => { - self.show_video(content, &event.room().unwrap().session()); + self.show_video(content, &session); } _ => { panic!("Unexpected message type"); diff --git a/src/session/view/content/room_details/history_viewer/timeline.rs b/src/session/view/content/room_details/history_viewer/timeline.rs index a87f9b52..67d49f0a 100644 --- a/src/session/view/content/room_details/history_viewer/timeline.rs +++ b/src/session/view/content/room_details/history_viewer/timeline.rs @@ -136,7 +136,7 @@ impl Timeline { let room = self.room(); let matrix_room = room.matrix_room(); let last_token = imp.last_token.clone(); - let is_encrypted = room.is_encrypted(); + let is_encrypted = room.encrypted(); let handle: tokio::task::JoinHandle> = spawn_tokio!(async move { let last_token = last_token.lock().await; diff --git a/src/session/view/content/room_details/invite_subpage/invitee_list.rs b/src/session/view/content/room_details/invite_subpage/invitee_list.rs index 4c5ed16f..a2d94f3d 100644 --- a/src/session/view/content/room_details/invite_subpage/invitee_list.rs +++ b/src/session/view/content/room_details/invite_subpage/invitee_list.rs @@ -204,7 +204,9 @@ impl InviteeList { search_term: String, response: Result, ) { - let session = self.room().session(); + let Some(session) = self.room().session() else { + return; + }; // We should have a strong reference to the list in the main page so we can use // `get_or_create_members()`. let member_list = self.room().get_or_create_members(); @@ -312,7 +314,10 @@ impl InviteeList { } fn search_users(&self) { - let client = self.room().session().client(); + let Some(session) = self.room().session() else { + return; + }; + let client = session.client(); let search_term = if let Some(search_term) = self.search_term() { search_term } else { diff --git a/src/session/view/content/room_history/item_row.rs b/src/session/view/content/room_history/item_row.rs index 204a0e19..9f060f9a 100644 --- a/src/session/view/content/room_history/item_row.rs +++ b/src/session/view/content/room_history/item_row.rs @@ -275,7 +275,7 @@ impl ItemRow { self.set_action_group(None); self.set_event_actions(None); - match item.kind() { + match &*item.kind() { VirtualItemKind::Spinner => { if !self.child().map_or(false, |widget| widget.is::()) { let spinner = Spinner::default(); @@ -297,7 +297,8 @@ impl ItemRow { self.room_history() .room() .as_ref() - .map(|room| room.typing_list()), + .map(|room| room.typing_list()) + .as_ref(), ); } VirtualItemKind::TimelineStart => { @@ -427,12 +428,11 @@ impl ItemRow { /// Unsets the actions if `event` is `None`. fn set_event_actions(&self, event: Option<&Event>) -> Option { self.clear_expression_watches(); - let event = match event { - Some(event) => event, - None => { - self.insert_action_group("event", gio::ActionGroup::NONE); - return None; - } + let Some((event, room, session)) = + event.and_then(|e| e.room().and_then(|r| r.session().map(|s| (e, r, s)))) + else { + self.insert_action_group("event", gio::ActionGroup::NONE); + return None; }; let action_group = gio::SimpleActionGroup::new(); @@ -454,7 +454,10 @@ impl ItemRow { // Create a permalink gio::ActionEntry::builder("permalink") .activate(clone!(@weak self as widget, @weak event => move |_, _, _| { - let matrix_room = event.room().matrix_room(); + let Some(room) = event.room() else { + return; + }; + let matrix_room = room.matrix_room(); let event_id = event.event_id().unwrap(); spawn!(clone!(@weak widget => async move { let handle = spawn_tokio!(async move { @@ -477,7 +480,6 @@ impl ItemRow { ]); if let TimelineItemContent::Message(message) = event.content() { - let session = event.room().session(); let own_user_id = session.user_id(); let is_from_own_user = event.sender_id() == own_user_id; @@ -503,8 +505,7 @@ impl ItemRow { if is_from_own_user { update_remove_action(self, &action_group, true); } else { - let remove_watch = event - .room() + let remove_watch = room .own_user_is_allowed_to_expr(PowerLevelAction::Redact) .watch( glib::Object::NONE, @@ -726,6 +727,9 @@ impl ItemRow { let Some(event_id) = event.event_id() else { return; }; + let Some(room) = event.room() else { + return; + }; let confirm_dialog = adw::MessageDialog::builder() .transient_for(&window) @@ -745,7 +749,7 @@ impl ItemRow { return; } - if let Err(error) = event.room().redact(event_id, None).await { + if let Err(error) = room.redact(event_id, None).await { error!("Failed to redact event: {error}"); toast!(self, gettext("Failed to remove message")); } @@ -759,7 +763,9 @@ impl ItemRow { let Some(event_id) = event.event_id() else { return; }; - let room = event.room(); + let Some(room) = event.room() else { + return; + }; let reaction_group = event.reactions().reaction_group_by_key(&key); if let Some(reaction_key) = reaction_group.and_then(|group| group.user_reaction_event_key()) diff --git a/src/session/view/content/room_history/message_row/content.rs b/src/session/view/content/room_history/message_row/content.rs index 4423ae7c..482cb23a 100644 --- a/src/session/view/content/room_history/message_row/content.rs +++ b/src/session/view/content/room_history/message_row/content.rs @@ -125,6 +125,10 @@ impl MessageContent { } pub fn update_for_event(&self, event: &Event) { + let Some(room) = event.room() else { + return; + }; + let format = self.format(); if format == ContentFormat::Natural { if let Some(related_content) = event.reply_to_event_content() { @@ -146,7 +150,6 @@ impl MessageContent { ); } TimelineDetails::Ready(related_content) => { - let room = event.room(); // We should have a strong reference to the list in the RoomHistory so we // can use `get_or_create_members()`. let sender = room @@ -177,7 +180,7 @@ impl MessageContent { } } - build_content(self, event.content(), format, event.sender(), &event.room()); + build_content(self, event.content(), format, event.sender(), &room); } /// Get the texture displayed by this widget, if any. @@ -196,6 +199,10 @@ fn build_content( sender: Member, room: &Room, ) { + let Some(session) = room.session() else { + return; + }; + let parent = parent.upcast_ref(); match content { TimelineItemContent::Message(message) => { @@ -208,7 +215,7 @@ fn build_content( parent.set_child(Some(&child)); child }; - child.audio(message.clone(), &room.session(), format); + child.audio(message.clone(), &session, format); } MessageType::Emote(message) => { let child = if let Some(child) = parent.child().and_downcast::() { @@ -256,7 +263,7 @@ fn build_content( parent.set_child(Some(&child)); child }; - child.image(message.clone(), &room.session(), format); + child.image(message.clone(), &session, format); } MessageType::Location(message) => { let child = @@ -317,7 +324,7 @@ fn build_content( parent.set_child(Some(&child)); child }; - child.video(message.clone(), &room.session(), format); + child.video(message.clone(), &session, format); } MessageType::VerificationRequest(_) => { // TODO: show more information about the verification @@ -351,7 +358,7 @@ fn build_content( parent.set_child(Some(&child)); child }; - child.sticker(sticker.content().clone(), &room.session(), format); + child.sticker(sticker.content().clone(), &session, format); } TimelineItemContent::UnableToDecrypt(_) => { let child = if let Some(child) = parent.child().and_downcast::() { diff --git a/src/session/view/content/room_history/message_row/mod.rs b/src/session/view/content/room_history/message_row/mod.rs index d42d45ae..344d79dc 100644 --- a/src/session/view/content/room_history/message_row/mod.rs +++ b/src/session/view/content/room_history/message_row/mod.rs @@ -195,6 +195,9 @@ impl MessageRow { } pub fn set_event(&self, event: Event) { + let Some(room) = event.room() else { + return; + }; let imp = self.imp(); // Remove signals and bindings from the previous event. @@ -243,8 +246,8 @@ impl MessageRow { ); imp.reactions - .set_reaction_list(&event.room().get_or_create_members(), event.reactions()); - imp.read_receipts.set_source(event.read_receipts()); + .set_reaction_list(&room.get_or_create_members(), &event.reactions()); + imp.read_receipts.set_source(&event.read_receipts()); imp.event .set(event, vec![timestamp_handler, source_handler]); self.notify("event"); diff --git a/src/session/view/content/room_history/message_row/reaction/mod.rs b/src/session/view/content/room_history/message_row/reaction/mod.rs index 0f942f7f..fb1a6477 100644 --- a/src/session/view/content/room_history/message_row/reaction/mod.rs +++ b/src/session/view/content/room_history/message_row/reaction/mod.rs @@ -135,9 +135,9 @@ impl MessageReaction { fn set_group(&self, group: ReactionGroup) { let imp = self.imp(); let key = group.key(); - imp.reaction_key.set_label(key); + imp.reaction_key.set_label(&key); - if EMOJI_REGEX.is_match(key) { + if EMOJI_REGEX.is_match(&key) { imp.reaction_key.add_css_class("emoji"); } else { imp.reaction_key.remove_css_class("emoji"); diff --git a/src/session/view/content/room_history/message_toolbar/mod.rs b/src/session/view/content/room_history/message_toolbar/mod.rs index a2dcb8d8..0dc6fdcc 100644 --- a/src/session/view/content/room_history/message_toolbar/mod.rs +++ b/src/session/view/content/room_history/message_toolbar/mod.rs @@ -431,6 +431,9 @@ impl MessageToolbar { /// Set the event to edit. pub fn set_edit(&self, event: Event) { + let Some(room) = event.room() else { + return; + }; // We don't support editing non-text messages. let Some((text, formatted)) = event.message().and_then(|msg| match msg { MessageType::Emote(emote) => Some((format!("/me {}", emote.body), emote.formatted)), @@ -443,7 +446,7 @@ impl MessageToolbar { let mentions = if let Some(html) = formatted.and_then(|f| (f.format == MessageFormat::Html).then_some(f.body)) { - let (_, mentions) = extract_mentions(&html, &event.room()); + let (_, mentions) = extract_mentions(&html, &room); let mut pos = 0; // This is looking for the mention link's inner text in the Markdown // so it is not super reliable: if there is other text that matches @@ -880,7 +883,10 @@ impl MessageToolbar { fn update_completion(&self, room: Option<&Room>) { let completion = &self.imp().completion; - completion.set_user_id(room.map(|r| r.session().user_id().to_string())); + completion.set_user_id( + room.and_then(|r| r.session()) + .map(|s| s.user_id().to_string()), + ); // `RoomHistory` should have a strong reference to the list so we can use // `get_or_create_members()`. completion.set_members(room.map(|r| r.get_or_create_members())); @@ -926,8 +932,9 @@ impl MessageToolbar { } fn set_up_can_send_messages(&self, room: Option<&Room>) { - if let Some(room) = room { - let own_user_id = room.session().user_id().to_owned(); + if let Some((room, own_user_id)) = + room.and_then(|r| r.session().map(|s| (r, s.user_id().to_owned()))) + { let imp = self.imp(); let own_member = room @@ -936,12 +943,9 @@ impl MessageToolbar { // We don't need to keep the handler around, the member should be dropped when // switching rooms. - own_member.connect_notify_local( - Some("membership"), - clone!(@weak self as obj => move |_, _| { - obj.update_can_send_messages(); - }), - ); + own_member.connect_membership_notify(clone!(@weak self as obj => move |_| { + obj.update_can_send_messages(); + })); imp.own_member.set(Some(&own_member)); let power_levels_handler = room.power_levels().connect_notify_local( diff --git a/src/session/view/content/room_history/mod.rs b/src/session/view/content/room_history/mod.rs index d5e16927..09a6ac1a 100644 --- a/src/session/view/content/room_history/mod.rs +++ b/src/session/view/content/room_history/mod.rs @@ -495,7 +495,7 @@ impl RoomHistory { .replace(room.as_ref().map(|r| r.get_or_create_members())); let model = room.as_ref().map(|room| room.timeline().items()); - self.selection_model().set_model(model); + self.selection_model().set_model(model.as_ref()); imp.is_loading.set(false); imp.room.replace(room); @@ -630,7 +630,7 @@ impl RoomHistory { let imp = self.imp(); if let Some(room) = &*imp.room.borrow() { - if room.timeline().is_empty() { + if room.timeline().empty() { if room.timeline().state() == TimelineState::Error { imp.stack.set_visible_child(&*imp.error); } else { @@ -654,7 +654,7 @@ impl RoomHistory { return false; } - if timeline.is_empty() { + if timeline.empty() { // We definitely want messages if the timeline is ready but empty. return true; }; @@ -784,7 +784,7 @@ impl RoomHistory { }; let timeline = room.timeline(); - if !timeline.is_empty() { + if !timeline.empty() { let imp = self.imp(); if let Some(source_id) = imp.scroll_timeout.take() { @@ -934,6 +934,9 @@ impl RoomHistory { let Some(room) = self.room() else { return; }; + let Some(session) = room.session() else { + return; + }; if !room.is_joined() || !room.is_tombstoned() { return; @@ -944,11 +947,10 @@ impl RoomHistory { return; }; - let session = room.session(); window.show_room(session.session_id(), successor.room_id()); } else if let Some(successor_id) = room.successor_id().map(ToOwned::to_owned) { - spawn!(clone!(@weak self as obj, @weak room => async move { - if let Err(error) = room.session() + spawn!(clone!(@weak self as obj, @weak session => async move { + if let Err(error) = session .room_list() .join_by_id_or_alias(successor_id.into(), vec![]).await { diff --git a/src/session/view/content/room_history/state_row/mod.rs b/src/session/view/content/room_history/state_row/mod.rs index b53590dd..d23b69bb 100644 --- a/src/session/view/content/room_history/state_row/mod.rs +++ b/src/session/view/content/room_history/state_row/mod.rs @@ -121,12 +121,16 @@ impl StateRow { } let imp = self.imp(); - imp.read_receipts.set_source(event.read_receipts()); + imp.read_receipts.set_source(&event.read_receipts()); imp.event.replace(Some(event)); self.notify("event"); } fn update_with_other_state(&self, event: &Event, other_state: &OtherState) { + let Some(room) = event.room() else { + return; + }; + let widget = match other_state.content() { AnyOtherFullStateEventContent::RoomCreate(content) => { WidgetType::Creation(StateCreation::new(content)) @@ -152,7 +156,7 @@ impl StateRow { )) } AnyOtherFullStateEventContent::RoomTombstone(_) => { - WidgetType::Tombstone(StateTombstone::new(&event.room())) + WidgetType::Tombstone(StateTombstone::new(&room)) } _ => { warn!( diff --git a/src/session/view/content/room_history/state_row/tombstone.rs b/src/session/view/content/room_history/state_row/tombstone.rs index 2b6d0602..f5892528 100644 --- a/src/session/view/content/room_history/state_row/tombstone.rs +++ b/src/session/view/content/room_history/state_row/tombstone.rs @@ -129,7 +129,9 @@ impl StateTombstone { let Some(room) = self.room() else { return; }; - let session = room.session(); + let Some(session) = room.session() else { + return; + }; let room_list = session.room_list(); // Join or view the room with the given identifier. diff --git a/src/session/view/media_viewer.rs b/src/session/view/media_viewer.rs index 0c04065e..49b995dd 100644 --- a/src/session/view/media_viewer.rs +++ b/src/session/view/media_viewer.rs @@ -414,8 +414,11 @@ impl MediaViewer { let Some(message) = self.message() else { return; }; + let Some(session) = room.session() else { + return; + }; - let client = room.session().client(); + let client = session.client(); match &message { MessageType::Image(image) => { @@ -540,7 +543,10 @@ impl MediaViewer { let Some(message) = self.message() else { return; }; - let client = room.session().client(); + let Some(session) = room.session() else { + return; + }; + let client = session.client(); let (filename, data) = match get_media_content(client, message).await { Ok(res) => res, diff --git a/src/session/view/session_view.rs b/src/session/view/session_view.rs index 2eb90a40..14d53338 100644 --- a/src/session/view/session_view.rs +++ b/src/session/view/session_view.rs @@ -331,6 +331,9 @@ impl SessionView { /// Show a media event. pub fn show_media(&self, event: &Event, source_widget: &impl IsA) { + let Some(room) = event.room() else { + return; + }; let Some(message) = event.message() else { error!("Trying to open the media viewer with an event that is not a message"); return; @@ -338,7 +341,7 @@ impl SessionView { let imp = self.imp(); imp.media_viewer - .set_message(&event.room(), event.event_id().unwrap(), message); + .set_message(&room, event.event_id().unwrap(), message); imp.media_viewer.reveal(source_widget); } } diff --git a/src/utils/matrix.rs b/src/utils/matrix.rs index e3e8f180..f92637f2 100644 --- a/src/utils/matrix.rs +++ b/src/utils/matrix.rs @@ -289,7 +289,7 @@ pub async fn get_media_content( /// Returns a new string with placeholders and the corresponding widgets and the /// string they are replacing. pub fn extract_mentions(s: &str, room: &Room) -> (String, Vec<(Pill, String)>) { - let session = room.session(); + let session = room.session().unwrap(); let mut mentions = Vec::new(); let mut mention = None; let mut new_string = String::new();