[fractal/fractal-next] Add support for user-defined avatars
- From: Julian Sparber <jsparber src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [fractal/fractal-next] Add support for user-defined avatars
- Date: Thu, 3 Jun 2021 14:23:12 +0000 (UTC)
commit 2adec1644be50ef9c7c20c97c8667e5ebb833ef2
Author: Julian Sparber <julian sparber net>
Date: Tue Jun 1 17:14:28 2021 +0200
Add support for user-defined avatars
Fixes: https://gitlab.gnome.org/GNOME/fractal/-/issues/785
data/resources/resources.gresource.xml | 1 +
data/resources/ui/components-avatar.ui | 35 +++++
data/resources/ui/content-invite.ui | 7 +-
data/resources/ui/content-message-row.ui | 5 +-
data/resources/ui/pill.ui | 4 +-
data/resources/ui/sidebar-room-row.ui | 5 +-
po/POTFILES.in | 2 +
src/components/avatar.rs | 155 +++++++++++++++++++++++
src/components/mod.rs | 2 +
src/components/pill.rs | 5 +-
src/meson.build | 2 +
src/session/avatar.rs | 211 +++++++++++++++++++++++++++++++
src/session/content/invite.rs | 3 +-
src/session/content/message_row.rs | 7 +-
src/session/mod.rs | 2 +
src/session/room/room.rs | 25 +++-
src/session/sidebar/room_row.rs | 8 +-
src/session/user.rs | 31 ++++-
18 files changed, 476 insertions(+), 34 deletions(-)
---
diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index b38a6083..4e2683c3 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -23,6 +23,7 @@
<file compressed="true" preprocess="xml-stripblanks" alias="pill.ui">ui/pill.ui</file>
<file compressed="true" preprocess="xml-stripblanks"
alias="spinner-button.ui">ui/spinner-button.ui</file>
<file compressed="true" preprocess="xml-stripblanks"
alias="in-app-notification.ui">ui/in-app-notification.ui</file>
+ <file compressed="true" preprocess="xml-stripblanks"
alias="components-avatar.ui">ui/components-avatar.ui</file>
<file compressed="true">style.css</file>
<file preprocess="xml-stripblanks">icons/scalable/actions/send-symbolic.svg</file>
<file preprocess="xml-stripblanks">icons/scalable/status/welcome.svg</file>
diff --git a/data/resources/ui/components-avatar.ui b/data/resources/ui/components-avatar.ui
new file mode 100644
index 00000000..a692dd5a
--- /dev/null
+++ b/data/resources/ui/components-avatar.ui
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="ComponentsAvatar" parent="AdwBin">
+ <property name="child">
+ <object class="AdwAvatar" id="avatar">
+ <property name="show-initials">True</property>
+ <binding name="text">
+ <lookup name="display-name" type="Room">
+ <lookup name="item">ComponentsAvatar</lookup>
+ </lookup>
+ </binding>
+ <binding name="text">
+ <lookup name="display-name" type="User">
+ <lookup name="item">ComponentsAvatar</lookup>
+ </lookup>
+ </binding>
+ <binding name="custom-image">
+ <lookup name="image" type="Avatar">
+ <lookup name="avatar" type="Room">
+ <lookup name="item">ComponentsAvatar</lookup>
+ </lookup>
+ </lookup>
+ </binding>
+ <binding name="custom-image">
+ <lookup name="image" type="Avatar">
+ <lookup name="avatar" type="User">
+ <lookup name="item">ComponentsAvatar</lookup>
+ </lookup>
+ </lookup>
+ </binding>
+ </object>
+ </property>
+ </template>
+</interface>
+
diff --git a/data/resources/ui/content-invite.ui b/data/resources/ui/content-invite.ui
index 0cb680c1..3e59ff07 100644
--- a/data/resources/ui/content-invite.ui
+++ b/data/resources/ui/content-invite.ui
@@ -46,10 +46,11 @@
<property name="label" translatable="yes">Invite</property>
</accessibility>
<child>
- <object class="AdwAvatar">
- <property name="show-initials">True</property>
+ <object class="ComponentsAvatar">
<property name="size">150</property>
- <property name="text" bind-source="display_name" bind-property="label"
bind-flags="sync-create"/>
+ <binding name="item">
+ <lookup name="room">ContentInvite</lookup>
+ </binding>
</object>
</child>
<child>
diff --git a/data/resources/ui/content-message-row.ui b/data/resources/ui/content-message-row.ui
index 87a7e300..bba2bfbf 100644
--- a/data/resources/ui/content-message-row.ui
+++ b/data/resources/ui/content-message-row.ui
@@ -5,11 +5,9 @@
<object class="GtkBox">
<property name="spacing">10</property>
<child>
- <object class="AdwAvatar" id="avatar">
- <property name="show-initials">True</property>
+ <object class="ComponentsAvatar" id="avatar">
<property name="size">36</property>
<property name="valign">start</property>
- <property name="text" bind-source="display_name" bind-property="label" bind-flags="sync-create"/>
</object>
</child>
<child>
@@ -55,3 +53,4 @@
</child>
</template>
</interface>
+
diff --git a/data/resources/ui/pill.ui b/data/resources/ui/pill.ui
index 8a51ec22..0f57182a 100644
--- a/data/resources/ui/pill.ui
+++ b/data/resources/ui/pill.ui
@@ -10,10 +10,8 @@
<object class="GtkBox">
<property name="spacing">6</property>
<child>
- <object class="AdwAvatar" id="avatar">
- <property name="show-initials">True</property>
+ <object class="ComponentsAvatar" id="avatar">
<property name="size">24</property>
- <property name="text" bind-source="display_name" bind-property="label" bind-flags="sync-create"
/>
</object>
</child>
<child>
diff --git a/data/resources/ui/sidebar-room-row.ui b/data/resources/ui/sidebar-room-row.ui
index d5060291..68410ddb 100644
--- a/data/resources/ui/sidebar-room-row.ui
+++ b/data/resources/ui/sidebar-room-row.ui
@@ -5,10 +5,8 @@
<object class="GtkBox">
<property name="spacing">12</property>
<child>
- <object class="AdwAvatar" id="avatar">
- <property name="show-initials">True</property>
+ <object class="ComponentsAvatar" id="avatar">
<property name="size">24</property>
- <property name="text" bind-source="display_name" bind-property="label" bind-flags="sync-create"
/>
</object>
</child>
<child>
@@ -31,3 +29,4 @@
</child>
</template>
</interface>
+
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 2d43d112..28cd433e 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -5,6 +5,7 @@ data/org.gnome.FractalNext.gschema.xml.in
data/org.gnome.FractalNext.metainfo.xml.in.in
# UI files
+data/resources/ui/components-avatar.ui
data/resources/ui/content-divider-row.ui
data/resources/ui/content-item-row-menu.ui
data/resources/ui/content-item.ui
@@ -30,6 +31,7 @@ data/resources/ui/window.ui
# Rust files
src/application.rs
+src/components/avatar.rs
src/components/context_menu_bin.rs
src/components/label_with_widgets.rs
src/components/in_app_notification.rs
diff --git a/src/components/avatar.rs b/src/components/avatar.rs
new file mode 100644
index 00000000..8a4f78af
--- /dev/null
+++ b/src/components/avatar.rs
@@ -0,0 +1,155 @@
+use adw::subclass::prelude::*;
+use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate};
+
+use crate::session::{Room, User};
+
+mod imp {
+ use super::*;
+ use glib::subclass::InitializingObject;
+ use std::cell::RefCell;
+
+ #[derive(Debug, Default, CompositeTemplate)]
+ #[template(resource = "/org/gnome/FractalNext/components-avatar.ui")]
+ pub struct Avatar {
+ /// A `Room` or `User`
+ pub item: RefCell<Option<glib::Object>>,
+ #[template_child]
+ pub avatar: TemplateChild<adw::Avatar>,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for Avatar {
+ const NAME: &'static str = "ComponentsAvatar";
+ type Type = super::Avatar;
+ type ParentType = adw::Bin;
+
+ fn class_init(klass: &mut Self::Class) {
+ Self::bind_template(klass);
+ }
+
+ fn instance_init(obj: &InitializingObject<Self>) {
+ obj.init_template();
+ }
+ }
+
+ impl ObjectImpl for Avatar {
+ fn properties() -> &'static [glib::ParamSpec] {
+ use once_cell::sync::Lazy;
+ static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+ vec![
+ glib::ParamSpec::new_object(
+ "item",
+ "Item",
+ "The Room or User of this Avatar",
+ glib::Object::static_type(),
+ glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+ ),
+ glib::ParamSpec::new_int(
+ "size",
+ "Size",
+ "The size of the Avatar",
+ -1,
+ i32::MAX,
+ -1,
+ glib::ParamFlags::READWRITE,
+ ),
+ ]
+ });
+
+ PROPERTIES.as_ref()
+ }
+
+ fn set_property(
+ &self,
+ obj: &Self::Type,
+ _id: usize,
+ value: &glib::Value,
+ pspec: &glib::ParamSpec,
+ ) {
+ match pspec.name() {
+ "item" => obj.set_item(value.get().unwrap()),
+ "size" => self.avatar.set_size(value.get().unwrap()),
+ _ => unimplemented!(),
+ }
+ }
+
+ fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ match pspec.name() {
+ "item" => obj.item().to_value(),
+ "size" => self.avatar.size().to_value(),
+ _ => unimplemented!(),
+ }
+ }
+
+ fn constructed(&self, obj: &Self::Type) {
+ self.parent_constructed(obj);
+ obj.connect_map(clone!(@weak obj => move |_| {
+ obj.request_custom_avatar();
+ }));
+ }
+ }
+
+ impl WidgetImpl for Avatar {}
+
+ impl BinImpl for Avatar {}
+}
+
+glib::wrapper! {
+ pub struct Avatar(ObjectSubclass<imp::Avatar>)
+ @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
+}
+
+/// A widget displaying an `Avatar` for a `Room` or `User`
+impl Avatar {
+ pub fn new() -> Self {
+ glib::Object::new(&[]).expect("Failed to create Avatar")
+ }
+
+ pub fn set_room(&self, room: Option<Room>) {
+ self.set_item(room.map(glib::object::Cast::upcast));
+ }
+
+ pub fn room(&self) -> Option<Room> {
+ self.item().and_then(|item| item.downcast().ok())
+ }
+
+ pub fn set_user(&self, user: Option<User>) {
+ self.set_item(user.map(glib::object::Cast::upcast));
+ }
+
+ pub fn user(&self) -> Option<User> {
+ self.item().and_then(|item| item.downcast().ok())
+ }
+
+ fn set_item(&self, item: Option<glib::Object>) {
+ let priv_ = imp::Avatar::from_instance(self);
+
+ if *priv_.item.borrow() == item {
+ return;
+ }
+
+ priv_.item.replace(item);
+
+ if self.is_mapped() {
+ self.request_custom_avatar();
+ }
+
+ self.notify("item");
+ }
+
+ fn item(&self) -> Option<glib::Object> {
+ let priv_ = imp::Avatar::from_instance(self);
+ priv_.item.borrow().clone()
+ }
+
+ fn request_custom_avatar(&self) {
+ let priv_ = imp::Avatar::from_instance(self);
+ if let Some(item) = &*priv_.item.borrow() {
+ if let Some(room) = item.downcast_ref::<Room>() {
+ room.avatar().set_needed(true);
+ } else if let Some(user) = item.downcast_ref::<User>() {
+ user.avatar().set_needed(true);
+ }
+ }
+ }
+}
diff --git a/src/components/mod.rs b/src/components/mod.rs
index bf600ffe..7296e89d 100644
--- a/src/components/mod.rs
+++ b/src/components/mod.rs
@@ -1,9 +1,11 @@
+mod avatar;
mod context_menu_bin;
mod in_app_notification;
mod label_with_widgets;
mod pill;
mod spinner_button;
+pub use self::avatar::Avatar;
pub use self::context_menu_bin::{ContextMenuBin, ContextMenuBinExt, ContextMenuBinImpl};
pub use self::in_app_notification::InAppNotification;
pub use self::label_with_widgets::LabelWithWidgets;
diff --git a/src/components/pill.rs b/src/components/pill.rs
index 7ff1668c..be2333f9 100644
--- a/src/components/pill.rs
+++ b/src/components/pill.rs
@@ -1,3 +1,4 @@
+use crate::components::Avatar;
use adw::subclass::prelude::*;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
@@ -20,7 +21,7 @@ mod imp {
#[template_child]
pub display_name: TemplateChild<gtk::Label>,
#[template_child]
- pub avatar: TemplateChild<adw::Avatar>,
+ pub avatar: TemplateChild<Avatar>,
pub bindings: RefCell<Vec<glib::Binding>>,
}
@@ -124,6 +125,7 @@ impl Pill {
priv_.bindings.borrow_mut().push(display_name_binding);
}
+ priv_.avatar.set_user(user.clone());
priv_.user.replace(user);
self.notify("user");
@@ -155,6 +157,7 @@ impl Pill {
priv_.bindings.borrow_mut().push(display_name_binding);
}
+ priv_.avatar.set_room(room.clone());
priv_.room.replace(room);
self.notify("room");
diff --git a/src/meson.build b/src/meson.build
index bfd51e20..779b0324 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -20,6 +20,7 @@ run_command(
sources = files(
'application.rs',
+ 'components/avatar.rs',
'components/context_menu_bin.rs',
'components/label_with_widgets.rs',
'components/mod.rs',
@@ -33,6 +34,7 @@ sources = files(
'login.rs',
'secret.rs',
'utils.rs',
+ 'session/avatar.rs',
'session/event_source_dialog.rs',
'session/user.rs',
'session/mod.rs',
diff --git a/src/session/avatar.rs b/src/session/avatar.rs
new file mode 100644
index 00000000..0cd91962
--- /dev/null
+++ b/src/session/avatar.rs
@@ -0,0 +1,211 @@
+use gtk::{gdk, gdk_pixbuf::Pixbuf, gio, glib, glib::clone, prelude::*, subclass::prelude::*};
+
+use log::error;
+use matrix_sdk::{
+ identifiers::MxcUri,
+ media::{MediaFormat, MediaRequest, MediaType},
+};
+
+use crate::utils::do_async;
+
+use crate::session::Session;
+
+mod imp {
+ use super::*;
+ use once_cell::sync::{Lazy, OnceCell};
+ use std::cell::{Cell, RefCell};
+
+ #[derive(Debug, Default)]
+ pub struct Avatar {
+ pub image: RefCell<Option<gdk::Paintable>>,
+ pub needed: Cell<bool>,
+ pub url: RefCell<Option<MxcUri>>,
+ pub session: OnceCell<Session>,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for Avatar {
+ const NAME: &'static str = "Avatar";
+ type Type = super::Avatar;
+ type ParentType = glib::Object;
+ }
+
+ impl ObjectImpl for Avatar {
+ fn properties() -> &'static [glib::ParamSpec] {
+ static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+ vec![
+ glib::ParamSpec::new_object(
+ "image",
+ "Image",
+ "The user defined image if any",
+ gdk::Paintable::static_type(),
+ glib::ParamFlags::READABLE | glib::ParamFlags::EXPLICIT_NOTIFY,
+ ),
+ glib::ParamSpec::new_object(
+ "needed",
+ "Needed",
+ "Whether the user defnied image should be loaded or it's not needed",
+ gdk::Paintable::static_type(),
+ glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+ ),
+ glib::ParamSpec::new_string(
+ "url",
+ "Url",
+ "The url of the Avatar",
+ None,
+ glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+ ),
+ glib::ParamSpec::new_object(
+ "session",
+ "Session",
+ "The session",
+ Session::static_type(),
+ glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
+ ),
+ ]
+ });
+
+ PROPERTIES.as_ref()
+ }
+
+ fn set_property(
+ &self,
+ obj: &Self::Type,
+ _id: usize,
+ value: &glib::Value,
+ pspec: &glib::ParamSpec,
+ ) {
+ match pspec.name() {
+ "needed" => obj.set_needed(value.get().unwrap()),
+ "url" => obj.set_url(value.get::<Option<&str>>().unwrap().map(Into::into)),
+ "session" => self.session.set(value.get().unwrap()).unwrap(),
+ _ => unimplemented!(),
+ }
+ }
+
+ fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ match pspec.name() {
+ "image" => obj.image().to_value(),
+ "needed" => obj.needed().to_value(),
+ "url" => obj.url().map_or_else(
+ || {
+ let none: Option<&str> = None;
+ none.to_value()
+ },
+ |url| url.as_str().to_value(),
+ ),
+ _ => unimplemented!(),
+ }
+ }
+ }
+}
+
+glib::wrapper! {
+ pub struct Avatar(ObjectSubclass<imp::Avatar>);
+}
+
+/// This an object that holds information about a Users or Rooms `Avatar`
+impl Avatar {
+ pub fn new(session: &Session, url: Option<MxcUri>) -> Self {
+ glib::Object::new(&[
+ ("session", session),
+ ("url", &url.map(|url| url.to_string())),
+ ])
+ .expect("Failed to create Avatar")
+ }
+
+ fn session(&self) -> &Session {
+ let priv_ = imp::Avatar::from_instance(self);
+ priv_.session.get().unwrap()
+ }
+
+ pub fn image(&self) -> Option<gdk::Paintable> {
+ let priv_ = imp::Avatar::from_instance(self);
+ priv_.image.borrow().clone()
+ }
+
+ fn set_image_data(&self, data: Option<Vec<u8>>) {
+ let priv_ = imp::Avatar::from_instance(self);
+
+ let image = if let Some(data) = data {
+ let stream = gio::MemoryInputStream::from_bytes(&glib::Bytes::from(&data));
+ Pixbuf::from_stream(&stream, gio::NONE_CANCELLABLE)
+ .ok()
+ .and_then(|pixbuf| Some(gdk::Texture::for_pixbuf(&pixbuf).upcast()))
+ } else {
+ None
+ };
+ priv_.image.replace(image);
+ self.notify("image");
+ }
+
+ fn load(&self) {
+ // Don't do anything here if we don't need the avatar
+ if !self.needed() {
+ return;
+ }
+
+ if let Some(url) = self.url() {
+ let client = self.session().client().clone();
+ let request = MediaRequest {
+ media_type: MediaType::Uri(url),
+ format: MediaFormat::File,
+ };
+ do_async(
+ glib::PRIORITY_LOW,
+ async move { client.get_media_content(&request, true).await },
+ clone!(@weak self as obj => move |result| async move {
+ // FIXME: We should retry if the request failed
+ match result {
+ Ok(data) => obj.set_image_data(Some(data)),
+ Err(error) => error!("Couldn't fetch avatar: {}", error),
+ };
+ }),
+ );
+ }
+ }
+
+ pub fn set_needed(&self, needed: bool) {
+ let priv_ = imp::Avatar::from_instance(self);
+ if self.needed() == needed {
+ return;
+ }
+
+ priv_.needed.set(needed);
+
+ if needed {
+ self.load();
+ }
+
+ self.notify("needed");
+ }
+
+ pub fn needed(&self) -> bool {
+ let priv_ = imp::Avatar::from_instance(self);
+ priv_.needed.get()
+ }
+
+ pub fn set_url(&self, url: Option<MxcUri>) {
+ let priv_ = imp::Avatar::from_instance(self);
+
+ if priv_.url.borrow().as_ref() == url.as_ref() {
+ return;
+ }
+
+ let has_url = url.is_some();
+ priv_.url.replace(url);
+
+ if has_url {
+ self.load();
+ } else {
+ self.set_image_data(None);
+ }
+
+ self.notify("url");
+ }
+
+ pub fn url(&self) -> Option<MxcUri> {
+ let priv_ = imp::Avatar::from_instance(self);
+ priv_.url.borrow().to_owned()
+ }
+}
diff --git a/src/session/content/invite.rs b/src/session/content/invite.rs
index 557aff33..2d967d38 100644
--- a/src/session/content/invite.rs
+++ b/src/session/content/invite.rs
@@ -1,5 +1,5 @@
use crate::{
- components::{LabelWithWidgets, Pill, SpinnerButton},
+ components::{Avatar, LabelWithWidgets, Pill, SpinnerButton},
session::{categories::CategoryType, room::Room},
};
use adw::subclass::prelude::*;
@@ -40,6 +40,7 @@ mod imp {
Pill::static_type();
SpinnerButton::static_type();
LabelWithWidgets::static_type();
+ Avatar::static_type();
Self::bind_template(klass);
klass.set_accessible_role(gtk::AccessibleRole::Group);
diff --git a/src/session/content/message_row.rs b/src/session/content/message_row.rs
index 82b41b42..1a4c0c62 100644
--- a/src/session/content/message_row.rs
+++ b/src/session/content/message_row.rs
@@ -1,3 +1,4 @@
+use crate::components::Avatar;
use adw::{prelude::*, subclass::prelude::*};
use gtk::{
gio, glib, glib::clone, glib::signal::SignalHandlerId, prelude::*, subclass::prelude::*,
@@ -28,7 +29,7 @@ mod imp {
#[template(resource = "/org/gnome/FractalNext/content-message-row.ui")]
pub struct MessageRow {
#[template_child]
- pub avatar: TemplateChild<adw::Avatar>,
+ pub avatar: TemplateChild<Avatar>,
#[template_child]
pub header: TemplateChild<gtk::Box>,
#[template_child]
@@ -49,6 +50,7 @@ mod imp {
type ParentType = adw::Bin;
fn class_init(klass: &mut Self::Class) {
+ Avatar::static_type();
Self::bind_template(klass);
}
@@ -147,7 +149,8 @@ impl MessageRow {
}
}
- //TODO: bind the user's avatar to the message row
+ priv_.avatar.set_user(Some(event.sender().clone()));
+
let display_name_binding = event
.sender()
.bind_property("display-name", &priv_.display_name.get(), "label")
diff --git a/src/session/mod.rs b/src/session/mod.rs
index e1906d14..72c25473 100644
--- a/src/session/mod.rs
+++ b/src/session/mod.rs
@@ -1,3 +1,4 @@
+mod avatar;
mod categories;
mod content;
mod event_source_dialog;
@@ -6,6 +7,7 @@ mod room_list;
mod sidebar;
mod user;
+pub use self::avatar::Avatar;
use self::categories::Categories;
use self::content::Content;
pub use self::room::Room;
diff --git a/src/session/room/room.rs b/src/session/room/room.rs
index c99b38b5..05d2da4e 100644
--- a/src/session/room/room.rs
+++ b/src/session/room/room.rs
@@ -1,5 +1,5 @@
use gettextrs::gettext;
-use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*};
+use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*};
use log::{debug, error, warn};
use matrix_sdk::{
api::r0::sync::sync_events::InvitedRoom,
@@ -29,7 +29,7 @@ use crate::event_from_sync_event;
use crate::session::{
categories::CategoryType,
room::{HighlightFlags, Timeline},
- Session, User,
+ Avatar, Session, User,
};
use crate::utils::do_async;
use crate::Error;
@@ -47,7 +47,7 @@ mod imp {
pub matrix_room: RefCell<Option<MatrixRoom>>,
pub session: OnceCell<Session>,
pub name: RefCell<Option<String>>,
- pub avatar: RefCell<Option<gio::LoadableIcon>>,
+ pub avatar: OnceCell<Avatar>,
pub category: Cell<CategoryType>,
pub timeline: OnceCell<Timeline>,
pub room_members: RefCell<HashMap<UserId, User>>,
@@ -97,8 +97,8 @@ mod imp {
glib::ParamSpec::new_object(
"avatar",
"Avatar",
- "The url of the avatar of this room",
- gio::LoadableIcon::static_type(),
+ "The Avatar of this room",
+ Avatar::static_type(),
glib::ParamFlags::READABLE,
),
glib::ParamSpec::new_object(
@@ -161,7 +161,7 @@ mod imp {
}
"room-id" => self
.room_id
- .set(RoomId::try_from(value.get::<String>().unwrap()).unwrap())
+ .set(RoomId::try_from(value.get::<&str>().unwrap()).unwrap())
.unwrap(),
_ => unimplemented!(),
}
@@ -175,7 +175,7 @@ mod imp {
"session" => obj.session().to_value(),
"inviter" => obj.inviter().to_value(),
"display-name" => obj.display_name().to_value(),
- "avatar" => self.avatar.borrow().to_value(),
+ "avatar" => obj.avatar().to_value(),
"timeline" => self.timeline.get().unwrap().to_value(),
"category" => obj.category().to_value(),
"highlight" => obj.highlight().to_value(),
@@ -200,6 +200,9 @@ mod imp {
obj.set_matrix_room(obj.session().client().get_room(obj.room_id()).unwrap());
self.timeline.set(Timeline::new(obj)).unwrap();
+ self.avatar
+ .set(Avatar::new(obj.session(), obj.matrix_room().avatar_url()))
+ .unwrap();
}
}
}
@@ -461,6 +464,11 @@ impl Room {
);
}
+ pub fn avatar(&self) -> &Avatar {
+ let priv_ = imp::Room::from_instance(&self);
+ priv_.avatar.get().unwrap()
+ }
+
pub fn topic(&self) -> Option<String> {
self.matrix_room()
.topic()
@@ -533,6 +541,9 @@ impl Room {
AnyRoomEvent::State(AnyStateEvent::RoomMember(ref event)) => {
self.update_member_for_member_event(event)
}
+ AnyRoomEvent::State(AnyStateEvent::RoomAvatar(event)) => {
+ self.avatar().set_url(event.content.url.to_owned());
+ }
AnyRoomEvent::State(AnyStateEvent::RoomName(_)) => {
// FIXME: this doesn't take in account changes in the calculated name
self.load_display_name()
diff --git a/src/session/sidebar/room_row.rs b/src/session/sidebar/room_row.rs
index 6183564c..d592189e 100644
--- a/src/session/sidebar/room_row.rs
+++ b/src/session/sidebar/room_row.rs
@@ -1,3 +1,4 @@
+use crate::components::Avatar;
use adw::subclass::prelude::BinImpl;
use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate};
@@ -16,7 +17,7 @@ mod imp {
pub bindings: RefCell<Vec<glib::Binding>>,
pub signal_handler: RefCell<Option<SignalHandlerId>>,
#[template_child]
- pub avatar: TemplateChild<adw::Avatar>,
+ pub avatar: TemplateChild<Avatar>,
#[template_child]
pub display_name: TemplateChild<gtk::Label>,
#[template_child]
@@ -30,6 +31,7 @@ mod imp {
type ParentType = adw::Bin;
fn class_init(klass: &mut Self::Class) {
+ Avatar::static_type();
Self::bind_template(klass);
}
@@ -115,8 +117,6 @@ impl RoomRow {
}
if let Some(ref room) = room {
- // TODO: set custom avatar https://gitlab.gnome.org/exalm/libadwaita/-/issues/29
-
let display_name_binding = room
.bind_property("display-name", &priv_.display_name.get(), "label")
.flags(glib::BindingFlags::SYNC_CREATE)
@@ -158,7 +158,7 @@ impl RoomRow {
notification_count_vislbe_binding,
]);
}
-
+ priv_.avatar.set_room(room.clone());
priv_.room.replace(room);
self.notify("room");
}
diff --git a/src/session/user.rs b/src/session/user.rs
index 2cc4af30..82cfedcc 100644
--- a/src/session/user.rs
+++ b/src/session/user.rs
@@ -1,4 +1,4 @@
-use gtk::{gio, glib, prelude::*, subclass::prelude::*};
+use gtk::{glib, prelude::*, subclass::prelude::*};
use crate::session::Session;
use matrix_sdk::{
@@ -7,6 +7,8 @@ use matrix_sdk::{
RoomMember,
};
+use crate::session::Avatar;
+
mod imp {
use super::*;
use once_cell::sync::{Lazy, OnceCell};
@@ -16,8 +18,8 @@ mod imp {
pub struct User {
pub user_id: OnceCell<String>,
pub display_name: RefCell<Option<String>>,
- pub avatar: RefCell<Option<gio::LoadableIcon>>,
pub session: OnceCell<Session>,
+ pub avatar: OnceCell<Avatar>,
}
#[glib::object_subclass]
@@ -49,7 +51,7 @@ mod imp {
"avatar",
"Avatar",
"The avatar of this user",
- gio::LoadableIcon::static_type(),
+ Avatar::static_type(),
glib::ParamFlags::READABLE,
),
glib::ParamSpec::new_object(
@@ -86,11 +88,18 @@ mod imp {
match pspec.name() {
"display-name" => obj.display_name().to_value(),
"user-id" => self.user_id.get().to_value(),
- "avatar" => self.avatar.borrow().to_value(),
"session" => obj.session().to_value(),
+ "avatar" => obj.avatar().to_value(),
_ => unimplemented!(),
}
}
+
+ fn constructed(&self, obj: &Self::Type) {
+ self.parent_constructed(obj);
+
+ let avatar = Avatar::new(obj.session(), None);
+ self.avatar.set(avatar).unwrap();
+ }
}
}
@@ -131,11 +140,16 @@ impl User {
}
}
+ pub fn avatar(&self) -> &Avatar {
+ let priv_ = imp::User::from_instance(&self);
+ priv_.avatar.get().unwrap()
+ }
+
/// Update the user based on the the room member state event
- //TODO: create the GLoadableIcon and set `avatar`
pub fn update_from_room_member(&self, member: &RoomMember) {
let changed = {
let priv_ = imp::User::from_instance(&self);
+
let user_id = priv_.user_id.get().unwrap();
if member.user_id().as_str() != user_id {
return;
@@ -143,6 +157,7 @@ impl User {
//let content = event.content;
let display_name = member.display_name().map(|name| name.to_owned());
+ self.avatar().set_url(member.avatar_url().cloned());
let mut current_display_name = priv_.display_name.borrow_mut();
if *current_display_name != display_name {
@@ -159,7 +174,6 @@ impl User {
}
/// Update the user based on the the room member state event
- //TODO: create the GLoadableIcon and set `avatar`
pub fn update_from_member_event(&self, event: &StateEvent<MemberEventContent>) {
let changed = {
let priv_ = imp::User::from_instance(&self);
@@ -178,6 +192,8 @@ impl User {
.map(|i| i.display_name.to_owned())
};
+ self.avatar().set_url(event.content.avatar_url.to_owned());
+
let mut current_display_name = priv_.display_name.borrow_mut();
if *current_display_name != display_name {
*current_display_name = display_name;
@@ -193,7 +209,6 @@ impl User {
}
/// Update the user based on the the stripped room member state event
- //TODO: create the GLoadableIcon and set `avatar`
pub fn update_from_stripped_member_event(
&self,
event: &StrippedStateEvent<MemberEventContent>,
@@ -215,6 +230,8 @@ impl User {
.map(|i| i.display_name.to_owned())
};
+ self.avatar().set_url(event.content.avatar_url.to_owned());
+
let mut current_display_name = priv_.display_name.borrow_mut();
if *current_display_name != display_name {
*current_display_name = display_name;
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]