history-viewer: Use a single timeline for all the viewers

Reduces the number of requests to the server.
This commit is contained in:
Kévin Commaille 2023-12-22 17:43:46 +01:00
parent 1bac6724ad
commit 6ab0cfd33a
No known key found for this signature in database
GPG Key ID: 29A48C1F03620416
11 changed files with 334 additions and 308 deletions

View File

@ -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;
}

View File

@ -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()
}
}

View File

@ -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;
}));
}
}
}

View File

@ -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
}
}

View File

@ -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()
}
}

View File

@ -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"));
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
_ => {}
}
}

View File

@ -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},
};

View File

@ -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
}

View File

@ -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 {