[fractal/fractal-next] Add support for user-defined avatars



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]