diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index a036852a8..475399083 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -17,6 +17,7 @@
ui/content.ui
ui/content-chat-action-bar.ui
ui/content-chat-history.ui
+ ui/content-chat-info-member-row.ui
ui/content-chat-info-window.ui
ui/content-event-row.ui
ui/content-message-document.ui
diff --git a/data/resources/style.css b/data/resources/style.css
index 0efaeb116..0b5390d14 100644
--- a/data/resources/style.css
+++ b/data/resources/style.css
@@ -298,3 +298,7 @@ window.chat-info .main-page > avatar {
window.chat-info .main-page > list {
margin-top: 12px;
}
+
+chatmember {
+ padding: 6px 0px;
+}
diff --git a/data/resources/ui/content-chat-info-member-row.ui b/data/resources/ui/content-chat-info-member-row.ui
new file mode 100644
index 000000000..e539e16df
--- /dev/null
+++ b/data/resources/ui/content-chat-info-member-row.ui
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/data/resources/ui/content-chat-info-window.ui b/data/resources/ui/content-chat-info-window.ui
index 91d7c5001..f084210d7 100644
--- a/data/resources/ui/content-chat-info-window.ui
+++ b/data/resources/ui/content-chat-info-window.ui
@@ -1,7 +1,8 @@
- 360
+ 400
+ 600
True
-
-
-
- vertical
-
-
-
- 128
-
+
+ True
+
+
+ info
+ Info
+ help-about-symbolic
+
+
+
+
+
+
+ vertical
+
+
+
+ 128
+
+ ContentChatInfoWindow
+
+
+
+
+
+ True
+ center
+
+
+
+
+
+ ellipsize-end
+ 0.5
+
+
+
+
+ none
+
+
+
+
+
+
+
+
+
+
+
+
+
+ members
+ Members
+ avatar-default-symbolic
+ False
+
+ members_list
+
+
+
+
ContentChatInfoWindow
-
-
-
- True
- center
-
-
-
-
-
- ellipsize-middle
- 0.5
-
-
-
-
- none
-
-
-
+
-
+
+
+
+
+
+ stack
+
+ title
+
diff --git a/src/session/content/chat_info_window/member_row.rs b/src/session/content/chat_info_window/member_row.rs
new file mode 100644
index 000000000..f339a0aad
--- /dev/null
+++ b/src/session/content/chat_info_window/member_row.rs
@@ -0,0 +1,91 @@
+use crate::tdlib::ChatMember;
+use gettextrs::gettext;
+use gtk::prelude::*;
+use gtk::subclass::prelude::*;
+use gtk::{glib, CompositeTemplate};
+
+use crate::session::components::Avatar;
+use crate::{expressions, strings};
+use tdlib::enums::{UserStatus, UserType};
+
+mod imp {
+ use super::*;
+ #[derive(Debug, Default, CompositeTemplate)]
+ #[template(resource = "/com/github/melix99/telegrand/ui/content-chat-info-member-row.ui")]
+ pub(crate) struct MemberRow {
+ #[template_child]
+ pub(super) avatar: TemplateChild,
+ #[template_child]
+ pub(super) user_name_label: TemplateChild,
+ #[template_child]
+ pub(super) member_status_label: TemplateChild,
+ #[template_child]
+ pub(super) user_status_label: TemplateChild,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for MemberRow {
+ const NAME: &'static str = "ContentChatInfoMemberRow";
+ type Type = super::MemberRow;
+ type ParentType = gtk::Widget;
+
+ fn class_init(klass: &mut Self::Class) {
+ Self::bind_template(klass);
+ klass.set_css_name("chatmember");
+ }
+
+ fn instance_init(obj: &glib::subclass::InitializingObject) {
+ obj.init_template();
+ }
+ }
+
+ impl ObjectImpl for MemberRow {
+ fn dispose(&self) {
+ self.avatar.unparent();
+ self.user_status_label.parent().unwrap().unparent();
+ }
+ }
+
+ impl WidgetImpl for MemberRow {}
+
+ impl BoxImpl for MemberRow {}
+}
+
+glib::wrapper! {
+ pub(crate) struct MemberRow(ObjectSubclass)
+ @extends gtk::Widget;
+}
+
+impl MemberRow {
+ pub fn new() -> Self {
+ glib::Object::new(&[])
+ }
+ pub fn bind_member(&self, member: ChatMember) {
+ let imp = self.imp();
+
+ let user = member.user();
+
+ let user_expression = gtk::ObjectExpression::new(&user);
+ let name_expression = expressions::user_display_name(&user_expression);
+ name_expression.bind(&*imp.user_name_label, "label", Some(&user));
+
+ if let UserType::Bot(_) = user.type_().0 {
+ imp.user_status_label.set_text(Some(&gettext("bot")));
+ } else {
+ let status = user.status().0;
+ let status_label = &*imp.user_status_label;
+
+ match status {
+ UserStatus::Online(_) => status_label.set_css_classes(&["accent"]),
+ _ => status_label.set_css_classes(&["dim-label"]),
+ }
+
+ let status = strings::user_status(&status);
+ imp.user_status_label.set_text(Some(&status));
+ };
+
+ imp.member_status_label.set_label(&member.status());
+
+ imp.avatar.set_item(Some(user.upcast()));
+ }
+}
diff --git a/src/session/content/chat_info_window/members_page.rs b/src/session/content/chat_info_window/members_page.rs
new file mode 100644
index 000000000..2f85cdbd4
--- /dev/null
+++ b/src/session/content/chat_info_window/members_page.rs
@@ -0,0 +1,266 @@
+use super::member_row::MemberRow;
+
+use adw::prelude::*;
+use adw::subclass::prelude::*;
+use glib::clone;
+use gtk::{glib, CompositeTemplate};
+use tdlib::enums::BasicGroupFullInfo::BasicGroupFullInfo as TdBasicGroupFullInfo;
+use tdlib::enums::ChatMembers::ChatMembers as TdChatMembers;
+use tdlib::enums::MessageSender;
+use tdlib::enums::SupergroupFullInfo::SupergroupFullInfo as TdSupergroupFullInfo;
+use tdlib::enums::User::User as TdUser;
+use tdlib::functions;
+use tdlib::types::{ChatMember as TdChatMember, ChatMembers};
+
+use crate::tdlib::{BasicGroup, Chat, ChatMember, ChatType, Supergroup, User};
+use crate::utils::spawn;
+
+mod imp {
+ use super::*;
+ use once_cell::sync::{Lazy, OnceCell};
+ use std::cell::Cell;
+
+ #[derive(Debug, Default, CompositeTemplate)]
+ #[template(string = r#"
+
+
+
+
+
+
+ 440
+ 200
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ "#)]
+ pub(crate) struct ChatInfoMembers {
+ pub(super) loading: Cell,
+ pub(super) chat: OnceCell,
+ #[template_child]
+ pub(super) members_list: TemplateChild,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for ChatInfoMembers {
+ const NAME: &'static str = "ContentChatInfoMembers";
+ type Type = super::ChatInfoMembers;
+ type ParentType = adw::Bin;
+
+ fn class_init(klass: &mut Self::Class) {
+ klass.bind_template();
+ }
+
+ fn instance_init(obj: &glib::subclass::InitializingObject) {
+ obj.init_template();
+ }
+ }
+
+ impl ObjectImpl for ChatInfoMembers {
+ fn properties() -> &'static [glib::ParamSpec] {
+ static PROPERTIES: Lazy> = Lazy::new(|| {
+ vec![glib::ParamSpecObject::builder::("chat")
+ // .construct_only()
+ .build()]
+ });
+ PROPERTIES.as_ref()
+ }
+
+ fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
+ match pspec.name() {
+ "chat" => {
+ if let Some(chat) = value.get().unwrap() {
+ self.chat.set(chat).unwrap();
+ self.obj().setup_page();
+ }
+ }
+ _ => unimplemented!(),
+ }
+ }
+
+ fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ let obj = self.obj();
+
+ match pspec.name() {
+ "chat" => obj.chat().to_value(),
+ _ => unimplemented!(),
+ }
+ }
+ }
+
+ impl WidgetImpl for ChatInfoMembers {}
+ impl BinImpl for ChatInfoMembers {}
+}
+
+glib::wrapper! {
+ pub(crate) struct ChatInfoMembers(ObjectSubclass)
+ @extends gtk::Widget;
+}
+
+impl ChatInfoMembers {
+ fn setup_page(&self) {
+ match self.chat().unwrap().type_() {
+ ChatType::BasicGroup(basic_group) => {
+ self.setup_basic_group_info(basic_group);
+ }
+ ChatType::Supergroup(supergroup) => {
+ self.setup_supergroup_info(supergroup);
+ }
+ _ => {
+ self.set_visible(false);
+ }
+ }
+ }
+
+ fn setup_basic_group_info(&self, basic_group: &BasicGroup) {
+ let client_id = self.chat().unwrap().session().client_id();
+ let basic_group_id = basic_group.id();
+
+ spawn(clone!(@weak self as obj => async move {
+ let result = functions::get_basic_group_full_info(basic_group_id, client_id).await;
+ if let Ok(TdBasicGroupFullInfo(full_info)) = result {
+ obj.append_members(full_info.members).await;
+ }
+ }));
+ }
+
+ fn setup_supergroup_info(&self, supergroup: &Supergroup) {
+ let client_id = self.chat().unwrap().session().client_id();
+ let supergroup_id = supergroup.id();
+
+ spawn(clone!(@weak self as obj => async move {
+ let imp = obj.imp();
+ let result = functions::get_supergroup_full_info(supergroup_id, client_id).await;
+ if let Ok(TdSupergroupFullInfo(full_info)) = result {
+ if full_info.can_get_members {
+ imp.loading.set(true);
+ let result = functions::get_supergroup_members(
+ supergroup_id,
+ None,
+ 0,
+ 200,
+ client_id,
+ ).await;
+ if let Ok(TdChatMembers(ChatMembers {members, total_count})) = result {
+ obj.append_members(members).await;
+
+ if total_count > 200 {
+ obj.imp().members_list.vadjustment().unwrap()
+ .connect_changed(clone!(@weak obj => move |adj| {
+ obj.load_more_members(adj, supergroup_id);
+ }));
+ }
+ }
+ imp.loading.set(false);
+ }
+ }
+ }));
+ }
+
+ fn load_more_members(&self, adj: >k::Adjustment, supergroup_id: i64) {
+ let imp = self.imp();
+ if imp.loading.get() {
+ return;
+ }
+ imp.loading.set(true);
+
+ if adj.value() > adj.page_size() * 2.0 || adj.upper() >= adj.page_size() * 2.0 {
+ let offset = imp.members_list.model().unwrap().n_items() as i32;
+ let limit = 200;
+ let client_id = self.chat().unwrap().session().client_id();
+
+ spawn(clone!(@weak self as obj => async move {
+ let result = functions::get_supergroup_members(
+ supergroup_id,
+ None,
+ offset,
+ limit,
+ client_id,
+ ).await;
+ if let Ok(TdChatMembers(ChatMembers {members, ..})) = result {
+ obj.append_members(members).await;
+ obj.imp().loading.set(false);
+ } else {
+ log::error!("can't load members {result:?}");
+ }
+ }));
+ }
+ }
+
+ async fn append_members(&self, members: Vec) {
+ let members: Vec<_> = {
+ let mut users: Vec = vec![];
+
+ let session = self.chat().unwrap().session();
+ let client_id = session.client_id();
+
+ for member in &members {
+ let user = match member.member_id {
+ MessageSender::User(ref user) => {
+ let TdUser(user) =
+ functions::get_user(user.user_id, client_id).await.unwrap();
+ User::from_td_object(user, &session)
+ }
+ MessageSender::Chat(_) => unreachable!(),
+ };
+ users.push(user);
+ }
+
+ members
+ .into_iter()
+ .zip(users.into_iter())
+ .map(|(member, user)| ChatMember::new(member, user))
+ .collect()
+ };
+
+ let members_list = &self.imp().members_list;
+
+ let selection_model: gtk::NoSelection = members_list.model().unwrap().downcast().unwrap();
+
+ let model: gtk::gio::ListStore = if let Some(model) = selection_model.model() {
+ model.downcast().unwrap()
+ } else {
+ let model = gtk::gio::ListStore::new(ChatMember::static_type());
+ selection_model.set_model(Some(&model));
+ model
+ };
+
+ model.extend_from_slice(&members);
+
+ if members_list.factory().is_none() {
+ let factory = gtk::SignalListItemFactory::new();
+
+ factory.connect_setup(move |_, list_item| {
+ list_item.set_property("child", MemberRow::new());
+ });
+
+ factory.connect_bind(move |_, list_item| {
+ let list_item: >k::ListItem = list_item.downcast_ref().unwrap();
+
+ let user_row: MemberRow = list_item.child().unwrap().downcast().unwrap();
+ let member: ChatMember = list_item.item().unwrap().downcast().unwrap();
+
+ user_row.bind_member(member);
+ });
+
+ members_list.set_factory(Some(&factory));
+ }
+ }
+
+ pub(crate) fn chat(&self) -> Option<&Chat> {
+ self.imp().chat.get()
+ }
+}
diff --git a/src/session/content/chat_info_window.rs b/src/session/content/chat_info_window/mod.rs
similarity index 98%
rename from src/session/content/chat_info_window.rs
rename to src/session/content/chat_info_window/mod.rs
index bf1a3e3d2..1e8a942de 100644
--- a/src/session/content/chat_info_window.rs
+++ b/src/session/content/chat_info_window/mod.rs
@@ -1,3 +1,8 @@
+mod member_row;
+mod members_page;
+
+use self::members_page::ChatInfoMembers;
+
use adw::prelude::*;
use gettextrs::gettext;
use glib::{clone, closure};
@@ -29,6 +34,8 @@ mod imp {
pub(super) subtitle_label: TemplateChild,
#[template_child]
pub(super) info_list: TemplateChild,
+ #[template_child]
+ pub(super) members_list: TemplateChild,
}
#[glib::object_subclass]
diff --git a/src/tdlib/chat_member.rs b/src/tdlib/chat_member.rs
new file mode 100644
index 000000000..26b42e69b
--- /dev/null
+++ b/src/tdlib/chat_member.rs
@@ -0,0 +1,62 @@
+use super::User;
+use gettextrs::gettext;
+use glib::subclass::prelude::*;
+use gtk::glib;
+use tdlib::enums::ChatMemberStatus;
+use tdlib::types::ChatMember as TdChatMember;
+
+mod imp {
+ use super::*;
+ use glib::once_cell::sync::OnceCell;
+
+ #[derive(Default)]
+ pub(crate) struct ChatMember {
+ pub(super) member: OnceCell,
+ pub(super) user: OnceCell,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for ChatMember {
+ const NAME: &'static str = "ChatMember";
+ type Type = super::ChatMember;
+ }
+
+ impl ObjectImpl for ChatMember {}
+}
+
+glib::wrapper! {
+ pub(crate) struct ChatMember(ObjectSubclass);
+}
+
+impl ChatMember {
+ pub fn new(member: TdChatMember, user: User) -> Self {
+ let obj: Self = glib::Object::new(&[]);
+ obj.imp().member.set(member).unwrap();
+ obj.imp().user.set(user).unwrap();
+ obj
+ }
+
+ pub fn status(&self) -> String {
+ match self.imp().member.get().unwrap().status {
+ ChatMemberStatus::Creator(ref owner) => {
+ if owner.custom_title.is_empty() {
+ gettext("Owner")
+ } else {
+ owner.custom_title.to_owned()
+ }
+ }
+ ChatMemberStatus::Administrator(ref admin) => {
+ if admin.custom_title.is_empty() {
+ gettext("Admin")
+ } else {
+ admin.custom_title.to_owned()
+ }
+ }
+ _ => "".to_string(),
+ }
+ }
+
+ pub fn user(&self) -> User {
+ self.imp().user.get().unwrap().clone()
+ }
+}
diff --git a/src/tdlib/mod.rs b/src/tdlib/mod.rs
index 0a9edaf37..9623ddcc7 100644
--- a/src/tdlib/mod.rs
+++ b/src/tdlib/mod.rs
@@ -6,6 +6,7 @@ mod chat_action_list;
mod chat_history;
mod chat_history_item;
mod chat_list;
+mod chat_member;
mod country_info;
mod country_list;
mod message;
@@ -24,6 +25,7 @@ use self::chat_history::ChatHistory;
pub(crate) use self::chat_history::ChatHistoryError;
pub(crate) use self::chat_history_item::{ChatHistoryItem, ChatHistoryItemType};
pub(crate) use self::chat_list::ChatList;
+pub(crate) use self::chat_member::ChatMember;
pub(crate) use self::country_info::CountryInfo;
pub(crate) use self::country_list::CountryList;
pub(crate) use self::message::{Message, MessageSender};