fractal/session_view/
content.rs1use adw::{prelude::*, subclass::prelude::*};
2use gtk::{glib, glib::clone};
3
4use super::{Explore, Invite, InviteRequest, RoomHistory};
5use crate::{
6 identity_verification_view::IdentityVerificationView,
7 session::{
8 IdentityVerification, Room, RoomCategory, Session, SidebarIconItem, SidebarIconItemType,
9 },
10 utils::BoundObject,
11};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::EnumString, strum::AsRefStr)]
15#[strum(serialize_all = "kebab-case")]
16enum ContentPage {
17 Empty,
19 RoomHistory,
21 InviteRequest,
23 Invite,
25 Explore,
27 Verification,
29}
30
31mod imp {
32 use std::cell::{Cell, RefCell};
33
34 use glib::subclass::InitializingObject;
35
36 use super::*;
37
38 #[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)]
39 #[template(resource = "/org/gnome/Fractal/ui/session_view/content.ui")]
40 #[properties(wrapper_type = super::Content)]
41 pub struct Content {
42 #[template_child]
43 stack: TemplateChild<gtk::Stack>,
44 #[template_child]
45 room_history: TemplateChild<RoomHistory>,
46 #[template_child]
47 invite_request: TemplateChild<InviteRequest>,
48 #[template_child]
49 invite: TemplateChild<Invite>,
50 #[template_child]
51 explore: TemplateChild<Explore>,
52 #[template_child]
53 empty_page: TemplateChild<adw::ToolbarView>,
54 #[template_child]
55 empty_page_header_bar: TemplateChild<adw::HeaderBar>,
56 #[template_child]
57 verification_page: TemplateChild<adw::ToolbarView>,
58 #[template_child]
59 verification_page_header_bar: TemplateChild<adw::HeaderBar>,
60 #[template_child]
61 identity_verification_widget: TemplateChild<IdentityVerificationView>,
62 #[property(get, set = Self::set_session, explicit_notify, nullable)]
64 session: glib::WeakRef<Session>,
65 #[property(get, set)]
67 only_view: Cell<bool>,
68 item_binding: RefCell<Option<glib::Binding>>,
69 #[property(get, set = Self::set_item, explicit_notify, nullable)]
71 item: BoundObject<glib::Object>,
72 }
73
74 #[glib::object_subclass]
75 impl ObjectSubclass for Content {
76 const NAME: &'static str = "Content";
77 type Type = super::Content;
78 type ParentType = adw::NavigationPage;
79
80 fn class_init(klass: &mut Self::Class) {
81 Self::bind_template(klass);
82
83 klass.set_accessible_role(gtk::AccessibleRole::Group);
84 }
85
86 fn instance_init(obj: &InitializingObject<Self>) {
87 obj.init_template();
88 }
89 }
90
91 #[glib::derived_properties]
92 impl ObjectImpl for Content {
93 fn constructed(&self) {
94 self.parent_constructed();
95
96 self.stack.connect_visible_child_notify(clone!(
97 #[weak(rename_to = imp)]
98 self,
99 move |_| {
100 if imp.visible_page() != ContentPage::Verification {
101 imp.identity_verification_widget
102 .set_verification(None::<IdentityVerification>);
103 }
104 }
105 ));
106 }
107
108 fn dispose(&self) {
109 if let Some(binding) = self.item_binding.take() {
110 binding.unbind();
111 }
112 }
113 }
114
115 impl WidgetImpl for Content {}
116
117 impl NavigationPageImpl for Content {
118 fn hidden(&self) {
119 self.obj().set_item(None::<glib::Object>);
120 }
121 }
122
123 impl Content {
124 pub(super) fn visible_page(&self) -> ContentPage {
126 self.stack
127 .visible_child_name()
128 .expect("stack should always have a visible child name")
129 .as_str()
130 .try_into()
131 .expect("stack child name should be convertible to a ContentPage")
132 }
133
134 fn set_visible_page(&self, page: ContentPage) {
136 if self.visible_page() == page {
137 return;
138 }
139
140 self.stack.set_visible_child_name(page.as_ref());
141 }
142
143 fn set_session(&self, session: Option<&Session>) {
145 if session == self.session.upgrade().as_ref() {
146 return;
147 }
148 let obj = self.obj();
149
150 if let Some(binding) = self.item_binding.take() {
151 binding.unbind();
152 }
153
154 if let Some(session) = session {
155 let item_binding = session
156 .sidebar_list_model()
157 .selection_model()
158 .bind_property("selected-item", &*obj, "item")
159 .sync_create()
160 .bidirectional()
161 .build();
162
163 self.item_binding.replace(Some(item_binding));
164 }
165
166 self.session.set(session);
167 obj.notify_session();
168 }
169
170 fn set_item(&self, item: Option<glib::Object>) {
172 if self.item.obj() == item {
173 return;
174 }
175
176 self.item.disconnect_signals();
177
178 if let Some(item) = item {
179 let handler = if let Some(room) = item.downcast_ref::<Room>() {
180 let category_handler = room.connect_category_notify(clone!(
181 #[weak(rename_to = imp)]
182 self,
183 move |_| {
184 imp.update_visible_child();
185 }
186 ));
187
188 Some(category_handler)
189 } else if let Some(verification) = item.downcast_ref::<IdentityVerification>() {
190 let dismiss_handler = verification.connect_dismiss(clone!(
191 #[weak(rename_to = imp)]
192 self,
193 move |_| {
194 imp.set_item(None);
195 }
196 ));
197
198 Some(dismiss_handler)
199 } else {
200 None
201 };
202
203 self.item.set(item, handler.into_iter().collect());
204 }
205
206 self.update_visible_child();
207 self.obj().notify_item();
208
209 if let Some(page) = self.stack.visible_child() {
210 page.grab_focus();
211 }
212 }
213
214 fn update_visible_child(&self) {
216 let Some(item) = self.item.obj() else {
217 self.set_visible_page(ContentPage::Empty);
218 return;
219 };
220
221 if let Some(room) = item.downcast_ref::<Room>() {
222 match room.category() {
223 RoomCategory::Knocked => {
224 self.invite_request.set_room(Some(room.clone()));
225 self.set_visible_page(ContentPage::InviteRequest);
226 }
227 RoomCategory::Invited => {
228 self.invite.set_room(Some(room.clone()));
229 self.set_visible_page(ContentPage::Invite);
230 }
231 _ => {
232 self.room_history.set_timeline(Some(room.live_timeline()));
233 self.set_visible_page(ContentPage::RoomHistory);
234 }
235 }
236 } else if item
237 .downcast_ref::<SidebarIconItem>()
238 .is_some_and(|i| i.item_type() == SidebarIconItemType::Explore)
239 {
240 self.set_visible_page(ContentPage::Explore);
241 } else if let Some(verification) = item.downcast_ref::<IdentityVerification>() {
242 self.identity_verification_widget
243 .set_verification(Some(verification.clone()));
244 self.set_visible_page(ContentPage::Verification);
245 }
246 }
247
248 pub(super) fn handle_paste_action(&self) {
250 if self.visible_page() == ContentPage::RoomHistory {
251 self.room_history.handle_paste_action();
252 }
253 }
254
255 pub(super) fn header_bars(&self) -> [&adw::HeaderBar; 6] {
257 [
258 &self.empty_page_header_bar,
259 self.room_history.header_bar(),
260 self.invite_request.header_bar(),
261 self.invite.header_bar(),
262 self.explore.header_bar(),
263 &self.verification_page_header_bar,
264 ]
265 }
266 }
267}
268
269glib::wrapper! {
270 pub struct Content(ObjectSubclass<imp::Content>)
272 @extends gtk::Widget, adw::NavigationPage,
273 @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
274}
275
276impl Content {
277 pub fn new(session: &Session) -> Self {
278 glib::Object::builder().property("session", session).build()
279 }
280
281 pub(crate) fn handle_paste_action(&self) {
283 self.imp().handle_paste_action();
284 }
285
286 pub(crate) fn header_bars(&self) -> [&adw::HeaderBar; 6] {
288 self.imp().header_bars()
289 }
290}