Browse Source

room-details: Use EditableAvatar

merge-requests/1327/merge
Kévin Commaille 3 years ago committed by Kévin Commaille
parent
commit
beb9dd4e91
  1. 21
      data/resources/style.css
  2. 55
      data/resources/ui/content-room-details-general-page.ui
  3. 231
      src/session/content/room_details/general_page/mod.rs
  4. 45
      src/session/room/mod.rs

21
data/resources/style.css

@ -163,6 +163,12 @@ entry .inline-pill {
margin-bottom: -0.5em;
}
.cutout-button {
background-color: @window_bg_color;
border-radius: 9999px;
padding: 2px;
}
/* Login */
@ -582,19 +588,10 @@ typing-bar avatar {
/* Room Details */
.room-details-group overlay {
margin-bottom: 6px;
}
.room-details listview {
background: transparent;
}
.room-details-group avatar * {
/* Undo non-sensitive style. */
filter: none;
}
.room-details-group entry:disabled {
border-color: transparent;
/* Undo non-sensitive style. */
@ -627,12 +624,6 @@ typing-bar avatar {
filter: opacity(1);
}
.cutout-button {
background-color: @window_bg_color;
border-radius: 9999px;
padding: 2px;
}
dragoverlay statuspage {
background-color: alpha(@accent_bg_color, 0.5);
color: @accent_fg_color;

55
data/resources/ui/content-room-details-general-page.ui

@ -22,54 +22,12 @@
<class name="room-details-group"/>
</style>
<child>
<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">ContentRoomDetailsGeneralPage</lookup>
</lookup>
</binding>
</object>
</child>
<child type="overlay">
<object class="AdwBin" id="avatar_remove_button">
<style>
<class name="cutout-button"/>
</style>
<property name="halign">end</property>
<property name="valign">start</property>
<child>
<object class="GtkButton">
<property name="icon-name">user-trash-symbolic</property>
<property name="action-name">details.remove-avatar</property>
<style>
<class name="circular"/>
</style>
</object>
</child>
</object>
</child>
<child type="overlay">
<object class="AdwBin" id="avatar_edit_button">
<style>
<class name="cutout-button"/>
</style>
<property name="halign">end</property>
<property name="valign">end</property>
<child>
<object class="GtkButton">
<property name="icon-name">document-edit-symbolic</property>
<property name="action-name">details.choose-avatar</property>
<style>
<class name="circular"/>
</style>
</object>
</child>
</object>
</child>
<object class="ComponentsEditableAvatar" id="avatar">
<binding name="avatar">
<lookup name="avatar">
<lookup name="room">ContentRoomDetailsGeneralPage</lookup>
</lookup>
</binding>
</object>
</child>
<child>
@ -177,4 +135,3 @@
</property>
</template>
</interface>

231
src/session/content/room_details/general_page/mod.rs

@ -3,21 +3,30 @@ use std::convert::From;
use adw::{prelude::*, subclass::prelude::*};
use gettextrs::gettext;
use gtk::{
gdk,
glib::{self, clone, closure},
gio,
glib::{self, clone},
CompositeTemplate,
};
use log::error;
use matrix_sdk::ruma::events::StateEventType;
use matrix_sdk::room::Room as MatrixRoom;
use ruma::{
assign,
events::{room::avatar::ImageInfo, StateEventType},
OwnedMxcUri,
};
use crate::{
components::CustomEntry,
session::{self, room::RoomAction, Room},
utils::{and_expr, or_expr},
components::{CustomEntry, EditableAvatar},
session::{room::RoomAction, Room},
spawn, spawn_tokio, toast,
utils::{
media::{get_image_info, load_file},
or_expr, OngoingAsyncAction,
},
};
mod imp {
use std::cell::Cell;
use std::cell::{Cell, RefCell};
use glib::subclass::InitializingObject;
use once_cell::unsync::OnceCell;
@ -28,11 +37,8 @@ mod imp {
#[template(resource = "/org/gnome/Fractal/content-room-details-general-page.ui")]
pub struct GeneralPage {
pub room: OnceCell<Room>,
pub avatar_chooser: OnceCell<gtk::FileChooserNative>,
#[template_child]
pub avatar_remove_button: TemplateChild<adw::Bin>,
#[template_child]
pub avatar_edit_button: TemplateChild<adw::Bin>,
pub avatar: TemplateChild<EditableAvatar>,
#[template_child]
pub edit_toggle: TemplateChild<gtk::Button>,
#[template_child]
@ -46,6 +52,7 @@ mod imp {
#[template_child]
pub members_count: TemplateChild<gtk::Label>,
pub edit_mode: Cell<bool>,
pub changing_avatar: RefCell<Option<OngoingAsyncAction<OwnedMxcUri>>>,
}
#[glib::object_subclass]
@ -56,13 +63,6 @@ mod imp {
fn class_init(klass: &mut Self::Class) {
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>) {
@ -135,30 +135,155 @@ impl GeneralPage {
/// Set the room backing all the details of the preference window.
fn set_room(&self, room: Room) {
room.avatar().connect_notify_local(
Some("url"),
clone!(@weak self as obj => move |avatar, _| {
obj.avatar_changed(avatar.url());
}),
);
self.imp().room.set(room).expect("Room already initialized");
}
fn init_avatar(&self) {
let imp = self.imp();
let avatar_remove_button = &imp.avatar_remove_button;
let avatar_edit_button = &imp.avatar_edit_button;
let avatar = &*self.imp().avatar;
avatar.connect_edit_avatar(clone!(@weak self as obj => move |_, file| {
spawn!(
clone!(@weak obj => async move {
obj.change_avatar(file).await;
})
);
}));
avatar.connect_remove_avatar(clone!(@weak self as obj => move |_| {
spawn!(
clone!(@weak obj => async move {
obj.remove_avatar().await;
})
);
}));
// Hide avatar controls when the user is not eligible to perform the actions.
let room = self.room();
let room_avatar_exists = room
.property_expression("avatar")
.chain_property::<session::Avatar>("image")
.chain_closure::<bool>(closure!(
|_: Option<glib::Object>, image: Option<gdk::Paintable>| { image.is_some() }
));
let room_avatar_changeable =
room.new_allowed_expr(RoomAction::StateEvent(StateEventType::RoomAvatar));
let room_avatar_removable = and_expr(&room_avatar_changeable, &room_avatar_exists);
room_avatar_removable.bind(&avatar_remove_button.get(), "visible", gtk::Widget::NONE);
room_avatar_changeable.bind(&avatar_edit_button.get(), "visible", gtk::Widget::NONE);
room_avatar_changeable.bind(avatar, "editable", gtk::Widget::NONE);
}
fn avatar_changed(&self, uri: Option<OwnedMxcUri>) {
let imp = self.imp();
if let Some(action) = imp.changing_avatar.borrow().as_ref() {
if uri.as_ref() != action.as_value() {
// This is not the change we expected, maybe another device did a change too.
// Let's wait for another change.
return;
}
} else {
// No action is ongoing, we don't need to do anything.
return;
};
// Reset the state.
imp.changing_avatar.take();
imp.avatar.success();
if uri.is_none() {
toast!(self, gettext("Avatar removed successfully"));
} else {
toast!(self, gettext("Avatar changed successfully"));
}
}
async fn change_avatar(&self, file: gio::File) {
let room = self.room();
let MatrixRoom::Joined(matrix_room) = room.matrix_room() else {
error!("Cannot change avatar of room not joined");
return;
};
let imp = self.imp();
let avatar = &imp.avatar;
avatar.edit_in_progress();
let (data, info) = match load_file(&file).await {
Ok(res) => res,
Err(error) => {
error!("Could not load room avatar file: {error}");
toast!(self, gettext("Could not load file"));
avatar.reset();
return;
}
};
let base_image_info = get_image_info(&file).await;
let image_info = assign!(ImageInfo::new(), {
width: base_image_info.width,
height: base_image_info.height,
size: info.size.map(Into::into),
mimetype: Some(info.mime.to_string()),
});
let client = room.session().client();
let handle = spawn_tokio!(async move { client.media().upload(&info.mime, data).await });
let uri = match handle.await.unwrap() {
Ok(res) => res.content_uri,
Err(error) => {
error!("Could not upload room avatar: {}", error);
toast!(self, gettext("Could not upload avatar"));
avatar.reset();
return;
}
};
let (action, weak_action) = OngoingAsyncAction::set(uri.clone());
imp.changing_avatar.replace(Some(action));
let handle =
spawn_tokio!(async move { matrix_room.set_avatar_url(&uri, Some(image_info)).await });
// We don't need to handle the success of the request, we should receive the
// change via sync.
if let Err(error) = handle.await.unwrap() {
// Because this action can finish in avatar_changed, we must only act if this is
// still the current action.
if weak_action.is_ongoing() {
imp.changing_avatar.take();
error!("Could not change room avatar: {error}");
toast!(self, gettext("Could not change avatar"));
avatar.reset();
}
}
}
async fn remove_avatar(&self) {
let room = self.room();
let MatrixRoom::Joined(matrix_room) = room.matrix_room() else {
error!("Cannot remove avatar of room not joined");
return;
};
let imp = self.imp();
let avatar = &*imp.avatar;
avatar.removal_in_progress();
let (action, weak_action) = OngoingAsyncAction::remove();
imp.changing_avatar.replace(Some(action));
let handle = spawn_tokio!(async move { matrix_room.remove_avatar().await });
// We don't need to handle the success of the request, we should receive the
// change via sync.
if let Err(error) = handle.await.unwrap() {
// Because this action can finish in avatar_changed, we must only act if this is
// still the current action.
if weak_action.is_ongoing() {
imp.changing_avatar.take();
error!("Could not remove room avatar: {}", error);
toast!(self, gettext("Could not remove avatar"));
avatar.reset();
}
}
}
fn init_edit_toggle(&self) {
@ -214,48 +339,6 @@ impl GeneralPage {
edit_toggle_visible.bind(&edit_toggle.get(), "visible", gtk::Widget::NONE);
}
fn avatar_chooser(&self) -> Option<&gtk::FileChooserNative> {
if let Some(avatar_chooser) = self.imp().avatar_chooser.get() {
Some(avatar_chooser)
} else {
let window = self.root()?.downcast::<adw::Window>().ok()?;
let avatar_chooser = gtk::FileChooserNative::new(
Some(&gettext("Choose avatar")),
Some(&window),
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.
self.imp()
.avatar_chooser
.set(avatar_chooser)
.expect("File chooser already initialized");
self.avatar_chooser()
}
}
fn open_avatar_chooser(&self) {
if let Some(avatar_chooser) = self.avatar_chooser() {
avatar_chooser.show();
} else {
error!("Failed to create the FileChooserNative");
}
}
fn member_count_changed(&self, n: u32) {
self.imp().members_count.set_text(&format!("{n}"));
}

45
src/session/room/mod.rs

@ -8,10 +8,10 @@ mod room_type;
mod timeline;
mod typing_list;
use std::{cell::RefCell, io::Cursor, path::PathBuf};
use std::{cell::RefCell, io::Cursor};
use gettextrs::{gettext, ngettext};
use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*};
use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*};
use log::{debug, error, info, warn};
use matrix_sdk::{
attachment::{generate_image_thumbnail, AttachmentConfig, AttachmentInfo, Thumbnail},
@ -1332,47 +1332,6 @@ 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 MatrixRoom::Joined(joined_room) = self.matrix_room() else {
error!("Cannot change avatar of a room not joined.");
return;
};
let handle = spawn_tokio!(async move {
if let Some(filename) = filename {
debug!("Getting mime type of file {:?}", filename);
let image = tokio::fs::read(filename).await?;
let content_type = gio::content_type_guess(Option::<String>::None, &image)
.0
.to_string();
joined_room
.upload_avatar(
&content_type
.parse()
.unwrap_or(mime::APPLICATION_OCTET_STREAM),
image,
None,
)
.await
} else {
joined_room.remove_avatar().await
}
});
spawn!(
glib::PRIORITY_DEFAULT_IDLE,
clone!(@weak self as this => async move {
match handle.await.unwrap() {
Ok(_) => info!("Successfully updated room avatar"),
Err(error) => error!("Couldn’t update room avatar: {}", error),
};
})
);
}
pub async fn accept_invite(&self) -> MatrixResult<()> {
let matrix_room = self.matrix_room();

Loading…
Cancel
Save