diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index 2339f435..1f0edb68 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -54,6 +54,8 @@
ui/content-invitee-row.ui
ui/content-markdown-popover.ui
ui/content-member-item.ui
+ ui/content-member-page-list-view.ui
+ ui/content-member-page-membership-subpage-row.ui
ui/content-member-page.ui
ui/content-member-row.ui
ui/content-message-audio.ui
diff --git a/data/resources/style.css b/data/resources/style.css
index c5727594..9ec71735 100644
--- a/data/resources/style.css
+++ b/data/resources/style.css
@@ -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;
diff --git a/data/resources/ui/content-invite-subpage.ui b/data/resources/ui/content-invite-subpage.ui
index 8a0af478..b97a7849 100644
--- a/data/resources/ui/content-invite-subpage.ui
+++ b/data/resources/ui/content-invite-subpage.ui
@@ -34,10 +34,10 @@
30
30
6
- true
+ True
-
+
+
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 6c9b4d2c..c75a4fbd 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -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
diff --git a/src/session/content/room_details/invite_subpage/mod.rs b/src/session/content/room_details/invite_subpage/mod.rs
index ca3e8c84..42e86a67 100644
--- a/src/session/content/room_details/invite_subpage/mod.rs
+++ b/src/session/content/room_details/invite_subpage/mod.rs
@@ -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::().unwrap();
- window.close_invite_subpage();
+ self.activate_action("details.previous-page", None).unwrap();
}
fn add_user_pill(&self, user: &Invitee) {
diff --git a/src/session/content/room_details/member_page/members_list_view/extra_lists.rs b/src/session/content/room_details/member_page/members_list_view/extra_lists.rs
new file mode 100644
index 00000000..49ec0507
--- /dev/null
+++ b/src/session/content/room_details/member_page/members_list_view/extra_lists.rs
@@ -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,
+ pub invited: OnceCell,
+ pub banned: OnceCell,
+ pub invited_is_empty: Cell,
+ pub banned_is_empty: Cell,
+ }
+
+ #[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> = 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 {
+ 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)
+ @implements gio::ListModel;
+}
+
+impl ExtraLists {
+ pub fn new(
+ joined: &impl IsA,
+ 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
+ }
+}
diff --git a/src/session/content/room_details/member_page/members_list_view/item_row.rs b/src/session/content/room_details/member_page/members_list_view/item_row.rs
new file mode 100644
index 00000000..4cfc4dbd
--- /dev/null
+++ b/src/session/content/room_details/member_page/members_list_view/item_row.rs
@@ -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