[fractal/fractal-next] room_details: Enable editing of room avatar
- From: Julian Sparber <jsparber src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [fractal/fractal-next] room_details: Enable editing of room avatar
- Date: Mon, 23 Aug 2021 15:02:20 +0000 (UTC)
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) -> >k::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]