diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index 2aeb0ab5..a1de5846 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -85,6 +85,8 @@
ui/content-invite.ui
ui/content-invitee-item.ui
ui/content-invitee-row.ui
+ ui/content-media-history-viewer-item.ui
+ ui/content-media-history-viewer.ui
ui/content-member-item.ui
ui/content-member-page-list-view.ui
ui/content-member-page-membership-subpage-row.ui
diff --git a/data/resources/style.css b/data/resources/style.css
index 3b2e050b..33c1c902 100644
--- a/data/resources/style.css
+++ b/data/resources/style.css
@@ -620,6 +620,45 @@ typing-bar avatar {
border-radius: 6px;
}
+/* Media History Viewer */
+
+mediahistoryviewer {
+ background: black;
+ color: white;
+}
+
+mediahistoryviewer headerbar {
+ background: none;
+ box-shadow: none;
+}
+
+mediahistoryviewer gridview {
+ background: none;
+ padding: 2px;
+}
+
+mediahistoryviewer 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 {
+ transform: scale(1.03);
+}
+
+mediahistoryviewer gridview > child:active {
+ transform: scale(0.98);
+}
+
+mediahistoryvieweritem > overlay > image {
+ border-radius: 100%;
+ padding: 12px;
+ -gtk-icon-size: 24px;
+}
+
+
/* Room Details */
.room-details listview {
diff --git a/data/resources/ui/content-media-history-viewer-item.ui b/data/resources/ui/content-media-history-viewer-item.ui
new file mode 100644
index 00000000..482f3971
--- /dev/null
+++ b/data/resources/ui/content-media-history-viewer-item.ui
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
diff --git a/data/resources/ui/content-media-history-viewer.ui b/data/resources/ui/content-media-history-viewer.ui
new file mode 100644
index 00000000..1ab66e74
--- /dev/null
+++ b/data/resources/ui/content-media-history-viewer.ui
@@ -0,0 +1,70 @@
+
+
+
+
+
+ vertical
+
+
+
+
+
+ never
+ True
+
+
+
+ 1000
+ 800
+ natural
+
+
+ 2
+ 5
+
+
+
+
+
+
+
+ 150
+ 150
+
+ GtkListItem
+
+
+
+
+
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/data/resources/ui/content-room-details-general-page.ui b/data/resources/ui/content-room-details-general-page.ui
index 0cafd7bb..625cf0ac 100644
--- a/data/resources/ui/content-room-details-general-page.ui
+++ b/data/resources/ui/content-room-details-general-page.ui
@@ -139,6 +139,25 @@
+
+
+
+
+ Media
+ details.next-page
+ 'media-history'
+ True
+
+
+ center
+ center
+ go-next-symbolic
+
+
+
+
+
+
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 8bb43cf8..1f1b68db 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -22,6 +22,7 @@ data/resources/ui/content-explore-servers-popover.ui
data/resources/ui/content-explore.ui
data/resources/ui/content-invite-subpage.ui
data/resources/ui/content-invite.ui
+data/resources/ui/content-media-history-viewer.ui
data/resources/ui/content-member-page.ui
data/resources/ui/content-message-file.ui
data/resources/ui/content-message-row.ui
diff --git a/src/session/content/room_details/history_viewer/media.rs b/src/session/content/room_details/history_viewer/media.rs
new file mode 100644
index 00000000..b931d6d6
--- /dev/null
+++ b/src/session/content/room_details/history_viewer/media.rs
@@ -0,0 +1,108 @@
+use adw::{prelude::*, subclass::prelude::*};
+use gtk::{glib, glib::clone, CompositeTemplate};
+
+use crate::{
+ session::{
+ content::room_details::history_viewer::{MediaItem, Timeline, TimelineFilter},
+ Room,
+ },
+ spawn,
+};
+
+const MIN_N_ITEMS: u32 = 50;
+
+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-media-history-viewer.ui")]
+ pub struct MediaHistoryViewer {
+ pub room_timeline: OnceCell,
+ #[template_child]
+ pub grid_view: TemplateChild,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for MediaHistoryViewer {
+ const NAME: &'static str = "ContentMediaHistoryViewer";
+ type Type = super::MediaHistoryViewer;
+ type ParentType = adw::Bin;
+
+ fn class_init(klass: &mut Self::Class) {
+ MediaItem::static_type();
+ Self::bind_template(klass);
+
+ klass.set_css_name("mediahistoryviewer");
+ }
+
+ fn instance_init(obj: &InitializingObject) {
+ obj.init_template();
+ }
+ }
+
+ impl ObjectImpl for MediaHistoryViewer {
+ 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 MediaHistoryViewer {}
+ impl BinImpl for MediaHistoryViewer {}
+}
+
+glib::wrapper! {
+ pub struct MediaHistoryViewer(ObjectSubclass)
+ @extends gtk::Widget, adw::Bin;
+}
+
+impl MediaHistoryViewer {
+ 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::Media);
+ let model = gtk::NoSelection::new(Some(timeline.clone()));
+ imp.grid_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/media_item.rs b/src/session/content/room_details/history_viewer/media_item.rs
new file mode 100644
index 00000000..3917b5c9
--- /dev/null
+++ b/src/session/content/room_details/history_viewer/media_item.rs
@@ -0,0 +1,227 @@
+use gtk::{gdk, glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate};
+use log::warn;
+use matrix_sdk::{
+ media::{MediaEventContent, MediaThumbnailSize},
+ ruma::{
+ api::client::media::get_content_thumbnail::v3::Method,
+ events::{
+ room::message::{ImageMessageEventContent, MessageType, VideoMessageEventContent},
+ AnyMessageLikeEventContent,
+ },
+ uint,
+ },
+};
+
+use crate::{
+ session::content::room_details::history_viewer::HistoryViewerEvent, spawn, spawn_tokio, Session,
+};
+
+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-media-history-viewer-item.ui")]
+ pub struct MediaItem {
+ pub event: RefCell