11 changed files with 1035 additions and 0 deletions
@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<interface> |
||||
<template class="CreateDmDialogUserRow" parent="GtkListBoxRow"> |
||||
<property name="child"> |
||||
<object class="GtkBox"> |
||||
<property name="spacing">12</property> |
||||
<property name="margin-top">9</property> |
||||
<property name="margin-bottom">9</property> |
||||
<child> |
||||
<object class="ComponentsAvatar"> |
||||
<property name="size">32</property> |
||||
<binding name="data"> |
||||
<lookup name="avatar-data" type="CreateDmDialogUser"> |
||||
<lookup name="user">CreateDmDialogUserRow</lookup> |
||||
</lookup> |
||||
</binding> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="GtkBox"> |
||||
<property name="orientation">vertical</property> |
||||
<style> |
||||
<class name="title"/> |
||||
</style> |
||||
<child> |
||||
<object class="GtkLabel" id="display-name"> |
||||
<property name="halign">start</property> |
||||
<property name="ellipsize">end</property> |
||||
<binding name="label"> |
||||
<lookup name="display-name" type="CreateDmDialogUser"> |
||||
<lookup name="user">CreateDmDialogUserRow</lookup> |
||||
</lookup> |
||||
</binding> |
||||
<style> |
||||
<class name="title"/> |
||||
</style> |
||||
</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="CreateDmDialogUser"> |
||||
<lookup name="user">CreateDmDialogUserRow</lookup> |
||||
</lookup> |
||||
</binding> |
||||
<style> |
||||
<class name="subtitle"/> |
||||
</style> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</property> |
||||
</template> |
||||
</interface> |
||||
|
||||
@ -0,0 +1,140 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<interface> |
||||
<template class="CreateDmDialog" parent="AdwWindow"> |
||||
<property name="title" translatable="yes">Direct Chat</property> |
||||
<property name="modal">True</property> |
||||
<property name="default-width">380</property> |
||||
<property name="default-height">620</property> |
||||
<property name="content"> |
||||
<object class="GtkWindowHandle"> |
||||
<property name="child"> |
||||
<object class="GtkBox"> |
||||
<property name="orientation">vertical</property> |
||||
<child> |
||||
<object class="GtkHeaderBar"> |
||||
<style> |
||||
<class name="flat"/> |
||||
</style> |
||||
<property name="title-widget"> |
||||
<object class="GtkBox"> |
||||
<property name="visible">False</property> |
||||
</object> |
||||
</property> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="AdwClamp"> |
||||
<property name="hexpand">True</property> |
||||
<child> |
||||
<object class="GtkBox"> |
||||
<property name="orientation">vertical</property> |
||||
<property name="spacing">18</property> |
||||
<property name="margin-start">12</property> |
||||
<property name="margin-end">12</property> |
||||
<child> |
||||
<object class="GtkLabel" id="heading"> |
||||
<property name="wrap">True</property> |
||||
<property name="wrap-mode">word-char</property> |
||||
<property name="max-width-chars">20</property> |
||||
<property name="justify">center</property> |
||||
<property name="xalign">0.5</property> |
||||
<property name="label" translatable="yes">New Direct Chat</property> |
||||
<style> |
||||
<class name="title-2"/> |
||||
</style> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="GtkSearchEntry" id="search_entry"> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="GtkStack" id="stack"> |
||||
<child> |
||||
<object class="GtkStackPage"> |
||||
<property name="name">no-search-page</property> |
||||
<property name="child"> |
||||
<object class="AdwStatusPage"> |
||||
<property name="vexpand">True</property> |
||||
<property name="icon-name">system-search-symbolic</property> |
||||
<property name="title" translatable="yes">Search</property> |
||||
<property name="description" translatable="yes">Search for people to start a new chat with</property> |
||||
</object> |
||||
</property> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="GtkStackPage"> |
||||
<property name="name">matching-page</property> |
||||
<property name="child"> |
||||
<object class="GtkScrolledWindow" id="matching_page"> |
||||
<property name="child"> |
||||
<object class="AdwClamp"> |
||||
<property name="child"> |
||||
<object class="GtkListBox" id="list_box"> |
||||
<property name="activate-on-single-click">True</property> |
||||
<property name="margin-start">6</property> |
||||
<property name="margin-end">6</property> |
||||
<signal name="row-activated" handler="row_activated_cb" swapped="yes"/> |
||||
<style> |
||||
<class name="navigation-sidebar"/> |
||||
</style> |
||||
</object> |
||||
</property> |
||||
</object> |
||||
</property> |
||||
</object> |
||||
</property> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="GtkStackPage"> |
||||
<property name="name">no-matching-page</property> |
||||
<property name="child"> |
||||
<object class="AdwStatusPage"> |
||||
<property name="icon-name">system-search-symbolic</property> |
||||
<property name="title" translatable="yes">No Users Found</property> |
||||
<property name="description" translatable="yes">No users matching the search pattern were found</property> |
||||
</object> |
||||
</property> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="GtkStackPage"> |
||||
<property name="name">error-page</property> |
||||
<property name="child"> |
||||
<object class="AdwStatusPage" id="error_page"> |
||||
<property name="icon-name">dialog-error-symbolic</property> |
||||
<property name="title" translatable="yes">Error</property> |
||||
<property name="description" translatable="yes">An error occurred while searching for matches</property> |
||||
</object> |
||||
</property> |
||||
</object> |
||||
</child> |
||||
<child> |
||||
<object class="GtkStackPage"> |
||||
<property name="name">loading-page</property> |
||||
<property name="child"> |
||||
<object class="Spinner"> |
||||
<property name="valign">center</property> |
||||
<property name="halign">center</property> |
||||
<style> |
||||
<class name="session-loading-spinner"/> |
||||
</style> |
||||
</object> |
||||
</property> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</child> |
||||
</object> |
||||
</property> |
||||
</object> |
||||
</property> |
||||
</template> |
||||
</interface> |
||||
|
||||
@ -0,0 +1,161 @@
|
||||
use gtk::{glib, prelude::*, subclass::prelude::*}; |
||||
use log::{debug, error}; |
||||
use matrix_sdk::ruma::{ |
||||
api::client::room::create_room, |
||||
assign, |
||||
events::{room::encryption::RoomEncryptionEventContent, InitialStateEvent}, |
||||
MxcUri, UserId, |
||||
}; |
||||
|
||||
use crate::{ |
||||
session::{user::UserExt, Room, Session, User}, |
||||
spawn_tokio, |
||||
}; |
||||
|
||||
mod imp { |
||||
use once_cell::sync::Lazy; |
||||
|
||||
use super::*; |
||||
|
||||
#[derive(Debug, Default)] |
||||
pub struct DmUser { |
||||
pub dm_room: glib::WeakRef<Room>, |
||||
} |
||||
|
||||
#[glib::object_subclass] |
||||
impl ObjectSubclass for DmUser { |
||||
const NAME: &'static str = "CreateDmDialogUser"; |
||||
type Type = super::DmUser; |
||||
type ParentType = User; |
||||
} |
||||
|
||||
impl ObjectImpl for DmUser { |
||||
fn properties() -> &'static [glib::ParamSpec] { |
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| { |
||||
vec![glib::ParamSpecObject::builder::<Room>("dm-room") |
||||
.explicit_notify() |
||||
.build()] |
||||
}); |
||||
|
||||
PROPERTIES.as_ref() |
||||
} |
||||
|
||||
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { |
||||
let obj = self.obj(); |
||||
|
||||
match pspec.name() { |
||||
"dm-room" => obj.set_dm_room(value.get().unwrap()), |
||||
_ => unimplemented!(), |
||||
} |
||||
} |
||||
|
||||
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { |
||||
let obj = self.obj(); |
||||
|
||||
match pspec.name() { |
||||
"dm-room" => obj.dm_room().to_value(), |
||||
_ => unimplemented!(), |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
glib::wrapper! { |
||||
/// A User in the context of creating a direct chat.
|
||||
pub struct DmUser(ObjectSubclass<imp::DmUser>) @extends User; |
||||
} |
||||
|
||||
impl DmUser { |
||||
pub fn new( |
||||
session: &Session, |
||||
user_id: &UserId, |
||||
display_name: Option<&str>, |
||||
avatar_url: Option<&MxcUri>, |
||||
dm_room: Option<&Room>, |
||||
) -> Self { |
||||
let obj: Self = glib::Object::builder() |
||||
.property("session", session) |
||||
.property("user-id", user_id.as_str()) |
||||
.property("display-name", display_name) |
||||
.property("dm-room", dm_room) |
||||
.build(); |
||||
// FIXME: we should make the avatar_url settable as property
|
||||
obj.set_avatar_url(avatar_url.map(std::borrow::ToOwned::to_owned)); |
||||
obj |
||||
} |
||||
|
||||
/// Get the DM chat with this user, if any.
|
||||
pub fn dm_room(&self) -> Option<Room> { |
||||
self.imp().dm_room.upgrade() |
||||
} |
||||
|
||||
/// Set the DM chat with this user.
|
||||
pub fn set_dm_room(&self, dm_room: Option<&Room>) { |
||||
if self.dm_room().as_ref() == dm_room { |
||||
return; |
||||
} |
||||
|
||||
self.imp().dm_room.set(dm_room); |
||||
self.notify("dm-room"); |
||||
} |
||||
|
||||
/// Creates a new DM chat with this user
|
||||
////
|
||||
/// If A DM chat exists already no new room is created and the existing one
|
||||
/// is returned.
|
||||
pub async fn start_chat(&self) -> Result<Room, matrix_sdk::Error> { |
||||
let session = self.session(); |
||||
let client = session.client(); |
||||
let other_user = self.user_id(); |
||||
|
||||
if let Some(room) = self.dm_room() { |
||||
debug!( |
||||
"A Direct Chat with the user {other_user} exists already, not creating a new one" |
||||
); |
||||
|
||||
// We can be sure that this room has only ourself and maybe the other user as
|
||||
// member.
|
||||
if room.matrix_room().active_members_count() < 2 { |
||||
room.invite(&[self.clone().upcast()]).await; |
||||
debug!("{other_user} left the chat, re-invite them"); |
||||
} |
||||
|
||||
return Ok(room); |
||||
} |
||||
|
||||
let handle = spawn_tokio!(async move { create_dm(client, other_user).await }); |
||||
|
||||
match handle.await.unwrap() { |
||||
Ok(matrix_room) => { |
||||
let room = session |
||||
.room_list() |
||||
.get_wait(matrix_room.room_id()) |
||||
.await |
||||
.expect("The newly created room was not found"); |
||||
self.set_dm_room(Some(&room)); |
||||
Ok(room) |
||||
} |
||||
Err(error) => { |
||||
error!("Couldn’t create a new Direct Chat: {error}"); |
||||
Err(error) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
async fn create_dm( |
||||
client: matrix_sdk::Client, |
||||
other_user: ruma::OwnedUserId, |
||||
) -> Result<matrix_sdk::room::Joined, matrix_sdk::Error> { |
||||
let request = assign!(create_room::v3::Request::new(), |
||||
{ |
||||
is_direct: true, |
||||
invite: vec![other_user], |
||||
preset: Some(create_room::v3::RoomPreset::TrustedPrivateChat), |
||||
initial_state: vec![ |
||||
InitialStateEvent::new(RoomEncryptionEventContent::with_recommended_defaults()).to_raw_any(), |
||||
], |
||||
}); |
||||
|
||||
client.create_room(request).await |
||||
} |
||||
@ -0,0 +1,341 @@
|
||||
use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*}; |
||||
use log::{debug, error}; |
||||
use matrix_sdk::ruma::{api::client::user_directory::search_users, OwnedUserId, UserId}; |
||||
|
||||
use super::DmUser; |
||||
use crate::{ |
||||
session::{room::Member, user::UserExt, Room, 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}, |
||||
collections::HashMap, |
||||
}; |
||||
|
||||
use futures::future::AbortHandle; |
||||
use once_cell::sync::Lazy; |
||||
|
||||
use super::*; |
||||
|
||||
#[derive(Debug, Default)] |
||||
pub struct DmUserList { |
||||
pub list: RefCell<Vec<DmUser>>, |
||||
pub session: glib::WeakRef<Session>, |
||||
pub state: Cell<DmUserListState>, |
||||
pub search_term: RefCell<Option<String>>, |
||||
pub abort_handle: RefCell<Option<AbortHandle>>, |
||||
pub dm_rooms: RefCell<HashMap<OwnedUserId, Vec<glib::WeakRef<Room>>>>, |
||||
} |
||||
|
||||
#[glib::object_subclass] |
||||
impl ObjectSubclass for DmUserList { |
||||
const NAME: &'static str = "DmUserList"; |
||||
type Type = super::DmUserList; |
||||
type Interfaces = (gio::ListModel,); |
||||
} |
||||
|
||||
impl ObjectImpl for DmUserList { |
||||
fn properties() -> &'static [glib::ParamSpec] { |
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| { |
||||
vec![ |
||||
glib::ParamSpecObject::builder::<Session>("session") |
||||
.construct_only() |
||||
.build(), |
||||
glib::ParamSpecString::builder("search-term") |
||||
.explicit_notify() |
||||
.build(), |
||||
glib::ParamSpecEnum::builder::<DmUserListState>("state") |
||||
.read_only() |
||||
.build(), |
||||
] |
||||
}); |
||||
|
||||
PROPERTIES.as_ref() |
||||
} |
||||
|
||||
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { |
||||
match pspec.name() { |
||||
"session" => self.session.set(value.get().unwrap()), |
||||
"search-term" => self.obj().set_search_term(value.get().unwrap()), |
||||
_ => unimplemented!(), |
||||
} |
||||
} |
||||
|
||||
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { |
||||
let obj = self.obj(); |
||||
|
||||
match pspec.name() { |
||||
"session" => obj.session().to_value(), |
||||
"search-term" => obj.search_term().to_value(), |
||||
"state" => obj.state().to_value(), |
||||
_ => unimplemented!(), |
||||
} |
||||
} |
||||
} |
||||
|
||||
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) |
||||
.map(glib::object::Cast::upcast_ref::<glib::Object>) |
||||
.cloned() |
||||
} |
||||
} |
||||
} |
||||
|
||||
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() |
||||
} |
||||
|
||||
/// The session this list refers to.
|
||||
pub fn session(&self) -> Session { |
||||
self.imp().session.upgrade().unwrap() |
||||
} |
||||
|
||||
/// Set the search term.
|
||||
pub fn set_search_term(&self, search_term: Option<String>) { |
||||
let imp = self.imp(); |
||||
let search_term = search_term.filter(|s| !s.is_empty()); |
||||
|
||||
if search_term.as_ref() == imp.search_term.borrow().as_ref() { |
||||
return; |
||||
} |
||||
|
||||
imp.search_term.replace(search_term); |
||||
|
||||
spawn!(clone!(@weak self as obj => async move { |
||||
obj.search_users().await; |
||||
})); |
||||
|
||||
self.notify("search_term"); |
||||
} |
||||
|
||||
/// The search term.
|
||||
fn search_term(&self) -> Option<String> { |
||||
self.imp().search_term.borrow().clone() |
||||
} |
||||
|
||||
/// 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"); |
||||
} |
||||
|
||||
/// The state of the list.
|
||||
pub fn state(&self) -> DmUserListState { |
||||
self.imp().state.get() |
||||
} |
||||
|
||||
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 session = self.session(); |
||||
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::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 an 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; |
||||
} |
||||
} |
||||
|
||||
self.load_dm_rooms().await; |
||||
let own_user_id = session.user().unwrap().user_id(); |
||||
let dm_rooms = self.imp().dm_rooms.borrow().clone(); |
||||
|
||||
let mut users: Vec<DmUser> = vec![]; |
||||
for item in response.results.into_iter() { |
||||
let other_user_id = &item.user_id; |
||||
let Some(rooms) = dm_rooms.get(other_user_id) else { |
||||
continue; |
||||
}; |
||||
|
||||
let mut final_rooms: Vec<Room> = vec![]; |
||||
for room in rooms { |
||||
let Some(room) = room.upgrade() else { continue; }; |
||||
let members = room.members(); |
||||
|
||||
if !room.is_joined() || room.matrix_room().active_members_count() > 2 { |
||||
continue; |
||||
} |
||||
|
||||
// Make sure we have all members loaded, in most cases members should
|
||||
// already be loaded
|
||||
room.load_members().await; |
||||
|
||||
if members.n_items() >= 1 { |
||||
let mut found_others = false; |
||||
for member in members.iter::<Member>() { |
||||
match member { |
||||
Ok(member) => { |
||||
if member.user_id() != own_user_id |
||||
&& &member.user_id() != other_user_id |
||||
{ |
||||
// We found other members in this room, let's ignore the
|
||||
// room
|
||||
found_others = true; |
||||
break; |
||||
} |
||||
} |
||||
Err(error) => { |
||||
debug!("Error iterating through room members: {error}"); |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
|
||||
if found_others { |
||||
continue; |
||||
} |
||||
} |
||||
|
||||
final_rooms.push(room); |
||||
} |
||||
|
||||
let room = final_rooms |
||||
.into_iter() |
||||
.max_by(|x, y| x.latest_unread().cmp(&y.latest_unread())); |
||||
|
||||
let user = DmUser::new( |
||||
&session, |
||||
&item.user_id, |
||||
item.display_name.as_deref(), |
||||
item.avatar_url.as_deref(), |
||||
room.as_ref(), |
||||
); |
||||
// 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(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
async fn load_dm_rooms(&self) { |
||||
let client = self.session().client(); |
||||
let handle = spawn_tokio!(async move { |
||||
client |
||||
.account() |
||||
.account_data::<ruma::events::direct::DirectEventContent>() |
||||
.await? |
||||
.map(|c| c.deserialize()) |
||||
.transpose() |
||||
.map_err(matrix_sdk::Error::from) |
||||
}); |
||||
|
||||
match handle.await.unwrap() { |
||||
Ok(Some(list)) => { |
||||
let session = self.session(); |
||||
let room_list = session.room_list(); |
||||
let list = list |
||||
.into_iter() |
||||
.map(|(user_id, room_ids)| { |
||||
let rooms = room_ids |
||||
.iter() |
||||
.filter_map(|room_id| Some(room_list.get(room_id)?.downgrade())) |
||||
.collect(); |
||||
(user_id, rooms) |
||||
}) |
||||
.collect(); |
||||
self.imp().dm_rooms.replace(list); |
||||
} |
||||
Ok(None) => { |
||||
self.imp().dm_rooms.take(); |
||||
} |
||||
Err(error) => { |
||||
error!("Can’t read account data: {error}"); |
||||
self.imp().dm_rooms.take(); |
||||
} |
||||
}; |
||||
} |
||||
} |
||||
@ -0,0 +1,90 @@
|
||||
use gtk::{glib, prelude::*, subclass::prelude::*, CompositeTemplate}; |
||||
|
||||
use super::DmUser; |
||||
|
||||
mod imp { |
||||
use std::cell::RefCell; |
||||
|
||||
use glib::subclass::InitializingObject; |
||||
use once_cell::sync::Lazy; |
||||
|
||||
use super::*; |
||||
|
||||
#[derive(Debug, Default, CompositeTemplate)] |
||||
#[template(resource = "/org/gnome/Fractal/create-dm-dialog-user-row.ui")] |
||||
pub struct DmUserRow { |
||||
pub user: RefCell<Option<DmUser>>, |
||||
} |
||||
|
||||
#[glib::object_subclass] |
||||
impl ObjectSubclass for DmUserRow { |
||||
const NAME: &'static str = "CreateDmDialogUserRow"; |
||||
type Type = super::DmUserRow; |
||||
type ParentType = gtk::ListBoxRow; |
||||
|
||||
fn class_init(klass: &mut Self::Class) { |
||||
Self::bind_template(klass); |
||||
} |
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) { |
||||
obj.init_template(); |
||||
} |
||||
} |
||||
|
||||
impl ObjectImpl for DmUserRow { |
||||
fn properties() -> &'static [glib::ParamSpec] { |
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| { |
||||
vec![glib::ParamSpecObject::builder::<DmUser>("user") |
||||
.explicit_notify() |
||||
.build()] |
||||
}); |
||||
|
||||
PROPERTIES.as_ref() |
||||
} |
||||
|
||||
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { |
||||
match pspec.name() { |
||||
"user" => self.obj().set_user(value.get().unwrap()), |
||||
_ => unimplemented!(), |
||||
} |
||||
} |
||||
|
||||
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { |
||||
match pspec.name() { |
||||
"user" => self.obj().user().to_value(), |
||||
_ => unimplemented!(), |
||||
} |
||||
} |
||||
} |
||||
impl WidgetImpl for DmUserRow {} |
||||
impl ListBoxRowImpl for DmUserRow {} |
||||
} |
||||
|
||||
glib::wrapper! { |
||||
pub struct DmUserRow(ObjectSubclass<imp::DmUserRow>) |
||||
@extends gtk::Widget, gtk::ListBoxRow, @implements gtk::Accessible; |
||||
} |
||||
|
||||
impl DmUserRow { |
||||
pub fn new(user: &DmUser) -> Self { |
||||
glib::Object::builder().property("user", user).build() |
||||
} |
||||
|
||||
/// The user displayed by this row.
|
||||
pub fn user(&self) -> Option<DmUser> { |
||||
self.imp().user.borrow().clone() |
||||
} |
||||
|
||||
/// Set the user displayed by this row.
|
||||
pub fn set_user(&self, user: Option<DmUser>) { |
||||
let imp = self.imp(); |
||||
let prev_user = self.user(); |
||||
|
||||
if prev_user == user { |
||||
return; |
||||
} |
||||
|
||||
imp.user.replace(user); |
||||
self.notify("user"); |
||||
} |
||||
} |
||||
@ -0,0 +1,199 @@
|
||||
use adw::subclass::prelude::*; |
||||
use gtk::{gdk, glib, glib::clone, prelude::*, CompositeTemplate}; |
||||
|
||||
mod dm_user; |
||||
use self::dm_user::DmUser; |
||||
mod dm_user_list; |
||||
mod dm_user_row; |
||||
use self::{ |
||||
dm_user_list::{DmUserList, DmUserListState}, |
||||
dm_user_row::DmUserRow, |
||||
}; |
||||
use crate::{ |
||||
gettext, |
||||
session::{user::UserExt, Session}, |
||||
spawn, |
||||
}; |
||||
|
||||
mod imp { |
||||
use glib::{object::WeakRef, subclass::InitializingObject}; |
||||
|
||||
use super::*; |
||||
|
||||
#[derive(Debug, Default, CompositeTemplate)] |
||||
#[template(resource = "/org/gnome/Fractal/create-dm-dialog.ui")] |
||||
pub struct CreateDmDialog { |
||||
pub session: WeakRef<Session>, |
||||
#[template_child] |
||||
pub list_box: TemplateChild<gtk::ListBox>, |
||||
#[template_child] |
||||
pub search_entry: TemplateChild<gtk::SearchEntry>, |
||||
#[template_child] |
||||
pub stack: TemplateChild<gtk::Stack>, |
||||
#[template_child] |
||||
pub error_page: TemplateChild<adw::StatusPage>, |
||||
} |
||||
|
||||
#[glib::object_subclass] |
||||
impl ObjectSubclass for CreateDmDialog { |
||||
const NAME: &'static str = "CreateDmDialog"; |
||||
type Type = super::CreateDmDialog; |
||||
type ParentType = adw::Window; |
||||
|
||||
fn class_init(klass: &mut Self::Class) { |
||||
DmUserRow::static_type(); |
||||
Self::bind_template(klass); |
||||
Self::Type::bind_template_callbacks(klass); |
||||
|
||||
klass.add_binding( |
||||
gdk::Key::Escape, |
||||
gdk::ModifierType::empty(), |
||||
|obj, _| { |
||||
obj.close(); |
||||
true |
||||
}, |
||||
None, |
||||
); |
||||
} |
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) { |
||||
obj.init_template(); |
||||
} |
||||
} |
||||
|
||||
impl ObjectImpl for CreateDmDialog { |
||||
fn properties() -> &'static [glib::ParamSpec] { |
||||
use once_cell::sync::Lazy; |
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| { |
||||
vec![glib::ParamSpecObject::builder::<Session>("session") |
||||
.explicit_notify() |
||||
.build()] |
||||
}); |
||||
|
||||
PROPERTIES.as_ref() |
||||
} |
||||
|
||||
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { |
||||
match pspec.name() { |
||||
"session" => self.obj().set_session(value.get().unwrap()), |
||||
_ => unimplemented!(), |
||||
} |
||||
} |
||||
|
||||
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { |
||||
match pspec.name() { |
||||
"session" => self.obj().session().to_value(), |
||||
_ => unimplemented!(), |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl WidgetImpl for CreateDmDialog {} |
||||
impl WindowImpl for CreateDmDialog {} |
||||
impl AdwWindowImpl for CreateDmDialog {} |
||||
} |
||||
|
||||
glib::wrapper! { |
||||
/// Preference Window to display and update room details.
|
||||
pub struct CreateDmDialog(ObjectSubclass<imp::CreateDmDialog>) |
||||
@extends gtk::Widget, gtk::Window, adw::Window, adw::Bin, @implements gtk::Accessible; |
||||
} |
||||
|
||||
#[gtk::template_callbacks] |
||||
impl CreateDmDialog { |
||||
pub fn new(parent_window: Option<&impl IsA<gtk::Window>>, session: &Session) -> Self { |
||||
glib::Object::builder() |
||||
.property("transient-for", parent_window) |
||||
.property("session", session) |
||||
.build() |
||||
} |
||||
|
||||
/// The current session.
|
||||
pub fn session(&self) -> Option<Session> { |
||||
self.imp().session.upgrade() |
||||
} |
||||
|
||||
/// Set the current session.
|
||||
pub fn set_session(&self, session: Option<Session>) { |
||||
let imp = self.imp(); |
||||
|
||||
if self.session() == session { |
||||
return; |
||||
} |
||||
|
||||
if let Some(ref session) = session { |
||||
let user_list = DmUserList::new(session); |
||||
|
||||
// We don't need to disconnect this signal since the `DmUserList` will be
|
||||
// disposed once unbound from the `gtk::ListBox`
|
||||
user_list.connect_notify_local( |
||||
Some("state"), |
||||
clone!(@weak self as obj => move |model, _| { |
||||
obj.update_view(model); |
||||
}), |
||||
); |
||||
|
||||
imp.search_entry |
||||
.bind_property("text", &user_list, "search-term") |
||||
.flags(glib::BindingFlags::SYNC_CREATE) |
||||
.build(); |
||||
|
||||
imp.list_box.bind_model(Some(&user_list), |user| { |
||||
DmUserRow::new( |
||||
user.downcast_ref::<DmUser>() |
||||
.expect("DmUserList must contain only `DmUser`"), |
||||
) |
||||
.upcast() |
||||
}); |
||||
|
||||
self.update_view(&user_list); |
||||
} else { |
||||
imp.list_box.unbind_model(); |
||||
} |
||||
|
||||
imp.session.set(session.as_ref()); |
||||
self.notify("session"); |
||||
} |
||||
|
||||
fn update_view(&self, model: &DmUserList) { |
||||
let visible_child_name = match model.state() { |
||||
DmUserListState::Initial => "no-search-page", |
||||
DmUserListState::Loading => "loading-page", |
||||
DmUserListState::NoMatching => "no-matching-page", |
||||
DmUserListState::Matching => "matching-page", |
||||
DmUserListState::Error => { |
||||
self.show_error(&gettext("An error occurred while searching for users")); |
||||
return; |
||||
} |
||||
}; |
||||
|
||||
self.imp().stack.set_visible_child_name(visible_child_name); |
||||
} |
||||
|
||||
fn show_error(&self, message: &str) { |
||||
self.imp().error_page.set_description(Some(message)); |
||||
self.imp().stack.set_visible_child_name("error-page"); |
||||
} |
||||
|
||||
#[template_callback] |
||||
fn row_activated_cb(&self, row: gtk::ListBoxRow) { |
||||
let Some(user): Option<DmUser> = row.downcast::<DmUserRow>().ok().and_then(|r| r.user()) else { return; }; |
||||
|
||||
// TODO: For now we show the loading page while we create the room,
|
||||
// ideally we would like to have the same behavior as Element:
|
||||
// Create the room only once the user sends a message
|
||||
self.imp().stack.set_visible_child_name("loading-page"); |
||||
self.imp().search_entry.set_sensitive(false); |
||||
spawn!(clone!(@weak self as obj, @weak user => async move { |
||||
match user.start_chat().await { |
||||
Ok(room) => { |
||||
user.session().select_room(Some(room)); |
||||
obj.close(); |
||||
} |
||||
Err(_) => { |
||||
obj.show_error(&gettext("Failed to create a new Direct Chat")); |
||||
} |
||||
} |
||||
})); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue