From bfcf5550cf4f026f422affc9845a3d6fe9756d35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Wed, 16 Jul 2025 10:23:32 +0200 Subject: [PATCH] sidebar: Show our invite requests --- po/POTFILES.in | 2 + src/session/model/room/category.rs | 8 +- src/session/model/room/mod.rs | 8 +- src/session/model/room_list/mod.rs | 17 ++ src/session/model/session_settings.rs | 1 + src/session/model/sidebar_data/item_list.rs | 19 +- .../model/sidebar_data/section/name.rs | 7 +- src/session/view/content/invite_request.rs | 200 ++++++++++++++++++ src/session/view/content/invite_request.ui | 137 ++++++++++++ src/session/view/content/mod.rs | 38 ++-- src/session/view/content/mod.ui | 9 + src/session/view/session_view.rs | 7 +- src/session/view/sidebar/mod.ui | 5 + src/session/view/sidebar/row.rs | 17 ++ src/ui-resources.gresource.xml | 1 + 15 files changed, 450 insertions(+), 26 deletions(-) create mode 100644 src/session/view/content/invite_request.rs create mode 100644 src/session/view/content/invite_request.ui diff --git a/po/POTFILES.in b/po/POTFILES.in index f8825122..b65c8905 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -113,6 +113,8 @@ src/session/view/content/explore/servers_popover.ui src/session/view/content/explore/server_row.ui src/session/view/content/invite.rs src/session/view/content/invite.ui +src/session/view/content/invite_request.rs +src/session/view/content/invite_request.ui src/session/view/content/mod.ui src/session/view/content/room_details/addresses_subpage/completion_popover.ui src/session/view/content/room_details/addresses_subpage/mod.rs diff --git a/src/session/model/room/category.rs b/src/session/model/room/category.rs index 03bee92d..9b523f0a 100644 --- a/src/session/model/room/category.rs +++ b/src/session/model/room/category.rs @@ -9,6 +9,8 @@ use crate::session::model::SidebarSectionName; #[derive(Debug, Default, Hash, Eq, PartialEq, Clone, Copy, glib::Enum)] #[enum_type(name = "RoomCategory")] pub enum RoomCategory { + /// The user requested an invite to the room. + Knocked, /// The user was invited to the room. Invited, /// The room is joined and has the `m.favourite` tag. @@ -77,13 +79,14 @@ impl RoomCategory { | TargetRoomCategory::LowPriority ) } - Self::Ignored | Self::Outdated | Self::Space => false, + Self::Knocked | Self::Ignored | Self::Outdated | Self::Space => false, } } /// Whether this `RoomCategory` corresponds to the given state. pub(crate) fn is_state(self, state: RoomState) -> bool { match self { + RoomCategory::Knocked => state == RoomState::Knocked, RoomCategory::Invited | RoomCategory::Ignored => state == RoomState::Invited, RoomCategory::Favorite | RoomCategory::Normal @@ -101,7 +104,8 @@ impl RoomCategory { RoomCategory::Normal => TargetRoomCategory::Normal, RoomCategory::LowPriority => TargetRoomCategory::LowPriority, RoomCategory::Left => TargetRoomCategory::Left, - RoomCategory::Invited + RoomCategory::Knocked + | RoomCategory::Invited | RoomCategory::Outdated | RoomCategory::Space | RoomCategory::Ignored => return None, diff --git a/src/session/model/room/mod.rs b/src/session/model/room/mod.rs index 1df2eb87..6ca6910a 100644 --- a/src/session/model/room/mod.rs +++ b/src/session/model/room/mod.rs @@ -591,7 +591,8 @@ mod imp { RoomCategory::Invited } } - RoomState::Left | RoomState::Knocked | RoomState::Banned => RoomCategory::Left, + RoomState::Knocked => RoomCategory::Knocked, + RoomState::Left | RoomState::Banned => RoomCategory::Left, }; self.set_category(category); @@ -1771,7 +1772,10 @@ impl Room { } } TargetRoomCategory::Left => { - if matches!(room_state, RoomState::Invited | RoomState::Joined) { + if matches!( + room_state, + RoomState::Knocked | RoomState::Invited | RoomState::Joined + ) { matrix_room.leave().await?; } } diff --git a/src/session/model/room_list/mod.rs b/src/session/model/room_list/mod.rs index 4d52d038..5fb88e8b 100644 --- a/src/session/model/room_list/mod.rs +++ b/src/session/model/room_list/mod.rs @@ -271,6 +271,23 @@ mod imp { self.metainfo.watch_room(&room); } + for (room_id, _knocked_room) in rooms.knocked { + let room = if let Some(room) = self.get(&room_id) { + room + } else if let Some(matrix_room) = client.get_room(&room_id) { + new_rooms + .entry(room_id.clone()) + .or_insert_with(|| Room::new(&session, matrix_room, None)) + .clone() + } else { + warn!("Could not find knocked room {room_id}"); + continue; + }; + + self.remove_joining_room((*room_id).into()); + self.metainfo.watch_room(&room); + } + if !new_rooms.is_empty() { let added = new_rooms.len(); self.list.borrow_mut().extend(new_rooms); diff --git a/src/session/model/session_settings.rs b/src/session/model/session_settings.rs index e48d4a07..8122577b 100644 --- a/src/session/model/session_settings.rs +++ b/src/session/model/session_settings.rs @@ -294,6 +294,7 @@ impl Default for SectionsExpanded { fn default() -> Self { Self(BTreeSet::from([ SidebarSectionName::VerificationRequest, + SidebarSectionName::InviteRequest, SidebarSectionName::Invited, SidebarSectionName::Favorite, SidebarSectionName::Normal, diff --git a/src/session/model/sidebar_data/item_list.rs b/src/session/model/sidebar_data/item_list.rs index 0dceb267..88e0fa3d 100644 --- a/src/session/model/sidebar_data/item_list.rs +++ b/src/session/model/sidebar_data/item_list.rs @@ -8,7 +8,7 @@ use super::{ use crate::session::model::{RoomCategory, RoomList, VerificationList}; /// The number of top-level items in the sidebar. -const TOP_LEVEL_ITEMS_COUNT: usize = 8; +const TOP_LEVEL_ITEMS_COUNT: usize = 9; mod imp { use std::cell::OnceCell; @@ -57,6 +57,10 @@ mod imp { SidebarSectionName::VerificationRequest, &verification_list, )), + SidebarItem::new(SidebarSection::new( + SidebarSectionName::InviteRequest, + &room_list, + )), SidebarItem::new(SidebarSection::new(SidebarSectionName::Invited, &room_list)), SidebarItem::new(SidebarSection::new( SidebarSectionName::Favorite, @@ -177,12 +181,15 @@ impl SidebarItemList { &self, category: RoomCategory, ) -> Option { + const FIRST_ROOM_SECTION_INDEX: usize = 2; + let index = match category { - RoomCategory::Invited => 2, - RoomCategory::Favorite => 3, - RoomCategory::Normal => 4, - RoomCategory::LowPriority => 5, - RoomCategory::Left => 6, + RoomCategory::Knocked => FIRST_ROOM_SECTION_INDEX, + RoomCategory::Invited => FIRST_ROOM_SECTION_INDEX + 1, + RoomCategory::Favorite => FIRST_ROOM_SECTION_INDEX + 2, + RoomCategory::Normal => FIRST_ROOM_SECTION_INDEX + 3, + RoomCategory::LowPriority => FIRST_ROOM_SECTION_INDEX + 4, + RoomCategory::Left => FIRST_ROOM_SECTION_INDEX + 5, _ => return None, }; diff --git a/src/session/model/sidebar_data/section/name.rs b/src/session/model/sidebar_data/section/name.rs index 4b5e65af..42d6347a 100644 --- a/src/session/model/sidebar_data/section/name.rs +++ b/src/session/model/sidebar_data/section/name.rs @@ -15,6 +15,8 @@ use crate::session::model::{RoomCategory, TargetRoomCategory}; pub enum SidebarSectionName { /// The section for verification requests. VerificationRequest, + /// The section for invite requests. + InviteRequest, /// The section for room invites. Invited, /// The section for favorite rooms. @@ -32,6 +34,7 @@ impl SidebarSectionName { /// Convert the given `RoomCategory` to a `SidebarSectionName`, if possible. pub(crate) fn from_room_category(category: RoomCategory) -> Option { let name = match category { + RoomCategory::Knocked => Self::InviteRequest, RoomCategory::Invited => Self::Invited, RoomCategory::Favorite => Self::Favorite, RoomCategory::Normal => Self::Normal, @@ -47,6 +50,7 @@ impl SidebarSectionName { pub(crate) fn into_room_category(self) -> Option { let category = match self { Self::VerificationRequest => return None, + Self::InviteRequest => RoomCategory::Knocked, Self::Invited => RoomCategory::Invited, Self::Favorite => RoomCategory::Favorite, Self::Normal => RoomCategory::Normal, @@ -61,7 +65,7 @@ impl SidebarSectionName { /// possible. pub(crate) fn into_target_room_category(self) -> Option { let category = match self { - Self::VerificationRequest | Self::Invited => return None, + Self::VerificationRequest | Self::InviteRequest | Self::Invited => return None, Self::Favorite => TargetRoomCategory::Favorite, Self::Normal => TargetRoomCategory::Normal, Self::LowPriority => TargetRoomCategory::LowPriority, @@ -76,6 +80,7 @@ impl fmt::Display for SidebarSectionName { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let label = match self { SidebarSectionName::VerificationRequest => gettext("Verifications"), + SidebarSectionName::InviteRequest => gettext("Invite Requests"), SidebarSectionName::Invited => gettext("Invited"), SidebarSectionName::Favorite => gettext("Favorites"), SidebarSectionName::Normal => gettext("Rooms"), diff --git a/src/session/view/content/invite_request.rs b/src/session/view/content/invite_request.rs new file mode 100644 index 00000000..abf15ab0 --- /dev/null +++ b/src/session/view/content/invite_request.rs @@ -0,0 +1,200 @@ +use adw::subclass::prelude::*; +use gettextrs::gettext; +use gtk::{CompositeTemplate, glib, glib::clone, prelude::*}; + +use crate::{ + components::{Avatar, LoadingButton}, + session::model::{Room, RoomCategory, TargetRoomCategory}, + toast, + utils::matrix::MatrixIdUri, +}; + +mod imp { + use std::cell::RefCell; + + use glib::subclass::InitializingObject; + + use super::*; + + #[derive(Debug, Default, CompositeTemplate, glib::Properties)] + #[template(resource = "/org/gnome/Fractal/ui/session/view/content/invite_request.ui")] + #[properties(wrapper_type = super::InviteRequest)] + pub struct InviteRequest { + #[template_child] + pub(super) header_bar: TemplateChild, + #[template_child] + avatar: TemplateChild, + #[template_child] + room_alias: TemplateChild, + #[template_child] + room_topic: TemplateChild, + #[template_child] + retract_button: TemplateChild, + /// The room currently displayed. + #[property(get, set = Self::set_room, explicit_notify, nullable)] + room: RefCell>, + category_handler: RefCell>, + } + + #[glib::object_subclass] + impl ObjectSubclass for InviteRequest { + const NAME: &'static str = "ContentInviteRequest"; + type Type = super::InviteRequest; + type ParentType = adw::Bin; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + Self::bind_template_callbacks(klass); + + klass.set_accessible_role(gtk::AccessibleRole::Group); + } + + fn instance_init(obj: &InitializingObject) { + obj.init_template(); + } + } + + #[glib::derived_properties] + impl ObjectImpl for InviteRequest { + fn constructed(&self) { + self.parent_constructed(); + let obj = self.obj(); + + self.room_alias.connect_label_notify(|room_alias| { + room_alias.set_visible(!room_alias.label().is_empty()); + }); + self.room_alias + .set_visible(!self.room_alias.label().is_empty()); + + self.room_topic.connect_label_notify(|room_topic| { + room_topic.set_visible(!room_topic.label().is_empty()); + }); + self.room_topic + .set_visible(!self.room_topic.label().is_empty()); + self.room_topic.connect_activate_link(clone!( + #[weak] + obj, + #[upgrade_or] + glib::Propagation::Proceed, + move |_, uri| { + if MatrixIdUri::parse(uri).is_ok() { + let _ = + obj.activate_action("session.show-matrix-uri", Some(&uri.to_variant())); + glib::Propagation::Stop + } else { + glib::Propagation::Proceed + } + } + )); + } + + fn dispose(&self) { + self.disconnect_signals(); + } + } + + impl WidgetImpl for InviteRequest { + fn grab_focus(&self) -> bool { + self.retract_button.grab_focus() + } + } + + impl BinImpl for InviteRequest {} + + #[gtk::template_callbacks] + impl InviteRequest { + /// Set the room currently displayed. + fn set_room(&self, room: Option) { + if *self.room.borrow() == room { + return; + } + + self.disconnect_signals(); + + if let Some(room) = &room { + let category_handler = room.connect_category_notify(clone!( + #[weak(rename_to = imp)] + self, + move |room| { + let category = room.category(); + + if category == RoomCategory::Left { + // We retracted the request or the request was denied, we should close + // the room if it is opened. + let Some(session) = room.session() else { + return; + }; + let selection = session.sidebar_list_model().selection_model(); + if let Some(selected_room) = + selection.selected_item().and_downcast::() + { + if selected_room == *room { + selection.set_selected_item(None::); + } + } + } + + if category != RoomCategory::Knocked { + imp.retract_button.set_is_loading(false); + + if let Some(category_handler) = imp.category_handler.take() { + room.disconnect(category_handler); + } + } + } + )); + self.category_handler.replace(Some(category_handler)); + } + + self.room.replace(room); + + self.obj().notify_room(); + } + + /// Retract the request. + #[template_callback] + async fn retract(&self) { + let Some(room) = self.room.borrow().clone() else { + return; + }; + + self.retract_button.set_is_loading(true); + + if room + .change_category(TargetRoomCategory::Left) + .await + .is_err() + { + toast!(self.obj(), gettext("Could not retract invite request",),); + + self.retract_button.set_is_loading(false); + } + } + + /// Disconnect the signal handlers of this view. + fn disconnect_signals(&self) { + if let Some(room) = self.room.take() { + if let Some(handler) = self.category_handler.take() { + room.disconnect(handler); + } + } + } + } +} + +glib::wrapper! { + /// A view presenting an invitate request to a room. + pub struct InviteRequest(ObjectSubclass) + @extends gtk::Widget, adw::Bin, @implements gtk::Accessible; +} + +impl InviteRequest { + pub fn new() -> Self { + glib::Object::new() + } + + /// The header bar of the invite request. + pub fn header_bar(&self) -> &adw::HeaderBar { + &self.imp().header_bar + } +} diff --git a/src/session/view/content/invite_request.ui b/src/session/view/content/invite_request.ui new file mode 100644 index 00000000..723f750b --- /dev/null +++ b/src/session/view/content/invite_request.ui @@ -0,0 +1,137 @@ + + + + diff --git a/src/session/view/content/mod.rs b/src/session/view/content/mod.rs index 6d49428e..b36248f3 100644 --- a/src/session/view/content/mod.rs +++ b/src/session/view/content/mod.rs @@ -1,13 +1,15 @@ +use adw::subclass::prelude::*; +use gtk::{CompositeTemplate, glib, glib::clone, prelude::*}; + mod explore; mod invite; +mod invite_request; mod room_details; mod room_history; -use adw::subclass::prelude::*; -use gtk::{CompositeTemplate, glib, glib::clone, prelude::*}; - use self::{ - explore::Explore, invite::Invite, room_details::RoomDetails, room_history::RoomHistory, + explore::Explore, invite::Invite, invite_request::InviteRequest, room_details::RoomDetails, + room_history::RoomHistory, }; use crate::{ identity_verification_view::IdentityVerificationView, @@ -25,6 +27,8 @@ enum ContentPage { Empty, /// The history of the selected room. RoomHistory, + /// The selected invite request. + InviteRequest, /// The selected room invite. Invite, /// The explore page. @@ -49,6 +53,8 @@ mod imp { #[template_child] room_history: TemplateChild, #[template_child] + invite_request: TemplateChild, + #[template_child] invite: TemplateChild, #[template_child] explore: TemplateChild, @@ -222,12 +228,19 @@ mod imp { }; if let Some(room) = item.downcast_ref::() { - if room.category() == RoomCategory::Invited { - self.invite.set_room(Some(room.clone())); - self.set_visible_page(ContentPage::Invite); - } else { - self.room_history.set_timeline(Some(room.live_timeline())); - self.set_visible_page(ContentPage::RoomHistory); + match room.category() { + RoomCategory::Knocked => { + self.invite_request.set_room(Some(room.clone())); + self.set_visible_page(ContentPage::InviteRequest); + } + RoomCategory::Invited => { + self.invite.set_room(Some(room.clone())); + self.set_visible_page(ContentPage::Invite); + } + _ => { + self.room_history.set_timeline(Some(room.live_timeline())); + self.set_visible_page(ContentPage::RoomHistory); + } } } else if item .downcast_ref::() @@ -249,10 +262,11 @@ mod imp { } /// All the header bars of the children of the content. - pub(super) fn header_bars(&self) -> [&adw::HeaderBar; 5] { + pub(super) fn header_bars(&self) -> [&adw::HeaderBar; 6] { [ &self.empty_page_header_bar, self.room_history.header_bar(), + self.invite_request.header_bar(), self.invite.header_bar(), self.explore.header_bar(), &self.verification_page_header_bar, @@ -278,7 +292,7 @@ impl Content { } /// All the header bars of the children of the content. - pub(crate) fn header_bars(&self) -> [&adw::HeaderBar; 5] { + pub(crate) fn header_bars(&self) -> [&adw::HeaderBar; 6] { self.imp().header_bars() } } diff --git a/src/session/view/content/mod.ui b/src/session/view/content/mod.ui index 7150427f..a330eaa1 100644 --- a/src/session/view/content/mod.ui +++ b/src/session/view/content/mod.ui @@ -44,6 +44,15 @@ + + + invite-request + Invite Request + + + + + invite diff --git a/src/session/view/session_view.rs b/src/session/view/session_view.rs index 14c2c753..c074df8a 100644 --- a/src/session/view/session_view.rs +++ b/src/session/view/session_view.rs @@ -432,9 +432,10 @@ mod imp { RoomCategory::Normal => 3, RoomCategory::LowPriority => 2, RoomCategory::Left => 1, - RoomCategory::Ignored | RoomCategory::Outdated | RoomCategory::Space => { - return None; - } + RoomCategory::Knocked + | RoomCategory::Ignored + | RoomCategory::Outdated + | RoomCategory::Space => return None, }; Some(( diff --git a/src/session/view/sidebar/mod.ui b/src/session/view/sidebar/mod.ui index 56d03d56..e1c71617 100644 --- a/src/session/view/sidebar/mod.ui +++ b/src/session/view/sidebar/mod.ui @@ -38,6 +38,11 @@ room-row.decline-invite action-missing + + _Retract + room-row.retract-invite-request + action-missing +
diff --git a/src/session/view/sidebar/row.rs b/src/session/view/sidebar/row.rs index 3c8f4069..9d79725d 100644 --- a/src/session/view/sidebar/row.rs +++ b/src/session/view/sidebar/row.rs @@ -302,6 +302,23 @@ mod imp { let category = room.category(); match category { + RoomCategory::Knocked => { + action_group.add_action_entries([gio::ActionEntry::builder( + "retract-invite-request", + ) + .activate(clone!( + #[weak(rename_to = imp)] + self, + move |_, _, _| { + if let Some(room) = imp.room() { + spawn!(async move { + imp.set_room_category(&room, TargetRoomCategory::Left).await; + }); + } + } + )) + .build()]); + } RoomCategory::Invited => { action_group.add_action_entries([ gio::ActionEntry::builder("accept-invite") diff --git a/src/ui-resources.gresource.xml b/src/ui-resources.gresource.xml index 30256495..3a8362a4 100644 --- a/src/ui-resources.gresource.xml +++ b/src/ui-resources.gresource.xml @@ -83,6 +83,7 @@ session/view/content/explore/server_row.ui session/view/content/explore/servers_popover.ui session/view/content/invite.ui + session/view/content/invite_request.ui session/view/content/mod.ui session/view/content/room_details/addresses_subpage/completion_popover.ui session/view/content/room_details/addresses_subpage/mod.ui