Browse Source

create-dm: Add dialog to create DM room

merge-requests/1327/merge
Julian Sparber 3 years ago
parent
commit
d6decbebaa
  1. 3
      data/resources/resources.gresource.xml
  2. 60
      data/resources/ui/create-dm-dialog-user-row.ui
  3. 140
      data/resources/ui/create-dm-dialog.ui
  4. 5
      data/resources/ui/sidebar.ui
  5. 2
      po/POTFILES.in
  6. 161
      src/session/create_dm_dialog/dm_user.rs
  7. 341
      src/session/create_dm_dialog/dm_user_list.rs
  8. 90
      src/session/create_dm_dialog/dm_user_row.rs
  9. 199
      src/session/create_dm_dialog/mod.rs
  10. 11
      src/session/mod.rs
  11. 23
      src/session/user.rs

3
data/resources/resources.gresource.xml

@ -116,6 +116,8 @@
<file compressed="true" preprocess="xml-stripblanks" alias="content-verification-info-bar.ui">ui/content-verification-info-bar.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="content.ui">ui/content.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="context-menu-bin.ui">ui/context-menu-bin.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="create-dm-dialog-user-row.ui">ui/create-dm-dialog-user-row.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="create-dm-dialog.ui">ui/create-dm-dialog.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="error-page.ui">ui/error-page.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="event-menu.ui">ui/event-menu.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="event-source-dialog.ui">ui/event-source-dialog.ui</file>
@ -149,3 +151,4 @@
<file compressed="true" preprocess="xml-stripblanks" alias="window.ui">ui/window.ui</file>
</gresource>
</gresources>

60
data/resources/ui/create-dm-dialog-user-row.ui

@ -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>

140
data/resources/ui/create-dm-dialog.ui

@ -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>

5
data/resources/ui/sidebar.ui

@ -2,6 +2,10 @@
<interface>
<menu id="primary_menu">
<section>
<item>
<attribute name="label" translatable="yes">New _Direct Chat</attribute>
<attribute name="action">session.create-dm</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_New Room</attribute>
<attribute name="action">session.room-creation</attribute>
@ -165,3 +169,4 @@
</child>
</template>
</interface>

2
po/POTFILES.in

@ -34,6 +34,7 @@ data/resources/ui/content-room-history.ui
data/resources/ui/content-state-creation.ui
data/resources/ui/content-state-tombstone.ui
data/resources/ui/content.ui
data/resources/ui/create-dm-dialog.ui
data/resources/ui/error-page.ui
data/resources/ui/event-menu.ui
data/resources/ui/event-source-dialog.ui
@ -93,6 +94,7 @@ src/session/content/room_history/typing_row.rs
src/session/content/room_history/verification_info_bar.rs
src/session/content/verification/identity_verification_widget.rs
src/session/content/verification/session_verification.rs
src/session/create_dm_dialog/mod.rs
src/session/join_room_dialog.rs
src/session/media_viewer.rs
src/session/mod.rs

161
src/session/create_dm_dialog/dm_user.rs

@ -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
}

341
src/session/create_dm_dialog/dm_user_list.rs

@ -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();
}
};
}
}

90
src/session/create_dm_dialog/dm_user_row.rs

@ -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");
}
}

199
src/session/create_dm_dialog/mod.rs

@ -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"));
}
}
}));
}
}

11
src/session/mod.rs

@ -1,6 +1,7 @@
mod account_settings;
mod avatar;
mod content;
mod create_dm_dialog;
mod event_source_dialog;
mod join_room_dialog;
mod media_viewer;
@ -55,6 +56,7 @@ use self::{
pub use self::{
avatar::{AvatarData, AvatarImage, AvatarUriSource},
content::verification::SessionVerification,
create_dm_dialog::CreateDmDialog,
room::{Event, Room},
room_creation::RoomCreation,
settings::SessionSettings,
@ -170,6 +172,10 @@ mod imp {
}));
});
klass.install_action("session.create-dm", None, move |session, _, _| {
session.show_create_dm_dialog();
});
klass.add_binding_action(
gdk::Key::Escape,
gdk::ModifierType::empty(),
@ -681,6 +687,11 @@ impl Session {
window.present();
}
fn show_create_dm_dialog(&self) {
let window = CreateDmDialog::new(self.parent_window().as_ref(), self);
window.present();
}
async fn show_join_room_dialog(&self) {
let dialog = JoinRoomDialog::new(self.parent_window().as_ref(), self);
dialog.present();

23
src/session/user.rs

@ -255,6 +255,29 @@ pub trait UserExt: IsA<User> {
let uri = self.user_id().matrix_to_uri();
format!("<a href=\"{uri}\">{}</a>", self.display_name())
}
/// Load the user profile from the homeserver.
///
/// This overwrites the already loaded display name and avatar.
fn load_profile(&self) {
let client = self.session().client();
let user_id = self.user_id();
let user = self.upcast_ref::<User>();
let handle = spawn_tokio!(async move { client.get_profile(&user_id).await });
spawn!(clone!(@weak user => async move {
match handle.await.unwrap() {
Ok(response) => {
user.set_display_name(response.displayname);
user.set_avatar_url(response.avatar_url);
},
Err(error) => {
error!("Failed to load user profile for {}: {}", user.user_id(), error);
}
};
}));
}
}
impl<T: IsA<User>> UserExt for T {}

Loading…
Cancel
Save