session: Add Media Viewer

This commit is contained in:
Kévin Commaille 2021-11-30 19:32:24 +01:00
parent a92c21770a
commit f21eccfc15
No known key found for this signature in database
GPG key ID: DD507DAE96E8245C
13 changed files with 470 additions and 11 deletions

View file

@ -22,6 +22,7 @@
<file compressed="true" preprocess="xml-stripblanks" alias="event-menu.ui">ui/event-menu.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="event-source-dialog.ui">ui/event-source-dialog.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="login.ui">ui/login.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="media-viewer.ui">ui/media-viewer.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="session.ui">ui/session.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="sidebar.ui">ui/sidebar.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="sidebar-account-switcher.ui">ui/sidebar-account-switcher.ui</file>

View file

@ -1,12 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="GtkListItem">
<property name="activatable">False</property>
<binding name="activatable">
<lookup name="activatable">
<lookup name="item">row</lookup>
</lookup>
</binding>
<property name="selectable">False</property>
<property name="child">
<object class="ContentItemRow">
<object class="ContentItemRow" id="row">
<binding name="item">
<lookup name="item">GtkListItem</lookup>
<lookup name="item">GtkListItem</lookup>
</binding>
</object>
</property>

View file

@ -142,6 +142,7 @@
<property name="resource">/org/gnome/FractalNext/content-item.ui</property>
</object>
</property>
<property name="single-click-activate">True</property>
<accessibility>
<property name="label" translatable="yes">Room History</property>
</accessibility>
@ -227,4 +228,3 @@
</child>
</template>
</interface>

View file

@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="MediaViewer" parent="ContextMenuBin">
<property name="child">
<object class="GtkOverlay">
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="GtkHeaderBar">
<property name="visible" bind-source="MediaViewer" bind-property="fullscreened" bind-flags="sync-create | invert-boolean"/>
<property name="title-widget">
<object class="GtkLabel">
<binding name="label">
<lookup name="body">MediaViewer</lookup>
</binding>
<property name="single-line-mode">True</property>
<property name="ellipsize">end</property>
<style>
<class name="title"/>
</style>
</object>
</property>
<child type="start">
<object class="GtkButton" id="back">
<property name="icon-name">go-previous-symbolic</property>
<property name="action-name">session.show-content</property>
</object>
</child>
<child type="end">
<object class="GtkMenuButton" id="menu_unfull">
<property name="icon-name">view-more-symbolic</property>
<property name="menu-model" bind-source="MediaViewer" bind-property="context-menu" bind-flags="sync-create"/>
</object>
</child>
<child type="end">
<object class="GtkButton">
<property name="icon-name">view-fullscreen-symbolic</property>
<property name="action-name">win.toggle-fullscreen</property>
</object>
</child>
</object>
</child>
<child>
<object class="AdwBin" id="media">
<property name="halign">center</property>
</object>
</child>
</object>
</child>
<child type="overlay">
<object class="GtkRevealer" id="headerbar_revealer">
<property name="visible" bind-source="MediaViewer" bind-property="fullscreened" bind-flags="sync-create"/>
<property name="valign">GTK_ALIGN_START</property>
<property name="transition-type">GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN</property>
<child>
<object class="GtkHeaderBar">
<property name="show-title-buttons">false</property>
<property name="title-widget">
<object class="GtkLabel">
<binding name="label">
<lookup name="body">MediaViewer</lookup>
</binding>
<property name="single-line-mode">True</property>
<property name="ellipsize">end</property>
<style>
<class name="title"/>
</style>
</object>
</property>
<child type="end">
<object class="GtkMenuButton" id="menu_full">
<property name="icon-name">view-more-symbolic</property>
<property name="menu-model" bind-source="MediaViewer" bind-property="context-menu" bind-flags="sync-create"/>
</object>
</child>
<child type="end">
<object class="GtkButton">
<property name="icon-name">view-restore-symbolic</property>
<property name="action-name">win.toggle-fullscreen</property>
</object>
</child>
</object>
</child>
<layout>
<property name="measure">True</property>
</layout>
</object>
</child>
</object>
</property>
</template>
</interface>

View file

@ -61,8 +61,10 @@
</child>
</object>
</child>
<child>
<object class="MediaViewer" id="media_viewer" />
</child>
</object>
</property>
</template>
</interface>

View file

@ -28,6 +28,7 @@ data/resources/ui/event-menu.ui
data/resources/ui/event-source-dialog.ui
data/resources/ui/login.ui
data/resources/ui/in-app-notification.ui
data/resources/ui/media-viewer.ui
data/resources/ui/room-creation.ui
data/resources/ui/session.ui
data/resources/ui/session-verification.ui
@ -86,6 +87,7 @@ src/session/content/room_history/message_row/mod.rs
src/session/content/room_history/message_row/text.rs
src/session/content/room_history/mod.rs
src/session/content/room_history/state_row.rs
src/session/media_viewer.rs
src/session/mod.rs
src/session/room_creation/mod.rs
src/session/room_list.rs

View file

@ -71,7 +71,7 @@ sources = files(
'session/content/mod.rs',
'session/content/room_details/member_page.rs',
'session/content/room_details/mod.rs',
'session/room/event_actions.rs',
'session/media_viewer.rs',
'session/room/event.rs',
'session/room/highlight_flags.rs',
'session/room/item.rs',

View file

@ -16,7 +16,7 @@ use sourceview::prelude::*;
use crate::components::{CustomEntry, RoomTitle};
use crate::session::content::{MarkdownPopover, RoomDetails};
use crate::session::room::{Room, RoomType};
use crate::session::room::{Item, Room, RoomType};
mod imp {
use super::*;
@ -193,6 +193,21 @@ mod imp {
self.listview
.set_vscroll_policy(gtk::ScrollablePolicy::Natural);
self.listview
.connect_activate(clone!(@weak obj => move |listview, pos| {
if let Some(item) = listview
.model()
.and_then(|model| model.item(pos))
.and_then(|o| o.downcast::<Item>().ok())
{
if let Some(event) = item.event() {
if let Some(room) = obj.room() {
room.session().show_media(event);
}
}
}
}));
obj.set_sticky(true);
let adj = self.listview.vadjustment().unwrap();

272
src/session/media_viewer.rs Normal file
View file

@ -0,0 +1,272 @@
use adw::{prelude::*, subclass::prelude::*};
use gettextrs::gettext;
use gtk::{
gdk, gdk_pixbuf::Pixbuf, gio, glib, glib::clone, subclass::prelude::*, CompositeTemplate,
};
use log::warn;
use matrix_sdk::ruma::events::{room::message::MessageType, AnyMessageEventContent};
use crate::{
components::{ContextMenuBin, ContextMenuBinImpl},
session::room::Event,
spawn, Window,
};
use super::room::EventActions;
mod imp {
use crate::components::ContextMenuBinExt;
use super::*;
use glib::object::WeakRef;
use glib::subclass::InitializingObject;
use once_cell::sync::Lazy;
use std::cell::{Cell, RefCell};
#[derive(Debug, Default, CompositeTemplate)]
#[template(resource = "/org/gnome/FractalNext/media-viewer.ui")]
pub struct MediaViewer {
pub fullscreened: Cell<bool>,
pub event: RefCell<Option<WeakRef<Event>>>,
pub body: RefCell<Option<String>>,
#[template_child]
pub headerbar_revealer: TemplateChild<gtk::Revealer>,
#[template_child]
pub menu_full: TemplateChild<gtk::MenuButton>,
#[template_child]
pub media: TemplateChild<adw::Bin>,
}
#[glib::object_subclass]
impl ObjectSubclass for MediaViewer {
const NAME: &'static str = "MediaViewer";
type Type = super::MediaViewer;
type ParentType = ContextMenuBin;
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for MediaViewer {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpec::new_boolean(
"fullscreened",
"Fullscreened",
"Whether the viewer is fullscreen",
false,
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
glib::ParamSpec::new_object(
"event",
"Event",
"The media event to display",
Event::static_type(),
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
glib::ParamSpec::new_string(
"body",
"Body",
"The body of the media event",
None,
glib::ParamFlags::READABLE,
),
]
});
PROPERTIES.as_ref()
}
fn set_property(
&self,
obj: &Self::Type,
_id: usize,
value: &glib::Value,
pspec: &glib::ParamSpec,
) {
match pspec.name() {
"fullscreened" => obj.set_fullscreened(value.get().unwrap()),
"event" => obj.set_event(value.get().unwrap()),
_ => unimplemented!(),
}
}
fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"fullscreened" => obj.fullscreened().to_value(),
"event" => obj.event().to_value(),
"body" => obj.body().to_value(),
_ => unimplemented!(),
}
}
fn constructed(&self, obj: &Self::Type) {
self.parent_constructed(obj);
obj.set_context_menu(Some(Self::Type::event_menu_model()));
// Bind `fullscreened` to the window property of the same name.
obj.connect_notify_local(Some("root"), |obj, _| {
if let Some(window) = obj.root().and_then(|root| root.downcast::<Window>().ok()) {
window
.bind_property("fullscreened", obj, "fullscreened")
.flags(glib::BindingFlags::SYNC_CREATE)
.build();
}
});
// Toggle fullscreen on double click.
let click_gesture = gtk::GestureClick::builder().button(1).build();
click_gesture.connect_pressed(clone!(@weak obj => move |_, n_pressed, _, _| {
if n_pressed == 2 {
obj.activate_action("win.toggle-fullscreen", None);
}
}));
obj.add_controller(&click_gesture);
// Show headerbar when revealer is hovered.
let revealer: &gtk::Revealer = &*self.headerbar_revealer;
let menu: &gtk::MenuButton = &*self.menu_full;
let motion_controller = gtk::EventControllerMotion::new();
motion_controller.connect_enter(clone!(@weak revealer => move |_, _, _| {
revealer.set_reveal_child(true);
}));
// Hide the headerbar when revealer is not hovered and header menu is closed.
motion_controller.connect_leave(clone!(@weak revealer, @weak menu => move |_| {
if menu.popover().filter(|popover| popover.is_visible()).is_none() {
revealer.set_reveal_child(false);
}
}));
menu.popover().unwrap().connect_closed(
clone!(@weak revealer, @weak motion_controller, => move |_| {
if !motion_controller.contains_pointer() {
revealer.set_reveal_child(false);
}
}),
);
revealer.add_controller(&motion_controller);
}
}
impl WidgetImpl for MediaViewer {}
impl BinImpl for MediaViewer {}
impl ContextMenuBinImpl for MediaViewer {}
}
glib::wrapper! {
pub struct MediaViewer(ObjectSubclass<imp::MediaViewer>)
@extends gtk::Widget, adw::Bin, ContextMenuBin, @implements gtk::Accessible;
}
impl MediaViewer {
pub fn new() -> Self {
glib::Object::new(&[]).expect("Failed to create MediaViewer")
}
pub fn event(&self) -> Option<Event> {
let priv_ = imp::MediaViewer::from_instance(self);
priv_
.event
.borrow()
.as_ref()
.and_then(|event| event.upgrade())
}
pub fn set_event(&self, event: Option<Event>) {
let priv_ = imp::MediaViewer::from_instance(self);
if event == self.event() {
return;
}
priv_.event.replace(event.map(|event| event.downgrade()));
self.build();
self.notify("event");
}
pub fn body(&self) -> Option<String> {
let priv_ = imp::MediaViewer::from_instance(self);
priv_.body.borrow().clone()
}
pub fn set_body(&self, body: Option<String>) {
let priv_ = imp::MediaViewer::from_instance(self);
if body == self.body() {
return;
}
priv_.body.replace(body);
self.notify("body");
}
pub fn fullscreened(&self) -> bool {
let priv_ = imp::MediaViewer::from_instance(self);
priv_.fullscreened.get()
}
pub fn set_fullscreened(&self, fullscreened: bool) {
let priv_ = imp::MediaViewer::from_instance(self);
if fullscreened == self.fullscreened() {
return;
}
priv_.fullscreened.set(fullscreened);
// Upscale the media on fullscreen
if fullscreened {
priv_.media.set_halign(gtk::Align::Fill);
} else {
priv_.media.set_halign(gtk::Align::Center);
}
self.notify("fullscreened");
}
fn build(&self) {
if let Some(event) = self.event() {
self.set_event_actions(Some(&event));
if let Some(AnyMessageEventContent::RoomMessage(content)) = event.message_content() {
match content.msgtype {
MessageType::Image(image) => {
self.set_body(Some(image.body.clone()));
spawn!(
glib::PRIORITY_LOW,
clone!(@weak self as obj => async move {
let priv_ = imp::MediaViewer::from_instance(&obj);
match event.get_media_content().await {
Ok((_, data)) => {
let stream = gio::MemoryInputStream::from_bytes(&glib::Bytes::from(&data));
let texture = Pixbuf::from_stream(&stream, gio::NONE_CANCELLABLE)
.ok()
.map(|pixbuf| gdk::Texture::for_pixbuf(&pixbuf));
let child = gtk::Picture::for_paintable(texture.as_ref());
priv_.media.set_child(Some(&child));
}
Err(error) => {
warn!("Could not retrieve image file: {}", error);
let child = gtk::Label::new(Some(&gettext("Could not retrieve image")));
priv_.media.set_child(Some(&child));
}
}
})
);
}
_ => {}
}
}
}
}
}
impl EventActions for MediaViewer {}

View file

@ -2,6 +2,7 @@ mod account_settings;
mod avatar;
mod content;
mod event_source_dialog;
mod media_viewer;
pub mod room;
mod room_creation;
mod room_list;
@ -12,7 +13,8 @@ pub mod verification;
use self::account_settings::AccountSettings;
pub use self::avatar::Avatar;
use self::content::Content;
pub use self::room::Room;
use self::media_viewer::MediaViewer;
pub use self::room::{Event, Item, Room};
pub use self::room_creation::RoomCreation;
use self::room_list::RoomList;
use self::sidebar::Sidebar;
@ -75,6 +77,8 @@ mod imp {
pub content: TemplateChild<adw::Leaflet>,
#[template_child]
pub sidebar: TemplateChild<Sidebar>,
#[template_child]
pub media_viewer: TemplateChild<MediaViewer>,
pub client: RefCell<Option<Client>>,
pub item_list: OnceCell<ItemList>,
pub user: OnceCell<User>,
@ -728,6 +732,14 @@ impl Session {
self.emit_by_name("ready", &[]).unwrap();
}
/// Show a media event
pub fn show_media(&self, event: &Event) {
let priv_ = imp::Session::from_instance(self);
priv_.media_viewer.set_event(Some(event.clone()));
priv_.stack.set_visible_child(&*priv_.media_viewer);
}
}
impl Default for Session {

View file

@ -107,6 +107,13 @@ mod imp {
None,
glib::ParamFlags::READABLE,
),
glib::ParamSpec::new_boolean(
"can-view-media",
"Can View Media",
"Whether this is a media event that can be viewed",
false,
glib::ParamFlags::READABLE,
),
]
});
@ -146,6 +153,7 @@ mod imp {
"show-header" => obj.show_header().to_value(),
"can-hide-header" => obj.can_hide_header().to_value(),
"time" => obj.time().to_value(),
"can-view-media" => obj.can_view_media().to_value(),
_ => unimplemented!(),
}
}
@ -200,6 +208,7 @@ impl Event {
priv_.pure_event.replace(Some(event));
self.notify("event");
self.notify("can-view-media");
}
pub fn matrix_sender(&self) -> UserId {
@ -506,6 +515,7 @@ impl Event {
/// Compatible events:
///
/// - File message (`MessageType::File`).
/// - Image message (`MessageType::Image`).
///
/// Returns `Ok((filename, binary_content))` on success, `Err` if an error occured while
/// fetching the content. Panics on an incompatible event.
@ -513,11 +523,17 @@ impl Event {
if let AnyMessageEventContent::RoomMessage(content) = self.message_content().unwrap() {
let client = self.room().session().client();
match content.msgtype {
MessageType::File(file_content) => {
let content = file_content.clone();
MessageType::File(content) => {
let filename = content.filename.clone().unwrap_or(content.body.clone());
let handle = spawn_tokio!(async move { client.get_file(content, true).await });
let data = handle.await.unwrap()?.unwrap();
return Ok((file_content.filename.unwrap_or(file_content.body), data));
return Ok((filename, data));
}
MessageType::Image(content) => {
let filename = content.body.clone();
let handle = spawn_tokio!(async move { client.get_file(content, true).await });
let data = handle.await.unwrap()?.unwrap();
return Ok((filename, data));
}
_ => {}
};
@ -525,4 +541,14 @@ impl Event {
panic!("Trying to get the media content of an event of incompatible type");
}
/// Whether this is a media event that can be viewed.
pub fn can_view_media(&self) -> bool {
match self.message_content() {
Some(AnyMessageEventContent::RoomMessage(message)) => {
matches!(message.msgtype, MessageType::Image(_))
}
_ => false,
}
}
}

View file

@ -27,12 +27,15 @@ impl From<ItemType> for BoxedItemType {
}
mod imp {
use std::cell::Cell;
use super::*;
use once_cell::{sync::Lazy, unsync::OnceCell};
#[derive(Debug, Default)]
pub struct Item {
pub type_: OnceCell<ItemType>,
pub activatable: Cell<bool>,
}
#[glib::object_subclass]
@ -74,6 +77,13 @@ mod imp {
false,
glib::ParamFlags::READABLE,
),
glib::ParamSpec::new_boolean(
"activatable",
"Activatable",
"Whether this item is activatable.",
false,
glib::ParamFlags::READWRITE,
),
]
});
@ -96,6 +106,7 @@ mod imp {
let show_header = value.get().unwrap();
let _ = obj.set_show_header(show_header);
}
"activatable" => self.activatable.set(value.get().unwrap()),
_ => unimplemented!(),
}
}
@ -105,9 +116,19 @@ mod imp {
"selectable" => obj.selectable().to_value(),
"show-header" => obj.show_header().to_value(),
"can-hide-header" => obj.can_hide_header().to_value(),
"activatable" => self.activatable.get().to_value(),
_ => unimplemented!(),
}
}
fn constructed(&self, obj: &Self::Type) {
if let Some(event) = obj.event() {
event
.bind_property("can-view-media", obj, "activatable")
.flags(glib::BindingFlags::SYNC_CREATE)
.build();
}
}
}
}

View file

@ -74,6 +74,17 @@ mod imp {
);
obj.set_default_by_child();
// Ask for the toggle fullscreen state
let fullscreen = gio::SimpleAction::new("toggle-fullscreen", None);
fullscreen.connect_activate(clone!(@weak obj as window => move |_, _| {
if window.is_fullscreened() {
window.unfullscreen();
} else {
window.fullscreen();
}
}));
obj.add_action(&fullscreen);
}
}