Browse Source

room-history: Show a banner when there are pending invite requests

And the user can accept or deny it.
fractal-12
Kévin Commaille 8 months ago
parent
commit
7f39417349
No known key found for this signature in database
GPG Key ID: F26F4BE20A08255B
  1. 7
      src/session/view/content/room_details/members_page/mod.rs
  2. 107
      src/session/view/content/room_details/mod.rs
  3. 136
      src/session/view/content/room_history/mod.rs
  4. 8
      src/session/view/content/room_history/mod.ui

7
src/session/view/content/room_details/members_page/mod.rs

@ -70,7 +70,7 @@ mod imp {
impl MembersPage {
/// Show the subpage for the list with the given membership.
fn show_membership_list(&self, kind: MembershipListKind) {
pub(super) fn show_membership_list(&self, kind: MembershipListKind) {
let tag = kind.as_ref();
if self.navigation_view.find_page(tag).is_some() {
@ -105,4 +105,9 @@ impl MembersPage {
.property("members", members)
.build()
}
/// Show the subpage for the list with the given membership.
pub(super) fn show_membership_list(&self, kind: MembershipListKind) {
self.imp().show_membership_list(kind);
}
}

107
src/session/view/content/room_details/mod.rs

@ -35,13 +35,13 @@ use self::{
};
use crate::{
components::UserPage,
session::model::{MemberList, Room},
session::model::{MemberList, MembershipListKind, Room},
toast,
};
/// The possible subpages of the room details.
#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy, glib::Variant)]
pub(crate) enum SubpageName {
pub(super) enum SubpageName {
/// The page to edit the name, topic and avatar of the room.
EditDetails,
/// The list of members of the room.
@ -62,6 +62,17 @@ pub(crate) enum SubpageName {
JoinRule,
}
/// The view to present when opening the room details.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum InitialView {
/// Present the default page.
None,
/// Present the given subpage.
Subpage(SubpageName),
/// Present the members subpage with the given kind.
Members(MembershipListKind),
}
mod imp {
use std::{
cell::{OnceCell, RefCell},
@ -105,11 +116,11 @@ mod imp {
klass.install_action(
"details.show-subpage",
Some(&String::static_variant_type()),
Some(&SubpageName::static_variant_type()),
|obj, _, param| {
let subpage = param
.and_then(glib::Variant::get::<SubpageName>)
.expect("The parameter should be a valid subpage name");
.expect("parameter should be a valid subpage name");
obj.imp().show_subpage(subpage, false);
},
@ -208,30 +219,65 @@ mod imp {
self.timeline.get().expect("timeline should be initialized")
}
/// Show the subpage with the given name.
pub(super) fn show_subpage(&self, name: SubpageName, is_initial: bool) {
/// Get the given subpage.
fn subpage(&self, name: SubpageName) -> adw::NavigationPage {
let room = self.room.get().expect("room should be initialized");
let mut subpages = self.subpages.borrow_mut();
let subpage = subpages.entry(name).or_insert_with(|| match name {
SubpageName::EditDetails => EditDetailsSubpage::new(room).upcast(),
SubpageName::Members => MembersPage::new(room, self.members()).upcast(),
SubpageName::Invite => InviteSubpage::new(room).upcast(),
SubpageName::VisualMediaHistory => {
VisualMediaHistoryViewer::new(self.timeline()).upcast()
}
SubpageName::FileHistory => FileHistoryViewer::new(self.timeline()).upcast(),
SubpageName::AudioHistory => AudioHistoryViewer::new(self.timeline()).upcast(),
SubpageName::Addresses => AddressesSubpage::new(room).upcast(),
SubpageName::Permissions => PermissionsSubpage::new(&room.permissions()).upcast(),
SubpageName::JoinRule => JoinRuleSubpage::new(room).upcast(),
});
self.subpages
.borrow_mut()
.entry(name)
.or_insert_with(|| match name {
SubpageName::EditDetails => EditDetailsSubpage::new(room).upcast(),
SubpageName::Members => MembersPage::new(room, self.members()).upcast(),
SubpageName::Invite => InviteSubpage::new(room).upcast(),
SubpageName::VisualMediaHistory => {
VisualMediaHistoryViewer::new(self.timeline()).upcast()
}
SubpageName::FileHistory => FileHistoryViewer::new(self.timeline()).upcast(),
SubpageName::AudioHistory => AudioHistoryViewer::new(self.timeline()).upcast(),
SubpageName::Addresses => AddressesSubpage::new(room).upcast(),
SubpageName::Permissions => {
PermissionsSubpage::new(&room.permissions()).upcast()
}
SubpageName::JoinRule => JoinRuleSubpage::new(room).upcast(),
})
.clone()
}
/// Show the subpage with the given name.
pub(super) fn show_subpage(&self, name: SubpageName, is_initial: bool) {
let subpage = self.subpage(name);
if is_initial {
subpage.set_can_pop(false);
}
self.obj().push_subpage(subpage);
self.obj().push_subpage(&subpage);
}
/// Show the members subpage with the given kind.
fn show_members_subpage(&self, kind: MembershipListKind) {
let subpage = self
.subpage(SubpageName::Members)
.downcast::<MembersPage>()
.expect("we should have the members subpage");
subpage.show_membership_list(kind);
self.obj().push_subpage(&subpage);
}
/// Show the given initial view.
pub(super) fn show_initial_view(&self, initial_view: InitialView) {
match initial_view {
InitialView::None => {}
InitialView::Subpage(name) => {
self.show_subpage(name, true);
}
InitialView::Members(kind) => {
self.show_members_subpage(kind);
}
}
}
}
}
@ -245,16 +291,19 @@ glib::wrapper! {
impl RoomDetails {
/// Construct a `RoomDetails` for the given room with the given parent
/// window.
pub fn new(parent_window: Option<&gtk::Window>, room: &Room) -> Self {
glib::Object::builder()
/// window, showing the given initial view.
pub(super) fn new(
parent_window: Option<&gtk::Window>,
room: &Room,
initial_view: InitialView,
) -> Self {
let obj = glib::Object::builder::<Self>()
.property("transient-for", parent_window)
.property("room", room)
.build()
}
.build();
obj.imp().show_initial_view(initial_view);
/// Show the given subpage as the initial page.
pub(crate) fn show_initial_subpage(&self, name: SubpageName) {
self.imp().show_subpage(name, true);
obj
}
}

136
src/session/view/content/room_history/mod.rs

@ -6,8 +6,9 @@ use gtk::{CompositeTemplate, gdk, gio, glib, glib::clone, graphene::Point};
use matrix_sdk::ruma::EventId;
use matrix_sdk_ui::timeline::TimelineEventItemId;
use ruma::{
OwnedEventId, api::client::receipt::create_receipt::v3::ReceiptType,
events::room::message::MessageType,
OwnedEventId,
api::client::receipt::create_receipt::v3::ReceiptType,
events::room::{message::MessageType, power_levels::PowerLevelAction},
};
use tracing::{error, warn};
@ -40,10 +41,11 @@ use self::{
use super::{RoomDetails, room_details};
use crate::{
components::{DragOverlay, confirm_leave_room_dialog},
ngettext_f,
prelude::*,
session::model::{
Event, MemberList, Membership, ReceiptPosition, Room, TargetRoomCategory, Timeline,
VirtualItem, VirtualItemKind,
Event, MemberList, Membership, MembershipListKind, ReceiptPosition, Room,
TargetRoomCategory, Timeline, VirtualItem, VirtualItemKind,
},
spawn, toast,
utils::{BoundObject, GroupingListGroup, GroupingListModel, LoadingState, TemplateCallbacks},
@ -77,6 +79,8 @@ mod imp {
#[template_child]
room_menu: TemplateChild<gtk::MenuButton>,
#[template_child]
pending_knocks_banner: TemplateChild<adw::Banner>,
#[template_child]
listview: TemplateChild<gtk::ListView>,
#[template_child]
content: TemplateChild<gtk::Widget>,
@ -121,9 +125,10 @@ mod imp {
scroll_timeout: RefCell<Option<glib::SourceId>>,
read_timeout: RefCell<Option<glib::SourceId>>,
room_handler: RefCell<Option<glib::SignalHandlerId>>,
can_invite_handler: RefCell<Option<glib::SignalHandlerId>>,
permissions_handlers: RefCell<Vec<glib::SignalHandlerId>>,
membership_handler: RefCell<Option<glib::SignalHandlerId>>,
join_rule_handler: RefCell<Option<glib::SignalHandlerId>>,
knock_items_changed_handler: RefCell<Option<glib::SignalHandlerId>>,
}
#[glib::object_subclass]
@ -132,6 +137,7 @@ mod imp {
type Type = super::RoomHistory;
type ParentType = adw::Bin;
#[allow(clippy::too_many_lines)]
fn class_init(klass: &mut Self::Class) {
VerificationInfoBar::ensure_type();
@ -152,10 +158,13 @@ mod imp {
});
klass.install_action("room-history.details", None, |obj, _, _| {
obj.open_room_details(None);
obj.imp().open_room_details(room_details::InitialView::None);
});
klass.install_action("room-history.invite-members", None, |obj, _, _| {
obj.open_room_details(Some(room_details::SubpageName::Invite));
obj.imp()
.open_room_details(room_details::InitialView::Subpage(
room_details::SubpageName::Invite,
));
});
klass.install_action(
@ -392,9 +401,11 @@ mod imp {
room.disconnect(handler);
}
if let Some(handler) = self.can_invite_handler.take() {
room.permissions().disconnect(handler);
let permissions = room.permissions();
for handler in self.permissions_handlers.take() {
permissions.disconnect(handler);
}
if let Some(handler) = self.membership_handler.take() {
room.own_member().disconnect(handler);
}
@ -403,6 +414,14 @@ mod imp {
}
}
if let Some(members) = self.room_members.take() {
if let Some(handler) = self.knock_items_changed_handler.take() {
members
.membership_list(MembershipListKind::Knock)
.disconnect(handler);
}
}
self.timeline.disconnect_signals();
}
@ -426,8 +445,21 @@ mod imp {
// Keep a strong reference to the members list before changing the model, so all
// events use the same list.
self.room_members
.replace(Some(room.get_or_create_members()));
let room_members = room.get_or_create_members();
let knock_items_changed_handler = room_members
.membership_list(MembershipListKind::Knock)
.connect_items_changed(clone!(
#[weak(rename_to = imp)]
self,
move |_, _, _, _| {
imp.update_pending_knocks();
}
));
self.knock_items_changed_handler
.replace(Some(knock_items_changed_handler));
self.room_members.replace(Some(room_members));
let membership_handler = room.own_member().connect_membership_notify(clone!(
#[weak(rename_to = imp)]
@ -454,7 +486,15 @@ mod imp {
imp.update_invite_action();
}
));
self.can_invite_handler.replace(Some(can_invite_handler));
let changed_handler = room.permissions().connect_changed(clone!(
#[weak(rename_to = imp)]
self,
move |_| {
imp.update_pending_knocks();
}
));
self.permissions_handlers
.replace(vec![can_invite_handler, changed_handler]);
let is_direct_handler = room.connect_is_direct_notify(clone!(
#[weak(rename_to = imp)]
@ -505,6 +545,7 @@ mod imp {
self.load_more_events_if_needed();
self.update_room_menu();
self.update_invite_action();
self.update_pending_knocks();
self.obj().notify_timeline();
}
@ -982,6 +1023,41 @@ mod imp {
.action_set_enabled("room-history.invite-members", can_invite);
}
// Update the pending knocks according to the current state.
fn update_pending_knocks(&self) {
if self.room().is_none_or(|room| {
let permissions = room.permissions();
!permissions.is_allowed_to(PowerLevelAction::Invite)
&& !permissions.is_allowed_to(PowerLevelAction::Kick)
&& !permissions.is_allowed_to(PowerLevelAction::Ban)
}) {
// Our user cannot act on the knock.
self.pending_knocks_banner.set_revealed(false);
return;
}
let Some(members) = self.room_members.borrow().clone() else {
self.pending_knocks_banner.set_revealed(false);
return;
};
let n = members.membership_list(MembershipListKind::Knock).n_items();
let reveal = n > 0;
if reveal {
self.pending_knocks_banner.set_title(&ngettext_f(
// Translators: Do NOT translate the content between '{' and '}',
// this is a variable name.
"There is a pending invite request",
"There are {n} pending invite requests",
n,
&[("n", &n.to_string())],
));
}
self.pending_knocks_banner.set_revealed(reveal);
}
/// The context menu for rows presenting an [`Event`].
pub(super) fn event_context_menu(&self) -> &EventActionsContextMenu {
self.event_context_menu.get_or_init(Default::default)
@ -1001,6 +1077,26 @@ mod imp {
popover
})
}
/// Opens the room details with the given initial view.
fn open_room_details(&self, initial_view: room_details::InitialView) {
let Some(room) = self.room() else {
return;
};
let window =
RoomDetails::new(self.obj().root().and_downcast_ref(), &room, initial_view);
window.present();
}
/// View the list of pending knock requests.
#[template_callback]
fn view_pending_knocks(&self) {
self.open_room_details(room_details::InitialView::Members(
MembershipListKind::Knock,
));
}
}
}
@ -1025,22 +1121,6 @@ impl RoomHistory {
&self.imp().message_toolbar
}
/// Opens the room details.
///
/// If `subpage_name` is set, the room details will be opened on the given
/// subpage.
pub(crate) fn open_room_details(&self, subpage_name: Option<room_details::SubpageName>) {
let Some(room) = self.imp().room() else {
return;
};
let window = RoomDetails::new(self.root().and_downcast_ref(), &room);
if let Some(subpage_name) = subpage_name {
window.show_initial_subpage(subpage_name);
}
window.present();
}
/// Enable or disable the mode allowing the room history to stick to the
/// bottom based on scrollbar position.
pub(crate) fn enable_sticky_mode(&self, enable: bool) {

8
src/session/view/content/room_history/mod.ui

@ -163,6 +163,14 @@
</binding>
</object>
</child>
<child>
<object class="AdwBanner" id="pending_knocks_banner">
<!-- Translators: This is a verb, as in 'View Room' -->
<property name="button-label" translatable="yes">View</property>
<property name="button-style">suggested</property>
<signal name="button-clicked" handler="view_pending_knocks" swapped="true"/>
</object>
</child>
<child>
<object class="GtkStack" id="stack">
<property name="transition-type">crossfade</property>

Loading…
Cancel
Save