history-viewer: Use a single timeline for all the viewers
Reduces the number of requests to the server.
This commit is contained in:
parent
1bac6724ad
commit
6ab0cfd33a
|
@ -678,37 +678,37 @@ roomtitle .subtitle {
|
|||
|
||||
/* Media History Viewer */
|
||||
|
||||
mediahistoryviewer {
|
||||
media-history-viewer {
|
||||
background: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
mediahistoryviewer headerbar {
|
||||
media-history-viewer headerbar {
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
mediahistoryviewer gridview {
|
||||
media-history-viewer gridview {
|
||||
background: none;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
mediahistoryviewer gridview > child {
|
||||
media-history-viewer 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 {
|
||||
media-history-viewer gridview > child:hover {
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
mediahistoryviewer gridview > child:active {
|
||||
media-history-viewer gridview > child:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
mediahistoryvieweritem > overlay > image {
|
||||
media-history-vieweritem > overlay > image {
|
||||
border-radius: 100%;
|
||||
padding: 12px;
|
||||
-gtk-icon-size: 24px;
|
||||
|
@ -717,14 +717,14 @@ mediahistoryvieweritem > overlay > image {
|
|||
|
||||
/* File History Viewer */
|
||||
|
||||
filehistoryviewer listview > row {
|
||||
file-history-viewer listview > row {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
|
||||
/* Audio History Viewer */
|
||||
|
||||
audiohistoryviewer listview > row {
|
||||
audio-history-viewer listview > row {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
use adw::{prelude::*, subclass::prelude::*};
|
||||
use gtk::{glib, glib::clone, CompositeTemplate};
|
||||
|
||||
use super::{AudioRow, Timeline, TimelineFilter};
|
||||
use crate::{session::model::Room, spawn};
|
||||
use super::{AudioRow, HistoryViewerEvent, HistoryViewerEventType, HistoryViewerTimeline};
|
||||
use crate::spawn;
|
||||
|
||||
const MIN_N_ITEMS: u32 = 20;
|
||||
|
||||
mod imp {
|
||||
use std::{cell::OnceCell, marker::PhantomData};
|
||||
use std::cell::OnceCell;
|
||||
|
||||
use glib::subclass::InitializingObject;
|
||||
|
||||
|
@ -19,10 +19,9 @@ mod imp {
|
|||
)]
|
||||
#[properties(wrapper_type = super::AudioHistoryViewer)]
|
||||
pub struct AudioHistoryViewer {
|
||||
/// The room to search for audio events.
|
||||
#[property(get = Self::room, set = Self::set_room, construct_only)]
|
||||
pub room: PhantomData<Room>,
|
||||
pub room_timeline: OnceCell<Timeline>,
|
||||
/// The timeline containing the audio events.
|
||||
#[property(get, set = Self::set_timeline, construct_only)]
|
||||
pub timeline: OnceCell<HistoryViewerTimeline>,
|
||||
#[template_child]
|
||||
pub list_view: TemplateChild<gtk::ListView>,
|
||||
}
|
||||
|
@ -35,9 +34,10 @@ mod imp {
|
|||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
AudioRow::static_type();
|
||||
|
||||
Self::bind_template(klass);
|
||||
|
||||
klass.set_css_name("audiohistoryviewer");
|
||||
klass.set_css_name("audio-history-viewer");
|
||||
}
|
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) {
|
||||
|
@ -52,34 +52,36 @@ mod imp {
|
|||
impl NavigationPageImpl for AudioHistoryViewer {}
|
||||
|
||||
impl AudioHistoryViewer {
|
||||
/// The room to search for audio events.
|
||||
fn room(&self) -> Room {
|
||||
self.room_timeline.get().unwrap().room()
|
||||
}
|
||||
/// Set the timeline containing the audio events.
|
||||
fn set_timeline(&self, timeline: HistoryViewerTimeline) {
|
||||
let filter = gtk::CustomFilter::new(|obj| {
|
||||
obj.downcast_ref::<HistoryViewerEvent>()
|
||||
.is_some_and(|e| e.event_type() == HistoryViewerEventType::Audio)
|
||||
});
|
||||
let filter_model = gtk::FilterListModel::new(Some(timeline.clone()), Some(filter));
|
||||
|
||||
/// Set the room to search for audio events.
|
||||
fn set_room(&self, room: Room) {
|
||||
let timeline = Timeline::new(&room, TimelineFilter::Audio);
|
||||
let model = gtk::NoSelection::new(Some(timeline.clone()));
|
||||
let model = gtk::NoSelection::new(Some(filter_model));
|
||||
self.list_view.set_model(Some(&model));
|
||||
|
||||
// Load an initial number of items
|
||||
spawn!(clone!(@weak self as imp, @weak timeline => async move {
|
||||
while timeline.n_items() < MIN_N_ITEMS {
|
||||
if !timeline.load().await {
|
||||
break;
|
||||
spawn!(
|
||||
clone!(@weak self as imp, @weak timeline, @weak model => async move {
|
||||
while model.n_items() < MIN_N_ITEMS {
|
||||
if !timeline.load().await {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let adj = imp.list_view.vadjustment().unwrap();
|
||||
adj.connect_value_notify(clone!(@weak timeline => move |adj| {
|
||||
if adj.value() + adj.page_size() * 2.0 >= adj.upper() {
|
||||
spawn!(async move { timeline.load().await; });
|
||||
}
|
||||
}));
|
||||
}));
|
||||
let adj = imp.list_view.vadjustment().unwrap();
|
||||
adj.connect_value_notify(clone!(@weak timeline => move |adj| {
|
||||
if adj.value() + adj.page_size() * 2.0 >= adj.upper() {
|
||||
spawn!(async move { timeline.load().await; });
|
||||
}
|
||||
}));
|
||||
})
|
||||
);
|
||||
|
||||
self.room_timeline.set(timeline).unwrap();
|
||||
self.timeline.set(timeline).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -91,7 +93,9 @@ glib::wrapper! {
|
|||
}
|
||||
|
||||
impl AudioHistoryViewer {
|
||||
pub fn new(room: &Room) -> Self {
|
||||
glib::Object::builder().property("room", room).build()
|
||||
pub fn new(timeline: &HistoryViewerTimeline) -> Self {
|
||||
glib::Object::builder()
|
||||
.property("timeline", timeline)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,10 +2,7 @@ use adw::{prelude::*, subclass::prelude::*};
|
|||
use gettextrs::gettext;
|
||||
use glib::clone;
|
||||
use gtk::{gio, glib, CompositeTemplate};
|
||||
use matrix_sdk::ruma::events::{
|
||||
room::message::{AudioMessageEventContent, MessageType},
|
||||
AnyMessageLikeEventContent,
|
||||
};
|
||||
use matrix_sdk::ruma::events::room::message::{AudioMessageEventContent, MessageType};
|
||||
use tracing::warn;
|
||||
|
||||
use super::HistoryViewerEvent;
|
||||
|
@ -70,34 +67,30 @@ mod imp {
|
|||
let obj = self.obj();
|
||||
|
||||
if let Some(event) = &event {
|
||||
if let Some(AnyMessageLikeEventContent::RoomMessage(content)) =
|
||||
event.original_content()
|
||||
{
|
||||
if let MessageType::Audio(audio) = content.msgtype {
|
||||
self.title_label.set_label(&audio.body);
|
||||
if let MessageType::Audio(audio) = event.message_content() {
|
||||
self.title_label.set_label(&audio.body);
|
||||
|
||||
if let Some(duration) = audio.info.as_ref().and_then(|i| i.duration) {
|
||||
let duration_secs = duration.as_secs();
|
||||
let secs = duration_secs % 60;
|
||||
let mins = (duration_secs % (60 * 60)) / 60;
|
||||
let hours = duration_secs / (60 * 60);
|
||||
if let Some(duration) = audio.info.as_ref().and_then(|i| i.duration) {
|
||||
let duration_secs = duration.as_secs();
|
||||
let secs = duration_secs % 60;
|
||||
let mins = (duration_secs % (60 * 60)) / 60;
|
||||
let hours = duration_secs / (60 * 60);
|
||||
|
||||
let duration = if hours > 0 {
|
||||
format!("{hours:02}:{mins:02}:{secs:02}")
|
||||
} else {
|
||||
format!("{mins:02}:{secs:02}")
|
||||
};
|
||||
|
||||
self.duration_label.set_label(&duration);
|
||||
let duration = if hours > 0 {
|
||||
format!("{hours:02}:{mins:02}:{secs:02}")
|
||||
} else {
|
||||
self.duration_label.set_label(&gettext("Unknown duration"));
|
||||
}
|
||||
format!("{mins:02}:{secs:02}")
|
||||
};
|
||||
|
||||
if let Some(session) = event.room().and_then(|r| r.session()) {
|
||||
spawn!(clone!(@weak obj, @weak session => async move {
|
||||
obj.download_audio(audio, &session).await;
|
||||
}));
|
||||
}
|
||||
self.duration_label.set_label(&duration);
|
||||
} else {
|
||||
self.duration_label.set_label(&gettext("Unknown duration"));
|
||||
}
|
||||
|
||||
if let Some(session) = event.room().and_then(|r| r.session()) {
|
||||
spawn!(clone!(@weak obj, @weak session => async move {
|
||||
obj.download_audio(audio, &session).await;
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,27 +1,70 @@
|
|||
use std::ops::Deref;
|
||||
|
||||
use gtk::{glib, prelude::*, subclass::prelude::*};
|
||||
use matrix_sdk::deserialized_responses::TimelineEvent;
|
||||
use ruma::events::{room::message::MessageType, AnyMessageLikeEventContent, AnySyncTimelineEvent};
|
||||
use ruma::{
|
||||
events::{
|
||||
room::message::{MessageType, OriginalSyncRoomMessageEvent, Relation},
|
||||
AnySyncMessageLikeEvent, AnyTimelineEvent, SyncMessageLikeEvent,
|
||||
},
|
||||
OwnedEventId,
|
||||
};
|
||||
|
||||
use crate::{session::model::Room, spawn_tokio, utils::media::filename_for_mime};
|
||||
use crate::{session::model::Room, utils::matrix::get_media_content};
|
||||
|
||||
/// The types of events that can be displayer in the history viewers.
|
||||
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, glib::Enum)]
|
||||
#[enum_type(name = "HistoryViewerEventType")]
|
||||
pub enum HistoryViewerEventType {
|
||||
#[default]
|
||||
File,
|
||||
Media,
|
||||
Audio,
|
||||
}
|
||||
|
||||
impl HistoryViewerEventType {
|
||||
fn with_msgtype(msgtype: &MessageType) -> Option<Self> {
|
||||
let event_type = match msgtype {
|
||||
MessageType::Audio(_) => Self::Audio,
|
||||
MessageType::File(_) => Self::File,
|
||||
MessageType::Image(_) => Self::Media,
|
||||
MessageType::Video(_) => Self::Media,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
Some(event_type)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, glib::Boxed)]
|
||||
#[boxed_type(name = "BoxedAnySyncTimelineEvent")]
|
||||
pub struct BoxedAnySyncTimelineEvent(pub AnySyncTimelineEvent);
|
||||
#[boxed_type(name = "BoxedSyncRoomMessageEvent")]
|
||||
pub struct BoxedSyncRoomMessageEvent(pub OriginalSyncRoomMessageEvent);
|
||||
|
||||
impl Deref for BoxedSyncRoomMessageEvent {
|
||||
type Target = OriginalSyncRoomMessageEvent;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
mod imp {
|
||||
use std::cell::OnceCell;
|
||||
use std::cell::{Cell, OnceCell};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Default, glib::Properties)]
|
||||
#[properties(wrapper_type = super::HistoryViewerEvent)]
|
||||
pub struct HistoryViewerEvent {
|
||||
/// The Matrix event.
|
||||
#[property(get)]
|
||||
pub matrix_event: OnceCell<BoxedAnySyncTimelineEvent>,
|
||||
/// The room containing this event.
|
||||
#[property(get)]
|
||||
#[property(get, construct_only)]
|
||||
pub room: glib::WeakRef<Room>,
|
||||
/// The Matrix event.
|
||||
#[property(construct_only)]
|
||||
pub matrix_event: OnceCell<BoxedSyncRoomMessageEvent>,
|
||||
/// The type of the event.
|
||||
#[property(get, construct_only, builder(HistoryViewerEventType::default()))]
|
||||
pub event_type: Cell<HistoryViewerEventType>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
|
@ -35,68 +78,88 @@ mod imp {
|
|||
}
|
||||
|
||||
glib::wrapper! {
|
||||
/// An event in the history viewer's timeline.
|
||||
pub struct HistoryViewerEvent(ObjectSubclass<imp::HistoryViewerEvent>);
|
||||
}
|
||||
|
||||
impl HistoryViewerEvent {
|
||||
pub fn try_new(event: TimelineEvent, room: &Room) -> Option<Self> {
|
||||
if let Ok(matrix_event) = event.event.deserialize() {
|
||||
let obj: Self = glib::Object::new();
|
||||
obj.imp()
|
||||
.matrix_event
|
||||
.set(BoxedAnySyncTimelineEvent(matrix_event.into()))
|
||||
.unwrap();
|
||||
obj.imp().room.set(Some(room));
|
||||
Some(obj)
|
||||
} else {
|
||||
None
|
||||
/// Constructs a new `HistoryViewerEvent` with the given event, if it is
|
||||
/// viewable in one of the history viewers.
|
||||
pub fn try_new(room: &Room, event: TimelineEvent) -> Option<Self> {
|
||||
let Ok(AnyTimelineEvent::MessageLike(message_like_event)) = event.event.deserialize()
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
let AnySyncMessageLikeEvent::RoomMessage(SyncMessageLikeEvent::Original(mut message_event)) =
|
||||
message_like_event.into()
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
// Filter out edits, they should be bundled with the original event.
|
||||
if matches!(
|
||||
message_event.content.relates_to,
|
||||
Some(Relation::Replacement(_))
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Apply bundled edit.
|
||||
if let Some(Relation::Replacement(replacement)) = message_event
|
||||
.unsigned
|
||||
.relations
|
||||
.replace
|
||||
.as_ref()
|
||||
.and_then(|e| e.content.relates_to.as_ref())
|
||||
{
|
||||
message_event
|
||||
.content
|
||||
.apply_replacement(replacement.new_content.clone());
|
||||
}
|
||||
|
||||
let event_type = HistoryViewerEventType::with_msgtype(&message_event.content.msgtype)?;
|
||||
|
||||
let obj: Self = glib::Object::builder()
|
||||
.property("room", room)
|
||||
.property("matrix-event", BoxedSyncRoomMessageEvent(message_event))
|
||||
.property("event-type", event_type)
|
||||
.build();
|
||||
|
||||
Some(obj)
|
||||
}
|
||||
|
||||
pub fn original_content(&self) -> Option<AnyMessageLikeEventContent> {
|
||||
match self.matrix_event().0 {
|
||||
AnySyncTimelineEvent::MessageLike(message) => message.original_content(),
|
||||
_ => None,
|
||||
}
|
||||
/// The Matrix event.
|
||||
fn matrix_event(&self) -> &OriginalSyncRoomMessageEvent {
|
||||
self.imp().matrix_event.get().unwrap()
|
||||
}
|
||||
|
||||
/// The event ID of the inner event.
|
||||
pub fn event_id(&self) -> OwnedEventId {
|
||||
self.matrix_event().event_id.clone()
|
||||
}
|
||||
|
||||
/// The message content of the inner event.
|
||||
pub fn message_content(&self) -> MessageType {
|
||||
self.matrix_event().content.msgtype.clone()
|
||||
}
|
||||
|
||||
/// Get the binary content of this event.
|
||||
///
|
||||
/// Returns `Ok((filename, binary_content))` on success.
|
||||
pub async fn get_file_content(&self) -> Result<(String, Vec<u8>), matrix_sdk::Error> {
|
||||
if let AnyMessageLikeEventContent::RoomMessage(content) = self.original_content().unwrap() {
|
||||
let Some(room) = self.room() else {
|
||||
return Err(matrix_sdk::Error::UnknownError(
|
||||
"Failed to upgrade Room".into(),
|
||||
));
|
||||
};
|
||||
let Some(session) = room.session() else {
|
||||
return Err(matrix_sdk::Error::UnknownError(
|
||||
"Failed to upgrade Session".into(),
|
||||
));
|
||||
};
|
||||
let media = session.client().media();
|
||||
let Some(room) = self.room() else {
|
||||
return Err(matrix_sdk::Error::UnknownError(
|
||||
"Failed to upgrade Room".into(),
|
||||
));
|
||||
};
|
||||
let Some(session) = room.session() else {
|
||||
return Err(matrix_sdk::Error::UnknownError(
|
||||
"Failed to upgrade Session".into(),
|
||||
));
|
||||
};
|
||||
let client = session.client();
|
||||
let message_content = self.message_content();
|
||||
|
||||
if let MessageType::File(content) = content.msgtype {
|
||||
let filename = content
|
||||
.filename
|
||||
.as_ref()
|
||||
.filter(|name| !name.is_empty())
|
||||
.or(Some(&content.body))
|
||||
.filter(|name| !name.is_empty())
|
||||
.cloned()
|
||||
.unwrap_or_else(|| {
|
||||
filename_for_mime(
|
||||
content
|
||||
.info
|
||||
.as_ref()
|
||||
.and_then(|info| info.mimetype.as_deref()),
|
||||
None,
|
||||
)
|
||||
});
|
||||
let handle = spawn_tokio!(async move { media.get_file(content, true).await });
|
||||
let data = handle.await.unwrap()?.unwrap();
|
||||
return Ok((filename, data));
|
||||
}
|
||||
}
|
||||
|
||||
panic!("Trying to get the content of an event of incompatible type");
|
||||
get_media_content(client, message_content).await
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
use adw::{prelude::*, subclass::prelude::*};
|
||||
use gtk::{glib, glib::clone, CompositeTemplate};
|
||||
|
||||
use super::{FileRow, Timeline, TimelineFilter};
|
||||
use crate::{session::model::Room, spawn};
|
||||
use super::{FileRow, HistoryViewerEvent, HistoryViewerEventType, HistoryViewerTimeline};
|
||||
use crate::spawn;
|
||||
|
||||
const MIN_N_ITEMS: u32 = 20;
|
||||
|
||||
mod imp {
|
||||
use std::{cell::OnceCell, marker::PhantomData};
|
||||
use std::cell::OnceCell;
|
||||
|
||||
use glib::subclass::InitializingObject;
|
||||
|
||||
|
@ -19,10 +19,9 @@ mod imp {
|
|||
)]
|
||||
#[properties(wrapper_type = super::FileHistoryViewer)]
|
||||
pub struct FileHistoryViewer {
|
||||
/// The room to search for audio events.
|
||||
#[property(get = Self::room, set = Self::set_room, construct_only)]
|
||||
pub room: PhantomData<Room>,
|
||||
pub room_timeline: OnceCell<Timeline>,
|
||||
/// The timeline containing the file events.
|
||||
#[property(get, set = Self::set_timeline, construct_only)]
|
||||
pub timeline: OnceCell<HistoryViewerTimeline>,
|
||||
#[template_child]
|
||||
pub list_view: TemplateChild<gtk::ListView>,
|
||||
}
|
||||
|
@ -35,9 +34,10 @@ mod imp {
|
|||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
FileRow::static_type();
|
||||
|
||||
Self::bind_template(klass);
|
||||
|
||||
klass.set_css_name("filehistoryviewer");
|
||||
klass.set_css_name("file-history-viewer");
|
||||
}
|
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) {
|
||||
|
@ -52,34 +52,36 @@ mod imp {
|
|||
impl NavigationPageImpl for FileHistoryViewer {}
|
||||
|
||||
impl FileHistoryViewer {
|
||||
/// The room to search for audio events.
|
||||
fn room(&self) -> Room {
|
||||
self.room_timeline.get().unwrap().room()
|
||||
}
|
||||
/// Set the timeline containing the media events.
|
||||
fn set_timeline(&self, timeline: HistoryViewerTimeline) {
|
||||
let filter = gtk::CustomFilter::new(|obj| {
|
||||
obj.downcast_ref::<HistoryViewerEvent>()
|
||||
.is_some_and(|e| e.event_type() == HistoryViewerEventType::File)
|
||||
});
|
||||
let filter_model = gtk::FilterListModel::new(Some(timeline.clone()), Some(filter));
|
||||
|
||||
/// Set the room to search for audio events.
|
||||
fn set_room(&self, room: Room) {
|
||||
let timeline = Timeline::new(&room, TimelineFilter::Files);
|
||||
let model = gtk::NoSelection::new(Some(timeline.clone()));
|
||||
let model = gtk::NoSelection::new(Some(filter_model));
|
||||
self.list_view.set_model(Some(&model));
|
||||
|
||||
// Load an initial number of items
|
||||
spawn!(clone!(@weak self as imp, @weak timeline => async move {
|
||||
while timeline.n_items() < MIN_N_ITEMS {
|
||||
if !timeline.load().await {
|
||||
break;
|
||||
spawn!(
|
||||
clone!(@weak self as imp, @weak timeline, @weak model => async move {
|
||||
while model.n_items() < MIN_N_ITEMS {
|
||||
if !timeline.load().await {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let adj = imp.list_view.vadjustment().unwrap();
|
||||
adj.connect_value_notify(clone!(@weak timeline => move |adj| {
|
||||
if adj.value() + adj.page_size() * 2.0 >= adj.upper() {
|
||||
spawn!(async move { timeline.load().await; });
|
||||
}
|
||||
}));
|
||||
}));
|
||||
let adj = imp.list_view.vadjustment().unwrap();
|
||||
adj.connect_value_notify(clone!(@weak timeline => move |adj| {
|
||||
if adj.value() + adj.page_size() * 2.0 >= adj.upper() {
|
||||
spawn!(async move { timeline.load().await; });
|
||||
}
|
||||
}));
|
||||
})
|
||||
);
|
||||
|
||||
self.room_timeline.set(timeline).unwrap();
|
||||
self.timeline.set(timeline).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -91,7 +93,9 @@ glib::wrapper! {
|
|||
}
|
||||
|
||||
impl FileHistoryViewer {
|
||||
pub fn new(room: &Room) -> Self {
|
||||
glib::Object::builder().property("room", room).build()
|
||||
pub fn new(timeline: &HistoryViewerTimeline) -> Self {
|
||||
glib::Object::builder()
|
||||
.property("timeline", timeline)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use adw::{prelude::*, subclass::prelude::*};
|
||||
use gettextrs::gettext;
|
||||
use gtk::{gio, glib, CompositeTemplate};
|
||||
use matrix_sdk::ruma::events::{room::message::MessageType, AnyMessageLikeEventContent};
|
||||
use matrix_sdk::ruma::events::room::message::MessageType;
|
||||
use tracing::error;
|
||||
|
||||
use super::HistoryViewerEvent;
|
||||
|
@ -72,18 +72,14 @@ mod imp {
|
|||
}
|
||||
|
||||
if let Some(event) = &event {
|
||||
if let Some(AnyMessageLikeEventContent::RoomMessage(content)) =
|
||||
event.original_content()
|
||||
{
|
||||
if let MessageType::File(file) = content.msgtype {
|
||||
self.title_label.set_label(&file.body);
|
||||
if let MessageType::File(file) = event.message_content() {
|
||||
self.title_label.set_label(&file.body);
|
||||
|
||||
if let Some(size) = file.info.and_then(|i| i.size) {
|
||||
let size = glib::format_size(size.into());
|
||||
self.size_label.set_label(&size);
|
||||
} else {
|
||||
self.size_label.set_label(&gettext("Unknown size"));
|
||||
}
|
||||
if let Some(size) = file.info.and_then(|i| i.size) {
|
||||
let size = glib::format_size(size.into());
|
||||
self.size_label.set_label(&size);
|
||||
} else {
|
||||
self.size_label.set_label(&gettext("Unknown size"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,13 @@
|
|||
use adw::{prelude::*, subclass::prelude::*};
|
||||
use gtk::{glib, glib::clone, CompositeTemplate};
|
||||
use ruma::events::AnyMessageLikeEventContent;
|
||||
use tracing::error;
|
||||
|
||||
use super::{MediaItem, Timeline, TimelineFilter};
|
||||
use crate::{
|
||||
session::{model::Room, view::MediaViewer},
|
||||
spawn,
|
||||
};
|
||||
use super::{HistoryViewerEvent, HistoryViewerEventType, HistoryViewerTimeline, MediaItem};
|
||||
use crate::{session::view::MediaViewer, spawn};
|
||||
|
||||
const MIN_N_ITEMS: u32 = 50;
|
||||
|
||||
mod imp {
|
||||
use std::{cell::OnceCell, marker::PhantomData};
|
||||
use std::cell::OnceCell;
|
||||
|
||||
use glib::subclass::InitializingObject;
|
||||
|
||||
|
@ -24,10 +19,9 @@ mod imp {
|
|||
)]
|
||||
#[properties(wrapper_type = super::MediaHistoryViewer)]
|
||||
pub struct MediaHistoryViewer {
|
||||
/// The room to search for media events.
|
||||
#[property(get = Self::room, set = Self::set_room, construct_only)]
|
||||
pub room: PhantomData<Room>,
|
||||
pub room_timeline: OnceCell<Timeline>,
|
||||
/// The timeline containing the media events.
|
||||
#[property(get, set = Self::set_timeline, construct_only)]
|
||||
pub timeline: OnceCell<HistoryViewerTimeline>,
|
||||
#[template_child]
|
||||
pub media_viewer: TemplateChild<MediaViewer>,
|
||||
#[template_child]
|
||||
|
@ -42,9 +36,10 @@ mod imp {
|
|||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
MediaItem::static_type();
|
||||
|
||||
Self::bind_template(klass);
|
||||
|
||||
klass.set_css_name("mediahistoryviewer");
|
||||
klass.set_css_name("media-history-viewer");
|
||||
}
|
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) {
|
||||
|
@ -59,34 +54,36 @@ mod imp {
|
|||
impl NavigationPageImpl for MediaHistoryViewer {}
|
||||
|
||||
impl MediaHistoryViewer {
|
||||
/// The room to search for media events.
|
||||
fn room(&self) -> Room {
|
||||
self.room_timeline.get().unwrap().room()
|
||||
}
|
||||
/// Set the timeline containing the media events.
|
||||
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)
|
||||
});
|
||||
let filter_model = gtk::FilterListModel::new(Some(timeline.clone()), Some(filter));
|
||||
|
||||
/// Set the room to search for media events.
|
||||
fn set_room(&self, room: Room) {
|
||||
let timeline = Timeline::new(&room, TimelineFilter::Media);
|
||||
let model = gtk::NoSelection::new(Some(timeline.clone()));
|
||||
let model = gtk::NoSelection::new(Some(filter_model));
|
||||
self.grid_view.set_model(Some(&model));
|
||||
|
||||
// Load an initial number of items
|
||||
spawn!(clone!(@weak self as imp, @weak timeline => async move {
|
||||
while timeline.n_items() < MIN_N_ITEMS {
|
||||
if !timeline.load().await {
|
||||
break;
|
||||
// Load an initial number of items.
|
||||
spawn!(
|
||||
clone!(@weak self as imp, @weak timeline, @weak model => async move {
|
||||
while model.n_items() < MIN_N_ITEMS {
|
||||
if !timeline.load().await {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let adj = imp.grid_view.vadjustment().unwrap();
|
||||
adj.connect_value_notify(clone!(@weak timeline => move |adj| {
|
||||
if adj.value() + adj.page_size() * 2.0 >= adj.upper() {
|
||||
spawn!(async move { timeline.load().await; });
|
||||
}
|
||||
}));
|
||||
}));
|
||||
let adj = imp.grid_view.vadjustment().unwrap();
|
||||
adj.connect_value_notify(clone!(@weak timeline => move |adj| {
|
||||
if adj.value() + adj.page_size() * 2.0 >= adj.upper() {
|
||||
spawn!(async move { timeline.load().await; });
|
||||
}
|
||||
}));
|
||||
})
|
||||
);
|
||||
|
||||
self.room_timeline.set(timeline).unwrap();
|
||||
self.timeline.set(timeline).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -98,26 +95,24 @@ glib::wrapper! {
|
|||
}
|
||||
|
||||
impl MediaHistoryViewer {
|
||||
pub fn new(room: &Room) -> Self {
|
||||
glib::Object::builder().property("room", room).build()
|
||||
pub fn new(timeline: &HistoryViewerTimeline) -> Self {
|
||||
glib::Object::builder()
|
||||
.property("timeline", timeline)
|
||||
.build()
|
||||
}
|
||||
|
||||
/// Show the given media item.
|
||||
pub fn show_media(&self, item: &MediaItem) {
|
||||
let imp = self.imp();
|
||||
let event = item.event().unwrap();
|
||||
|
||||
let Some(AnyMessageLikeEventContent::RoomMessage(message)) = event.original_content()
|
||||
else {
|
||||
error!("Trying to open the media viewer with an event that is not a message");
|
||||
let Some(event) = item.event() else {
|
||||
return;
|
||||
};
|
||||
let Some(room) = event.room() else {
|
||||
return;
|
||||
};
|
||||
|
||||
imp.media_viewer.set_message(
|
||||
&event.room().unwrap(),
|
||||
event.matrix_event().0.event_id().into(),
|
||||
message.msgtype,
|
||||
);
|
||||
let imp = self.imp();
|
||||
imp.media_viewer
|
||||
.set_message(&room, event.event_id(), event.message_content());
|
||||
imp.media_viewer.reveal(item);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,10 +3,7 @@ use matrix_sdk::{
|
|||
media::{MediaEventContent, MediaThumbnailSize},
|
||||
ruma::{
|
||||
api::client::media::get_content_thumbnail::v3::Method,
|
||||
events::{
|
||||
room::message::{ImageMessageEventContent, MessageType, VideoMessageEventContent},
|
||||
AnyMessageLikeEventContent,
|
||||
},
|
||||
events::room::message::{ImageMessageEventContent, MessageType, VideoMessageEventContent},
|
||||
uint,
|
||||
},
|
||||
};
|
||||
|
@ -48,7 +45,7 @@ mod imp {
|
|||
Self::bind_template(klass);
|
||||
Self::Type::bind_template_callbacks(klass);
|
||||
|
||||
klass.set_css_name("mediahistoryvieweritem");
|
||||
klass.set_css_name("media-history-vieweritem");
|
||||
}
|
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) {
|
||||
|
@ -94,22 +91,14 @@ mod imp {
|
|||
let Some(session) = room.session() else {
|
||||
return;
|
||||
};
|
||||
match event.original_content() {
|
||||
Some(AnyMessageLikeEventContent::RoomMessage(message)) => match message.msgtype
|
||||
{
|
||||
MessageType::Image(content) => {
|
||||
obj.show_image(content, &session);
|
||||
}
|
||||
MessageType::Video(content) => {
|
||||
obj.show_video(content, &session);
|
||||
}
|
||||
_ => {
|
||||
panic!("Unexpected message type");
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
panic!("Unexpected message type");
|
||||
match event.message_content() {
|
||||
MessageType::Image(content) => {
|
||||
obj.show_image(content, &session);
|
||||
}
|
||||
MessageType::Video(content) => {
|
||||
obj.show_video(content, &session);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,11 +7,13 @@ mod media;
|
|||
mod media_item;
|
||||
mod timeline;
|
||||
|
||||
pub use self::{audio::AudioHistoryViewer, file::FileHistoryViewer, media::MediaHistoryViewer};
|
||||
pub use self::{
|
||||
audio::AudioHistoryViewer, file::FileHistoryViewer, media::MediaHistoryViewer,
|
||||
timeline::HistoryViewerTimeline,
|
||||
};
|
||||
use self::{
|
||||
audio_row::AudioRow,
|
||||
event::HistoryViewerEvent,
|
||||
event::{HistoryViewerEvent, HistoryViewerEventType},
|
||||
file_row::FileRow,
|
||||
media_item::MediaItem,
|
||||
timeline::{Timeline, TimelineFilter},
|
||||
};
|
||||
|
|
|
@ -4,7 +4,7 @@ use matrix_sdk::{
|
|||
ruma::{
|
||||
api::client::filter::{RoomEventFilter, UrlFilter},
|
||||
assign,
|
||||
events::{room::message::MessageType, AnyMessageLikeEventContent, MessageLikeEventType},
|
||||
events::MessageLikeEventType,
|
||||
uint,
|
||||
},
|
||||
};
|
||||
|
@ -16,15 +16,6 @@ use crate::{
|
|||
spawn_tokio,
|
||||
};
|
||||
|
||||
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, glib::Enum)]
|
||||
#[enum_type(name = "ContentHistoryViewerTimelineFilter")]
|
||||
pub enum TimelineFilter {
|
||||
#[default]
|
||||
Media,
|
||||
Files,
|
||||
Audio,
|
||||
}
|
||||
|
||||
mod imp {
|
||||
use std::{
|
||||
cell::{Cell, OnceCell, RefCell},
|
||||
|
@ -36,32 +27,29 @@ mod imp {
|
|||
use super::*;
|
||||
|
||||
#[derive(Debug, Default, glib::Properties)]
|
||||
#[properties(wrapper_type = super::Timeline)]
|
||||
pub struct Timeline {
|
||||
#[properties(wrapper_type = super::HistoryViewerTimeline)]
|
||||
pub struct HistoryViewerTimeline {
|
||||
/// The room that this timeline belongs to.
|
||||
#[property(get, construct_only)]
|
||||
pub room: OnceCell<Room>,
|
||||
/// The state of this timeline.
|
||||
#[property(get, builder(TimelineState::default()))]
|
||||
pub state: Cell<TimelineState>,
|
||||
/// The filter applied to this timeline.
|
||||
#[property(get, construct_only, builder(TimelineFilter::default()))]
|
||||
pub filter: Cell<TimelineFilter>,
|
||||
pub list: RefCell<Vec<HistoryViewerEvent>>,
|
||||
pub last_token: Arc<Mutex<String>>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for Timeline {
|
||||
const NAME: &'static str = "ContentHistoryViewerTimeline";
|
||||
type Type = super::Timeline;
|
||||
impl ObjectSubclass for HistoryViewerTimeline {
|
||||
const NAME: &'static str = "HistoryViewerTimeline";
|
||||
type Type = super::HistoryViewerTimeline;
|
||||
type Interfaces = (gio::ListModel,);
|
||||
}
|
||||
|
||||
#[glib::derived_properties]
|
||||
impl ObjectImpl for Timeline {}
|
||||
impl ObjectImpl for HistoryViewerTimeline {}
|
||||
|
||||
impl ListModelImpl for Timeline {
|
||||
impl ListModelImpl for HistoryViewerTimeline {
|
||||
fn item_type(&self) -> glib::Type {
|
||||
HistoryViewerEvent::static_type()
|
||||
}
|
||||
|
@ -79,19 +67,19 @@ mod imp {
|
|||
}
|
||||
|
||||
glib::wrapper! {
|
||||
/// A room timeline that filters its events by media type.
|
||||
pub struct Timeline(ObjectSubclass<imp::Timeline>)
|
||||
/// A room timeline for the history viewers.
|
||||
pub struct HistoryViewerTimeline(ObjectSubclass<imp::HistoryViewerTimeline>)
|
||||
@implements gio::ListModel;
|
||||
}
|
||||
|
||||
impl Timeline {
|
||||
pub fn new(room: &Room, filter: TimelineFilter) -> Self {
|
||||
glib::Object::builder()
|
||||
.property("room", room)
|
||||
.property("filter", filter)
|
||||
.build()
|
||||
impl HistoryViewerTimeline {
|
||||
pub fn new(room: &Room) -> Self {
|
||||
glib::Object::builder().property("room", room).build()
|
||||
}
|
||||
|
||||
/// Load more events in the timeline.
|
||||
///
|
||||
/// Returns `true` if more events can be loaded.
|
||||
pub async fn load(&self) -> bool {
|
||||
let imp = self.imp();
|
||||
|
||||
|
@ -143,37 +131,7 @@ impl Timeline {
|
|||
let events: Vec<HistoryViewerEvent> = events
|
||||
.chunk
|
||||
.into_iter()
|
||||
.filter_map(|event| {
|
||||
let event = HistoryViewerEvent::try_new(event, &room)?;
|
||||
|
||||
match event.original_content() {
|
||||
Some(AnyMessageLikeEventContent::RoomMessage(content)) => {
|
||||
match self.filter() {
|
||||
TimelineFilter::Media
|
||||
if matches!(content.msgtype, MessageType::Image(_))
|
||||
|| matches!(
|
||||
content.msgtype,
|
||||
MessageType::Video(_)
|
||||
) =>
|
||||
{
|
||||
Some(event)
|
||||
}
|
||||
TimelineFilter::Files
|
||||
if matches!(content.msgtype, MessageType::File(_)) =>
|
||||
{
|
||||
Some(event)
|
||||
}
|
||||
TimelineFilter::Audio
|
||||
if matches!(content.msgtype, MessageType::Audio(_)) =>
|
||||
{
|
||||
Some(event)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.filter_map(|event| HistoryViewerEvent::try_new(&room, event))
|
||||
.collect();
|
||||
|
||||
self.append(events);
|
||||
|
@ -187,7 +145,7 @@ impl Timeline {
|
|||
}
|
||||
},
|
||||
Err(error) => {
|
||||
error!("Failed to load events: {}", error);
|
||||
error!("Failed to load history viewer timeline events: {error}");
|
||||
self.set_state(TimelineState::Error);
|
||||
false
|
||||
}
|
||||
|
|
|
@ -10,7 +10,9 @@ use gtk::{glib, CompositeTemplate};
|
|||
|
||||
pub use self::{
|
||||
general_page::GeneralPage,
|
||||
history_viewer::{AudioHistoryViewer, FileHistoryViewer, MediaHistoryViewer},
|
||||
history_viewer::{
|
||||
AudioHistoryViewer, FileHistoryViewer, HistoryViewerTimeline, MediaHistoryViewer,
|
||||
},
|
||||
invite_subpage::InviteSubpage,
|
||||
members_page::MembersPage,
|
||||
};
|
||||
|
@ -26,7 +28,10 @@ pub enum SubpageName {
|
|||
}
|
||||
|
||||
mod imp {
|
||||
use std::{cell::RefCell, collections::HashMap};
|
||||
use std::{
|
||||
cell::{OnceCell, RefCell},
|
||||
collections::HashMap,
|
||||
};
|
||||
|
||||
use glib::subclass::InitializingObject;
|
||||
|
||||
|
@ -39,6 +44,9 @@ mod imp {
|
|||
/// The room to show the details for.
|
||||
#[property(get, construct_only)]
|
||||
pub room: RefCell<Option<Room>>,
|
||||
/// The timeline for the history viewers.
|
||||
#[property(get = Self::timeline)]
|
||||
pub timeline: OnceCell<HistoryViewerTimeline>,
|
||||
/// The subpages that are loaded.
|
||||
///
|
||||
/// We keep them around to avoid reloading them if the user reopens the
|
||||
|
@ -81,6 +89,20 @@ mod imp {
|
|||
impl WindowImpl for RoomDetails {}
|
||||
impl AdwWindowImpl for RoomDetails {}
|
||||
impl PreferencesWindowImpl for RoomDetails {}
|
||||
|
||||
impl RoomDetails {
|
||||
/// The timeline for the history viewers.
|
||||
fn timeline(&self) -> HistoryViewerTimeline {
|
||||
self.timeline
|
||||
.get_or_init(|| {
|
||||
let room = self.room.borrow().clone().expect(
|
||||
"timeline should not be requested before RoomDetails is constructed",
|
||||
);
|
||||
HistoryViewerTimeline::new(&room)
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
|
@ -108,9 +130,9 @@ impl RoomDetails {
|
|||
let subpage = subpages.entry(name).or_insert_with(|| match name {
|
||||
SubpageName::Members => MembersPage::new(&room).upcast(),
|
||||
SubpageName::Invite => InviteSubpage::new(&room).upcast(),
|
||||
SubpageName::MediaHistory => MediaHistoryViewer::new(&room).upcast(),
|
||||
SubpageName::FileHistory => FileHistoryViewer::new(&room).upcast(),
|
||||
SubpageName::AudioHistory => AudioHistoryViewer::new(&room).upcast(),
|
||||
SubpageName::MediaHistory => MediaHistoryViewer::new(&self.timeline()).upcast(),
|
||||
SubpageName::FileHistory => FileHistoryViewer::new(&self.timeline()).upcast(),
|
||||
SubpageName::AudioHistory => AudioHistoryViewer::new(&self.timeline()).upcast(),
|
||||
});
|
||||
|
||||
if is_initial {
|
||||
|
|
Loading…
Reference in New Issue