204 lines
6.0 KiB
Rust
204 lines
6.0 KiB
Rust
use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*};
|
||
use matrix_sdk::ruma::{api::client::user_directory::search_users, UserId};
|
||
use tracing::error;
|
||
|
||
use super::DmUser;
|
||
use crate::{prelude::*, session::model::Session, spawn, spawn_tokio};
|
||
|
||
#[derive(Debug, Default, Eq, PartialEq, Clone, Copy, glib::Enum)]
|
||
#[repr(u32)]
|
||
#[enum_type(name = "ContentDmUserListState")]
|
||
pub enum DmUserListState {
|
||
#[default]
|
||
Initial = 0,
|
||
Loading = 1,
|
||
NoMatching = 2,
|
||
Matching = 3,
|
||
Error = 4,
|
||
}
|
||
|
||
mod imp {
|
||
use std::cell::{Cell, RefCell};
|
||
|
||
use futures_util::future::AbortHandle;
|
||
|
||
use super::*;
|
||
|
||
#[derive(Debug, Default, glib::Properties)]
|
||
#[properties(wrapper_type = super::DmUserList)]
|
||
pub struct DmUserList {
|
||
pub list: RefCell<Vec<DmUser>>,
|
||
/// The current session.
|
||
#[property(get, construct_only)]
|
||
pub session: glib::WeakRef<Session>,
|
||
/// The state of the list.
|
||
#[property(get, builder(DmUserListState::default()))]
|
||
pub state: Cell<DmUserListState>,
|
||
/// The search term.
|
||
#[property(get, set = Self::set_search_term, explicit_notify, nullable)]
|
||
pub search_term: RefCell<Option<String>>,
|
||
pub abort_handle: RefCell<Option<AbortHandle>>,
|
||
}
|
||
|
||
#[glib::object_subclass]
|
||
impl ObjectSubclass for DmUserList {
|
||
const NAME: &'static str = "DmUserList";
|
||
type Type = super::DmUserList;
|
||
type Interfaces = (gio::ListModel,);
|
||
}
|
||
|
||
#[glib::derived_properties]
|
||
impl ObjectImpl for DmUserList {}
|
||
|
||
impl ListModelImpl for DmUserList {
|
||
fn item_type(&self) -> glib::Type {
|
||
DmUser::static_type()
|
||
}
|
||
|
||
fn n_items(&self) -> u32 {
|
||
self.list.borrow().len() as u32
|
||
}
|
||
|
||
fn item(&self, position: u32) -> Option<glib::Object> {
|
||
self.list
|
||
.borrow()
|
||
.get(position as usize)
|
||
.cloned()
|
||
.and_upcast()
|
||
}
|
||
}
|
||
|
||
impl DmUserList {
|
||
/// Set the search term.
|
||
fn set_search_term(&self, search_term: Option<String>) {
|
||
let search_term = search_term.filter(|s| !s.is_empty());
|
||
|
||
if search_term.as_ref() == self.search_term.borrow().as_ref() {
|
||
return;
|
||
}
|
||
let obj = self.obj();
|
||
|
||
self.search_term.replace(search_term);
|
||
|
||
spawn!(clone!(@weak obj => async move {
|
||
obj.search_users().await;
|
||
}));
|
||
|
||
obj.notify_search_term();
|
||
}
|
||
}
|
||
}
|
||
|
||
glib::wrapper! {
|
||
/// List of users matching the `search term`.
|
||
pub struct DmUserList(ObjectSubclass<imp::DmUserList>)
|
||
@implements gio::ListModel;
|
||
}
|
||
|
||
impl DmUserList {
|
||
pub fn new(session: &Session) -> Self {
|
||
glib::Object::builder().property("session", session).build()
|
||
}
|
||
|
||
/// Set the state of the list.
|
||
fn set_state(&self, state: DmUserListState) {
|
||
let imp = self.imp();
|
||
|
||
if state == self.state() {
|
||
return;
|
||
}
|
||
|
||
imp.state.set(state);
|
||
self.notify("state");
|
||
}
|
||
|
||
fn set_list(&self, users: Vec<DmUser>) {
|
||
let added = users.len();
|
||
|
||
let prev_users = self.imp().list.replace(users);
|
||
|
||
self.items_changed(0, prev_users.len() as u32, added as u32);
|
||
}
|
||
|
||
fn clear_list(&self) {
|
||
self.set_list(Vec::new());
|
||
}
|
||
|
||
async fn search_users(&self) {
|
||
let Some(session) = self.session() else {
|
||
return;
|
||
};
|
||
let client = session.client();
|
||
let Some(search_term) = self.search_term() else {
|
||
self.set_state(DmUserListState::Initial);
|
||
return;
|
||
};
|
||
|
||
self.set_state(DmUserListState::Loading);
|
||
self.clear_list();
|
||
|
||
let search_term_clone = search_term.clone();
|
||
let handle = spawn_tokio!(async move { client.search_users(&search_term_clone, 20).await });
|
||
|
||
let (future, handle) = futures_util::future::abortable(handle);
|
||
|
||
if let Some(abort_handle) = self.imp().abort_handle.replace(Some(handle)) {
|
||
abort_handle.abort();
|
||
}
|
||
|
||
let response = if let Ok(result) = future.await {
|
||
result.unwrap()
|
||
} else {
|
||
return;
|
||
};
|
||
|
||
if Some(&search_term) != self.search_term().as_ref() {
|
||
return;
|
||
}
|
||
|
||
match response {
|
||
Ok(mut response) => {
|
||
let mut add_custom = false;
|
||
// If the search term looks like a UserId and is not already in the response,
|
||
// insert it.
|
||
if let Ok(user_id) = UserId::parse(&search_term) {
|
||
if !response.results.iter().any(|item| item.user_id == user_id) {
|
||
let user = search_users::v3::User::new(user_id);
|
||
response.results.insert(0, user);
|
||
add_custom = true;
|
||
}
|
||
}
|
||
|
||
let mut users: Vec<DmUser> = vec![];
|
||
for item in response.results.into_iter() {
|
||
let user = DmUser::new(
|
||
&session,
|
||
&item.user_id,
|
||
item.display_name.as_deref(),
|
||
item.avatar_url.as_deref(),
|
||
);
|
||
|
||
// If it is the "custom user" from the search term, fetch the avatar
|
||
// and display name
|
||
if add_custom && user.user_id() == search_term {
|
||
user.load_profile();
|
||
}
|
||
|
||
users.push(user);
|
||
}
|
||
|
||
match users.is_empty() {
|
||
true => self.set_state(DmUserListState::NoMatching),
|
||
false => self.set_state(DmUserListState::Matching),
|
||
}
|
||
self.set_list(users);
|
||
}
|
||
Err(error) => {
|
||
error!("Couldn’t load matching users: {error}");
|
||
self.set_state(DmUserListState::Error);
|
||
self.clear_list();
|
||
}
|
||
}
|
||
}
|
||
}
|