diff --git a/src/session/model/avatar/data.rs b/src/session/model/avatar/data.rs deleted file mode 100644 index 3e058387..00000000 --- a/src/session/model/avatar/data.rs +++ /dev/null @@ -1,145 +0,0 @@ -use gtk::{gdk, glib, prelude::*, subclass::prelude::*}; -use tracing::warn; - -use super::AvatarImage; -use crate::{ - application::Application, - utils::notifications::{paintable_as_notification_icon, string_as_notification_icon}, -}; - -mod imp { - use std::cell::RefCell; - - use once_cell::sync::Lazy; - - use super::*; - - #[derive(Debug, Default)] - pub struct AvatarData { - /// The data of the user-defined image. - pub image: RefCell>, - /// The display name used as a fallback for this avatar. - pub display_name: RefCell>, - } - - #[glib::object_subclass] - impl ObjectSubclass for AvatarData { - const NAME: &'static str = "AvatarData"; - type Type = super::AvatarData; - } - - impl ObjectImpl for AvatarData { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![ - glib::ParamSpecObject::builder::("image") - .explicit_notify() - .build(), - glib::ParamSpecString::builder("display-name") - .explicit_notify() - .build(), - ] - }); - - PROPERTIES.as_ref() - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - let obj = self.obj(); - - match pspec.name() { - "image" => obj.set_image(value.get().unwrap()), - "display-name" => obj.set_display_name(value.get().unwrap()), - _ => unimplemented!(), - } - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - let obj = self.obj(); - - match pspec.name() { - "image" => obj.image().to_value(), - "display-name" => obj.display_name().to_value(), - _ => unimplemented!(), - } - } - } -} - -glib::wrapper! { - /// Data about a User’s or Room’s avatar. - pub struct AvatarData(ObjectSubclass); -} - -impl AvatarData { - /// Construct a new empty `AvatarData`. - pub fn new() -> Self { - glib::Object::new() - } - - /// Constructs an `AvatarData` with the given image data. - pub fn with_image(image: AvatarImage) -> Self { - glib::Object::builder().property("image", image).build() - } - - /// The data of the user-defined image. - pub fn image(&self) -> Option { - self.imp().image.borrow().clone() - } - - /// Set the data of the user-defined image. - pub fn set_image(&self, image: Option) { - let imp = self.imp(); - - if imp.image.borrow().as_ref() == image.as_ref() { - return; - } - - imp.image.replace(image); - self.notify("image"); - } - - /// Set the display name used as a fallback for this avatar. - pub fn set_display_name(&self, display_name: Option) { - let imp = self.imp(); - - if imp.display_name.borrow().as_ref() == display_name.as_ref() { - return; - } - - imp.display_name.replace(display_name); - self.notify("display-name"); - } - - /// The display name used as a fallback for this avatar. - pub fn display_name(&self) -> Option { - self.imp().display_name.borrow().clone() - } - - /// Get this avatar as a notification icon. - /// - /// Returns `None` if an error occurred while generating the icon. - pub fn as_notification_icon(&self) -> Option { - let window = Application::default().active_window()?.upcast(); - - let icon = if let Some(paintable) = self.image().and_then(|i| i.paintable()) { - paintable_as_notification_icon(paintable.upcast_ref(), &window) - } else { - string_as_notification_icon(&self.display_name().unwrap_or_default(), &window) - }; - - match icon { - Ok(icon) => Some(icon), - Err(error) => { - warn!("Failed to generate icon for notification: {error}"); - None - } - } - } -} - -impl Default for AvatarData { - fn default() -> Self { - Self::new() - } -} diff --git a/src/session/model/avatar/image.rs b/src/session/model/avatar/image.rs deleted file mode 100644 index c4cd1f82..00000000 --- a/src/session/model/avatar/image.rs +++ /dev/null @@ -1,237 +0,0 @@ -use gtk::{gdk, glib, glib::clone, prelude::*, subclass::prelude::*}; -use matrix_sdk::{ - media::{MediaFormat, MediaRequest, MediaThumbnailSize}, - ruma::{ - api::client::media::get_content_thumbnail::v3::Method, events::room::MediaSource, MxcUri, - OwnedMxcUri, - }, -}; -use tracing::error; - -use crate::{components::ImagePaintable, session::model::Session, spawn, spawn_tokio}; - -/// The source of an avatar's URI. -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, glib::Enum)] -#[repr(u32)] -#[enum_type(name = "AvatarUriSource")] -pub enum AvatarUriSource { - /// The URI comes from a Matrix user. - #[default] - User = 0, - /// The URI comes from a Matrix room. - Room = 1, -} - -mod imp { - use std::cell::{Cell, RefCell}; - - use once_cell::sync::{Lazy, OnceCell}; - - use super::*; - - #[derive(Debug, Default)] - pub struct AvatarImage { - pub paintable: RefCell>, - pub needed_size: Cell, - pub uri: RefCell>, - /// The source of the avatar's URI. - pub uri_source: Cell, - pub session: OnceCell, - } - - #[glib::object_subclass] - impl ObjectSubclass for AvatarImage { - const NAME: &'static str = "AvatarImage"; - type Type = super::AvatarImage; - } - - impl ObjectImpl for AvatarImage { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![ - glib::ParamSpecObject::builder::("paintable") - .read_only() - .build(), - glib::ParamSpecUInt::builder("needed-size") - .minimum(0) - .explicit_notify() - .build(), - glib::ParamSpecString::builder("uri") - .explicit_notify() - .build(), - glib::ParamSpecEnum::builder::("uri-source") - .construct_only() - .build(), - glib::ParamSpecObject::builder::("session") - .construct_only() - .build(), - ] - }); - - PROPERTIES.as_ref() - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - let obj = self.obj(); - - match pspec.name() { - "needed-size" => obj.set_needed_size(value.get().unwrap()), - "uri" => obj.set_uri(value.get::<&str>().ok().map(Into::into)), - "uri-source" => obj.set_uri_source(value.get().unwrap()), - "session" => { - if let Some(session) = value.get().unwrap() { - if self.session.set(session).is_err() { - error!("Trying to set a session while it is already set"); - } - } - } - _ => unimplemented!(), - } - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - let obj = self.obj(); - - match pspec.name() { - "paintable" => obj.paintable().to_value(), - "needed-size" => obj.needed_size().to_value(), - "uri" => obj.uri().map_or_else( - || { - let none: Option<&str> = None; - none.to_value() - }, - |url| url.as_str().to_value(), - ), - "uri-source" => obj.uri_source().to_value(), - "session" => obj.session().to_value(), - _ => unimplemented!(), - } - } - } -} - -glib::wrapper! { - /// The image data for an avatar. - pub struct AvatarImage(ObjectSubclass); -} - -impl AvatarImage { - /// Construct a new `AvatarImage` with the given session and Matrix URI. - pub fn new(session: &Session, uri: Option<&MxcUri>, uri_source: AvatarUriSource) -> Self { - glib::Object::builder() - .property("session", session) - .property("uri", uri.map(|uri| uri.to_string())) - .property("uri-source", uri_source) - .build() - } - - /// The current session. - fn session(&self) -> &Session { - self.imp().session.get().unwrap() - } - - /// The image content as a paintable, if any. - pub fn paintable(&self) -> Option { - self.imp().paintable.borrow().clone() - } - - /// Set the content of the image. - fn set_image_data(&self, data: Option>) { - let paintable = data - .and_then(|data| ImagePaintable::from_bytes(&glib::Bytes::from(&data), None).ok()) - .map(|texture| texture.upcast()); - self.imp().paintable.replace(paintable); - self.notify("paintable"); - } - - fn load(&self) { - // Don't do anything here if we don't need the avatar. - if self.needed_size() == 0 { - return; - } - - let Some(uri) = self.uri() else { - return; - }; - - let client = self.session().client(); - let needed_size = self.needed_size(); - let request = MediaRequest { - source: MediaSource::Plain(uri), - format: MediaFormat::Thumbnail(MediaThumbnailSize { - width: needed_size.into(), - height: needed_size.into(), - method: Method::Scale, - }), - }; - let handle = - spawn_tokio!(async move { client.media().get_media_content(&request, true).await }); - - spawn!( - glib::Priority::LOW, - clone!(@weak self as obj => async move { - match handle.await.unwrap() { - Ok(data) => obj.set_image_data(Some(data)), - Err(error) => error!("Could not fetch avatar: {error}"), - }; - }) - ); - } - - /// Set the needed size of the user-defined image. - /// - /// Only the biggest size will be stored. - pub fn set_needed_size(&self, size: u32) { - let imp = self.imp(); - - if imp.needed_size.get() < size { - imp.needed_size.set(size); - - self.load(); - } - - self.notify("needed-size"); - } - - /// Get the biggest needed size of the user-defined image. - /// - /// If this is `0`, no image will be loaded. - pub fn needed_size(&self) -> u32 { - self.imp().needed_size.get() - } - - /// Set the Matrix URI of the `AvatarImage`. - pub fn set_uri(&self, uri: Option) { - let imp = self.imp(); - - if imp.uri.borrow().as_ref() == uri.as_ref() { - return; - } - - let has_uri = uri.is_some(); - imp.uri.replace(uri); - - if has_uri { - self.load(); - } else { - self.set_image_data(None); - } - - self.notify("uri"); - } - - /// The Matrix URI of the `AvatarImage`. - pub fn uri(&self) -> Option { - self.imp().uri.borrow().to_owned() - } - - /// The source of the avatar's URI. - pub fn uri_source(&self) -> AvatarUriSource { - self.imp().uri_source.get() - } - - /// Set the source of the avatar's URI. - fn set_uri_source(&self, uri_source: AvatarUriSource) { - self.imp().uri_source.set(uri_source); - } -} diff --git a/src/session/model/avatar/mod.rs b/src/session/model/avatar/mod.rs deleted file mode 100644 index 8975fb69..00000000 --- a/src/session/model/avatar/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod data; -mod image; - -pub use self::{ - data::AvatarData, - image::{AvatarImage, AvatarUriSource}, -}; diff --git a/src/session/model/avatar_data/avatar_image.rs b/src/session/model/avatar_data/avatar_image.rs new file mode 100644 index 00000000..c9ba6e31 --- /dev/null +++ b/src/session/model/avatar_data/avatar_image.rs @@ -0,0 +1,161 @@ +use gtk::{gdk, glib, glib::clone, prelude::*, subclass::prelude::*}; +use matrix_sdk::{ + media::{MediaFormat, MediaRequest, MediaThumbnailSize}, + ruma::{ + api::client::media::get_content_thumbnail::v3::Method, events::room::MediaSource, MxcUri, + OwnedMxcUri, + }, +}; +use tracing::error; + +use crate::{components::ImagePaintable, session::model::Session, spawn, spawn_tokio}; + +/// The source of an avatar's URI. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, glib::Enum)] +#[repr(u32)] +#[enum_type(name = "AvatarUriSource")] +pub enum AvatarUriSource { + /// The URI comes from a Matrix user. + #[default] + User = 0, + /// The URI comes from a Matrix room. + Room = 1, +} + +mod imp { + use std::cell::{Cell, OnceCell, RefCell}; + + use super::*; + + #[derive(Debug, Default, glib::Properties)] + #[properties(wrapper_type = super::AvatarImage)] + pub struct AvatarImage { + /// The image content as a paintable, if any. + #[property(get)] + pub paintable: RefCell>, + /// The biggest needed size of the user-defined image. + /// + /// If this is `0`, no image will be loaded. + #[property(get, set = Self::set_needed_size, explicit_notify, minimum = 0)] + pub needed_size: Cell, + /// The Matrix URI of the `AvatarImage`. + #[property(get = Self::uri, set = Self::set_uri, explicit_notify, nullable, type = Option)] + pub uri: RefCell>, + /// The source of the avatar's URI. + #[property(get, construct_only, builder(AvatarUriSource::default()))] + pub uri_source: Cell, + /// The current session. + #[property(get, construct_only)] + pub session: OnceCell, + } + + #[glib::object_subclass] + impl ObjectSubclass for AvatarImage { + const NAME: &'static str = "AvatarImage"; + type Type = super::AvatarImage; + } + + #[glib::derived_properties] + impl ObjectImpl for AvatarImage {} + + impl AvatarImage { + /// Set the needed size of the user-defined image. + /// + /// Only the biggest size will be stored. + fn set_needed_size(&self, size: u32) { + if self.needed_size.get() >= size { + return; + } + let obj = self.obj(); + + self.needed_size.set(size); + obj.load(); + obj.notify_needed_size(); + } + + /// The Matrix URI of the `AvatarImage`. + fn uri(&self) -> Option { + self.uri.borrow().as_ref().map(ToString::to_string) + } + + /// Set the Matrix URI of the `AvatarImage`. + fn set_uri(&self, uri: Option) { + let uri = uri.map(OwnedMxcUri::from); + + if self.uri.borrow().as_ref() == uri.as_ref() { + return; + } + let obj = self.obj(); + + let has_uri = uri.is_some(); + self.uri.replace(uri); + + if has_uri { + obj.load(); + } else { + obj.set_image_data(None); + } + + obj.notify_uri(); + } + } +} + +glib::wrapper! { + /// The image data for an avatar. + pub struct AvatarImage(ObjectSubclass); +} + +impl AvatarImage { + /// Construct a new `AvatarImage` with the given session and Matrix URI. + pub fn new(session: &Session, uri: Option<&MxcUri>, uri_source: AvatarUriSource) -> Self { + glib::Object::builder() + .property("session", session) + .property("uri", uri.map(|uri| uri.to_string())) + .property("uri-source", uri_source) + .build() + } + + /// Set the content of the image. + fn set_image_data(&self, data: Option>) { + let paintable = data + .and_then(|data| ImagePaintable::from_bytes(&glib::Bytes::from(&data), None).ok()) + .map(|texture| texture.upcast()); + self.imp().paintable.replace(paintable); + self.notify("paintable"); + } + + fn load(&self) { + // Don't do anything here if we don't need the avatar. + if self.needed_size() == 0 { + return; + } + + let Some(uri) = self.imp().uri.borrow().clone() else { + return; + }; + + let client = self.session().client(); + let needed_size = self.needed_size(); + let request = MediaRequest { + source: MediaSource::Plain(uri), + format: MediaFormat::Thumbnail(MediaThumbnailSize { + width: needed_size.into(), + height: needed_size.into(), + method: Method::Scale, + }), + }; + let handle = + spawn_tokio!(async move { client.media().get_media_content(&request, true).await }); + + spawn!( + glib::Priority::LOW, + clone!(@weak self as obj => async move { + match handle.await.unwrap() { + Ok(data) => obj.set_image_data(Some(data)), + Err(error) => error!("Could not fetch avatar: {error}"), + }; + }) + ); + } +} diff --git a/src/session/model/avatar_data/mod.rs b/src/session/model/avatar_data/mod.rs new file mode 100644 index 00000000..21cfe1ff --- /dev/null +++ b/src/session/model/avatar_data/mod.rs @@ -0,0 +1,102 @@ +use gtk::{gdk, glib, prelude::*, subclass::prelude::*}; +use tracing::warn; + +mod avatar_image; + +pub use self::avatar_image::{AvatarImage, AvatarUriSource}; +use crate::{ + application::Application, + utils::notifications::{paintable_as_notification_icon, string_as_notification_icon}, +}; + +mod imp { + use std::cell::RefCell; + + use super::*; + + #[derive(Debug, Default, glib::Properties)] + #[properties(wrapper_type = super::AvatarData)] + pub struct AvatarData { + /// The data of the user-defined image. + #[property(get, set = Self::set_image, explicit_notify, nullable)] + pub image: RefCell>, + /// The display name used as a fallback for this avatar. + #[property(get, set = Self::set_display_name, explicit_notify, nullable)] + pub display_name: RefCell>, + } + + #[glib::object_subclass] + impl ObjectSubclass for AvatarData { + const NAME: &'static str = "AvatarData"; + type Type = super::AvatarData; + } + + #[glib::derived_properties] + impl ObjectImpl for AvatarData {} + + impl AvatarData { + /// Set the data of the user-defined image. + fn set_image(&self, image: Option) { + if self.image.borrow().as_ref() == image.as_ref() { + return; + } + + self.image.replace(image); + self.obj().notify_image(); + } + + /// Set the display name used as a fallback for this avatar. + fn set_display_name(&self, display_name: Option) { + if self.display_name.borrow().as_ref() == display_name.as_ref() { + return; + } + + self.display_name.replace(display_name); + self.obj().notify_display_name(); + } + } +} + +glib::wrapper! { + /// Data about a User’s or Room’s avatar. + pub struct AvatarData(ObjectSubclass); +} + +impl AvatarData { + /// Construct a new empty `AvatarData`. + pub fn new() -> Self { + glib::Object::new() + } + + /// Constructs an `AvatarData` with the given image data. + pub fn with_image(image: AvatarImage) -> Self { + glib::Object::builder().property("image", image).build() + } + + /// Get this avatar as a notification icon. + /// + /// Returns `None` if an error occurred while generating the icon. + pub fn as_notification_icon(&self) -> Option { + let window = Application::default().active_window()?.upcast(); + + let icon = if let Some(paintable) = self.image().and_then(|i| i.paintable()) { + paintable_as_notification_icon(paintable.upcast_ref(), &window) + } else { + string_as_notification_icon(&self.display_name().unwrap_or_default(), &window) + }; + + match icon { + Ok(icon) => Some(icon), + Err(error) => { + warn!("Failed to generate icon for notification: {error}"); + None + } + } + } +} + +impl Default for AvatarData { + fn default() -> Self { + Self::new() + } +} diff --git a/src/session/model/mod.rs b/src/session/model/mod.rs index af8d0037..dab79ff7 100644 --- a/src/session/model/mod.rs +++ b/src/session/model/mod.rs @@ -1,4 +1,4 @@ -mod avatar; +mod avatar_data; mod notifications; mod room; mod room_list; @@ -9,7 +9,7 @@ mod user; mod verification; pub use self::{ - avatar::{AvatarData, AvatarImage, AvatarUriSource}, + avatar_data::{AvatarData, AvatarImage, AvatarUriSource}, notifications::Notifications, room::{ Event, EventKey, HighlightFlags, Member, MemberList, MemberRole, Membership, MessageState, diff --git a/src/session/model/room/member.rs b/src/session/model/room/member.rs index 7c0726e6..61f43f4a 100644 --- a/src/session/model/room/member.rs +++ b/src/session/model/room/member.rs @@ -199,7 +199,7 @@ impl Member { self.avatar_data() .image() .unwrap() - .set_uri(member.avatar_url().map(std::borrow::ToOwned::to_owned)); + .set_uri(member.avatar_url().map(ToString::to_string)); self.set_power_level(member.power_level()); self.set_membership(member.membership().into()); } @@ -215,7 +215,7 @@ impl Member { self.avatar_data() .image() .unwrap() - .set_uri(event.avatar_url()); + .set_uri(event.avatar_url().map(String::from)); self.set_membership((&event.content().membership).into()); let session = self.session(); diff --git a/src/session/model/room/mod.rs b/src/session/model/room/mod.rs index aeca0c18..47690c01 100644 --- a/src/session/model/room/mod.rs +++ b/src/session/model/room/mod.rs @@ -1651,7 +1651,10 @@ impl Room { } } - self.avatar_data().image().unwrap().set_uri(avatar_url); + self.avatar_data() + .image() + .unwrap() + .set_uri(avatar_url.map(String::from)); } /// Whether anyone can join this room. diff --git a/src/session/model/user.rs b/src/session/model/user.rs index ac3db5ef..36e2ce43 100644 --- a/src/session/model/user.rs +++ b/src/session/model/user.rs @@ -229,7 +229,10 @@ pub trait UserExt: IsA { /// Set the avatar URL of this user. fn set_avatar_url(&self, uri: Option) { - self.avatar_data().image().unwrap().set_uri(uri); + self.avatar_data() + .image() + .unwrap() + .set_uri(uri.map(String::from)); } /// The actions the currently logged-in user is allowed to perform on this diff --git a/src/session/view/account_settings/general_page/mod.rs b/src/session/view/account_settings/general_page/mod.rs index 4f40da2a..f2334b43 100644 --- a/src/session/view/account_settings/general_page/mod.rs +++ b/src/session/view/account_settings/general_page/mod.rs @@ -5,7 +5,7 @@ use gtk::{ glib::{self, clone}, CompositeTemplate, }; -use matrix_sdk::ruma::{api::client::discovery::get_capabilities, OwnedMxcUri}; +use matrix_sdk::ruma::api::client::discovery::get_capabilities; use tracing::error; mod change_password_subpage; @@ -57,7 +57,7 @@ mod imp { pub deactivate_account_subpage: TemplateChild, #[template_child] pub log_out_subpage: TemplateChild, - pub changing_avatar: RefCell>>, + pub changing_avatar: RefCell>>, pub changing_display_name: RefCell>>, } @@ -167,12 +167,9 @@ impl GeneralPage { .avatar_data() .image() .unwrap() - .connect_notify_local( - Some("uri"), - clone!(@weak self as obj => move |avatar_image, _| { - obj.avatar_changed(avatar_image.uri()); - }), - ); + .connect_uri_notify(clone!(@weak self as obj => move |avatar_image| { + obj.avatar_changed(avatar_image.uri()); + })); self.user().connect_notify_local( Some("display-name"), clone!(@weak self as obj => move |user, _| { @@ -224,7 +221,7 @@ impl GeneralPage { })); } - fn avatar_changed(&self, uri: Option) { + fn avatar_changed(&self, uri: Option) { let imp = self.imp(); if let Some(action) = imp.changing_avatar.borrow().as_ref() { @@ -278,7 +275,7 @@ impl GeneralPage { } }; - let (action, weak_action) = OngoingAsyncAction::set(uri.clone()); + let (action, weak_action) = OngoingAsyncAction::set(uri.to_string()); imp.changing_avatar.replace(Some(action)); let uri_clone = uri.clone(); diff --git a/src/session/view/content/explore/public_room.rs b/src/session/view/content/explore/public_room.rs index 36aecc3f..2768343b 100644 --- a/src/session/view/content/explore/public_room.rs +++ b/src/session/view/content/explore/public_room.rs @@ -164,12 +164,12 @@ impl PublicRoom { pub fn set_matrix_public_room(&self, room: PublicRoomsChunk) { let imp = self.imp(); - let display_name = room.name.clone().map(Into::into); + let display_name = room.name.clone(); self.avatar_data().set_display_name(display_name); self.avatar_data() .image() .unwrap() - .set_uri(room.avatar_url.clone()); + .set_uri(room.avatar_url.clone().map(String::from)); if let Some(room) = self.room_list().get(&room.room_id) { self.set_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 bc59bf6a..63c86efb 100644 --- a/src/session/view/content/room_details/general_page/mod.rs +++ b/src/session/view/content/room_details/general_page/mod.rs @@ -14,7 +14,6 @@ use ruma::{ room::{avatar::ImageInfo, power_levels::PowerLevelAction}, StateEventType, }, - OwnedMxcUri, }; use tracing::error; @@ -63,7 +62,7 @@ mod imp { pub members_count: TemplateChild, /// Whether edit mode is enabled. pub edit_mode_enabled: Cell, - pub changing_avatar: RefCell>>, + pub changing_avatar: RefCell>>, pub changing_name: RefCell>>, pub changing_topic: RefCell>>, pub expr_watches: RefCell>, @@ -232,7 +231,7 @@ impl GeneralPage { self.imp().expr_watches.borrow_mut().push(expr_watch); } - fn avatar_changed(&self, uri: Option) { + fn avatar_changed(&self, uri: Option) { let imp = self.imp(); if let Some(action) = imp.changing_avatar.borrow().as_ref() { @@ -302,7 +301,7 @@ impl GeneralPage { } }; - let (action, weak_action) = OngoingAsyncAction::set(uri.clone()); + let (action, weak_action) = OngoingAsyncAction::set(uri.to_string()); imp.changing_avatar.replace(Some(action)); let handle =