[fractal/fractal-next] room_details: Enable editing of room avatar



commit a20259ddec98d02895ce386b59f34c9b9b55f6e5
Author: Kai A. Hiller <V02460 gmail com>
Date:   Wed Aug 4 17:07:45 2021 +0200

    room_details: Enable editing of room avatar

 Cargo.lock                                       |  1 +
 Cargo.toml                                       |  1 +
 data/resources/style.css                         |  2 +-
 data/resources/ui/content-room-details.ui        | 41 ++++++++--
 src/session/avatar.rs                            | 96 +++++++++++++++++++++++-
 src/session/content/room_details/room_details.rs | 42 +++++++++++
 src/session/room/room.rs                         | 21 ++++++
 7 files changed, 195 insertions(+), 9 deletions(-)
---
diff --git a/Cargo.lock b/Cargo.lock
index 802c2683..e44e18aa 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -729,6 +729,7 @@ dependencies = [
  "libadwaita",
  "log",
  "matrix-sdk",
+ "mime",
  "once_cell",
  "rand 0.8.4",
  "secret-service",
diff --git a/Cargo.toml b/Cargo.toml
index 61ef04d1..3016c8cd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,6 +9,7 @@ opt-level = 3
 
 [dependencies]
 log = "0.4"
+mime = "0.3.16"
 tracing-subscriber = "0.2"
 gettext-rs = { version = "0.7", features = ["gettext-system"] }
 gtk-macros = "0.3"
diff --git a/data/resources/style.css b/data/resources/style.css
index 4f865da4..18ddec77 100644
--- a/data/resources/style.css
+++ b/data/resources/style.css
@@ -1,4 +1,4 @@
-.room-details-group avatar {
+.room-details-group overlay {
   margin-bottom: 6px;
 }
 
diff --git a/data/resources/ui/content-room-details.ui b/data/resources/ui/content-room-details.ui
index 20cda5d0..251936ad 100644
--- a/data/resources/ui/content-room-details.ui
+++ b/data/resources/ui/content-room-details.ui
@@ -14,13 +14,40 @@
               <class name="room-details-group" />
             </style>
             <child>
-              <object class="ComponentsAvatar">
-                <property name="size">128</property>
-                <binding name="item">
-                  <lookup name="avatar">
-                    <lookup name="room">RoomDetails</lookup>
-                  </lookup>
-                </binding>
+              <object class="GtkOverlay">
+                <property name="halign">center</property>
+                <child>
+                  <object class="ComponentsAvatar">
+                    <property name="size">128</property>
+                    <binding name="item">
+                      <lookup name="avatar">
+                        <lookup name="room">RoomDetails</lookup>
+                      </lookup>
+                    </binding>
+                  </object>
+                </child>
+                <child type="overlay">
+                  <object class="GtkButton">
+                    <property name="icon-name">window-close-symbolic</property>
+                    <property name="action-name">details.remove-avatar</property>
+                    <property name="halign">end</property>
+                    <property name="valign">start</property>
+                    <style>
+                      <class name="circular" />
+                    </style>
+                  </object>
+                </child>
+                <child type="overlay">
+                  <object class="GtkButton">
+                    <property name="icon-name">document-edit-symbolic</property>
+                    <property name="action-name">details.choose-avatar</property>
+                    <property name="halign">end</property>
+                    <property name="valign">end</property>
+                    <style>
+                      <class name="circular" />
+                    </style>
+                  </object>
+                </child>
               </object>
             </child>
             <child>
diff --git a/src/session/avatar.rs b/src/session/avatar.rs
index 1a5198b1..2badc400 100644
--- a/src/session/avatar.rs
+++ b/src/session/avatar.rs
@@ -1,6 +1,12 @@
+use std::path::Path;
+
 use gtk::{gdk, gdk_pixbuf::Pixbuf, gio, glib, glib::clone, prelude::*, subclass::prelude::*};
 
-use log::error;
+use log::{debug, error, info};
+use matrix_sdk::room::Room as MatrixRoom;
+use matrix_sdk::ruma::events::room::avatar::AvatarEventContent;
+use matrix_sdk::ruma::events::AnyStateEventContent;
+use matrix_sdk::Client;
 use matrix_sdk::{
     media::{MediaFormat, MediaRequest, MediaType},
     ruma::identifiers::MxcUri,
@@ -237,3 +243,91 @@ impl Avatar {
         priv_.url.borrow().to_owned()
     }
 }
+
+/// Uploads the given file and sets the room avatar.
+///
+/// Removes the avatar if `filename` is None.
+pub async fn update_room_avatar_from_file<P>(
+    matrix_client: &Client,
+    matrix_room: &MatrixRoom,
+    filename: Option<&P>,
+) -> Result<Option<MxcUri>, AvatarError>
+where
+    P: AsRef<Path> + std::fmt::Debug,
+{
+    let joined_room = match matrix_room {
+        MatrixRoom::Joined(joined_room) => joined_room,
+        _ => return Err(AvatarError::NotAMember),
+    };
+
+    let mut content = AvatarEventContent::new();
+
+    let uri = if let Some(filename) = filename {
+        Some(upload_avatar(matrix_client, filename).await?)
+    } else {
+        debug!("Removing room avatar");
+        None
+    };
+    content.url = uri.clone();
+
+    joined_room
+        .send_state_event(AnyStateEventContent::RoomAvatar(content), "")
+        .await?;
+    Ok(uri)
+}
+
+/// Returns the URI of the room avatar after uploading it.
+async fn upload_avatar<P>(matrix_client: &Client, filename: &P) -> Result<MxcUri, AvatarError>
+where
+    P: AsRef<Path> + std::fmt::Debug,
+{
+    debug!("Getting mime type of file {:?}", filename);
+    let image = tokio::fs::read(filename).await?;
+    let content_type = gio::content_type_guess(None, &image).0.to_string();
+
+    info!("Uploading avatar from file {:?}", filename);
+    // TODO: Use blurhash
+    let response = matrix_client
+        .upload(&content_type.parse()?, &mut image.as_slice())
+        .await?;
+    Ok(response.content_uri)
+}
+
+/// Error occuring when updating an avatar.
+#[derive(Debug)]
+pub enum AvatarError {
+    Filesystem(std::io::Error),
+    Upload(matrix_sdk::Error),
+    NotAMember,
+    UnknownFiletype(mime::FromStrError),
+}
+
+impl std::fmt::Display for AvatarError {
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+        use AvatarError::*;
+        match self {
+            Filesystem(e) => write!(f, "Could not open room avatar file: {}", e),
+            Upload(e) => write!(f, "Could not upload room avatar: {}", e),
+            NotAMember => write!(f, "Room avatar can’t be changed when not a member."),
+            UnknownFiletype(e) => write!(f, "Room avatar file has an unknown filetype: {}", e),
+        }
+    }
+}
+
+impl From<std::io::Error> for AvatarError {
+    fn from(err: std::io::Error) -> Self {
+        Self::Filesystem(err)
+    }
+}
+
+impl From<matrix_sdk::Error> for AvatarError {
+    fn from(err: matrix_sdk::Error) -> Self {
+        Self::Upload(err)
+    }
+}
+
+impl From<mime::FromStrError> for AvatarError {
+    fn from(err: mime::FromStrError) -> Self {
+        Self::UnknownFiletype(err)
+    }
+}
diff --git a/src/session/content/room_details/room_details.rs 
b/src/session/content/room_details/room_details.rs
index 10b3b313..4f2b0504 100644
--- a/src/session/content/room_details/room_details.rs
+++ b/src/session/content/room_details/room_details.rs
@@ -19,6 +19,7 @@ mod imp {
     #[template(resource = "/org/gnome/FractalNext/content-room-details.ui")]
     pub struct RoomDetails {
         pub room: OnceCell<Room>,
+        pub avatar_chooser: OnceCell<gtk::FileChooserNative>,
         #[template_child]
         pub edit_toggle: TemplateChild<gtk::ToggleButton>,
         #[template_child]
@@ -36,6 +37,12 @@ mod imp {
         fn class_init(klass: &mut Self::Class) {
             CustomEntry::static_type();
             Self::bind_template(klass);
+            klass.install_action("details.choose-avatar", None, move |widget, _, _| {
+                widget.open_avatar_chooser()
+            });
+            klass.install_action("details.remove-avatar", None, move |widget, _, _| {
+                widget.room().store_avatar(None)
+            });
         }
 
         fn instance_init(obj: &InitializingObject<Self>) {
@@ -83,6 +90,7 @@ mod imp {
             self.parent_constructed(obj);
 
             obj.init_edit_toggle();
+            obj.init_avatar_chooser();
         }
     }
 
@@ -150,4 +158,38 @@ impl RoomDetails {
                 priv_.edit_toggle.set_active(false);
             }));
     }
+
+    fn init_avatar_chooser(&self) {
+        let priv_ = imp::RoomDetails::from_instance(self);
+        let avatar_chooser = gtk::FileChooserNative::new(
+            Some(&gettext("Choose avatar")),
+            Some(self),
+            gtk::FileChooserAction::Open,
+            None,
+            None,
+        );
+        avatar_chooser.connect_response(clone!(@weak self as this => move |chooser, response| {
+            let file = chooser.file().and_then(|f| f.path());
+            if let (gtk::ResponseType::Accept, Some(file)) = (response, file) {
+                log::debug!("Chose file {:?}", file);
+                this.room().store_avatar(Some(file));
+            }
+        }));
+
+        // We must keep a reference to FileChooserNative around as it is not
+        // managed by GTK.
+        priv_
+            .avatar_chooser
+            .set(avatar_chooser)
+            .expect("File chooser already initialized");
+    }
+
+    fn avatar_chooser(&self) -> &gtk::FileChooserNative {
+        let priv_ = imp::RoomDetails::from_instance(self);
+        priv_.avatar_chooser.get().unwrap()
+    }
+
+    fn open_avatar_chooser(&self) {
+        self.avatar_chooser().show();
+    }
 }
diff --git a/src/session/room/room.rs b/src/session/room/room.rs
index dda7f3c7..17f4ef81 100644
--- a/src/session/room/room.rs
+++ b/src/session/room/room.rs
@@ -31,9 +31,11 @@ use matrix_sdk::{
 use serde_json::value::RawValue;
 use std::cell::RefCell;
 use std::convert::{TryFrom, TryInto};
+use std::path::PathBuf;
 
 use crate::components::{LabelWithWidgets, Pill};
 use crate::prelude::*;
+use crate::session::avatar::update_room_avatar_from_file;
 use crate::session::room::{
     Event, HighlightFlags, Member, PowerLevels, RoomAction, RoomType, Timeline,
 };
@@ -822,6 +824,25 @@ impl Room {
         self.power_levels().new_allowed_expr(&member, room_action)
     }
 
+    /// Uploads the given file to the server and makes it the room avatar.
+    ///
+    /// Removes the avatar if no filename is given.
+    pub fn store_avatar(&self, filename: Option<PathBuf>) {
+        let matrix_room = self.matrix_room();
+        let client = self.session().client().clone();
+
+        do_async(
+            glib::PRIORITY_DEFAULT_IDLE,
+            async move { update_room_avatar_from_file(&client, &matrix_room, filename.as_ref()).await },
+            clone!(@weak self as this => move |avatar_uri| async move {
+                match avatar_uri {
+                    Ok(_avatar_uri) => info!("Sucessfully updated room avatar"),
+                    Err(error) => error!("Couldn’t update room avatar: {}", error),
+                };
+            }),
+        );
+    }
+
     pub async fn accept_invite(&self) -> Result<(), Error> {
         let matrix_room = self.matrix_room();
 


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]