diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index a1de5846..56621076 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -81,6 +81,8 @@
ui/content-explore-server-row.ui
ui/content-explore-servers-popover.ui
ui/content-explore.ui
+ ui/content-file-history-viewer-row.ui
+ ui/content-file-history-viewer.ui
ui/content-invite-subpage.ui
ui/content-invite.ui
ui/content-invitee-item.ui
diff --git a/data/resources/style.css b/data/resources/style.css
index 33c1c902..69649669 100644
--- a/data/resources/style.css
+++ b/data/resources/style.css
@@ -659,6 +659,13 @@ mediahistoryvieweritem > overlay > image {
}
+/* File History Viewer */
+
+filehistoryviewer listview > row {
+ border-radius: 0;
+}
+
+
/* Room Details */
.room-details listview {
diff --git a/data/resources/ui/content-file-history-viewer-row.ui b/data/resources/ui/content-file-history-viewer-row.ui
new file mode 100644
index 00000000..ea0c619d
--- /dev/null
+++ b/data/resources/ui/content-file-history-viewer-row.ui
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
diff --git a/data/resources/ui/content-file-history-viewer.ui b/data/resources/ui/content-file-history-viewer.ui
new file mode 100644
index 00000000..a9cfcb37
--- /dev/null
+++ b/data/resources/ui/content-file-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 625cf0ac..b5a31780 100644
--- a/data/resources/ui/content-room-details-general-page.ui
+++ b/data/resources/ui/content-room-details-general-page.ui
@@ -156,6 +156,21 @@
+
+
+ File
+ details.next-page
+ 'file-history'
+ True
+
+
+ center
+ center
+ go-next-symbolic
+
+
+
+
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 1f1b68db..a9debd9f 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -20,6 +20,7 @@ data/resources/ui/components-auth-dialog.ui
data/resources/ui/components-loading-listbox-row.ui
data/resources/ui/content-explore-servers-popover.ui
data/resources/ui/content-explore.ui
+data/resources/ui/content-file-history-viewer.ui
data/resources/ui/content-invite-subpage.ui
data/resources/ui/content-invite.ui
data/resources/ui/content-media-history-viewer.ui
@@ -73,6 +74,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/file_row.rs
src/session/content/room_details/invite_subpage/invitee_list.rs
src/session/content/room_details/member_page/mod.rs
src/session/content/room_details/mod.rs
diff --git a/src/session/content/room_details/history_viewer/file.rs b/src/session/content/room_details/history_viewer/file.rs
new file mode 100644
index 00000000..e984b9f9
--- /dev/null
+++ b/src/session/content/room_details/history_viewer/file.rs
@@ -0,0 +1,108 @@
+use adw::{prelude::*, subclass::prelude::*};
+use gtk::{glib, glib::clone, CompositeTemplate};
+
+use crate::{
+ session::{
+ content::room_details::history_viewer::{FileRow, 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-file-history-viewer.ui")]
+ pub struct FileHistoryViewer {
+ pub room_timeline: OnceCell,
+ #[template_child]
+ pub list_view: TemplateChild,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for FileHistoryViewer {
+ const NAME: &'static str = "ContentFileHistoryViewer";
+ type Type = super::FileHistoryViewer;
+ type ParentType = adw::Bin;
+
+ fn class_init(klass: &mut Self::Class) {
+ FileRow::static_type();
+ Self::bind_template(klass);
+
+ klass.set_css_name("filehistoryviewer");
+ }
+
+ fn instance_init(obj: &InitializingObject) {
+ obj.init_template();
+ }
+ }
+
+ impl ObjectImpl for FileHistoryViewer {
+ 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 FileHistoryViewer {}
+ impl BinImpl for FileHistoryViewer {}
+}
+
+glib::wrapper! {
+ pub struct FileHistoryViewer(ObjectSubclass)
+ @extends gtk::Widget, adw::Bin;
+}
+
+impl FileHistoryViewer {
+ 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::Files);
+ 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/file_row.rs b/src/session/content/room_details/history_viewer/file_row.rs
new file mode 100644
index 00000000..d624f44c
--- /dev/null
+++ b/src/session/content/room_details/history_viewer/file_row.rs
@@ -0,0 +1,109 @@
+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-file-history-viewer-row.ui")]
+ pub struct FileRow {
+ pub event: RefCell