history-viewer: Implement MediaHistoryViewer

Also add it as a RoomDetails' subpage.
This commit is contained in:
Marco Melorio 2022-08-10 01:11:01 +02:00 committed by Kévin Commaille
parent 5528492801
commit 063b1b318c
10 changed files with 512 additions and 2 deletions

View file

@ -85,6 +85,8 @@
<file compressed="true" preprocess="xml-stripblanks" alias="content-invite.ui">ui/content-invite.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="content-invitee-item.ui">ui/content-invitee-item.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="content-invitee-row.ui">ui/content-invitee-row.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="content-media-history-viewer-item.ui">ui/content-media-history-viewer-item.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="content-media-history-viewer.ui">ui/content-media-history-viewer.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="content-member-item.ui">ui/content-member-item.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="content-member-page-list-view.ui">ui/content-member-page-list-view.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="content-member-page-membership-subpage-row.ui">ui/content-member-page-membership-subpage-row.ui</file>

View file

@ -620,6 +620,45 @@ typing-bar avatar {
border-radius: 6px;
}
/* Media History Viewer */
mediahistoryviewer {
background: black;
color: white;
}
mediahistoryviewer headerbar {
background: none;
box-shadow: none;
}
mediahistoryviewer gridview {
background: none;
padding: 2px;
}
mediahistoryviewer gridview > child {
background: none;
padding: 2px;
/* ease-out-quad */
transition: 100ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
mediahistoryviewer gridview > child:hover {
transform: scale(1.03);
}
mediahistoryviewer gridview > child:active {
transform: scale(0.98);
}
mediahistoryvieweritem > overlay > image {
border-radius: 100%;
padding: 12px;
-gtk-icon-size: 24px;
}
/* Room Details */
.room-details listview {

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="ContentMediaHistoryViewerItem" parent="GtkWidget">
<child>
<object class="GtkOverlay" id="overlay">
<child>
<object class="GtkPicture" id="picture">
<property name="content-fit">cover</property>
</object>
</child>
</object>
</child>
</template>
</interface>

View file

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="ContentMediaHistoryViewer" parent="AdwBin">
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="GtkHeaderBar">
<style>
<class name="osd"/>
</style>
<property name="title-widget">
<object class="AdwWindowTitle">
<property name="title" translatable="yes">Media</property>
</object>
</property>
<child type="start">
<object class="GtkButton">
<property name="action-name">details.previous-page</property>
<property name="icon-name">go-previous-symbolic</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="hscrollbar-policy">never</property>
<property name="vexpand">True</property>
<style>
<class name="osd"/>
</style>
<child>
<object class="AdwClampScrollable">
<property name="maximum-size">1000</property>
<property name="tightening-threshold">800</property>
<property name="vscroll-policy">natural</property>
<child>
<object class="GtkGridView" id="grid_view">
<property name="min-columns">2</property>
<property name="max-columns">5</property>
<property name="factory">
<object class="GtkBuilderListItemFactory">
<property name="bytes"><![CDATA[
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="GtkListItem">
<property name="child">
<object class="ContentMediaHistoryViewerItem">
<property name="width-request">150</property>
<property name="height-request">150</property>
<binding name="event">
<lookup name="item">GtkListItem</lookup>
</binding>
</object>
</property>
</template>
</interface>
]]></property>
</object>
</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</template>
</interface>

View file

@ -139,6 +139,25 @@
</child>
</object>
</child>
<child>
<object class="AdwPreferencesGroup">
<child>
<object class="AdwActionRow">
<property name="title" translatable="yes">Media</property>
<property name="action-name">details.next-page</property>
<property name="action-target">'media-history'</property>
<property name="activatable">True</property>
<child type="suffix">
<object class="GtkImage">
<property name="valign">center</property>
<property name="halign">center</property>
<property name="icon-name">go-next-symbolic</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</property>
</object>

View file

@ -22,6 +22,7 @@ data/resources/ui/content-explore-servers-popover.ui
data/resources/ui/content-explore.ui
data/resources/ui/content-invite-subpage.ui
data/resources/ui/content-invite.ui
data/resources/ui/content-media-history-viewer.ui
data/resources/ui/content-member-page.ui
data/resources/ui/content-message-file.ui
data/resources/ui/content-message-row.ui

View file

@ -0,0 +1,108 @@
use adw::{prelude::*, subclass::prelude::*};
use gtk::{glib, glib::clone, CompositeTemplate};
use crate::{
session::{
content::room_details::history_viewer::{MediaItem, Timeline, TimelineFilter},
Room,
},
spawn,
};
const MIN_N_ITEMS: u32 = 50;
mod imp {
use glib::subclass::InitializingObject;
use once_cell::{sync::Lazy, unsync::OnceCell};
use super::*;
#[derive(Debug, Default, CompositeTemplate)]
#[template(resource = "/org/gnome/Fractal/content-media-history-viewer.ui")]
pub struct MediaHistoryViewer {
pub room_timeline: OnceCell<Timeline>,
#[template_child]
pub grid_view: TemplateChild<gtk::GridView>,
}
#[glib::object_subclass]
impl ObjectSubclass for MediaHistoryViewer {
const NAME: &'static str = "ContentMediaHistoryViewer";
type Type = super::MediaHistoryViewer;
type ParentType = adw::Bin;
fn class_init(klass: &mut Self::Class) {
MediaItem::static_type();
Self::bind_template(klass);
klass.set_css_name("mediahistoryviewer");
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for MediaHistoryViewer {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![glib::ParamSpecObject::builder::<Room>("room")
.construct_only()
.build()]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"room" => self.obj().set_room(value.get().unwrap()),
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"room" => self.obj().room().to_value(),
_ => unimplemented!(),
}
}
}
impl WidgetImpl for MediaHistoryViewer {}
impl BinImpl for MediaHistoryViewer {}
}
glib::wrapper! {
pub struct MediaHistoryViewer(ObjectSubclass<imp::MediaHistoryViewer>)
@extends gtk::Widget, adw::Bin;
}
impl MediaHistoryViewer {
pub fn new(room: &Room) -> Self {
glib::Object::builder().property("room", room).build()
}
fn set_room(&self, room: &Room) {
let imp = self.imp();
let timeline = Timeline::new(room, TimelineFilter::Media);
let model = gtk::NoSelection::new(Some(timeline.clone()));
imp.grid_view.set_model(Some(&model));
// Load an initial number of items
spawn!(clone!(@weak timeline => async move {
while timeline.n_items() < MIN_N_ITEMS {
if !timeline.load().await {
break;
}
}
}));
imp.room_timeline.set(timeline).unwrap();
}
pub fn room(&self) -> &Room {
self.imp().room_timeline.get().unwrap().room()
}
}

View file

@ -0,0 +1,227 @@
use gtk::{gdk, glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate};
use log::warn;
use matrix_sdk::{
media::{MediaEventContent, MediaThumbnailSize},
ruma::{
api::client::media::get_content_thumbnail::v3::Method,
events::{
room::message::{ImageMessageEventContent, MessageType, VideoMessageEventContent},
AnyMessageLikeEventContent,
},
uint,
},
};
use crate::{
session::content::room_details::history_viewer::HistoryViewerEvent, spawn, spawn_tokio, Session,
};
mod imp {
use std::cell::RefCell;
use glib::subclass::InitializingObject;
use once_cell::sync::Lazy;
use super::*;
#[derive(Debug, Default, CompositeTemplate)]
#[template(resource = "/org/gnome/Fractal/content-media-history-viewer-item.ui")]
pub struct MediaItem {
pub event: RefCell<Option<HistoryViewerEvent>>,
pub overlay_icon: RefCell<Option<gtk::Image>>,
#[template_child]
pub overlay: TemplateChild<gtk::Overlay>,
#[template_child]
pub picture: TemplateChild<gtk::Picture>,
}
#[glib::object_subclass]
impl ObjectSubclass for MediaItem {
const NAME: &'static str = "ContentMediaHistoryViewerItem";
type Type = super::MediaItem;
type ParentType = gtk::Widget;
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
klass.set_css_name("mediahistoryvieweritem");
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for MediaItem {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecObject::builder::<HistoryViewerEvent>("event")
.explicit_notify()
.build(),
]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"event" => self.obj().set_event(value.get().unwrap()),
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"event" => self.obj().event().to_value(),
_ => unimplemented!(),
}
}
fn dispose(&self) {
self.overlay.unparent();
}
}
impl WidgetImpl for MediaItem {
fn measure(&self, orientation: gtk::Orientation, for_size: i32) -> (i32, i32, i32, i32) {
// Keep the widget squared
let (min, ..) = self.overlay.measure(orientation, for_size);
(min, for_size.max(min), -1, -1)
}
fn request_mode(&self) -> gtk::SizeRequestMode {
gtk::SizeRequestMode::HeightForWidth
}
fn size_allocate(&self, width: i32, height: i32, baseline: i32) {
self.overlay.allocate(width, height, baseline, None);
}
}
}
glib::wrapper! {
pub struct MediaItem(ObjectSubclass<imp::MediaItem>)
@extends gtk::Widget;
}
impl MediaItem {
pub fn set_event(&self, event: Option<HistoryViewerEvent>) {
if self.event() == event {
return;
}
if let Some(ref event) = event {
match event.original_content() {
Some(AnyMessageLikeEventContent::RoomMessage(message)) => match message.msgtype {
MessageType::Image(content) => {
self.show_image(content, &event.room().unwrap().session());
}
MessageType::Video(content) => {
self.show_video(content, &event.room().unwrap().session());
}
_ => {
panic!("Unexpected message type");
}
},
_ => {
panic!("Unexpected message type");
}
}
}
self.imp().event.replace(event);
self.notify("event");
}
pub fn event(&self) -> Option<HistoryViewerEvent> {
self.imp().event.borrow().clone()
}
fn show_image(&self, image: ImageMessageEventContent, session: &Session) {
let imp = self.imp();
if let Some(icon) = imp.overlay_icon.take() {
imp.overlay.remove_overlay(&icon);
}
self.load_thumbnail(image, session);
}
fn show_video(&self, video: VideoMessageEventContent, session: &Session) {
let imp = self.imp();
if imp.overlay_icon.borrow().is_none() {
let icon = gtk::Image::builder()
.icon_name("media-playback-start-symbolic")
.css_classes(vec!["osd".to_string()])
.halign(gtk::Align::Center)
.valign(gtk::Align::Center)
.build();
imp.overlay.add_overlay(&icon);
imp.overlay_icon.replace(Some(icon));
}
self.load_thumbnail(video, session);
}
fn load_thumbnail<C>(&self, content: C, session: &Session)
where
C: MediaEventContent + Send + Sync + Clone + 'static,
{
let media = session.client().media();
let handle = spawn_tokio!(async move {
let thumbnail = if content.thumbnail_source().is_some() {
media
.get_thumbnail(
content.clone(),
MediaThumbnailSize {
method: Method::Scale,
width: uint!(300),
height: uint!(300),
},
true,
)
.await
.ok()
.flatten()
} else {
None
};
if let Some(data) = thumbnail {
Ok(Some(data))
} else {
media.get_file(content, true).await
}
});
spawn!(
glib::PRIORITY_LOW,
clone!(@weak self as obj => async move {
let imp = obj.imp();
match handle.await.unwrap() {
Ok(Some(data)) => {
match gdk::Texture::from_bytes(&glib::Bytes::from(&data)) {
Ok(texture) => {
imp.picture.set_paintable(Some(&texture));
}
Err(error) => {
warn!("Image file not supported: {}", error);
}
}
}
Ok(None) => {
warn!("Could not retrieve invalid media file");
}
Err(error) => {
warn!("Could not retrieve media file: {}", error);
}
}
})
);
}
}

View file

@ -1,4 +1,11 @@
mod event;
mod media;
mod media_item;
mod timeline;
use self::event::HistoryViewerEvent;
pub use self::media::MediaHistoryViewer;
use self::{
event::HistoryViewerEvent,
media_item::MediaItem,
timeline::{Timeline, TimelineFilter},
};

View file

@ -11,7 +11,11 @@ use gtk::{glib, CompositeTemplate};
use log::warn;
pub use self::{general_page::GeneralPage, invite_subpage::InviteSubpage, member_page::MemberPage};
use crate::{components::ToastableWindow, prelude::*, session::Room};
use crate::{
components::ToastableWindow,
prelude::*,
session::{content::room_details::history_viewer::MediaHistoryViewer, Room},
};
#[derive(Debug, Default, Hash, Eq, PartialEq, Clone, Copy, glib::Enum)]
#[repr(u32)]
@ -22,6 +26,7 @@ pub enum PageName {
General,
Members,
Invite,
MediaHistory,
}
impl glib::variant::StaticVariantType for PageName {
@ -36,6 +41,7 @@ impl glib::variant::FromVariant for PageName {
"general" => Some(PageName::General),
"members" => Some(PageName::Members),
"invite" => Some(PageName::Invite),
"media-history" => Some(PageName::MediaHistory),
"" => Some(PageName::None),
_ => None,
}
@ -49,6 +55,7 @@ impl glib::variant::ToVariant for PageName {
PageName::General => "general",
PageName::Members => "members",
PageName::Invite => "invite",
PageName::MediaHistory => "media-history",
}
.to_variant()
}
@ -234,6 +241,22 @@ impl RoomDetails {
self.set_title(Some(&gettext("Invite new Members")));
imp.main_stack.set_visible_child(&invite_page);
}
PageName::MediaHistory => {
let media_page = if let Some(media_page) = list_stack_children
.get(&PageName::MediaHistory)
.and_then(glib::object::WeakRef::upgrade)
{
media_page
} else {
let media_page = MediaHistoryViewer::new(self.room()).upcast::<gtk::Widget>();
list_stack_children.insert(PageName::MediaHistory, media_page.downgrade());
imp.main_stack.add_child(&media_page);
media_page
};
self.set_title(Some(&gettext("Media")));
imp.main_stack.set_visible_child(&media_page);
}
PageName::None => {
warn!("Can't switch to PageName::None");
}