Browse Source

sidebar: Show our invite requests

fractal-12
Kévin Commaille 9 months ago
parent
commit
bfcf5550cf
No known key found for this signature in database
GPG Key ID: F26F4BE20A08255B
  1. 2
      po/POTFILES.in
  2. 8
      src/session/model/room/category.rs
  3. 8
      src/session/model/room/mod.rs
  4. 17
      src/session/model/room_list/mod.rs
  5. 1
      src/session/model/session_settings.rs
  6. 19
      src/session/model/sidebar_data/item_list.rs
  7. 7
      src/session/model/sidebar_data/section/name.rs
  8. 200
      src/session/view/content/invite_request.rs
  9. 137
      src/session/view/content/invite_request.ui
  10. 38
      src/session/view/content/mod.rs
  11. 9
      src/session/view/content/mod.ui
  12. 7
      src/session/view/session_view.rs
  13. 5
      src/session/view/sidebar/mod.ui
  14. 17
      src/session/view/sidebar/row.rs
  15. 1
      src/ui-resources.gresource.xml

2
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

8
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,

8
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?;
}
}

17
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);

1
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,

19
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<SidebarSection> {
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,
};

7
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<Self> {
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<RoomCategory> {
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<TargetRoomCategory> {
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"),

200
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<adw::HeaderBar>,
#[template_child]
avatar: TemplateChild<Avatar>,
#[template_child]
room_alias: TemplateChild<gtk::Label>,
#[template_child]
room_topic: TemplateChild<gtk::Label>,
#[template_child]
retract_button: TemplateChild<LoadingButton>,
/// The room currently displayed.
#[property(get, set = Self::set_room, explicit_notify, nullable)]
room: RefCell<Option<Room>>,
category_handler: RefCell<Option<glib::SignalHandlerId>>,
}
#[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<Self>) {
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<Room>) {
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::<Room>()
{
if selected_room == *room {
selection.set_selected_item(None::<glib::Object>);
}
}
}
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<imp::InviteRequest>)
@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
}
}

137
src/session/view/content/invite_request.ui

@ -0,0 +1,137 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="ContentInviteRequest" parent="AdwBin">
<property name="vexpand">True</property>
<property name="hexpand">True</property>
<child>
<object class="AdwToolbarView">
<child type="top">
<object class="AdwHeaderBar" id="header_bar">
<child type="title">
<object class="AdwWindowTitle">
<property name="title" translatable="yes">Invite Request</property>
</object>
</child>
</object>
</child>
<property name="content">
<object class="GtkScrolledWindow">
<property name="vexpand">True</property>
<property name="hscrollbar-policy">never</property>
<property name="child">
<object class="AdwClamp">
<property name="maximum-size">444</property>
<property name="tightening-threshold">300</property>
<property name="vexpand">True</property>
<property name="margin-top">24</property>
<property name="margin-bottom">24</property>
<property name="margin-start">24</property>
<property name="margin-end">24</property>
<property name="child">
<object class="GtkBox">
<property name="valign">center</property>
<property name="halign">center</property>
<property name="spacing">24</property>
<property name="orientation">vertical</property>
<accessibility>
<property name="label" translatable="yes">Invite Request</property>
</accessibility>
<child>
<object class="Avatar" id="avatar">
<property name="size">150</property>
<binding name="data">
<lookup name="avatar-data">
<lookup name="room">ContentInviteRequest</lookup>
</lookup>
</binding>
<property name="accessible-role">presentation</property>
</object>
</child>
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel" id="display_name">
<property name="ellipsize">end</property>
<binding name="label">
<lookup name="display-name">
<lookup name="room">ContentInviteRequest</lookup>
</lookup>
</binding>
<style>
<class name="invite-room-name"/>
</style>
</object>
</child>
<child>
<object class="GtkLabel" id="room_alias">
<property name="wrap">True</property>
<property name="wrap-mode">word-char</property>
<property name="justify">center</property>
<binding name="label">
<lookup name="alias-string">
<lookup name="aliases">
<lookup name="room">ContentInviteRequest</lookup>
</lookup>
</lookup>
</binding>
<style>
<class name="dimmed"/>
</style>
</object>
</child>
</object>
</child>
<child>
<object class="GtkLabel" id="room_topic">
<property name="wrap">True</property>
<property name="wrap-mode">word-char</property>
<property name="justify">center</property>
<property name="use-markup">True</property>
<binding name="label">
<lookup name="topic-linkified">
<lookup name="room">ContentInviteRequest</lookup>
</lookup>
</binding>
<style>
<class name="dimmed"/>
</style>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="halign">center</property>
<property name="justify">center</property>
<property name="wrap">True</property>
<property name="wrap-mode">word-char</property>
<property name="label" translatable="yes">You requested an invite to this room</property>
</object>
</child>
<child>
<object class="GtkBox">
<property name="halign">center</property>
<property name="spacing">24</property>
<property name="margin-top">24</property>
<child>
<object class="LoadingButton" id="retract_button">
<property name="content-label" translatable="yes">_Retract</property>
<signal name="clicked" handler="retract" swapped="yes"/>
<style>
<class name="pill"/>
<class name="large"/>
<class name="destructive-action"/>
</style>
</object>
</child>
</object>
</child>
</object>
</property>
</object>
</property>
</object>
</property>
</object>
</child>
</template>
</interface>

38
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<RoomHistory>,
#[template_child]
invite_request: TemplateChild<InviteRequest>,
#[template_child]
invite: TemplateChild<Invite>,
#[template_child]
explore: TemplateChild<Explore>,
@ -222,12 +228,19 @@ mod imp {
};
if let Some(room) = item.downcast_ref::<Room>() {
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::<SidebarIconItem>()
@ -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()
}
}

9
src/session/view/content/mod.ui

@ -44,6 +44,15 @@
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">invite-request</property>
<property name="title" translatable="yes">Invite Request</property>
<property name="child">
<object class="ContentInviteRequest" id="invite_request"/>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">invite</property>

7
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((

5
src/session/view/sidebar/mod.ui

@ -38,6 +38,11 @@
<attribute name="action">room-row.decline-invite</attribute>
<attribute name="hidden-when">action-missing</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Retract</attribute>
<attribute name="action">room-row.retract-invite-request</attribute>
<attribute name="hidden-when">action-missing</attribute>
</item>
</section>
<section>
<item>

17
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")

1
src/ui-resources.gresource.xml

@ -83,6 +83,7 @@
<file compressed="true" preprocess="xml-stripblanks">session/view/content/explore/server_row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/explore/servers_popover.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/invite.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/invite_request.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/mod.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_details/addresses_subpage/completion_popover.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/content/room_details/addresses_subpage/mod.ui</file>

Loading…
Cancel
Save