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 @@
+
+
+
+
+
+ vertical
+
+
+
+
+
+ never
+ True
+
+
+ 400
+ 400
+
+
+ True
+
+
+
+
+
+ False
+
+
+ 6
+ 6
+
+ GtkListItem
+
+
+
+
+
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
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