[fractal/fractal-next] room_details: Add members page



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: &gtk::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]