diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml index 56621076..905296bc 100644 --- a/data/resources/resources.gresource.xml +++ b/data/resources/resources.gresource.xml @@ -74,6 +74,8 @@ ui/components-reaction-chooser.ui ui/components-toastable-window.ui ui/components-video-player.ui + ui/content-audio-history-viewer-row.ui + ui/content-audio-history-viewer.ui ui/content-completion-popover.ui ui/content-completion-row.ui ui/content-divider-row.ui diff --git a/data/resources/style.css b/data/resources/style.css index 69649669..8db10a23 100644 --- a/data/resources/style.css +++ b/data/resources/style.css @@ -666,6 +666,13 @@ filehistoryviewer listview > row { } +/* Audio History Viewer */ + +audiohistoryviewer listview > row { + border-radius: 0; +} + + /* Room Details */ .room-details listview { diff --git a/data/resources/ui/content-audio-history-viewer-row.ui b/data/resources/ui/content-audio-history-viewer-row.ui new file mode 100644 index 00000000..99bac0d3 --- /dev/null +++ b/data/resources/ui/content-audio-history-viewer-row.ui @@ -0,0 +1,45 @@ + + + + diff --git a/data/resources/ui/content-audio-history-viewer.ui b/data/resources/ui/content-audio-history-viewer.ui new file mode 100644 index 00000000..2cd55598 --- /dev/null +++ b/data/resources/ui/content-audio-history-viewer.ui @@ -0,0 +1,66 @@ + + + + diff --git a/data/resources/ui/content-room-details-general-page.ui b/data/resources/ui/content-room-details-general-page.ui index b5a31780..1fbd4099 100644 --- a/data/resources/ui/content-room-details-general-page.ui +++ b/data/resources/ui/content-room-details-general-page.ui @@ -171,6 +171,21 @@ + + + Audio + details.next-page + 'audio-history' + True + + + center + center + go-next-symbolic + + + + diff --git a/po/POTFILES.in b/po/POTFILES.in index a9debd9f..6a0859e8 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -18,6 +18,7 @@ data/resources/ui/account-settings.ui data/resources/ui/attachment-dialog.ui data/resources/ui/components-auth-dialog.ui data/resources/ui/components-loading-listbox-row.ui +data/resources/ui/content-audio-history-viewer.ui data/resources/ui/content-explore-servers-popover.ui data/resources/ui/content-explore.ui data/resources/ui/content-file-history-viewer.ui @@ -74,6 +75,7 @@ src/session/account_settings/user_page/mod.rs src/session/content/explore/public_room_row.rs src/session/content/invite.rs src/session/content/room_details/general_page/mod.rs +src/session/content/room_details/history_viewer/audio_row.rs src/session/content/room_details/history_viewer/file_row.rs src/session/content/room_details/invite_subpage/invitee_list.rs src/session/content/room_details/member_page/mod.rs diff --git a/src/session/content/room_details/history_viewer/audio.rs b/src/session/content/room_details/history_viewer/audio.rs new file mode 100644 index 00000000..1b543178 --- /dev/null +++ b/src/session/content/room_details/history_viewer/audio.rs @@ -0,0 +1,108 @@ +use adw::{prelude::*, subclass::prelude::*}; +use gtk::{glib, glib::clone, CompositeTemplate}; + +use crate::{ + session::{ + content::room_details::history_viewer::{AudioRow, Timeline, TimelineFilter}, + Room, + }, + spawn, +}; + +const MIN_N_ITEMS: u32 = 20; + +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-audio-history-viewer.ui")] + pub struct AudioHistoryViewer { + pub room_timeline: OnceCell, + #[template_child] + pub list_view: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for AudioHistoryViewer { + const NAME: &'static str = "ContentAudioHistoryViewer"; + type Type = super::AudioHistoryViewer; + type ParentType = adw::Bin; + + fn class_init(klass: &mut Self::Class) { + AudioRow::static_type(); + Self::bind_template(klass); + + klass.set_css_name("audiohistoryviewer"); + } + + fn instance_init(obj: &InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for AudioHistoryViewer { + fn properties() -> &'static [glib::ParamSpec] { + static PROPERTIES: Lazy> = Lazy::new(|| { + vec![glib::ParamSpecObject::builder::("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 AudioHistoryViewer {} + impl BinImpl for AudioHistoryViewer {} +} + +glib::wrapper! { + pub struct AudioHistoryViewer(ObjectSubclass) + @extends gtk::Widget, adw::Bin; +} + +impl AudioHistoryViewer { + 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::Audio); + let model = gtk::NoSelection::new(Some(timeline.clone())); + imp.list_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() + } +} diff --git a/src/session/content/room_details/history_viewer/audio_row.rs b/src/session/content/room_details/history_viewer/audio_row.rs new file mode 100644 index 00000000..e921ad56 --- /dev/null +++ b/src/session/content/room_details/history_viewer/audio_row.rs @@ -0,0 +1,119 @@ +use adw::{prelude::*, subclass::prelude::*}; +use gettextrs::gettext; +use gtk::{glib, CompositeTemplate}; +use matrix_sdk::ruma::events::{room::message::MessageType, AnyMessageLikeEventContent}; + +use crate::session::content::room_details::history_viewer::HistoryViewerEvent; + +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-audio-history-viewer-row.ui")] + pub struct AudioRow { + pub event: RefCell>, + #[template_child] + pub title_label: TemplateChild, + #[template_child] + pub duration_label: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for AudioRow { + const NAME: &'static str = "ContentAudioHistoryViewerRow"; + type Type = super::AudioRow; + type ParentType = adw::Bin; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + } + + fn instance_init(obj: &InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for AudioRow { + fn properties() -> &'static [glib::ParamSpec] { + static PROPERTIES: Lazy> = Lazy::new(|| { + vec![ + glib::ParamSpecObject::builder::("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!(), + } + } + } + + impl WidgetImpl for AudioRow {} + impl BinImpl for AudioRow {} +} + +glib::wrapper! { + pub struct AudioRow(ObjectSubclass) + @extends gtk::Widget, adw::Bin; +} + +impl AudioRow { + pub fn set_event(&self, event: Option) { + let imp = self.imp(); + + if self.event() == event { + return; + } + + if let Some(ref event) = event { + if let Some(AnyMessageLikeEventContent::RoomMessage(content)) = event.original_content() + { + if let MessageType::Audio(audio) = content.msgtype { + imp.title_label.set_label(&audio.body); + + if let Some(duration) = audio.info.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}") + }; + + imp.duration_label.set_label(&duration); + } else { + imp.duration_label.set_label(&gettext("Unknown duration")); + } + } + } + } + + imp.event.replace(event); + self.notify("event"); + } + + pub fn event(&self) -> Option { + self.imp().event.borrow().clone() + } +} diff --git a/src/session/content/room_details/history_viewer/mod.rs b/src/session/content/room_details/history_viewer/mod.rs index 0aae7670..ac59d435 100644 --- a/src/session/content/room_details/history_viewer/mod.rs +++ b/src/session/content/room_details/history_viewer/mod.rs @@ -1,3 +1,5 @@ +mod audio; +mod audio_row; mod event; mod file; mod file_row; @@ -5,10 +7,11 @@ mod media; mod media_item; mod timeline; +pub use self::{audio::AudioHistoryViewer, file::FileHistoryViewer, media::MediaHistoryViewer}; use self::{ + audio_row::AudioRow, event::HistoryViewerEvent, file_row::FileRow, media_item::MediaItem, timeline::{Timeline, TimelineFilter}, }; -pub use self::{file::FileHistoryViewer, media::MediaHistoryViewer}; diff --git a/src/session/content/room_details/mod.rs b/src/session/content/room_details/mod.rs index 12540fcb..59c10f5a 100644 --- a/src/session/content/room_details/mod.rs +++ b/src/session/content/room_details/mod.rs @@ -15,7 +15,9 @@ use crate::{ components::ToastableWindow, prelude::*, session::{ - content::room_details::history_viewer::{FileHistoryViewer, MediaHistoryViewer}, + content::room_details::history_viewer::{ + AudioHistoryViewer, FileHistoryViewer, MediaHistoryViewer, + }, Room, }, }; @@ -31,6 +33,7 @@ pub enum PageName { Invite, MediaHistory, FileHistory, + AudioHistory, } impl glib::variant::StaticVariantType for PageName { @@ -47,6 +50,7 @@ impl glib::variant::FromVariant for PageName { "invite" => Some(PageName::Invite), "media-history" => Some(PageName::MediaHistory), "file-history" => Some(PageName::FileHistory), + "audio-history" => Some(PageName::AudioHistory), "" => Some(PageName::None), _ => None, } @@ -62,6 +66,7 @@ impl glib::variant::ToVariant for PageName { PageName::Invite => "invite", PageName::MediaHistory => "media-history", PageName::FileHistory => "file-history", + PageName::AudioHistory => "audio-history", } .to_variant() } @@ -279,6 +284,22 @@ impl RoomDetails { self.set_title(Some(&gettext("File"))); imp.main_stack.set_visible_child(&file_page); } + PageName::AudioHistory => { + let audio_page = if let Some(audio_page) = list_stack_children + .get(&PageName::AudioHistory) + .and_then(glib::object::WeakRef::upgrade) + { + audio_page + } else { + let audio_page = AudioHistoryViewer::new(self.room()).upcast::(); + list_stack_children.insert(PageName::AudioHistory, audio_page.downgrade()); + imp.main_stack.add_child(&audio_page); + audio_page + }; + + self.set_title(Some(&gettext("Audio"))); + imp.main_stack.set_visible_child(&audio_page); + } PageName::None => { warn!("Can't switch to PageName::None"); }