[fractal/fractal-next] room_details: Add members page
- From: Julian Sparber <jsparber src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [fractal/fractal-next] room_details: Add members page
- Date: Mon, 29 Nov 2021 11:25:40 +0000 (UTC)
commit 0a6a82008086bf972e2d4470d399175d0f080475
Author: Kai A. Hiller <V02460 gmail com>
Date: Sat Aug 14 11:54:41 2021 +0200
room_details: Add members page
data/resources/resources.gresource.xml | 2 +
data/resources/style.css | 29 ++++
data/resources/ui/content-member-page.ui | 60 ++++++++
data/resources/ui/content-member-row.ui | 73 ++++++++++
data/resources/ui/content-room-details.ui | 1 +
po/POTFILES.in | 2 +
src/components/badge.rs | 133 ++++++++++++++++++
src/components/mod.rs | 2 +
src/meson.build | 2 +
src/session/content/room_details/member_page.rs | 177 ++++++++++++++++++++++++
src/session/content/room_details/mod.rs | 7 +-
src/session/mod.rs | 2 +-
12 files changed, 488 insertions(+), 2 deletions(-)
---
diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index 57a90645..0aa5e481 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -12,6 +12,8 @@
<file compressed="true" preprocess="xml-stripblanks" alias="content-item.ui">ui/content-item.ui</file>
<file compressed="true" preprocess="xml-stripblanks"
alias="content-item-row-menu.ui">ui/content-item-row-menu.ui</file>
<file compressed="true" preprocess="xml-stripblanks"
alias="content-message-file.ui">ui/content-message-file.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-row.ui">ui/content-message-row.ui</file>
<file compressed="true" preprocess="xml-stripblanks"
alias="content-divider-row.ui">ui/content-divider-row.ui</file>
<file compressed="true" preprocess="xml-stripblanks"
alias="content-room-details.ui">ui/content-room-details.ui</file>
diff --git a/data/resources/style.css b/data/resources/style.css
index 1be37f3d..166c350c 100644
--- a/data/resources/style.css
+++ b/data/resources/style.css
@@ -35,6 +35,25 @@
margin-bottom: 6px;
}
+.badge {
+ color: white;
+ background-color: lightgrey;
+ border-radius: 0.4em;
+ padding: 0.1em 0.5em;
+ font-size: 0.8em;
+ margin-left: 0.5em;
+}
+
+.admin {
+ color: white;
+ background: #cc5959;
+}
+
+.mod {
+ color: black;
+ background-color: #cea63a;
+}
+
.title-header {
font-size: 36px;
font-weight: bold;
@@ -241,6 +260,16 @@ headerbar.flat {
padding: 12px;
}
+listview.content {
+ border-radius: 9px;
+ border-width: 1px;
+ border-style: solid;
+}
+
+listview.content row:last-child {
+ border-bottom-width: 0px;
+}
+
.bold {
font-weight: bold;
}
diff --git a/data/resources/ui/content-member-page.ui b/data/resources/ui/content-member-page.ui
new file mode 100644
index 00000000..91b97611
--- /dev/null
+++ b/data/resources/ui/content-member-page.ui
@@ -0,0 +1,60 @@
+<?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>
+ <child>
+ <object class="AdwPreferencesGroup">
+ <child>
+ <object class="GtkBox">
+ <property name="margin-bottom">12</property>
+ <child>
+ <object class="GtkLabel" id="member_count">
+ <property name="halign">start</property>
+ <property name="hexpand">True</property>
+ <style>
+ <class name="heading"/>
+ <class name="h4"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton" id="invite_button">
+ <property name="label" translatable="yes">Invite new member</property>
+ <property name="halign">end</property>
+ <!-- Make the invite button invisible for now till we implement the invite dialog -->
+ <property name="visible">False</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">
+ <property name="propagate-natural-height">True</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/FractalNext/content-member-row.ui</property>
+ </object>
+ </property>
+ <style>
+ <class name="content"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
+
diff --git a/data/resources/ui/content-member-row.ui b/data/resources/ui/content-member-row.ui
new file mode 100644
index 00000000..fa1e34df
--- /dev/null
+++ b/data/resources/ui/content-member-row.ui
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="GtkListItem">
+ <property name="child">
+ <object class="GtkBox" id="header">
+ <property name="spacing">12</property>
+ <style>
+ <class name="header" />
+ </style>
+ <child>
+ <object class="ComponentsAvatar">
+ <property name="size">32</property>
+ <binding name="item">
+ <lookup name="avatar" type="Member">
+ <lookup name="item">GtkListItem</lookup>
+ </lookup>
+ </binding>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <style>
+ <class name="title" />
+ </style>
+ <child>
+ <object class="GtkBox">
+ <child>
+ <object class="GtkLabel" id="title">
+ <property name="halign">start</property>
+ <property name="ellipsize">end</property>
+ <binding name="label">
+ <lookup name="display-name" type="Member">
+ <lookup name="item">GtkListItem</lookup>
+ </lookup>
+ </binding>
+ <style>
+ <class name="title" />
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="Badge">
+ <binding name="power-level">
+ <lookup name="power-level" type="Member">
+ <lookup name="item">GtkListItem</lookup>
+ </lookup>
+ </binding>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="subtitle">
+ <property name="hexpand">True</property>
+ <property name="halign">start</property>
+ <property name="ellipsize">end</property>
+ <binding name="label">
+ <lookup name="user-id" type="Member">
+ <lookup name="item">GtkListItem</lookup>
+ </lookup>
+ </binding>
+ <style>
+ <class name="subtitle" />
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </property>
+ </template>
+</interface>
diff --git a/data/resources/ui/content-room-details.ui b/data/resources/ui/content-room-details.ui
index 90946087..9f536974 100644
--- a/data/resources/ui/content-room-details.ui
+++ b/data/resources/ui/content-room-details.ui
@@ -104,5 +104,6 @@
</child>
</object>
</child>
+ <!-- ContentMemberPage goes here -->
</template>
</interface>
diff --git a/po/POTFILES.in b/po/POTFILES.in
index e4cc9f4f..728fcbc8 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -48,6 +48,7 @@ data/resources/ui/window.ui
src/application.rs
src/components/auth_dialog.rs
src/components/avatar.rs
+src/components/badge.rs
src/components/context_menu_bin.rs
src/components/custom_entry.rs
src/components/label_with_widgets.rs
@@ -81,6 +82,7 @@ src/session/content/message_row/image.rs
src/session/content/message_row/mod.rs
src/session/content/message_row/text.rs
src/session/content/mod.rs
+src/session/content/room_details/member_page.rs
src/session/content/room_details/mod.rs
src/session/content/room_history.rs
src/session/content/state_row.rs
diff --git a/src/components/badge.rs b/src/components/badge.rs
new file mode 100644
index 00000000..96a49af5
--- /dev/null
+++ b/src/components/badge.rs
@@ -0,0 +1,133 @@
+use adw::prelude::*;
+use adw::subclass::prelude::*;
+use gtk::glib;
+use gtk::subclass::prelude::*;
+
+use crate::session::room::{MemberRole, PowerLevel, POWER_LEVEL_MAX, POWER_LEVEL_MIN};
+
+mod imp {
+ use super::*;
+ use std::cell::Cell;
+
+ #[derive(Debug, Default)]
+ pub struct Badge {
+ pub power_level: Cell<PowerLevel>,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for Badge {
+ const NAME: &'static str = "Badge";
+ type Type = super::Badge;
+ type ParentType = adw::Bin;
+ }
+
+ impl ObjectImpl for Badge {
+ fn properties() -> &'static [glib::ParamSpec] {
+ use once_cell::sync::Lazy;
+ static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+ vec![glib::ParamSpec::new_int64(
+ "power-level",
+ "Power level",
+ "The power level this badge displays",
+ POWER_LEVEL_MIN,
+ POWER_LEVEL_MAX,
+ 0,
+ 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() {
+ "power-level" => obj.set_power_level(value.get().unwrap()),
+ _ => unimplemented!(),
+ }
+ }
+
+ fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ match pspec.name() {
+ "power-level" => obj.power_level().to_value(),
+ _ => unimplemented!(),
+ }
+ }
+
+ fn constructed(&self, obj: &Self::Type) {
+ self.parent_constructed(obj);
+
+ obj.add_css_class("badge");
+ let label = gtk::Label::new(Some("default"));
+ obj.set_child(Some(&label));
+ }
+ }
+
+ impl WidgetImpl for Badge {}
+ impl BinImpl for Badge {}
+}
+
+glib::wrapper! {
+ /// Inline widget displaying a badge with a power level.
+ ///
+ /// The badge displays admin for a power level of 100 and mod for levels
+ /// over or equal to 50.
+ pub struct Badge(ObjectSubclass<imp::Badge>)
+ @extends gtk::Widget, adw::Bin;
+}
+
+impl Badge {
+ pub fn new() -> Self {
+ glib::Object::new(&[]).expect("Failed to create Badge")
+ }
+
+ pub fn power_level(&self) -> PowerLevel {
+ let priv_ = imp::Badge::from_instance(self);
+ priv_.power_level.get()
+ }
+
+ pub fn set_power_level(&self, power_level: PowerLevel) {
+ let priv_ = imp::Badge::from_instance(self);
+ self.update_badge(power_level);
+ priv_.power_level.set(power_level);
+ self.notify("power-level");
+ }
+
+ fn update_badge(&self, power_level: PowerLevel) {
+ let label: gtk::Label = self.child().unwrap().downcast().unwrap();
+ let role = MemberRole::from(power_level);
+
+ match role {
+ MemberRole::ADMIN => {
+ label.set_text(&format!("{} {}", role, power_level));
+ self.add_css_class("admin");
+ self.remove_css_class("mod");
+ self.show();
+ }
+ MemberRole::MOD => {
+ label.set_text(&format!("{} {}", role, power_level));
+ self.add_css_class("mod");
+ self.remove_css_class("admin");
+ self.show();
+ }
+ MemberRole::PEASANT if power_level != 0 => {
+ label.set_text(&power_level.to_string());
+ self.remove_css_class("admin");
+ self.remove_css_class("mod");
+ self.show()
+ }
+ _ => self.hide(),
+ }
+ }
+}
+
+impl Default for Badge {
+ fn default() -> Self {
+ Self::new()
+ }
+}
diff --git a/src/components/mod.rs b/src/components/mod.rs
index 83d37e61..5ea7aa17 100644
--- a/src/components/mod.rs
+++ b/src/components/mod.rs
@@ -1,5 +1,6 @@
mod auth_dialog;
mod avatar;
+mod badge;
mod context_menu_bin;
mod custom_entry;
mod in_app_notification;
@@ -11,6 +12,7 @@ mod spinner_button;
pub use self::auth_dialog::{AuthData, AuthDialog};
pub use self::avatar::Avatar;
+pub use self::badge::Badge;
pub use self::context_menu_bin::{ContextMenuBin, ContextMenuBinExt, ContextMenuBinImpl};
pub use self::custom_entry::CustomEntry;
pub use self::in_app_notification::InAppNotification;
diff --git a/src/meson.build b/src/meson.build
index 8dbaccbc..826590c8 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -27,6 +27,7 @@ sources = files(
'contrib/qr_code_scanner/qr_code_detector.rs',
'components/avatar.rs',
'components/auth_dialog.rs',
+ 'components/badge.rs',
'components/context_menu_bin.rs',
'components/custom_entry.rs',
'components/label_with_widgets.rs',
@@ -67,6 +68,7 @@ sources = files(
'session/content/message_row/text.rs',
'session/content/mod.rs',
'session/content/room_history.rs',
+ 'session/content/room_details/member_page.rs',
'session/content/room_details/mod.rs',
'session/content/state_row.rs',
'session/room/event.rs',
diff --git a/src/session/content/room_details/member_page.rs b/src/session/content/room_details/member_page.rs
new file mode 100644
index 00000000..5dee1437
--- /dev/null
+++ b/src/session/content/room_details/member_page.rs
@@ -0,0 +1,177 @@
+use adw::subclass::prelude::*;
+use gettextrs::ngettext;
+use gtk::glib::{self, clone};
+use gtk::prelude::*;
+use gtk::subclass::prelude::*;
+use gtk::CompositeTemplate;
+
+use crate::components::{Avatar, Badge};
+use crate::prelude::*;
+use crate::session::room::{Member, RoomAction};
+use crate::session::Room;
+
+mod imp {
+ use super::*;
+ use glib::subclass::InitializingObject;
+ use once_cell::sync::Lazy;
+ use once_cell::unsync::OnceCell;
+
+ #[derive(Debug, Default, CompositeTemplate)]
+ #[template(resource = "/org/gnome/FractalNext/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>,
+ #[template_child]
+ pub members_search_entry: TemplateChild<gtk::SearchEntry>,
+ #[template_child]
+ pub members_list_view: TemplateChild<gtk::ListView>,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for MemberPage {
+ const NAME: &'static str = "ContentMemberPage";
+ type Type = super::MemberPage;
+ type ParentType = adw::PreferencesPage;
+
+ fn class_init(klass: &mut Self::Class) {
+ Avatar::static_type();
+ Badge::static_type();
+ Self::bind_template(klass);
+ }
+
+ fn instance_init(obj: &InitializingObject<Self>) {
+ obj.init_template();
+ }
+ }
+
+ impl ObjectImpl for MemberPage {
+ fn properties() -> &'static [glib::ParamSpec] {
+ static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+ vec![glib::ParamSpec::new_object(
+ "room",
+ "Room",
+ "The room backing all details of the member page",
+ Room::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() {
+ "room" => obj.set_room(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(),
+ _ => unimplemented!(),
+ }
+ }
+
+ fn constructed(&self, obj: &Self::Type) {
+ self.parent_constructed(obj);
+
+ obj.init_member_search();
+ obj.init_member_count();
+ obj.init_invite_button();
+ }
+ }
+ impl WidgetImpl for MemberPage {}
+ impl PreferencesPageImpl for MemberPage {}
+}
+
+glib::wrapper! {
+ pub struct MemberPage(ObjectSubclass<imp::MemberPage>)
+ @extends gtk::Widget, adw::PreferencesPage;
+}
+
+impl MemberPage {
+ pub fn new(room: &Room) -> Self {
+ glib::Object::new(&[("room", room)]).expect("Failed to create MemberPage")
+ }
+
+ pub fn room(&self) -> &Room {
+ let priv_ = imp::MemberPage::from_instance(self);
+ priv_.room.get().unwrap()
+ }
+
+ fn set_room(&self, room: Room) {
+ let priv_ = imp::MemberPage::from_instance(self);
+ priv_.room.set(room).expect("Room already initialized");
+ }
+
+ fn init_member_search(&self) {
+ let priv_ = imp::MemberPage::from_instance(self);
+ let members = self.room().members();
+
+ fn search_string(member: Member) -> String {
+ format!(
+ "{} {} {} {}",
+ member.display_name(),
+ member.user_id(),
+ member.role(),
+ member.power_level(),
+ )
+ }
+
+ let member_expr = gtk::ClosureExpression::new(
+ |value| {
+ value[0]
+ .get::<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 filter_model = gtk::FilterListModel::new(Some(members), Some(&filter));
+ let model = gtk::NoSelection::new(Some(&filter_model));
+ priv_.members_list_view.set_model(Some(&model));
+ }
+
+ fn init_member_count(&self) {
+ let priv_ = imp::MemberPage::from_instance(self);
+ let members = self.room().members();
+
+ let member_count = priv_.member_count.get();
+ fn set_member_count(member_count: >k::Label, n: u32) {
+ member_count.set_text(&ngettext!("{} Member", "{} Members", n, n));
+ }
+ set_member_count(&member_count, members.n_items());
+ members.connect_items_changed(clone!(@weak member_count => move |members, _, _, _| {
+ set_member_count(&member_count, members.n_items());
+ }));
+ }
+
+ fn init_invite_button(&self) {
+ let priv_ = imp::MemberPage::from_instance(self);
+
+ let invite_possible = self.room().new_allowed_expr(RoomAction::Invite);
+ const NONE_OBJECT: Option<&glib::Object> = None;
+ invite_possible.bind(&*priv_.invite_button, "sensitive", NONE_OBJECT);
+ }
+}
diff --git a/src/session/content/room_details/mod.rs b/src/session/content/room_details/mod.rs
index be62298a..a1d1ec7c 100644
--- a/src/session/content/room_details/mod.rs
+++ b/src/session/content/room_details/mod.rs
@@ -1,14 +1,17 @@
+mod member_page;
+
+use adw::prelude::*;
use adw::subclass::prelude::*;
use gettextrs::gettext;
use gtk::gdk;
use gtk::{
glib::{self, clone},
- prelude::*,
subclass::prelude::*,
CompositeTemplate,
};
use matrix_sdk::ruma::events::EventType;
+pub use self::member_page::MemberPage;
use crate::components::CustomEntry;
use crate::session::room::RoomAction;
use crate::session::{self, Room};
@@ -97,6 +100,8 @@ mod imp {
fn constructed(&self, obj: &Self::Type) {
self.parent_constructed(obj);
+ obj.add(&MemberPage::new(obj.room()));
+
obj.init_avatar();
obj.init_edit_toggle();
obj.init_avatar_chooser();
diff --git a/src/session/mod.rs b/src/session/mod.rs
index eae03535..0d35cc34 100644
--- a/src/session/mod.rs
+++ b/src/session/mod.rs
@@ -2,7 +2,7 @@ mod account_settings;
mod avatar;
mod content;
mod event_source_dialog;
-mod room;
+pub mod room;
mod room_creation;
mod room_list;
mod sidebar;
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]