fractal/session/view/content/room_details/history_viewer/
file.rs

1use adw::{prelude::*, subclass::prelude::*};
2use gtk::{glib, glib::clone, CompositeTemplate};
3use tracing::error;
4
5use super::{FileRow, HistoryViewerEvent, HistoryViewerEventType, HistoryViewerTimeline};
6use crate::{
7    components::LoadingRow,
8    prelude::*,
9    spawn,
10    utils::{BoundConstructOnlyObject, LoadingState},
11};
12
13/// The minimum number of items that should be loaded.
14const MIN_N_ITEMS: u32 = 20;
15
16mod imp {
17    use std::ops::ControlFlow;
18
19    use glib::subclass::InitializingObject;
20
21    use super::*;
22
23    #[derive(Debug, Default, CompositeTemplate, glib::Properties)]
24    #[template(
25        resource = "/org/gnome/Fractal/ui/session/view/content/room_details/history_viewer/file.ui"
26    )]
27    #[properties(wrapper_type = super::FileHistoryViewer)]
28    pub struct FileHistoryViewer {
29        #[template_child]
30        stack: TemplateChild<gtk::Stack>,
31        #[template_child]
32        list_view: TemplateChild<gtk::ListView>,
33        /// The timeline containing the file events.
34        #[property(get, set = Self::set_timeline, construct_only)]
35        timeline: BoundConstructOnlyObject<HistoryViewerTimeline>,
36    }
37
38    #[glib::object_subclass]
39    impl ObjectSubclass for FileHistoryViewer {
40        const NAME: &'static str = "ContentFileHistoryViewer";
41        type Type = super::FileHistoryViewer;
42        type ParentType = adw::NavigationPage;
43
44        fn class_init(klass: &mut Self::Class) {
45            Self::bind_template(klass);
46            Self::bind_template_callbacks(klass);
47
48            klass.set_css_name("file-history-viewer");
49        }
50
51        fn instance_init(obj: &InitializingObject<Self>) {
52            obj.init_template();
53        }
54    }
55
56    #[glib::derived_properties]
57    impl ObjectImpl for FileHistoryViewer {
58        fn constructed(&self) {
59            self.parent_constructed();
60
61            let factory = gtk::SignalListItemFactory::new();
62
63            factory.connect_bind(move |_, list_item| {
64                let Some(list_item) = list_item.downcast_ref::<gtk::ListItem>() else {
65                    error!("List item factory did not receive a list item: {list_item:?}");
66                    return;
67                };
68
69                list_item.set_activatable(false);
70                list_item.set_selectable(false);
71            });
72            factory.connect_bind(move |_, list_item| {
73                let Some(list_item) = list_item.downcast_ref::<gtk::ListItem>() else {
74                    error!("List item factory did not receive a list item: {list_item:?}");
75                    return;
76                };
77
78                let item = list_item.item();
79
80                if let Some(loading_row) = item
81                    .and_downcast_ref::<LoadingRow>()
82                    .filter(|_| !list_item.child().is_some_and(|c| c.is::<LoadingRow>()))
83                {
84                    loading_row.unparent();
85                    loading_row.set_width_request(-1);
86                    loading_row.set_height_request(-1);
87
88                    list_item.set_child(Some(loading_row));
89                } else if let Some(event) = item.and_downcast::<HistoryViewerEvent>() {
90                    let file_row = list_item.child_or_default::<FileRow>();
91                    file_row.set_event(Some(event));
92                }
93            });
94
95            self.list_view.set_factory(Some(&factory));
96        }
97    }
98
99    impl WidgetImpl for FileHistoryViewer {}
100    impl NavigationPageImpl for FileHistoryViewer {}
101
102    #[gtk::template_callbacks]
103    impl FileHistoryViewer {
104        /// Set the timeline containing the media events.
105        fn set_timeline(&self, timeline: HistoryViewerTimeline) {
106            let filter = gtk::CustomFilter::new(|obj| {
107                obj.downcast_ref::<HistoryViewerEvent>()
108                    .is_some_and(|e| e.event_type() == HistoryViewerEventType::File)
109                    || obj.is::<LoadingRow>()
110            });
111            let filter_model =
112                gtk::FilterListModel::new(Some(timeline.with_loading_item().clone()), Some(filter));
113
114            let model = gtk::NoSelection::new(Some(filter_model));
115            model.connect_items_changed(clone!(
116                #[weak(rename_to = imp)]
117                self,
118                move |_, _, _, _| {
119                    imp.update_state();
120                }
121            ));
122            self.list_view.set_model(Some(&model));
123
124            let timeline_state_handler = timeline.connect_state_notify(clone!(
125                #[weak(rename_to = imp)]
126                self,
127                move |_| {
128                    imp.update_state();
129                }
130            ));
131
132            self.timeline.set(timeline, vec![timeline_state_handler]);
133            self.update_state();
134
135            spawn!(clone!(
136                #[weak(rename_to = imp)]
137                self,
138                async move {
139                    imp.init_timeline().await;
140                }
141            ));
142        }
143
144        /// Initialize the timeline.
145        async fn init_timeline(&self) {
146            // Load an initial number of items.
147            self.load_more_items().await;
148
149            let adj = self
150                .list_view
151                .vadjustment()
152                .expect("GtkListView has a vadjustment");
153            adj.connect_value_notify(clone!(
154                #[weak(rename_to = imp)]
155                self,
156                move |_| {
157                    if imp.needs_more_items() {
158                        spawn!(async move {
159                            imp.load_more_items().await;
160                        });
161                    }
162                }
163            ));
164        }
165
166        /// Load more items in this viewer.
167        #[template_callback]
168        async fn load_more_items(&self) {
169            self.timeline
170                .obj()
171                .load(clone!(
172                    #[weak(rename_to = imp)]
173                    self,
174                    #[upgrade_or]
175                    ControlFlow::Break(()),
176                    move || {
177                        if imp.needs_more_items() {
178                            ControlFlow::Continue(())
179                        } else {
180                            ControlFlow::Break(())
181                        }
182                    }
183                ))
184                .await;
185        }
186
187        /// Whether this viewer needs more items.
188        fn needs_more_items(&self) -> bool {
189            let Some(model) = self.list_view.model() else {
190                return false;
191            };
192
193            // Make sure there is an initial number of items.
194            if model.n_items() < MIN_N_ITEMS {
195                return true;
196            }
197
198            let adj = self
199                .list_view
200                .vadjustment()
201                .expect("GtkListView has a vadjustment");
202            adj.value() + adj.page_size() * 2.0 >= adj.upper()
203        }
204
205        /// Update this viewer for the current state.
206        fn update_state(&self) {
207            let Some(model) = self.list_view.model() else {
208                return;
209            };
210            let timeline = self.timeline.obj();
211
212            let visible_child_name = match timeline.state() {
213                LoadingState::Initial => "loading",
214                LoadingState::Error => "error",
215                LoadingState::Ready if model.n_items() == 0 => "empty",
216                LoadingState::Loading => {
217                    if model.n_items() == 0
218                        || (model.n_items() == 1
219                            && model.item(0).is_some_and(|item| item.is::<LoadingRow>()))
220                    {
221                        "loading"
222                    } else {
223                        "content"
224                    }
225                }
226                LoadingState::Ready => "content",
227            };
228            self.stack.set_visible_child_name(visible_child_name);
229        }
230    }
231}
232
233glib::wrapper! {
234    /// A view presenting the list of file events in a room.
235    pub struct FileHistoryViewer(ObjectSubclass<imp::FileHistoryViewer>)
236        @extends gtk::Widget, adw::NavigationPage;
237}
238
239impl FileHistoryViewer {
240    pub fn new(timeline: &HistoryViewerTimeline) -> Self {
241        glib::Object::builder()
242            .property("timeline", timeline)
243            .build()
244    }
245}