use gtk::{
gio,
glib::{self, clone},
prelude::*,
subclass::prelude::*,
};
use indexmap::{map::Entry, IndexMap};
use matrix_sdk::RoomMemberships;
use ruma::{events::room::power_levels::RoomPowerLevels, OwnedUserId, UserId};
use tracing::error;
use super::{Event, Member, Membership, Room};
use crate::{prelude::*, spawn, spawn_tokio, utils::LoadingState};
mod imp {
use std::cell::{Cell, RefCell};
use super::*;
#[derive(Debug, Default, glib::Properties)]
#[properties(wrapper_type = super::MemberList)]
pub struct MemberList {
pub members: RefCell<IndexMap<OwnedUserId, Member>>,
#[property(get, set = Self::set_room, construct_only)]
pub room: glib::WeakRef<Room>,
#[property(get, builder(LoadingState::default()))]
pub state: Cell<LoadingState>,
}
#[glib::object_subclass]
impl ObjectSubclass for MemberList {
const NAME: &'static str = "MemberList";
type Type = super::MemberList;
type Interfaces = (gio::ListModel,);
}
#[glib::derived_properties]
impl ObjectImpl for MemberList {}
impl ListModelImpl for MemberList {
fn item_type(&self) -> glib::Type {
Member::static_type()
}
fn n_items(&self) -> u32 {
self.members.borrow().len() as u32
}
fn item(&self, position: u32) -> Option<glib::Object> {
let members = self.members.borrow();
members
.get_index(position as usize)
.map(|(_user_id, member)| member.clone().upcast())
}
}
impl MemberList {
fn set_room(&self, room: Room) {
let obj = self.obj();
let own_member = room.own_member();
self.members
.borrow_mut()
.insert(own_member.user_id().clone(), own_member);
if let Some(member) = room.direct_member() {
self.members
.borrow_mut()
.insert(member.user_id().clone(), member);
}
self.room.set(Some(&room));
obj.notify_room();
spawn!(
glib::Priority::LOW,
clone!(
#[weak]
obj,
async move {
obj.load().await;
}
)
);
}
}
}
glib::wrapper! {
pub struct MemberList(ObjectSubclass<imp::MemberList>)
@implements gio::ListModel;
}
impl MemberList {
pub fn new(room: &Room) -> Self {
glib::Object::builder::<Self>()
.property("room", room)
.build()
}
fn set_state(&self, state: LoadingState) {
if self.state() == state {
return;
}
self.imp().state.set(state);
self.notify_state();
}
pub fn reload(&self) {
self.set_state(LoadingState::Initial);
spawn!(clone!(
#[weak(rename_to = obj)]
self,
async move {
obj.load().await;
}
));
}
async fn load(&self) {
let Some(room) = self.room() else {
return;
};
if matches!(self.state(), LoadingState::Loading | LoadingState::Ready) {
return;
}
self.set_state(LoadingState::Loading);
let matrix_room = room.matrix_room();
let matrix_room_clone = matrix_room.clone();
let handle = spawn_tokio!(async move {
let mut memberships = RoomMemberships::all();
memberships.remove(RoomMemberships::LEAVE);
matrix_room_clone.members_no_sync(memberships).await
});
match handle.await.unwrap() {
Ok(members) => {
self.update_from_room_members(&members);
if matrix_room.are_members_synced() {
self.set_state(LoadingState::Ready);
return;
}
}
Err(error) => {
error!("Could not load room members from store: {error}");
}
}
let matrix_room = matrix_room.clone();
let handle = spawn_tokio!(async move {
let mut memberships = RoomMemberships::all();
memberships.remove(RoomMemberships::LEAVE);
matrix_room.members(memberships).await
});
match handle.await.unwrap() {
Ok(members) => {
self.update_from_room_members(&members);
self.set_state(LoadingState::Ready);
}
Err(error) => {
self.set_state(LoadingState::Error);
error!(%error, "Could not load room members from server");
}
}
}
fn update_from_room_members(&self, new_members: &[matrix_sdk::room::RoomMember]) {
let Some(room) = self.room() else {
return;
};
let imp = self.imp();
let mut members = imp.members.borrow_mut();
let prev_len = members.len();
for member in new_members {
if let Entry::Vacant(entry) = members.entry(member.user_id().into()) {
entry.insert(Member::new(&room, member.user_id().to_owned()));
}
}
let num_members_added = members.len().saturating_sub(prev_len);
std::mem::drop(members);
{
for room_member in new_members {
let member = imp.members.borrow().get(room_member.user_id()).cloned();
if let Some(member) = member {
member.update_from_room_member(room_member);
}
}
for item in room.timeline().items().iter::<glib::Object>().rev() {
let Ok(item) = item else {
break;
};
let Ok(event) = item.downcast::<Event>() else {
continue;
};
if !event.counts_as_unread() {
continue;
}
let member = imp.members.borrow().get(&event.sender_id()).cloned();
if let Some(member) = member {
member.set_latest_activity(event.origin_server_ts_u64());
}
}
}
if num_members_added > 0 {
self.items_changed(prev_len as u32, 0, num_members_added as u32);
}
}
pub fn get(&self, user_id: &UserId) -> Option<Member> {
self.imp().members.borrow().get(user_id).cloned()
}
pub fn get_or_create(&self, user_id: OwnedUserId) -> Member {
let mut members = self.imp().members.borrow_mut();
let mut was_member_added = false;
let prev_len = members.len();
let member = members
.entry(user_id)
.or_insert_with_key(|user_id| {
was_member_added = true;
Member::new(&self.room().unwrap(), user_id.clone())
})
.clone();
std::mem::drop(members);
if was_member_added {
self.items_changed(prev_len as u32, 0, 1);
}
member
}
pub(super) fn update_member(&self, user_id: OwnedUserId) {
self.get_or_create(user_id).update();
}
pub(super) fn update_power_levels(&self, power_levels: &RoomPowerLevels) {
for (user_id, member) in &*self.imp().members.borrow() {
member.set_power_level(power_levels.for_user(user_id).into());
}
}
pub fn get_membership(&self, user_id: &UserId) -> Membership {
self.imp()
.members
.borrow()
.get(user_id)
.map_or(Membership::Leave, |member| member.membership())
}
}