diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index 905296bc..a93e5d38 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -116,6 +116,8 @@
ui/content-verification-info-bar.ui
ui/content.ui
ui/context-menu-bin.ui
+ ui/create-dm-dialog-user-row.ui
+ ui/create-dm-dialog.ui
ui/error-page.ui
ui/event-menu.ui
ui/event-source-dialog.ui
@@ -149,3 +151,4 @@
ui/window.ui
+
diff --git a/data/resources/ui/create-dm-dialog-user-row.ui b/data/resources/ui/create-dm-dialog-user-row.ui
new file mode 100644
index 00000000..cdb2e364
--- /dev/null
+++ b/data/resources/ui/create-dm-dialog-user-row.ui
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
diff --git a/data/resources/ui/create-dm-dialog.ui b/data/resources/ui/create-dm-dialog.ui
new file mode 100644
index 00000000..3f4a925d
--- /dev/null
+++ b/data/resources/ui/create-dm-dialog.ui
@@ -0,0 +1,140 @@
+
+
+
+ Direct Chat
+ True
+ 380
+ 620
+
+
+
+
+ vertical
+
+
+
+
+
+ True
+
+
+ vertical
+ 18
+ 12
+ 12
+
+
+ True
+ word-char
+ 20
+ center
+ 0.5
+ New Direct Chat
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ no-search-page
+
+
+ True
+ system-search-symbolic
+ Search
+ Search for people to start a new chat with
+
+
+
+
+
+
+ matching-page
+
+
+
+
+
+
+ True
+ 6
+ 6
+
+
+
+
+
+
+
+
+
+
+
+
+ no-matching-page
+
+
+ system-search-symbolic
+ No Users Found
+ No users matching the search pattern were found
+
+
+
+
+
+
+ error-page
+
+
+ dialog-error-symbolic
+ Error
+ An error occurred while searching for matches
+
+
+
+
+
+
+ loading-page
+
+
+ center
+ center
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/data/resources/ui/sidebar.ui b/data/resources/ui/sidebar.ui
index 34155efb..e287a4cb 100644
--- a/data/resources/ui/sidebar.ui
+++ b/data/resources/ui/sidebar.ui
@@ -2,6 +2,10 @@
+
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 6a0859e8..6f4c605c 100644
--- a/po/POTFILES.in
+++ b/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
diff --git a/src/session/create_dm_dialog/dm_user.rs b/src/session/create_dm_dialog/dm_user.rs
new file mode 100644
index 00000000..23620573
--- /dev/null
+++ b/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,
+ }
+
+ #[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> = Lazy::new(|| {
+ vec![glib::ParamSpecObject::builder::("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) @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 {
+ 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 {
+ 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 {
+ 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
+}
diff --git a/src/session/create_dm_dialog/dm_user_list.rs b/src/session/create_dm_dialog/dm_user_list.rs
new file mode 100644
index 00000000..070fc2fd
--- /dev/null
+++ b/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>,
+ pub session: glib::WeakRef,
+ pub state: Cell,
+ pub search_term: RefCell