room-details: Rework navigation and fix listview styles

Fixes: https://gitlab.gnome.org/GNOME/fractal/-/issues/900
This commit is contained in:
Julian Sparber 2022-09-08 10:55:25 +02:00
parent f8e9147f7d
commit 15bda14f05
20 changed files with 1531 additions and 450 deletions

View file

@ -54,6 +54,8 @@
<file compressed="true" preprocess="xml-stripblanks" alias="content-invitee-row.ui">ui/content-invitee-row.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="content-markdown-popover.ui">ui/content-markdown-popover.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="content-member-item.ui">ui/content-member-item.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="content-member-page-list-view.ui">ui/content-member-page-list-view.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="content-member-page-membership-subpage-row.ui">ui/content-member-page-membership-subpage-row.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="content-member-page.ui">ui/content-member-page.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="content-member-row.ui">ui/content-member-row.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="content-message-audio.ui">ui/content-message-audio.ui</file>

View file

@ -62,6 +62,9 @@ button.row {
border-radius: 12px;
}
.round-corners {
border-radius: 6px;
}
/* Components */
@ -545,6 +548,13 @@ message-reactions .reaction-count {
font-size: 1.6em;
}
.invite-search-results {
padding: 12px 0px;
}
.invite-search-results > row {
border-radius: 6px;
}
/* Room Details */
@ -552,6 +562,10 @@ message-reactions .reaction-count {
margin-bottom: 6px;
}
.room-details listview {
background: transparent;
}
.room-details-group avatar * {
/* Undo non-sensitive style. */
filter: none;

View file

@ -34,10 +34,10 @@
<property name="margin-end">30</property>
<property name="margin-start">30</property>
<property name="margin-top">6</property>
<property name="hexpand">true</property>
<property name="hexpand">True</property>
<child>
<object class="CustomEntry">
<!-- FIXME: inserting a Pill makes the Entry grow, therefore we force more height so that it doesn't grow visually
<!-- FIXME: inserting a Pill makes the Entry grow, therefore we force more height so that it doesn't grow visually
Would be nice to fix it properly. Including the vertical alignment of Pills in the textview
-->
<property name="height-request">74</property>
@ -53,7 +53,7 @@
<object class="GtkScrolledWindow">
<child>
<object class="GtkTextView" id="text_view">
<property name="hexpand">true</property>
<property name="hexpand">True</property>
<property name="justification">left</property>
<property name="wrap-mode">word-char</property>
<property name="accepts-tab">False</property>
@ -80,8 +80,6 @@
<object class="GtkStack" id="stack">
<child>
<object class="AdwStatusPage" id="no_search_page">
<property name="visible">True</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="icon-name">system-search-symbolic</property>
<property name="description" translatable="yes">Search for users to invite them to this room.</property>
@ -89,16 +87,12 @@
</child>
<child>
<object class="GtkScrolledWindow" id="matching_page">
<property name="propagate-natural-height">True</property>
<property name="child">
<object class="AdwClampScrollable">
<property name="child">
<object class="GtkListView" id="list_view">
<property name="margin-bottom">24</property>
<property name="margin-end">12</property>
<property name="margin-start">12</property>
<property name="margin-top">24</property>
<property name="show-separators">True</property>
<property name="single-click-activate">True</property>
<property name="factory">
<object class="GtkBuilderListItemFactory">
@ -106,7 +100,7 @@
</object>
</property>
<style>
<class name="content"/>
<class name="invite-search-results"/>
</style>
</object>
</property>
@ -116,18 +110,12 @@
</child>
<child>
<object class="AdwStatusPage" id="no_matching_page">
<property name="visible">True</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="icon-name">system-search-symbolic</property>
<property name="description" translatable="yes">No users matching the search were found.</property>
</object>
</child>
<child>
<object class="AdwStatusPage" id="error_page">
<property name="visible">True</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="icon-name">dialog-error-symbolic</property>
<property name="description" translatable="yes">An error occurred while searching for matches</property>
</object>
@ -137,7 +125,6 @@
<property name="spinning">True</property>
<property name="valign">center</property>
<property name="halign">center</property>
<property name="vexpand">True</property>
<style>
<class name="session-loading-spinner"/>
</style>

View file

@ -4,8 +4,8 @@
<property name="activatable">False</property>
<property name="selectable">False</property>
<property name="child">
<object class="ContentMemberRow">
<binding name="member">
<object class="ContentMemberItemRow">
<binding name="item">
<lookup name="item">GtkListItem</lookup>
</binding>
</object>

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="ContentMembersListView" parent="AdwBin">
<child>
<object class="GtkScrolledWindow">
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="hscrollbar-policy">never</property>
<property name="propagate-natural-height">True</property>
<property name="child">
<object class="AdwClampScrollable">
<property name="tightening-threshold">300</property>
<property name="maximum-size">400</property>
<property name="margin-start">12</property>
<property name="margin-end">12</property>
<property name="child">
<object class="GtkListView" id="members_list_view">
<property name="single-click-activate">True</property>
<property name="factory">
<object class="GtkBuilderListItemFactory">
<property name="resource">/org/gnome/Fractal/content-member-item.ui</property>
</object>
</property>
</object>
</property>
</object>
</property>
</object>
</child>
</template>
</interface>

View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="ContentMemberPageMembershipSubpageRow" parent="AdwActionRow">
<property name="title" bind-source="ContentMemberPageMembershipSubpageRow" bind-property="label" bind-flags="sync-create"/>
<property name="icon-name">system-users-symbolic</property>
<property name="activatable">True</property>
<property name="margin-top">6</property>
<property name="margin-bottom">6</property>
<child type="suffix">
<object class="GtkLabel" id="members_count">
<property name="valign">center</property>
<property name="halign">center</property>
</object>
</child>
<child type="suffix">
<object class="GtkImage">
<property name="valign">center</property>
<property name="halign">center</property>
<property name="icon-name">go-next-symbolic</property>
</object>
</child>
<style>
<class name="round-corners"/>
</style>
</template>
</interface>

View file

@ -1,75 +1,79 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="ContentMemberPage" parent="AdwPreferencesPage">
<property name="icon-name">system-users-symbolic</property>
<property name="title" translatable="yes">Members</property>
<property name="name">members</property>
<template class="ContentMemberPage" parent="AdwBin">
<child>
<object class="AdwPreferencesGroup">
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="GtkBox">
<property name="margin-bottom">12</property>
<child>
<object class="GtkLabel" id="member_count">
<property name="halign">start</property>
<object class="GtkHeaderBar">
<child type="start">
<object class="GtkButton">
<property name="icon-name">go-previous-symbolic</property>
<property name="action_name">members.previous</property>
</object>
</child>
<child type="end">
<object class="GtkToggleButton" id="search_button">
<property name="icon-name">system-search-symbolic</property>
<accessibility>
<property name="label" translatable="yes">Search for Room Members</property>
</accessibility>
</object>
</child>
</object>
</child>
<child>
<object class="GtkSearchBar">
<property name="search-mode-enabled" bind-source="search_button" bind-property="active"/>
<property name="child">
<object class="AdwClamp">
<property name="hexpand">True</property>
<style>
<class name="heading"/>
<class name="h4"/>
</style>
<property name="maximum-size">750</property>
<property name="tightening-threshold">550</property>
<child>
<object class="GtkSearchEntry" id="members_search_entry">
<property name="placeholder-text" translatable="yes">Search for room members</property>
</object>
</child>
<accessibility>
<property name="label" translatable="yes">Search for room members</property>
</accessibility>
</object>
</property>
</object>
</child>
<child>
<object class="GtkOverlay">
<child>
<object class="GtkStack" id="list_stack">
<property name="transition-type">slide-left</property>
</object>
</child>
<child>
<child type="overlay">
<object class="GtkButton" id="invite_button">
<property name="label" translatable="yes">Invite new member</property>
<property name="halign">end</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkSearchEntry" id="members_search_entry">
<property name="margin-bottom">12</property>
<property name="placeholder-text" translatable="yes">Search for room members</property>
</object>
</child>
<child>
<object class="GtkScrolledWindow" id="members_scroll">
<property name="propagate-natural-height">True</property>
<property name="max-content-height">300</property>
<child>
<object class="GtkListView" id="members_list_view">
<property name="show-separators">True</property>
<property name="factory">
<object class="GtkBuilderListItemFactory">
<property name="resource">/org/gnome/Fractal/content-member-item.ui</property>
<property name="valign">end</property>
<property name="halign">center</property>
<property name="margin-bottom">24</property>
<property name="action-name">details.next-page</property>
<property name="action-target">&apos;invite&apos;</property>
<property name="child">
<object class="GtkBox">
<property name="spacing">6</property>
<child>
<object class="GtkImage">
<property name="icon-name">system-users-symbolic</property>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">Invite</property>
</object>
</child>
</object>
</property>
<style>
<class name="content"/>
</style>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="AdwPreferencesGroup" id="invited_section">
<child>
<object class="GtkScrolledWindow" id="invited_scroll">
<property name="propagate-natural-height">True</property>
<property name="max-content-height">300</property>
<child>
<object class="GtkListView" id="invited_list_view">
<property name="show-separators">True</property>
<property name="factory">
<object class="GtkBuilderListItemFactory">
<property name="resource">/org/gnome/Fractal/content-member-item.ui</property>
</object>
</property>
<style>
<class name="content"/>
<class name="pill"/>
<class name="suggested-action"/>
</style>
</object>
</child>
@ -79,3 +83,4 @@
</child>
</template>
</interface>

View file

@ -1,151 +1,200 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="RoomDetails" parent="AdwPreferencesWindow">
<template class="RoomDetails" parent="AdwWindow">
<property name="title" translatable="yes">Room Details</property>
<property name="default-widget">edit_toggle</property>
<property name="search-enabled">False</property>
<child>
<object class="AdwPreferencesPage">
<property name="icon-name">applications-system-symbolic</property>
<property name="title" translatable="yes">General</property>
<property name="name">general</property>
<property name="modal">True</property>
<property name="destroy_with_parent">True</property>
<property name="default-width">640</property>
<property name="default-height">576</property>
<property name="content">
<object class="GtkStack" id="main_stack">
<child>
<object class="AdwPreferencesGroup">
<style>
<class name="room-details-group"/>
</style>
<child>
<object class="GtkOverlay">
<property name="halign">center</property>
<object class="GtkStackPage">
<property name="icon-name">applications-system-symbolic</property>
<property name="title" translatable="yes">General</property>
<property name="name">general</property>
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="ComponentsAvatar">
<property name="size">128</property>
<binding name="item">
<lookup name="avatar">
<lookup name="room">RoomDetails</lookup>
</lookup>
</binding>
</object>
<object class="GtkHeaderBar"/>
</child>
<child type="overlay">
<object class="AdwBin" id="avatar_remove_button">
<style>
<class name="cutout-button"/>
</style>
<property name="halign">end</property>
<property name="valign">start</property>
<child>
<object class="GtkButton">
<property name="icon-name">user-trash-symbolic</property>
<property name="action-name">details.remove-avatar</property>
<style>
<class name="circular"/>
</style>
</object>
</child>
</object>
</child>
<child type="overlay">
<object class="AdwBin" id="avatar_edit_button">
<style>
<class name="cutout-button"/>
</style>
<property name="halign">end</property>
<property name="valign">end</property>
<child>
<object class="GtkButton">
<property name="icon-name">document-edit-symbolic</property>
<property name="action-name">details.choose-avatar</property>
<style>
<class name="circular"/>
</style>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="AdwClamp">
<property name="maximum-size">400</property>
<property name="tightening-threshold">400</property>
<property name="margin-top">12</property>
<property name="child">
<object class="GtkBox">
<property name="spacing">6</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkEntry" id="room_name_entry">
<property name="sensitive">false</property>
<property name="activates-default">True</property>
<property name="xalign">0.5</property>
<property name="buffer">
<object class="GtkEntryBuffer" id="room_name_buffer">
<binding name="text">
<lookup name="display-name">
<lookup name="room">RoomDetails</lookup>
</lookup>
</binding>
</object>
</property>
<style>
<class name="room-details-name"/>
</style>
</object>
</child>
<child>
<object class="GtkLabel" id="room_topic_label">
<property name="visible">false</property>
<property name="margin-top">12</property>
<property name="label" translatable="yes">Description</property>
<property name="halign">start</property>
<style>
<class name="dim-label"/>
<class name="caption-heading"/>
</style>
</object>
</child>
<child>
<object class="CustomEntry" id="room_topic_entry">
<property name="sensitive">false</property>
<property name="margin-bottom">18</property>
<child>
<object class="AdwClamp">
<property name="maximum-size">400</property>
<property name="tightening-threshold">400</property>
<property name="margin-top">12</property>
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">24</property>
<child>
<object class="GtkTextView" id="room_topic_text_view">
<property name="justification">center</property>
<property name="wrap-mode">word-char</property>
<property name="accepts-tab">False</property>
<property name="top-margin">7</property>
<property name="bottom-margin">7</property>
<property name="buffer">
<object class="GtkTextBuffer" id="room_topic_buffer">
<binding name="text">
<lookup name="topic">
<lookup name="room">RoomDetails</lookup>
</lookup>
</binding>
<object class="AdwPreferencesGroup">
<style>
<class name="room-details-group"/>
</style>
<child>
<object class="GtkOverlay">
<property name="halign">center</property>
<child>
<object class="ComponentsAvatar">
<property name="size">128</property>
<binding name="item">
<lookup name="avatar">
<lookup name="room">RoomDetails</lookup>
</lookup>
</binding>
</object>
</child>
<child type="overlay">
<object class="AdwBin" id="avatar_remove_button">
<style>
<class name="cutout-button"/>
</style>
<property name="halign">end</property>
<property name="valign">start</property>
<child>
<object class="GtkButton">
<property name="icon-name">user-trash-symbolic</property>
<property name="action-name">details.remove-avatar</property>
<style>
<class name="circular"/>
</style>
</object>
</child>
</object>
</child>
<child type="overlay">
<object class="AdwBin" id="avatar_edit_button">
<style>
<class name="cutout-button"/>
</style>
<property name="halign">end</property>
<property name="valign">end</property>
<child>
<object class="GtkButton">
<property name="icon-name">document-edit-symbolic</property>
<property name="action-name">details.choose-avatar</property>
<style>
<class name="circular"/>
</style>
</object>
</child>
</object>
</child>
</object>
</property>
</child>
<child>
<object class="GtkBox">
<property name="spacing">6</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkEntry" id="room_name_entry">
<property name="sensitive">false</property>
<property name="activates-default">True</property>
<property name="xalign">0.5</property>
<property name="buffer">
<object class="GtkEntryBuffer" id="room_name_buffer">
<binding name="text">
<lookup name="display-name">
<lookup name="room">RoomDetails</lookup>
</lookup>
</binding>
</object>
</property>
<style>
<class name="room-details-name"/>
</style>
</object>
</child>
<child>
<object class="GtkLabel" id="room_topic_label">
<property name="visible">false</property>
<property name="margin-top">12</property>
<property name="label" translatable="yes">Description</property>
<property name="halign">start</property>
<style>
<class name="dim-label"/>
<class name="caption-heading"/>
</style>
</object>
</child>
<child>
<object class="CustomEntry" id="room_topic_entry">
<property name="sensitive">false</property>
<property name="margin-bottom">18</property>
<child>
<object class="GtkTextView" id="room_topic_text_view">
<property name="justification">center</property>
<property name="wrap-mode">word-char</property>
<property name="accepts-tab">False</property>
<property name="top-margin">7</property>
<property name="bottom-margin">7</property>
<property name="buffer">
<object class="GtkTextBuffer" id="room_topic_buffer">
<binding name="text">
<lookup name="topic">
<lookup name="room">RoomDetails</lookup>
</lookup>
</binding>
</object>
</property>
</object>
</child>
<style>
<class name="room-details-topic"/>
</style>
</object>
</child>
<child>
<object class="GtkButton" id="edit_toggle">
<property name="halign">center</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="AdwPreferencesGroup">
<child>
<object class="AdwActionRow">
<property name="title" translatable="yes">Members</property>
<property name="icon-name">system-users-symbolic</property>
<property name="action-name">details.next-page</property>
<property name="action-target">&apos;members&apos;</property>
<property name="activatable">True</property>
<child type="suffix">
<object class="GtkLabel" id="members_count">
<property name="valign">center</property>
<property name="halign">center</property>
</object>
</child>
<child type="suffix">
<object class="GtkImage">
<property name="valign">center</property>
<property name="halign">center</property>
<property name="icon-name">go-next-symbolic</property>
</object>
</child>
</object>
</child>
</object>
</child>
<style>
<class name="room-details-topic"/>
</style>
</object>
</child>
<child>
<object class="GtkButton" id="edit_toggle">
<property name="halign">center</property>
</object>
</child>
</property>
</object>
</property>
</child>
</object>
</child>
</property>
</object>
</child>
</object>
</child>
<!-- ContentMemberPage goes here -->
</property>
<style>
<class name="room-details"/>
</style>
</template>
</interface>

View file

@ -70,6 +70,7 @@ src/session/content/verification/identity_verification_widget.rs
src/session/content/verification/session_verification.rs
src/session/mod.rs
src/session/room/event_actions.rs
src/session/room/member.rs
src/session/room/member_role.rs
src/session/room/mod.rs
src/session/room/timeline/timeline_day_divider.rs

View file

@ -11,7 +11,7 @@ use self::{
};
use crate::{
components::{Pill, SpinnerButton},
session::{content::RoomDetails, Room, User},
session::{Room, User},
spawn,
};
@ -246,8 +246,7 @@ impl InviteSubpage {
}
fn close(&self) {
let window = self.root().unwrap().downcast::<RoomDetails>().unwrap();
window.close_invite_subpage();
self.activate_action("details.previous-page", None).unwrap();
}
fn add_user_pill(&self, user: &Invitee) {

View file

@ -0,0 +1,233 @@
use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*};
use crate::session::content::room_details::member_page::MembershipSubpageItem;
mod imp {
use std::cell::Cell;
use once_cell::{sync::Lazy, unsync::OnceCell};
use super::*;
#[derive(Debug, Default)]
pub struct ExtraLists {
pub joined: OnceCell<gio::ListModel>,
pub invited: OnceCell<MembershipSubpageItem>,
pub banned: OnceCell<MembershipSubpageItem>,
pub invited_is_empty: Cell<bool>,
pub banned_is_empty: Cell<bool>,
}
#[glib::object_subclass]
impl ObjectSubclass for ExtraLists {
const NAME: &'static str = "ContentMembersExtraLists";
type Type = super::ExtraLists;
type Interfaces = (gio::ListModel,);
}
impl ObjectImpl for ExtraLists {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecObject::new(
"joined",
"Joined",
"The item for the subpage of joined members",
gio::ListModel::static_type(),
glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
),
glib::ParamSpecObject::new(
"invited",
"Invited",
"The item for the subpage of invited members",
MembershipSubpageItem::static_type(),
glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
),
glib::ParamSpecObject::new(
"banned",
"Banned",
"The item for the subpage of banned members",
MembershipSubpageItem::static_type(),
glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
),
]
});
PROPERTIES.as_ref()
}
fn set_property(
&self,
obj: &Self::Type,
_id: usize,
value: &glib::Value,
pspec: &glib::ParamSpec,
) {
match pspec.name() {
"joined" => obj.set_joined(value.get().unwrap()),
"invited" => obj.set_invited(value.get().unwrap()),
"banned" => obj.set_banned(value.get().unwrap()),
_ => unimplemented!(),
}
}
fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"joined" => obj.joined().to_value(),
"invited" => obj.invited().to_value(),
"banned" => obj.banned().to_value(),
_ => unimplemented!(),
}
}
fn constructed(&self, obj: &Self::Type) {
self.parent_constructed(obj);
let joined_members = obj.joined();
let invited_members = obj.invited().model();
let banned_members = obj.banned().model();
joined_members.connect_items_changed(
clone!(@weak obj => move |_, position, removed, added| {
obj.items_changed(position + obj.n_visible_extras(), removed, added)
}),
);
invited_members.connect_items_changed(clone!(@weak obj => move |_, _, _, _| {
obj.update_items();
}));
banned_members.connect_items_changed(clone!(@weak obj => move |_, _, _, _| {
obj.update_items();
}));
self.invited_is_empty.set(invited_members.n_items() == 0);
self.banned_is_empty.set(banned_members.n_items() == 0);
}
}
impl ListModelImpl for ExtraLists {
fn item_type(&self, _list_model: &Self::Type) -> glib::Type {
glib::Object::static_type()
}
fn n_items(&self, list_model: &Self::Type) -> u32 {
list_model.joined().n_items() + list_model.n_visible_extras()
}
fn item(&self, list_model: &Self::Type, position: u32) -> Option<glib::Object> {
if position == 0 && !self.invited_is_empty.get() {
let invited = self.invited.get().unwrap();
return Some(invited.clone().upcast());
}
if (position == 0 && self.invited_is_empty.get() && !self.banned_is_empty.get())
|| (position == 1 && !self.banned_is_empty.get())
{
let banned = self.banned.get().unwrap();
return Some(banned.clone().upcast());
}
list_model
.joined()
.item(position - list_model.n_visible_extras())
}
}
}
glib::wrapper! {
pub struct ExtraLists(ObjectSubclass<imp::ExtraLists>)
@implements gio::ListModel;
}
impl ExtraLists {
pub fn new(
joined: &impl IsA<gio::ListModel>,
invited: &MembershipSubpageItem,
banned: &MembershipSubpageItem,
) -> Self {
glib::Object::new(&[("joined", joined), ("invited", invited), ("banned", banned)])
.expect("Failed to create ExtraLists")
}
pub fn joined(&self) -> &gio::ListModel {
self.imp().joined.get().unwrap()
}
fn set_joined(&self, model: gio::ListModel) {
self.imp().joined.set(model).unwrap();
}
pub fn invited(&self) -> &MembershipSubpageItem {
self.imp().invited.get().unwrap()
}
fn set_invited(&self, item: MembershipSubpageItem) {
self.imp().invited.set(item).unwrap();
}
pub fn banned(&self) -> &MembershipSubpageItem {
self.imp().banned.get().unwrap()
}
fn set_banned(&self, item: MembershipSubpageItem) {
self.imp().banned.set(item).unwrap();
}
fn update_items(&self) {
let priv_ = self.imp();
let invited_was_empty = priv_.invited_is_empty.get();
let banned_was_empty = priv_.banned_is_empty.get();
let invited_is_empty = self.invited().model().n_items() == 0;
let banned_is_empty = self.banned().model().n_items() == 0;
let invited_changed = invited_was_empty != invited_is_empty;
let banned_changed = banned_was_empty != banned_is_empty;
if !invited_changed && !banned_changed {
// Nothing changed so don't do anything
return;
}
let mut position = 0;
let mut removed = 0;
let mut added = 0;
if invited_changed {
if invited_is_empty {
removed = 1;
} else {
added = 1;
}
} else if !invited_is_empty {
position = 1;
}
if banned_changed {
if banned_is_empty {
removed += 1;
} else {
added += 1;
}
}
priv_.invited_is_empty.set(invited_is_empty);
priv_.banned_is_empty.set(banned_is_empty);
self.items_changed(position, removed, added);
}
fn n_visible_extras(&self) -> u32 {
let priv_ = self.imp();
let mut len = 0;
if !priv_.invited_is_empty.get() {
len += 1;
}
if !priv_.banned_is_empty.get() {
len += 1;
}
len
}
}

View file

@ -0,0 +1,121 @@
use adw::{prelude::BinExt, subclass::prelude::*};
use gtk::{glib, glib::prelude::*};
use super::{MemberRow, MembershipSubpageItem, MembershipSubpageRow};
use crate::session::room::Member;
mod imp {
use std::cell::RefCell;
use super::*;
#[derive(Debug, Default)]
pub struct ItemRow {
pub item: RefCell<Option<glib::Object>>,
}
#[glib::object_subclass]
impl ObjectSubclass for ItemRow {
const NAME: &'static str = "ContentMemberItemRow";
type Type = super::ItemRow;
type ParentType = adw::Bin;
}
impl ObjectImpl for ItemRow {
fn properties() -> &'static [glib::ParamSpec] {
use once_cell::sync::Lazy;
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![glib::ParamSpecObject::new(
"item",
"Item",
"The membership subpage item represented by this row",
glib::Object::static_type(),
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
)]
});
PROPERTIES.as_ref()
}
fn set_property(
&self,
obj: &Self::Type,
_id: usize,
value: &glib::Value,
pspec: &glib::ParamSpec,
) {
match pspec.name() {
"item" => obj.set_item(value.get().unwrap()),
_ => unimplemented!(),
}
}
fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"item" => obj.item().to_value(),
_ => unimplemented!(),
}
}
}
impl WidgetImpl for ItemRow {}
impl BinImpl for ItemRow {}
}
glib::wrapper! {
pub struct ItemRow(ObjectSubclass<imp::ItemRow>)
@extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
}
impl ItemRow {
pub fn new() -> Self {
glib::Object::new(&[]).expect("Failed to create ItemRow")
}
pub fn item(&self) -> Option<glib::Object> {
self.imp().item.borrow().clone()
}
fn set_item(&self, item: Option<glib::Object>) {
if self.item() == item {
return;
}
if let Some(item) = item.as_ref() {
if let Some(member) = item.downcast_ref::<Member>() {
let child = if let Some(Ok(child)) = self.child().map(|w| w.downcast::<MemberRow>())
{
child
} else {
let child = MemberRow::new();
self.set_child(Some(&child));
child
};
child.set_member(Some(member.clone()));
} else if let Some(item) = item.downcast_ref::<MembershipSubpageItem>() {
let child = if let Some(Ok(child)) =
self.child().map(|w| w.downcast::<MembershipSubpageRow>())
{
child
} else {
let child = MembershipSubpageRow::new();
self.set_child(Some(&child));
child
};
child.set_item(Some(item.clone()));
} else {
unimplemented!("The object {:?} doesn't have a widget implementation", item);
}
}
self.imp().item.replace(item);
self.notify("item");
}
}
impl Default for ItemRow {
fn default() -> Self {
Self::new()
}
}

View file

@ -1,7 +1,10 @@
use adw::subclass::prelude::BinImpl;
use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate};
use crate::session::{content::RoomDetails, room::Member};
use crate::session::{
content::room_details::{member_page::MemberMenu, MemberPage},
room::Member,
};
mod imp {
use std::cell::RefCell;
@ -76,10 +79,8 @@ mod imp {
self.menu_btn
.connect_toggled(clone!(@weak obj => move |btn| {
if let Some(details) = obj.details() {
let page = details.member_page();
let menu = page.member_menu();
if btn.is_active() {
if btn.is_active() {
if let Some(menu) = obj.member_menu() {
menu.present_popover(btn, obj.member());
}
}
@ -96,8 +97,8 @@ glib::wrapper! {
}
impl MemberRow {
pub fn new(member: &Member) -> Self {
glib::Object::new(&[("member", member)]).expect("Failed to create MemberRow")
pub fn new() -> Self {
glib::Object::new(&[]).expect("Failed to create MemberRow")
}
pub fn member(&self) -> Option<Member> {
@ -113,10 +114,7 @@ impl MemberRow {
// We need to update the member of the menu if it's shown for this row
if priv_.menu_btn.is_active() {
if let Some(details) = self.details() {
let page = details.member_page();
let menu = page.member_menu();
if let Some(menu) = self.member_menu() {
menu.set_member(member.clone());
}
}
@ -125,7 +123,17 @@ impl MemberRow {
self.notify("member");
}
fn details(&self) -> Option<RoomDetails> {
Some(self.root()?.downcast::<RoomDetails>().unwrap())
fn member_menu(&self) -> Option<MemberMenu> {
let member_page = self
.ancestor(MemberPage::static_type())?
.downcast::<MemberPage>()
.unwrap();
Some(member_page.member_menu().clone())
}
}
impl Default for MemberRow {
fn default() -> Self {
Self::new()
}
}

View file

@ -0,0 +1,101 @@
use gtk::{
gio, glib,
glib::{prelude::*, subclass::prelude::*},
};
use crate::session::room::Membership;
mod imp {
use std::cell::Cell;
use once_cell::{sync::Lazy, unsync::OnceCell};
use super::*;
#[derive(Debug, Default)]
pub struct MembershipSubpageItem {
pub state: Cell<Membership>,
pub model: OnceCell<gio::ListModel>,
}
#[glib::object_subclass]
impl ObjectSubclass for MembershipSubpageItem {
const NAME: &'static str = "ContentMemberPageMembershipSubpageItem";
type Type = super::MembershipSubpageItem;
}
impl ObjectImpl for MembershipSubpageItem {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecEnum::new(
"state",
"State",
"The membership state this list contains",
Membership::static_type(),
Membership::default() as i32,
glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
),
glib::ParamSpecObject::new(
"model",
"Model",
"The model used for this subview",
gio::ListModel::static_type(),
glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
),
]
});
PROPERTIES.as_ref()
}
fn set_property(
&self,
obj: &Self::Type,
_id: usize,
value: &glib::Value,
pspec: &glib::ParamSpec,
) {
match pspec.name() {
"state" => obj.set_state(value.get().unwrap()),
"model" => obj.set_model(value.get().unwrap()),
_ => unimplemented!(),
}
}
fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"state" => obj.state().to_value(),
"model" => obj.model().to_value(),
_ => unimplemented!(),
}
}
}
}
glib::wrapper! {
pub struct MembershipSubpageItem(ObjectSubclass<imp::MembershipSubpageItem>);
}
impl MembershipSubpageItem {
pub fn new(state: Membership, model: &impl IsA<gio::ListModel>) -> Self {
glib::Object::new(&[("state", &state), ("model", model)])
.expect("Failed to create MembershipSubpageItem")
}
pub fn state(&self) -> Membership {
self.imp().state.get()
}
fn set_state(&self, state: Membership) {
self.imp().state.set(state);
}
pub fn model(&self) -> &gio::ListModel {
self.imp().model.get().unwrap()
}
fn set_model(&self, model: gio::ListModel) {
self.imp().model.set(model).unwrap();
}
}

View file

@ -0,0 +1,172 @@
use adw::subclass::prelude::*;
use gtk::{gdk, glib, glib::clone, prelude::*, CompositeTemplate};
use crate::session::content::room_details::member_page::MembershipSubpageItem;
mod imp {
use std::cell::RefCell;
use glib::{signal::SignalHandlerId, subclass::InitializingObject};
use super::*;
#[derive(Debug, Default, CompositeTemplate)]
#[template(resource = "/org/gnome/Fractal/content-member-page-membership-subpage-row.ui")]
pub struct MembershipSubpageRow {
/// The item of this row.
pub item: RefCell<Option<MembershipSubpageItem>>,
pub gesture: gtk::GestureClick,
#[template_child]
pub members_count: TemplateChild<gtk::Label>,
pub members_count_handler_id: RefCell<Option<SignalHandlerId>>,
}
#[glib::object_subclass]
impl ObjectSubclass for MembershipSubpageRow {
const NAME: &'static str = "ContentMemberPageMembershipSubpageRow";
type Type = super::MembershipSubpageRow;
type ParentType = adw::ActionRow;
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for MembershipSubpageRow {
fn properties() -> &'static [glib::ParamSpec] {
use once_cell::sync::Lazy;
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecObject::new(
"item",
"Item",
"The item of this row",
MembershipSubpageItem::static_type(),
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
glib::ParamSpecString::new(
"label",
"Label",
"The label to show for this row",
None,
glib::ParamFlags::READABLE,
),
]
});
PROPERTIES.as_ref()
}
fn set_property(
&self,
obj: &Self::Type,
_id: usize,
value: &glib::Value,
pspec: &glib::ParamSpec,
) {
match pspec.name() {
"item" => obj.set_item(value.get().unwrap()),
_ => unimplemented!(),
}
}
fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"item" => obj.item().to_value(),
"label" => obj.label().to_value(),
_ => unimplemented!(),
}
}
fn constructed(&self, obj: &Self::Type) {
self.parent_constructed(obj);
self.gesture.set_touch_only(false);
self.gesture.set_button(gdk::BUTTON_PRIMARY);
self.gesture
.connect_released(clone!(@weak obj => move |_, _, _, _| {
if let Some(item) = obj.item() {
obj.activate_action(
"members.subpage",
Some(&(item.state() as u32).to_variant()),
)
.unwrap();
}
}));
self.gesture
.set_propagation_phase(gtk::PropagationPhase::Capture);
obj.add_controller(&self.gesture);
}
}
impl WidgetImpl for MembershipSubpageRow {}
impl ListBoxRowImpl for MembershipSubpageRow {}
impl PreferencesRowImpl for MembershipSubpageRow {}
impl ActionRowImpl for MembershipSubpageRow {}
}
glib::wrapper! {
pub struct MembershipSubpageRow(ObjectSubclass<imp::MembershipSubpageRow>)
@extends gtk::Widget, adw::ActionRow, @implements gtk::Accessible;
}
impl MembershipSubpageRow {
pub fn new() -> Self {
glib::Object::new(&[]).expect("Failed to create MembershipSubpageRow")
}
pub fn item(&self) -> Option<MembershipSubpageItem> {
self.imp().item.borrow().clone()
}
pub fn set_item(&self, item: Option<MembershipSubpageItem>) {
let priv_ = self.imp();
let prev_item = self.item();
if prev_item == item {
return;
}
if let Some(signal_id) = priv_.members_count_handler_id.take() {
if let Some(prev_item) = prev_item {
prev_item.disconnect(signal_id);
}
}
if let Some(item) = item.as_ref() {
let model = item.model();
let signal_id =
model.connect_items_changed(clone!(@weak self as obj => move |model, _, _, _| {
obj.member_count_changed(model.n_items());
}));
self.member_count_changed(model.n_items());
self.imp().members_count_handler_id.replace(Some(signal_id));
}
self.imp().item.replace(item);
self.notify("item");
self.notify("label");
}
pub fn label(&self) -> Option<String> {
Some(self.item()?.state().to_string())
}
fn member_count_changed(&self, n: u32) {
self.imp().members_count.set_text(&format!("{}", n));
}
}
impl Default for MembershipSubpageRow {
fn default() -> Self {
Self::new()
}
}

View file

@ -0,0 +1,119 @@
use adw::{
prelude::*,
subclass::{bin::BinImpl, prelude::*},
};
use gtk::{gio, glib, CompositeTemplate};
use crate::components::{Avatar, Badge};
pub mod extra_lists;
mod item_row;
mod member_row;
mod membership_subpage_item;
mod membership_subpage_row;
use item_row::ItemRow;
use member_row::MemberRow;
pub use membership_subpage_item::MembershipSubpageItem;
use membership_subpage_row::MembershipSubpageRow;
mod imp {
use glib::subclass::InitializingObject;
use once_cell::sync::Lazy;
use super::*;
#[derive(Debug, Default, CompositeTemplate)]
#[template(resource = "/org/gnome/Fractal/content-member-page-list-view.ui")]
pub struct MembersListView {
#[template_child]
pub members_list_view: TemplateChild<gtk::ListView>,
pub model: glib::WeakRef<gio::ListModel>,
}
#[glib::object_subclass]
impl ObjectSubclass for MembersListView {
const NAME: &'static str = "ContentMembersListView";
type Type = super::MembersListView;
type ParentType = adw::Bin;
fn class_init(klass: &mut Self::Class) {
Avatar::static_type();
Badge::static_type();
MemberRow::static_type();
ItemRow::static_type();
Self::bind_template(klass);
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for MembersListView {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![glib::ParamSpecObject::new(
"model",
"Model",
"The model used for this view",
gio::ListModel::static_type(),
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
)]
});
PROPERTIES.as_ref()
}
fn set_property(
&self,
obj: &Self::Type,
_id: usize,
value: &glib::Value,
pspec: &glib::ParamSpec,
) {
match pspec.name() {
"model" => obj.set_model(value.get::<Option<&gio::ListModel>>().unwrap()),
_ => unimplemented!(),
}
}
fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"model" => obj.model().to_value(),
_ => unimplemented!(),
}
}
}
impl WidgetImpl for MembersListView {}
impl BinImpl for MembersListView {}
}
glib::wrapper! {
pub struct MembersListView(ObjectSubclass<imp::MembersListView>)
@extends gtk::Widget, adw::Bin;
}
impl MembersListView {
pub fn new(model: &impl IsA<gio::ListModel>) -> Self {
glib::Object::new(&[("model", model)]).expect("Failed to create MembersListView")
}
pub fn model(&self) -> Option<gio::ListModel> {
self.imp().model.upgrade()
}
pub fn set_model(&self, model: Option<&impl IsA<gio::ListModel>>) {
let model: Option<&gio::ListModel> = model.map(|model| model.upcast_ref());
if self.model().as_ref() == model {
return;
}
self.imp()
.members_list_view
.set_model(Some(&gtk::NoSelection::new(model)));
self.imp().model.set(model);
self.notify("model");
}
}

View file

@ -1,29 +1,37 @@
use adw::{prelude::*, subclass::prelude::*};
use adw::{
prelude::*,
subclass::{bin::BinImpl, prelude::*},
};
use gettextrs::gettext;
use gtk::{
gio,
glib::{self, clone, closure},
CompositeTemplate,
};
use log::warn;
mod member_menu;
mod member_row;
mod members_list_view;
use self::{member_menu::MemberMenu, member_row::MemberRow};
use members_list_view::{MembersListView, MembershipSubpageItem};
use self::member_menu::MemberMenu;
use crate::{
components::{Avatar, Badge},
ngettext_f,
prelude::*,
session::{
content::RoomDetails,
content::room_details::member_page::members_list_view::extra_lists::ExtraLists,
room::{Member, Membership, RoomAction},
Room, User, UserActions,
},
spawn,
};
const MAX_LIST_HEIGHT: i32 = 300;
mod imp {
use std::{
cell::{Cell, RefCell},
collections::HashMap,
};
use glib::subclass::InitializingObject;
use once_cell::{sync::Lazy, unsync::OnceCell};
@ -32,36 +40,27 @@ mod imp {
#[derive(Debug, Default, CompositeTemplate)]
#[template(resource = "/org/gnome/Fractal/content-member-page.ui")]
pub struct MemberPage {
pub room: OnceCell<Room>,
#[template_child]
pub member_count: TemplateChild<gtk::Label>,
#[template_child]
pub invite_button: TemplateChild<gtk::Button>,
pub room: RefCell<Option<Room>>,
#[template_child]
pub members_search_entry: TemplateChild<gtk::SearchEntry>,
#[template_child]
pub members_list_view: TemplateChild<gtk::ListView>,
pub list_stack: TemplateChild<gtk::Stack>,
#[template_child]
pub members_scroll: TemplateChild<gtk::ScrolledWindow>,
pub invite_button: TemplateChild<gtk::Button>,
pub member_menu: OnceCell<MemberMenu>,
#[template_child]
pub invited_section: TemplateChild<adw::PreferencesGroup>,
#[template_child]
pub invited_list_view: TemplateChild<gtk::ListView>,
#[template_child]
pub invited_scroll: TemplateChild<gtk::ScrolledWindow>,
pub list_stack_children: RefCell<HashMap<Membership, glib::WeakRef<MembersListView>>>,
pub state: Cell<Membership>,
pub invite_action_watch: RefCell<Option<gtk::ExpressionWatch>>,
}
#[glib::object_subclass]
impl ObjectSubclass for MemberPage {
const NAME: &'static str = "ContentMemberPage";
type Type = super::MemberPage;
type ParentType = adw::PreferencesPage;
type ParentType = adw::Bin;
fn class_init(klass: &mut Self::Class) {
Avatar::static_type();
Badge::static_type();
MemberRow::static_type();
MembersListView::static_type();
Self::bind_template(klass);
klass.install_action("member.verify", None, move |widget, _, _| {
@ -71,6 +70,28 @@ mod imp {
warn!("No member was selected to be verified");
}
});
klass.install_action("members.subpage", Some("u"), move |widget, _, param| {
use std::convert::TryFrom;
let state = param
.and_then(|variant| variant.get::<u32>())
.and_then(|u| Membership::try_from(u).ok());
if let Some(state) = state {
widget.set_state(state);
}
});
klass.install_action("members.previous", None, move |widget, _, _| {
if widget.state() == Membership::Join {
widget
.activate_action("details.previous-page", None)
.unwrap();
} else {
widget.set_state(Membership::Join);
}
});
}
fn instance_init(obj: &InitializingObject<Self>) {
@ -87,7 +108,7 @@ mod imp {
"Room",
"The room backing all details of the member page",
Room::static_type(),
glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
glib::ParamSpecObject::new(
"member-menu",
@ -96,6 +117,14 @@ mod imp {
MemberMenu::static_type(),
glib::ParamFlags::READABLE,
),
glib::ParamSpecEnum::new(
"state",
"State",
"The membership state of the displayed members",
Membership::static_type(),
Membership::default() as i32,
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
]
});
@ -111,33 +140,35 @@ mod imp {
) {
match pspec.name() {
"room" => obj.set_room(value.get().unwrap()),
"state" => obj.set_state(value.get().unwrap()),
_ => unimplemented!(),
}
}
fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"room" => self.room.get().to_value(),
"room" => obj.room().to_value(),
"member-menu" => obj.member_menu().to_value(),
"state" => obj.state().to_value(),
_ => unimplemented!(),
}
}
fn constructed(&self, obj: &Self::Type) {
self.parent_constructed(obj);
obj.init_members_list();
obj.init_invited_list();
obj.init_invite_button();
fn dispose(&self, _: &Self::Type) {
if let Some(invite_action) = self.invite_action_watch.take() {
invite_action.unwatch();
}
}
}
impl WidgetImpl for MemberPage {}
impl PreferencesPageImpl for MemberPage {}
impl BinImpl for MemberPage {}
}
glib::wrapper! {
pub struct MemberPage(ObjectSubclass<imp::MemberPage>)
@extends gtk::Widget, adw::PreferencesPage;
@extends gtk::Widget, adw::Bin;
}
impl MemberPage {
@ -145,35 +176,34 @@ impl MemberPage {
glib::Object::new(&[("room", room)]).expect("Failed to create MemberPage")
}
pub fn room(&self) -> &Room {
self.imp().room.get().unwrap()
pub fn room(&self) -> Option<Room> {
self.imp().room.borrow().as_ref().cloned()
}
fn set_room(&self, room: Room) {
self.imp().room.set(room).expect("Room already initialized");
}
fn init_members_list(&self) {
pub fn set_room(&self, room: Option<Room>) {
let priv_ = self.imp();
let members = self.room().members();
let prev_room = self.room();
// Only keep the members that are in the join membership state
let joined_expression = gtk::PropertyExpression::new(
Member::static_type(),
gtk::Expression::NONE,
"membership",
)
.chain_closure::<bool>(closure!(
|_: Option<glib::Object>, membership: Membership| { membership == Membership::Join }
));
let joined_filter = gtk::BoolFilter::new(Some(joined_expression));
let joined_members = gtk::FilterListModel::new(Some(members), Some(&joined_filter));
if prev_room == room {
return;
}
// Set up the members count.
self.member_count_changed(joined_members.n_items());
joined_members.connect_items_changed(clone!(@weak self as obj => move |members, _, _, _| {
obj.member_count_changed(members.n_items());
}));
if let Some(invite_action) = priv_.invite_action_watch.take() {
invite_action.unwatch();
}
if let Some(room) = room.as_ref() {
self.init_members_list(room);
self.init_invite_button(room);
self.set_state(Membership::Join);
}
priv_.room.replace(room);
self.notify("room");
}
fn init_members_list(&self, room: &Room) {
let priv_ = self.imp();
// Sort the members list by power level, then display name.
let sorter = gtk::MultiSorter::new();
@ -187,6 +217,7 @@ impl MemberPage {
.sort_order(gtk::SortType::Descending)
.build(),
);
sorter.append(&gtk::StringSorter::new(Some(
&gtk::PropertyExpression::new(
Member::static_type(),
@ -194,134 +225,29 @@ impl MemberPage {
"display-name",
),
)));
let sorted_members = gtk::SortListModel::new(Some(&joined_members), Some(&sorter));
fn search_string(member: Member) -> String {
format!(
"{} {} {} {}",
member.display_name(),
member.user_id(),
member.role(),
member.power_level(),
)
}
let members = gtk::SortListModel::new(Some(room.members()), Some(&sorter));
let member_expr = gtk::ClosureExpression::new::<String, &[gtk::Expression], _>(
&[],
closure!(|member: Option<Member>| { member.map(search_string).unwrap_or_default() }),
);
let filter = gtk::StringFilter::builder()
.match_mode(gtk::StringFilterMatchMode::Substring)
.expression(&member_expr)
.ignore_case(true)
.build();
priv_
.members_search_entry
.bind_property("text", &filter, "search")
.flags(glib::BindingFlags::SYNC_CREATE)
.build();
let joined_members = self.build_filtered_list(&members, Membership::Join);
let invited_members = self.build_filtered_list(&members, Membership::Invite);
let banned_members = self.build_filtered_list(&members, Membership::Ban);
let filter_model = gtk::FilterListModel::new(Some(&sorted_members), Some(&filter));
let model = gtk::NoSelection::new(Some(&filter_model));
priv_.members_list_view.set_model(Some(&model));
}
fn member_count_changed(&self, n: u32) {
let priv_ = self.imp();
priv_
.member_count
// Translators: Do NOT translate the content between '{' and '}', this is a variable
// name.
.set_text(&ngettext_f(
"1 Member",
"{n} Members",
n,
&[("n", &n.to_string())],
));
// FIXME: This won't be needed when we can request the natural height
// on AdwPreferencesPage
// See: https://gitlab.gnome.org/GNOME/libadwaita/-/issues/77
if n > 5 {
priv_.members_scroll.set_min_content_height(MAX_LIST_HEIGHT);
} else {
priv_.members_scroll.set_min_content_height(-1);
}
}
fn init_invited_list(&self) {
let priv_ = self.imp();
let members = self.room().members();
// Only keep the members that are in the join membership state
let invited_expression = gtk::PropertyExpression::new(
Member::static_type(),
gtk::Expression::NONE,
"membership",
)
.chain_closure::<bool>(closure!(
|_: Option<glib::Object>, membership: Membership| { membership == Membership::Invite }
));
let invited_filter = gtk::BoolFilter::new(Some(invited_expression));
let invited_members = gtk::FilterListModel::new(Some(members), Some(&invited_filter));
// Set up the invited section visibility and the invited count.
self.invited_count_changed(invited_members.n_items());
invited_members.connect_items_changed(
clone!(@weak self as obj => move |members, _, _, _| {
obj.invited_count_changed(members.n_items());
}),
let main_list = ExtraLists::new(
&joined_members,
&MembershipSubpageItem::new(Membership::Invite, &invited_members),
&MembershipSubpageItem::new(Membership::Ban, &banned_members),
);
// Sort the invited list by display name.
let sorter = gtk::StringSorter::new(Some(&gtk::PropertyExpression::new(
Member::static_type(),
gtk::Expression::NONE,
"display-name",
)));
let sorted_invited = gtk::SortListModel::new(Some(&invited_members), Some(&sorter));
let model = gtk::NoSelection::new(Some(&sorted_invited));
priv_.invited_list_view.set_model(Some(&model));
}
fn invited_count_changed(&self, n: u32) {
let priv_ = self.imp();
priv_.invited_section.set_visible(n > 0);
priv_
.invited_section
// Translators: Do NOT translate the content between '{' and '}', this is a variable
// name.
.set_title(&ngettext_f(
"1 Invited",
"{n} Invited",
n,
&[("n", &n.to_string())],
));
// FIXME: This won't be needed when we can request the natural height
// on AdwPreferencesPage
// See: https://gitlab.gnome.org/GNOME/libadwaita/-/issues/77
if n > 5 {
priv_.invited_scroll.set_min_content_height(MAX_LIST_HEIGHT);
} else {
priv_.invited_scroll.set_min_content_height(-1);
}
}
fn init_invite_button(&self) {
let invite_button = &*self.imp().invite_button;
let invite_possible = self.room().new_allowed_expr(RoomAction::Invite);
const NONE_OBJECT: Option<&glib::Object> = None;
invite_possible.bind(invite_button, "sensitive", NONE_OBJECT);
invite_button.connect_clicked(clone!(@weak self as obj => move |_| {
let window = obj
.root()
.unwrap()
.downcast::<RoomDetails>()
.unwrap();
window.present_invite_subpage();
}));
let mut list_stack_children = priv_.list_stack_children.borrow_mut();
let joined_view = MembersListView::new(&main_list);
priv_.list_stack.add_child(&joined_view);
list_stack_children.insert(Membership::Join, joined_view.downgrade());
let invited_view = MembersListView::new(&invited_members);
priv_.list_stack.add_child(&invited_view);
list_stack_children.insert(Membership::Invite, invited_view.downgrade());
let banned_view = MembersListView::new(&banned_members);
priv_.list_stack.add_child(&banned_view);
list_stack_children.insert(Membership::Ban, banned_view.downgrade());
}
pub fn member_menu(&self) -> &MemberMenu {
@ -352,4 +278,120 @@ impl MemberPage {
member.upcast::<User>().verify_identity().await;
}));
}
pub fn state(&self) -> Membership {
self.imp().state.get()
}
pub fn set_state(&self, state: Membership) {
let priv_ = self.imp();
if self.state() == state {
return;
}
if state == Membership::Join {
priv_
.list_stack
.set_transition_type(gtk::StackTransitionType::SlideRight)
} else {
priv_
.list_stack
.set_transition_type(gtk::StackTransitionType::SlideLeft)
}
if let Some(window) = self.root().and_then(|w| w.downcast::<adw::Window>().ok()) {
match state {
Membership::Invite => window.set_title(Some(&gettext("Invited Room Members"))),
Membership::Ban => window.set_title(Some(&gettext("Banned Room Members"))),
_ => window.set_title(Some(&gettext("Room Members"))),
}
}
if let Some(view) = priv_
.list_stack_children
.borrow()
.get(&state)
.and_then(glib::WeakRef::upgrade)
{
priv_.list_stack.set_visible_child(&view);
}
self.imp().state.set(state);
self.notify("state");
}
fn build_filtered_list(
&self,
model: &impl IsA<gio::ListModel>,
state: Membership,
) -> gio::ListModel {
let membership_expression = gtk::PropertyExpression::new(
Member::static_type(),
gtk::Expression::NONE,
"membership",
)
.chain_closure::<bool>(closure!(
|_: Option<glib::Object>, this_state: Membership| this_state == state
));
let membership_filter = gtk::BoolFilter::new(Some(&membership_expression));
fn search_string(member: Member) -> String {
format!(
"{} {} {} {}",
member.display_name(),
member.user_id(),
member.role(),
member.power_level(),
)
}
let member_expr = gtk::ClosureExpression::new::<String, &[gtk::Expression], _>(
&[],
closure!(|member: Option<Member>| { member.map(search_string).unwrap_or_default() }),
);
let search_filter = gtk::StringFilter::builder()
.match_mode(gtk::StringFilterMatchMode::Substring)
.expression(&member_expr)
.ignore_case(true)
.build();
self.imp()
.members_search_entry
.bind_property("text", &search_filter, "search")
.flags(glib::BindingFlags::SYNC_CREATE)
.build();
let filter = gtk::EveryFilter::new();
filter.append(&membership_filter);
filter.append(&search_filter);
let filter_model = gtk::FilterListModel::new(Some(model), Some(&filter));
filter_model.upcast()
}
fn init_invite_button(&self, room: &Room) {
let invite_possible = room.new_allowed_expr(RoomAction::Invite);
let watch = invite_possible.watch(
glib::Object::NONE,
clone!(@weak self as obj => move || {
obj.update_invite_button();
}),
);
self.imp().invite_action_watch.replace(Some(watch));
self.update_invite_button();
}
fn update_invite_button(&self) {
if let Some(invite_action) = &*self.imp().invite_action_watch.borrow() {
let allow_invite = invite_action
.evaluate_as::<bool>()
.expect("Created expression needs to be valid and a boolean");
self.imp().invite_button.set_visible(allow_invite);
};
}
}

View file

@ -1,6 +1,8 @@
mod invite_subpage;
mod member_page;
use std::convert::From;
use adw::{prelude::*, subclass::prelude::*};
use gettextrs::gettext;
use gtk::{
@ -17,8 +19,54 @@ use crate::{
utils::{and_expr, or_expr},
};
#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy, glib::Enum)]
#[repr(u32)]
#[enum_type(name = "RoomDetailsPageName")]
pub enum PageName {
General,
Members,
Invite,
}
impl Default for PageName {
fn default() -> Self {
Self::General
}
}
impl glib::variant::StaticVariantType for PageName {
fn static_variant_type() -> std::borrow::Cow<'static, glib::VariantTy> {
String::static_variant_type()
}
}
impl glib::variant::FromVariant for PageName {
fn from_variant(variant: &glib::variant::Variant) -> Option<Self> {
match variant.str()? {
"general" => Some(PageName::General),
"members" => Some(PageName::Members),
"invite" => Some(PageName::Invite),
_ => None,
}
}
}
impl glib::variant::ToVariant for PageName {
fn to_variant(&self) -> glib::variant::Variant {
match self {
PageName::General => "general",
PageName::Members => "members",
PageName::Invite => "invite",
}
.to_variant()
}
}
mod imp {
use std::cell::Cell;
use std::{
cell::{Cell, RefCell},
collections::HashMap,
};
use glib::subclass::InitializingObject;
use once_cell::unsync::OnceCell;
@ -31,6 +79,8 @@ mod imp {
pub room: OnceCell<Room>,
pub avatar_chooser: OnceCell<gtk::FileChooserNative>,
#[template_child]
pub main_stack: TemplateChild<gtk::Stack>,
#[template_child]
pub avatar_remove_button: TemplateChild<adw::Bin>,
#[template_child]
pub avatar_edit_button: TemplateChild<adw::Bin>,
@ -44,15 +94,19 @@ mod imp {
pub room_topic_entry: TemplateChild<CustomEntry>,
#[template_child]
pub room_topic_label: TemplateChild<gtk::Label>,
pub member_page: OnceCell<MemberPage>,
#[template_child]
pub members_count: TemplateChild<gtk::Label>,
pub edit_mode: Cell<bool>,
pub list_stack_children: RefCell<HashMap<PageName, glib::WeakRef<gtk::Widget>>>,
pub visible_page: Cell<PageName>,
pub previous_visible_page: RefCell<Vec<PageName>>,
}
#[glib::object_subclass]
impl ObjectSubclass for RoomDetails {
const NAME: &'static str = "RoomDetails";
type Type = super::RoomDetails;
type ParentType = adw::PreferencesWindow;
type ParentType = adw::Window;
fn class_init(klass: &mut Self::Class) {
CustomEntry::static_type();
@ -63,6 +117,16 @@ mod imp {
klass.install_action("details.remove-avatar", None, move |widget, _, _| {
widget.room().store_avatar(None)
});
klass.install_action("details.next-page", Some("s"), move |widget, _, param| {
let page = param
.and_then(|variant| variant.get::<PageName>())
.expect("The parameter need to be a valid PageName");
widget.next_page(page);
});
klass.install_action("details.previous-page", None, move |widget, _, _| {
widget.previous_page();
});
}
fn instance_init(obj: &InitializingObject<Self>) {
@ -74,13 +138,23 @@ mod imp {
fn properties() -> &'static [glib::ParamSpec] {
use once_cell::sync::Lazy;
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![glib::ParamSpecObject::new(
"room",
"Room",
"The room backing all details of the preference window",
Room::static_type(),
glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
)]
vec![
glib::ParamSpecObject::new(
"room",
"Room",
"The room backing all details of the preference window",
Room::static_type(),
glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
),
glib::ParamSpecEnum::new(
"visible-page",
"Visible Page",
"The page currently visible",
PageName::static_type(),
PageName::default() as i32,
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
]
});
PROPERTIES.as_ref()
@ -95,13 +169,15 @@ mod imp {
) {
match pspec.name() {
"room" => obj.set_room(value.get().unwrap()),
"visible-page" => obj.set_visible_page(value.get().unwrap()),
_ => unimplemented!(),
}
}
fn property(&self, _obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"room" => self.room.get().to_value(),
"visible-page" => obj.visible_page().to_value(),
_ => unimplemented!(),
}
}
@ -109,26 +185,34 @@ mod imp {
fn constructed(&self, obj: &Self::Type) {
self.parent_constructed(obj);
let member_page = MemberPage::new(obj.room());
obj.add(&member_page);
self.member_page.set(member_page).unwrap();
obj.init_avatar();
obj.init_edit_toggle();
obj.init_avatar_chooser();
obj.init_member_action();
self.main_stack
.connect_visible_child_notify(clone!(@weak obj => move |_| {
obj.notify("visible-page");
}));
let members = obj.room().members();
members.connect_items_changed(clone!(@weak obj => move |members, _, _, _| {
obj.member_count_changed(members.n_items());
}));
obj.member_count_changed(members.n_items());
}
}
impl WidgetImpl for RoomDetails {}
impl WindowImpl for RoomDetails {}
impl AdwWindowImpl for RoomDetails {}
impl PreferencesWindowImpl for RoomDetails {}
}
glib::wrapper! {
/// Preference Window to display and update room details.
pub struct RoomDetails(ObjectSubclass<imp::RoomDetails>)
@extends gtk::Widget, gtk::Window, adw::Window, gtk::Root, adw::PreferencesWindow, @implements gtk::Accessible;
@extends gtk::Widget, gtk::Window, adw::Window, gtk::Root, @implements gtk::Accessible;
}
impl RoomDetails {
@ -146,6 +230,63 @@ impl RoomDetails {
self.imp().room.set(room).expect("Room already initialized");
}
pub fn visible_page(&self) -> PageName {
self.imp().visible_page.get()
}
pub fn set_visible_page(&self, name: PageName) {
let priv_ = self.imp();
let prev_name = self.visible_page();
let mut list_stack_children = priv_.list_stack_children.borrow_mut();
if prev_name == name {
return;
}
match name {
PageName::General => {
self.set_title(Some(&gettext("Room Details")));
priv_.main_stack.set_visible_child_name("general");
}
PageName::Members => {
let members_page = if let Some(members_page) = list_stack_children
.get(&PageName::Members)
.and_then(glib::object::WeakRef::upgrade)
{
members_page
} else {
let members_page = MemberPage::new(self.room()).upcast::<gtk::Widget>();
list_stack_children.insert(PageName::Members, members_page.downgrade());
self.imp().main_stack.add_child(&members_page);
members_page
};
self.set_title(Some(&gettext("Room Members")));
priv_.main_stack.set_visible_child(&members_page);
}
PageName::Invite => {
priv_.main_stack.set_visible_child_name("general");
let invite_page = if let Some(invite_page) = list_stack_children
.get(&PageName::Invite)
.and_then(glib::object::WeakRef::upgrade)
{
invite_page
} else {
let invite_page = InviteSubpage::new(self.room()).upcast::<gtk::Widget>();
list_stack_children.insert(PageName::Invite, invite_page.downgrade());
priv_.main_stack.add_child(&invite_page);
invite_page
};
self.set_title(Some(&gettext("Invite new Members")));
priv_.main_stack.set_visible_child(&invite_page);
}
}
priv_.visible_page.set(name);
self.notify("visible-page");
}
fn init_avatar(&self) {
let priv_ = self.imp();
let avatar_remove_button = &priv_.avatar_remove_button;
@ -254,18 +395,39 @@ impl RoomDetails {
self.avatar_chooser().show();
}
pub fn present_invite_subpage(&self) {
self.set_title(Some(&gettext("Invite new Members")));
let subpage = InviteSubpage::new(self.room());
self.present_subpage(&subpage);
fn member_count_changed(&self, n: u32) {
self.imp().members_count.set_text(&format!("{}", n));
}
pub fn close_invite_subpage(&self) {
self.set_title(Some(&gettext("Room Details")));
self.close_subpage();
fn next_page(&self, next_page: PageName) {
let priv_ = self.imp();
let prev_page = self.visible_page();
if prev_page == next_page {
return;
}
priv_
.main_stack
.set_transition_type(gtk::StackTransitionType::SlideLeft);
priv_.previous_visible_page.borrow_mut().push(prev_page);
self.set_visible_page(next_page);
}
pub fn member_page(&self) -> &MemberPage {
self.imp().member_page.get().unwrap()
fn previous_page(&self) {
let priv_ = self.imp();
priv_
.main_stack
.set_transition_type(gtk::StackTransitionType::SlideRight);
if let Some(prev_page) = priv_.previous_visible_page.borrow_mut().pop() {
self.set_visible_page(prev_page);
} else {
// If there isn't any previous page close the dialog since it was opened on a
// specific page
self.close();
};
}
}

View file

@ -37,7 +37,7 @@ use crate::{
components::{CustomEntry, DragOverlay, Pill, ReactionChooser, RoomTitle},
i18n::gettext_f,
session::{
content::{MarkdownPopover, RoomDetails},
content::{room_details, MarkdownPopover, RoomDetails},
room::{Room, RoomType, SupportedEvent, Timeline, TimelineItem, TimelineState},
user::UserExt,
},
@ -135,10 +135,10 @@ mod imp {
});
klass.install_action("room-history.details", None, move |widget, _, _| {
widget.open_room_details("general");
widget.open_room_details(room_details::PageName::General);
});
klass.install_action("room-history.invite-members", None, move |widget, _, _| {
widget.open_invite_members();
widget.open_room_details(room_details::PageName::Invite);
});
klass.install_action("room-history.scroll-down", None, move |widget, _, _| {
@ -574,19 +574,10 @@ impl RoomHistory {
}
/// Opens the room details on the page with the given name.
pub fn open_room_details(&self, page_name: &str) {
pub fn open_room_details(&self, page_name: room_details::PageName) {
if let Some(room) = self.room() {
let window = RoomDetails::new(&self.parent_window(), &room);
window.set_property("visible-page-name", page_name);
window.show();
}
}
pub fn open_invite_members(&self) {
if let Some(room) = self.room() {
let window = RoomDetails::new(&self.parent_window(), &room);
window.set_property("visible-page-name", "members");
window.present_invite_subpage();
window.set_visible_page(page_name);
window.show();
}
}

View file

@ -1,3 +1,4 @@
use gettextrs::gettext;
use gtk::{glib, prelude::*, subclass::prelude::*};
use matrix_sdk::{
room::RoomMember,
@ -9,6 +10,7 @@ use matrix_sdk::{
OwnedMxcUri, UserId,
},
};
use num_enum::{IntoPrimitive, TryFromPrimitive};
use crate::{
prelude::*,
@ -21,7 +23,7 @@ use crate::{
},
};
#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy, glib::Enum)]
#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy, glib::Enum, IntoPrimitive, TryFromPrimitive)]
#[repr(u32)]
#[enum_type(name = "Membership")]
pub enum Membership {
@ -39,6 +41,20 @@ impl Default for Membership {
}
}
impl std::fmt::Display for Membership {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let label = match self {
Membership::Leave => gettext("Left"),
Membership::Join => gettext("Joined"),
Membership::Invite => gettext("Invited"),
Membership::Ban => gettext("Banned"),
Membership::Knock => gettext("Knocked"),
Membership::Custom => gettext("Custom"),
};
f.write_str(&label)
}
}
impl From<&MembershipState> for Membership {
fn from(state: &MembershipState) -> Self {
match state {