fractal/session/view/content/room_details/history_viewer/
visual_media.rsuse adw::{prelude::*, subclass::prelude::*};
use gtk::{glib, glib::clone, CompositeTemplate};
use tracing::error;
use super::{HistoryViewerEvent, HistoryViewerEventType, HistoryViewerTimeline, VisualMediaItem};
use crate::{
components::LoadingRow,
session::{model::TimelineState, view::MediaViewer},
spawn,
utils::BoundConstructOnlyObject,
};
const MIN_N_ITEMS: u32 = 50;
const SIZE_REQUEST: i32 = 150;
mod imp {
use std::ops::ControlFlow;
use glib::subclass::InitializingObject;
use super::*;
#[derive(Debug, Default, CompositeTemplate, glib::Properties)]
#[template(
resource = "/org/gnome/Fractal/ui/session/view/content/room_details/history_viewer/visual_media.ui"
)]
#[properties(wrapper_type = super::VisualMediaHistoryViewer)]
pub struct VisualMediaHistoryViewer {
#[property(get, set = Self::set_timeline, construct_only)]
pub timeline: BoundConstructOnlyObject<HistoryViewerTimeline>,
#[template_child]
pub media_viewer: TemplateChild<MediaViewer>,
#[template_child]
pub stack: TemplateChild<gtk::Stack>,
#[template_child]
pub grid_view: TemplateChild<gtk::GridView>,
}
#[glib::object_subclass]
impl ObjectSubclass for VisualMediaHistoryViewer {
const NAME: &'static str = "ContentVisualMediaHistoryViewer";
type Type = super::VisualMediaHistoryViewer;
type ParentType = adw::NavigationPage;
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
Self::Type::bind_template_callbacks(klass);
klass.set_css_name("visual-media-history-viewer");
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
#[glib::derived_properties]
impl ObjectImpl for VisualMediaHistoryViewer {
fn constructed(&self) {
self.parent_constructed();
let factory = gtk::SignalListItemFactory::new();
factory.connect_bind(move |_, list_item| {
let Some(list_item) = list_item.downcast_ref::<gtk::ListItem>() else {
error!("List item factory did not receive a list item: {list_item:?}");
return;
};
list_item.set_activatable(false);
list_item.set_selectable(false);
});
factory.connect_bind(move |_, list_item| {
let Some(list_item) = list_item.downcast_ref::<gtk::ListItem>() else {
error!("List item factory did not receive a list item: {list_item:?}");
return;
};
let item = list_item.item();
if let Some(loading_row) = item
.and_downcast_ref::<LoadingRow>()
.filter(|_| !list_item.child().is_some_and(|c| c.is::<LoadingRow>()))
{
loading_row.unparent();
loading_row.set_width_request(SIZE_REQUEST);
loading_row.set_height_request(SIZE_REQUEST);
list_item.set_child(Some(loading_row));
} else if let Some(event) = item.and_downcast::<HistoryViewerEvent>() {
let media_item = if let Some(media_item) =
list_item.child().and_downcast::<VisualMediaItem>()
{
media_item
} else {
let media_item = VisualMediaItem::new();
media_item.set_width_request(SIZE_REQUEST);
media_item.set_height_request(SIZE_REQUEST);
list_item.set_child(Some(&media_item));
media_item
};
media_item.set_event(Some(event));
}
});
self.grid_view.set_factory(Some(&factory));
}
}
impl WidgetImpl for VisualMediaHistoryViewer {}
impl NavigationPageImpl for VisualMediaHistoryViewer {}
impl VisualMediaHistoryViewer {
fn set_timeline(&self, timeline: HistoryViewerTimeline) {
let filter = gtk::CustomFilter::new(|obj| {
obj.downcast_ref::<HistoryViewerEvent>()
.is_some_and(|e| e.event_type() == HistoryViewerEventType::Media)
|| obj.is::<LoadingRow>()
});
let filter_model =
gtk::FilterListModel::new(Some(timeline.with_loading_item().clone()), Some(filter));
let model = gtk::NoSelection::new(Some(filter_model));
model.connect_items_changed(clone!(
#[weak(rename_to = imp)]
self,
move |_, _, _, _| {
imp.update_state();
}
));
self.grid_view.set_model(Some(&model));
let timeline_state_handler = timeline.connect_state_notify(clone!(
#[weak(rename_to = imp)]
self,
move |_| {
imp.update_state();
}
));
self.timeline.set(timeline, vec![timeline_state_handler]);
self.update_state();
spawn!(clone!(
#[weak(rename_to = imp)]
self,
async move {
imp.init_timeline().await;
}
));
}
async fn init_timeline(&self) {
self.load_more_items().await;
let adj = self
.grid_view
.vadjustment()
.expect("GtkGridView has a vadjustment");
adj.connect_value_notify(clone!(
#[weak(rename_to = imp)]
self,
move |_| {
if imp.needs_more_items() {
spawn!(async move {
imp.load_more_items().await;
});
}
}
));
}
pub(super) async fn load_more_items(&self) {
self.timeline
.obj()
.load(clone!(
#[weak(rename_to = imp)]
self,
#[upgrade_or]
ControlFlow::Break(()),
move || {
if imp.needs_more_items() {
ControlFlow::Continue(())
} else {
ControlFlow::Break(())
}
}
))
.await;
}
fn needs_more_items(&self) -> bool {
let Some(model) = self.grid_view.model() else {
return false;
};
if model.n_items() < MIN_N_ITEMS {
return true;
}
let adj = self
.grid_view
.vadjustment()
.expect("GtkGridView has a vadjustment");
adj.value() + adj.page_size() * 2.0 >= adj.upper()
}
fn update_state(&self) {
let Some(model) = self.grid_view.model() else {
return;
};
let timeline = self.timeline.obj();
let visible_child_name = match timeline.state() {
TimelineState::Initial => "loading",
TimelineState::Error => "error",
TimelineState::Complete if model.n_items() == 0 => "empty",
TimelineState::Loading => {
if model.n_items() == 0
|| (model.n_items() == 1
&& model.item(0).is_some_and(|item| item.is::<LoadingRow>()))
{
"loading"
} else {
"content"
}
}
_ => "content",
};
self.stack.set_visible_child_name(visible_child_name);
}
}
}
glib::wrapper! {
pub struct VisualMediaHistoryViewer(ObjectSubclass<imp::VisualMediaHistoryViewer>)
@extends gtk::Widget, adw::NavigationPage;
}
#[gtk::template_callbacks]
impl VisualMediaHistoryViewer {
pub fn new(timeline: &HistoryViewerTimeline) -> Self {
glib::Object::builder()
.property("timeline", timeline)
.build()
}
pub fn show_media(&self, item: &VisualMediaItem) {
let Some(event) = item.event() else {
return;
};
let Some(room) = event.room() else {
return;
};
let imp = self.imp();
let media_message = event
.visual_media_message()
.expect("Visual media items contain only visual message content");
imp.media_viewer
.set_message(&room, event.event_id(), media_message);
imp.media_viewer.reveal(item);
}
#[template_callback]
async fn load_more_items(&self) {
self.imp().load_more_items().await;
}
}