[fractal/ui-refactor: 6/15] Move app::connect to appop module




commit 62c308fef0ac2c7650d70c7786fe78496eb4888b
Author: Alejandro Domínguez <adomu net-c com>
Date:   Wed Oct 14 02:28:05 2020 +0200

    Move app::connect to appop module

 fractal-gtk/src/actions/account_settings.rs        |  46 ++--
 fractal-gtk/src/app/connect/account.rs             | 231 ---------------------
 fractal-gtk/src/app/connect/autocomplete.rs        |  29 ---
 fractal-gtk/src/app/connect/direct.rs              | 126 -----------
 fractal-gtk/src/app/connect/directory.rs           | 192 -----------------
 fractal-gtk/src/app/connect/headerbar.rs           |  55 -----
 fractal-gtk/src/app/connect/invite.rs              | 151 --------------
 fractal-gtk/src/app/connect/join_room.rs           |  54 -----
 fractal-gtk/src/app/connect/language.rs            |  42 ----
 fractal-gtk/src/app/connect/leave_room.rs          |  37 ----
 fractal-gtk/src/app/connect/markdown.rs            |  90 --------
 fractal-gtk/src/app/connect/mod.rs                 |  40 ----
 fractal-gtk/src/app/connect/new_room.rs            |  70 -------
 fractal-gtk/src/app/connect/roomlist_search.rs     |  61 ------
 fractal-gtk/src/app/connect/send.rs                |  69 ------
 fractal-gtk/src/app/connect/swipeable_widgets.rs   |  67 ------
 fractal-gtk/src/app/mod.rs                         |   5 +-
 fractal-gtk/src/appop/attach.rs                    |  44 ++--
 fractal-gtk/src/appop/connect/account.rs           | 230 ++++++++++++++++++++
 fractal-gtk/src/appop/connect/autocomplete.rs      |  25 +++
 fractal-gtk/src/appop/connect/direct.rs            | 130 ++++++++++++
 fractal-gtk/src/appop/connect/directory.rs         | 192 +++++++++++++++++
 fractal-gtk/src/appop/connect/headerbar.rs         |  52 +++++
 fractal-gtk/src/appop/connect/invite.rs            | 158 ++++++++++++++
 fractal-gtk/src/appop/connect/join_room.rs         |  55 +++++
 fractal-gtk/src/appop/connect/language.rs          |  39 ++++
 fractal-gtk/src/appop/connect/leave_room.rs        |  36 ++++
 fractal-gtk/src/appop/connect/markdown.rs          |  87 ++++++++
 fractal-gtk/src/appop/connect/mod.rs               |  36 ++++
 fractal-gtk/src/appop/connect/new_room.rs          |  69 ++++++
 fractal-gtk/src/appop/connect/roomlist_search.rs   |  60 ++++++
 fractal-gtk/src/appop/connect/send.rs              |  69 ++++++
 fractal-gtk/src/appop/connect/swipeable_widgets.rs |  63 ++++++
 fractal-gtk/src/appop/mod.rs                       |   1 +
 fractal-gtk/src/meson.build                        |  30 +--
 fractal-gtk/src/widgets/autocomplete.rs            |   4 +-
 36 files changed, 1362 insertions(+), 1383 deletions(-)
---
diff --git a/fractal-gtk/src/actions/account_settings.rs b/fractal-gtk/src/actions/account_settings.rs
index 3526347f..04cd63da 100644
--- a/fractal-gtk/src/actions/account_settings.rs
+++ b/fractal-gtk/src/actions/account_settings.rs
@@ -4,17 +4,15 @@ use gio::prelude::*;
 use gio::SimpleAction;
 use gio::SimpleActionGroup;
 use glib::clone;
-use std::sync::{Arc, Mutex};
 
-use crate::app::RUNTIME;
-use crate::appop::AppOp;
+use crate::app::{UpdateApp, RUNTIME};
 
 use crate::widgets::FileDialog::open;
 
 use crate::actions::ButtonState;
 
 // This creates all actions a user can perform in the account settings
-pub fn new(window: &gtk::Window, op: Arc<Mutex<AppOp>>) -> gio::SimpleActionGroup {
+pub fn new(window: &gtk::Window, app_tx: glib::Sender<UpdateApp>) -> gio::SimpleActionGroup {
     let actions = SimpleActionGroup::new();
     // TODO create two stats loading interaction and connect it to the avatar box
     let change_avatar =
@@ -23,26 +21,28 @@ pub fn new(window: &gtk::Window, op: Arc<Mutex<AppOp>>) -> gio::SimpleActionGrou
     actions.add_action(&change_avatar);
 
     change_avatar.connect_activate(clone!(@weak window => move |a, _| {
-        let (session_client, uid) = unwrap_or_unit_return!(
-            op.lock().unwrap().login_data.as_ref().map(|ld| (ld.session_client.clone(), ld.uid.clone()))
-        );
-
-        let filter = gtk::FileFilter::new();
-        filter.add_mime_type("image/*");
-        filter.set_name(Some(i18n("Images").as_str()));
-        if let Some(path) = open(&window, i18n("Select a new avatar").as_str(), &[filter]) {
-            a.change_state(&ButtonState::Insensitive.into());
-            RUNTIME.spawn(async move {
-                match user::set_user_avatar(session_client, &uid, path).await {
-                    Ok(path) => {
-                        APPOP!(show_new_avatar, (path));
+        let _ = app_tx.send(Box::new(clone!(@weak a => move |op| {
+            let (session_client, uid) = unwrap_or_unit_return!(
+                op.login_data.as_ref().map(|ld| (ld.session_client.clone(), ld.uid.clone()))
+            );
+
+            let filter = gtk::FileFilter::new();
+            filter.add_mime_type("image/*");
+            filter.set_name(Some(i18n("Images").as_str()));
+            if let Some(path) = open(&window, i18n("Select a new avatar").as_str(), &[filter]) {
+                a.change_state(&ButtonState::Insensitive.into());
+                RUNTIME.spawn(async move {
+                    match user::set_user_avatar(session_client, &uid, path).await {
+                        Ok(path) => {
+                            APPOP!(show_new_avatar, (path));
+                        }
+                        Err(err) => {
+                            err.handle_error();
+                        }
                     }
-                    Err(err) => {
-                        err.handle_error();
-                    }
-                }
-            });
-        }
+                });
+            }
+        })));
     }));
 
     actions
diff --git a/fractal-gtk/src/app/mod.rs b/fractal-gtk/src/app/mod.rs
index 53d3cc34..5af5efa8 100644
--- a/fractal-gtk/src/app/mod.rs
+++ b/fractal-gtk/src/app/mod.rs
@@ -18,7 +18,6 @@ use crate::config;
 use crate::uibuilder;
 use crate::widgets;
 
-mod connect;
 mod windowstate;
 
 use windowstate::WindowState;
@@ -167,7 +166,7 @@ impl App {
 
         let app = AppRef::new(Self { ui });
 
-        app.connect_gtk();
+        let _ = get_app_tx().send(Box::new(|op| op.connect_gtk()));
 
         app
     }
@@ -223,7 +222,7 @@ impl App {
 }
 
 // TODO: Deprecated. It should be removed
-pub(self) fn get_op() -> &'static GlobalAppOp {
+pub fn get_op() -> &'static GlobalAppOp {
     unsafe { OP.as_ref().expect("Fatal: AppOp has not been initialized") }
 }
 
diff --git a/fractal-gtk/src/appop/attach.rs b/fractal-gtk/src/appop/attach.rs
index dd6688cc..21c77bbf 100644
--- a/fractal-gtk/src/appop/attach.rs
+++ b/fractal-gtk/src/appop/attach.rs
@@ -4,7 +4,6 @@ use glib::clone;
 use std::fs::File;
 use std::io::prelude::*;
 use std::path::PathBuf;
-use std::sync::{Arc, Mutex};
 
 use anyhow::Error;
 
@@ -72,6 +71,24 @@ impl AppOp {
             }
         }
     }
+
+    pub fn paste(&self) {
+        if let Some(display) = gdk::Display::get_default() {
+            if let Some(clipboard) = gtk::Clipboard::get_default(&display) {
+                if clipboard.wait_is_image_available() {
+                    if let Some(pixb) = clipboard.wait_for_image() {
+                        self.draw_image_paste_dialog(&pixb);
+
+                        // removing text from clipboard
+                        clipboard.set_text("");
+                        clipboard.set_image(&pixb);
+                    }
+                } else {
+                    // TODO: manage code pasting
+                }
+            }
+        }
+    }
 }
 
 fn store_pixbuf(pixb: &Pixbuf) -> Result<PathBuf, Error> {
@@ -85,28 +102,3 @@ fn store_pixbuf(pixb: &Pixbuf) -> Result<PathBuf, Error> {
 
     Ok(path)
 }
-
-/// This function receives the appop mutex to avoid lock the interface
-/// This was previously an appop method that receives &self, but that
-/// force us to lock the interface for the entire function that causes
-/// problems because we call to wait_is_image_available that makes that
-/// tries to continue the loop and that give us to a deadlock so
-/// this function minimize the lock and avoid that kind of problems
-/// See: https://gitlab.gnome.org/GNOME/fractal/issues/284
-pub fn paste(op: Arc<Mutex<AppOp>>) {
-    if let Some(display) = gdk::Display::get_default() {
-        if let Some(clipboard) = gtk::Clipboard::get_default(&display) {
-            if clipboard.wait_is_image_available() {
-                if let Some(pixb) = clipboard.wait_for_image() {
-                    op.lock().unwrap().draw_image_paste_dialog(&pixb);
-
-                    // removing text from clipboard
-                    clipboard.set_text("");
-                    clipboard.set_image(&pixb);
-                }
-            } else {
-                // TODO: manage code pasting
-            }
-        }
-    }
-}
diff --git a/fractal-gtk/src/appop/connect/account.rs b/fractal-gtk/src/appop/connect/account.rs
new file mode 100644
index 00000000..f1e431e0
--- /dev/null
+++ b/fractal-gtk/src/appop/connect/account.rs
@@ -0,0 +1,230 @@
+use gio::ActionMapExt;
+use glib::clone;
+use gtk::prelude::*;
+
+use crate::appop::AppOp;
+
+use crate::actions::{AccountSettings, StateExt};
+
+pub fn connect(appop: &AppOp) {
+    let app_tx = appop.app_tx.clone();
+    let builder = &appop.ui.builder;
+    let cancel_password = appop
+        .ui
+        .builder
+        .get_object::<gtk::Button>("password-dialog-cancel")
+        .expect("Can't find password-dialog-cancel in ui file.");
+    let confirm_password = appop
+        .ui
+        .builder
+        .get_object::<gtk::Button>("password-dialog-apply")
+        .expect("Can't find password-dialog-apply in ui file.");
+    let password_dialog = appop
+        .ui
+        .builder
+        .get_object::<gtk::Dialog>("password_dialog")
+        .expect("Can't find password_dialog in ui file.");
+    let avatar_btn = appop
+        .ui
+        .builder
+        .get_object::<gtk::Button>("account_settings_avatar_button")
+        .expect("Can't find account_settings_avatar_button in ui file.");
+    let name_entry = appop
+        .ui
+        .builder
+        .get_object::<gtk::Entry>("account_settings_name")
+        .expect("Can't find account_settings_name in ui file.");
+    let name_btn = appop
+        .ui
+        .builder
+        .get_object::<gtk::Button>("account_settings_name_button")
+        .expect("Can't find account_settings_name_button in ui file.");
+    let password_btn = appop
+        .ui
+        .builder
+        .get_object::<gtk::Button>("account_settings_password")
+        .expect("Can't find account_settings_password in ui file.");
+    let old_password = appop
+        .ui
+        .builder
+        .get_object::<gtk::Entry>("password-dialog-old-entry")
+        .expect("Can't find password-dialog-old-entry in ui file.");
+    let new_password = appop
+        .ui
+        .builder
+        .get_object::<gtk::Entry>("password-dialog-entry")
+        .expect("Can't find password-dialog-entry in ui file.");
+    let verify_password = appop
+        .ui
+        .builder
+        .get_object::<gtk::Entry>("password-dialog-verify-entry")
+        .expect("Can't find password-dialog-verify-entry in ui file.");
+    let destruction_entry = appop
+        .ui
+        .builder
+        .get_object::<gtk::Entry>("account_settings_delete_password_confirm")
+        .expect("Can't find account_settings_delete_password_confirm in ui file.");
+    let destruction_btn = appop
+        .ui
+        .builder
+        .get_object::<gtk::Button>("account_settings_delete_btn")
+        .expect("Can't find account_settings_delete_btn in ui file.");
+
+    let window = appop.ui.main_window.upcast_ref::<gtk::Window>();
+    let actions = AccountSettings::new(&window, app_tx.clone());
+    let container = appop
+        .ui
+        .builder
+        .get_object::<gtk::Box>("account_settings_box")
+        .expect("Can't find account_settings_box in ui file.");
+    container.insert_action_group("user-settings", Some(&actions));
+
+    /* Body */
+    if let Some(action) = actions.lookup_action("change-avatar") {
+        action.bind_button_state(&avatar_btn);
+        avatar_btn.set_action_name(Some("user-settings.change-avatar"));
+        let avatar_spinner = appop
+            .ui
+            .builder
+            .get_object::<gtk::Spinner>("account_settings_avatar_spinner")
+            .expect("Can't find account_settings_avatar_spinner in ui file.");
+        avatar_btn.connect_property_sensitive_notify(
+            clone!(@weak avatar_spinner as spinner => move |w| {
+                if w.get_sensitive() {
+                    spinner.hide();
+                    spinner.stop();
+                } else {
+                    spinner.start();
+                    spinner.show();
+                }
+            }),
+        );
+    }
+
+    name_entry.connect_property_text_notify(
+        clone!(@strong app_tx, @strong name_btn as button => move |w| {
+            let _ = app_tx.send(Box::new(clone!(@strong w, @strong button => move |op| {
+                let username = w.get_text();
+                if !username.is_empty()
+                    && op
+                        .login_data
+                        .as_ref()
+                        .and_then(|login_data| login_data.username.as_ref())
+                        .filter(|u| **u != username)
+                        .is_some()
+                {
+                    button.show();
+                    return;
+                }
+                button.hide();
+            })));
+        }),
+    );
+
+    let button = name_btn.clone();
+    name_entry.connect_activate(move |_w| {
+        let _ = button.emit("clicked", &[]);
+    });
+
+    name_btn.connect_clicked(clone!(@strong app_tx => move |_w| {
+        let _ = app_tx.send(Box::new(|op| op.update_username_account_settings()));
+    }));
+
+    /*
+    fn update_password_strength(builder: &gtk::Builder) {
+    let bar = builder
+    .get_object::<gtk::LevelBar>("password-dialog-strength-indicator")
+    .expect("Can't find password-dialog-strength-indicator in ui file.");
+    let label = builder
+    .get_object::<gtk::Label>("password-dialog-hint")
+    .expect("Can't find password-dialog-hint in ui file.");
+    let strength_level = 10f64;
+    bar.set_value(strength_level);
+    label.set_label("text");
+    }
+    */
+
+    fn validate_password_input(builder: &gtk::Builder) {
+        let hint = builder
+            .get_object::<gtk::Label>("password-dialog-verify-hint")
+            .expect("Can't find password-dialog-verify-hint in ui file.");
+        let confirm_password = builder
+            .get_object::<gtk::Button>("password-dialog-apply")
+            .expect("Can't find password-dialog-apply in ui file.");
+        let old = builder
+            .get_object::<gtk::Entry>("password-dialog-old-entry")
+            .expect("Can't find password-dialog-old-entry in ui file.");
+        let new = builder
+            .get_object::<gtk::Entry>("password-dialog-entry")
+            .expect("Can't find password-dialog-entry in ui file.");
+        let verify = builder
+            .get_object::<gtk::Entry>("password-dialog-verify-entry")
+            .expect("Can't find password-dialog-verify-entry in ui file.");
+
+        let mut empty = true;
+        let mut matching = true;
+        let old_p = old.get_text();
+        let new_p = new.get_text();
+        let verify_p = verify.get_text();
+
+        if new_p != verify_p {
+            matching = false;
+        }
+        if !new_p.is_empty() && !verify_p.is_empty() && !old_p.is_empty() {
+            empty = false;
+        }
+
+        if matching {
+            hint.hide();
+        } else {
+            hint.show();
+        }
+
+        confirm_password.set_sensitive(matching && !empty);
+    }
+
+    /* Passsword dialog */
+    password_btn.connect_clicked(clone!(@strong app_tx => move |_| {
+        let _ = app_tx.send(Box::new(|op| op.show_password_dialog()));
+    }));
+
+    password_dialog.connect_delete_event(clone!(@strong app_tx => move |_, _| {
+        let _ = app_tx.send(Box::new(|op| op.close_password_dialog()));
+        glib::signal::Inhibit(true)
+    }));
+
+    /* Headerbar */
+    cancel_password.connect_clicked(clone!(@strong app_tx => move |_| {
+        let _ = app_tx.send(Box::new(|op| op.close_password_dialog()));
+    }));
+
+    confirm_password.connect_clicked(clone!(@strong app_tx => move |_| {
+        let _ = app_tx.send(Box::new(|op| {
+            op.set_new_password();
+            op.close_password_dialog();
+        }));
+    }));
+
+    /* Body */
+    verify_password.connect_property_text_notify(clone!(@strong builder => move |_| {
+        validate_password_input(&builder.clone());
+    }));
+    new_password.connect_property_text_notify(clone!(@strong builder => move |_| {
+        validate_password_input(&builder.clone());
+    }));
+    old_password.connect_property_text_notify(clone!(@strong builder => move |_| {
+        validate_password_input(&builder)
+    }));
+
+    destruction_entry.connect_property_text_notify(clone!(@strong destruction_btn => move |w| {
+        if !w.get_text().is_empty() {
+            destruction_btn.set_sensitive(true);
+            return;
+        }
+        destruction_btn.set_sensitive(false);
+    }));
+
+    destruction_btn.connect_clicked(move |_| {
+        let _ = app_tx.send(Box::new(|op| op.account_destruction()));
+    });
+}
diff --git a/fractal-gtk/src/appop/connect/autocomplete.rs b/fractal-gtk/src/appop/connect/autocomplete.rs
new file mode 100644
index 00000000..a3c64474
--- /dev/null
+++ b/fractal-gtk/src/appop/connect/autocomplete.rs
@@ -0,0 +1,25 @@
+use gtk::prelude::*;
+
+use crate::widgets;
+
+use crate::appop::AppOp;
+
+pub fn connect(appop: &AppOp) {
+    let popover = appop
+        .ui
+        .builder
+        .get_object::<gtk::Popover>("autocomplete_popover")
+        .expect("Can't find autocomplete_popover in ui file.");
+    let listbox = appop
+        .ui
+        .builder
+        .get_object::<gtk::ListBox>("autocomplete_listbox")
+        .expect("Can't find autocomplete_listbox in ui file.");
+    let window: gtk::Window = appop
+        .ui
+        .builder
+        .get_object("main_window")
+        .expect("Can't find main_window in ui file.");
+
+    widgets::Autocomplete::new(window, appop.ui.sventry.view.clone(), popover, listbox).connect();
+}
diff --git a/fractal-gtk/src/appop/connect/direct.rs b/fractal-gtk/src/appop/connect/direct.rs
new file mode 100644
index 00000000..eefce9e6
--- /dev/null
+++ b/fractal-gtk/src/appop/connect/direct.rs
@@ -0,0 +1,130 @@
+use glib::clone;
+use gtk::prelude::*;
+
+use glib::source::Continue;
+use std::sync::{Arc, Mutex};
+
+use crate::appop::AppOp;
+
+pub fn connect(appop: &AppOp) {
+    let app_tx = appop.app_tx.clone();
+    let cancel = appop
+        .ui
+        .builder
+        .get_object::<gtk::Button>("cancel_direct_chat")
+        .expect("Can't find cancel_direct_chat in ui file.");
+    let invite = appop
+        .ui
+        .builder
+        .get_object::<gtk::Button>("direct_chat_button")
+        .expect("Can't find direct_chat_button in ui file.");
+    let to_chat_entry_box = appop
+        .ui
+        .builder
+        .get_object::<gtk::Box>("to_chat_entry_box")
+        .expect("Can't find to_chat_entry_box in ui file.");
+    let to_chat_entry = appop
+        .ui
+        .builder
+        .get_object::<gtk::TextView>("to_chat_entry")
+        .expect("Can't find to_chat_entry in ui file.");
+    let dialog = appop
+        .ui
+        .builder
+        .get_object::<gtk::Dialog>("direct_chat_dialog")
+        .expect("Can't find direct_chat_dialog in ui file.");
+
+    if let Some(buffer) = to_chat_entry.get_buffer() {
+        let placeholder_tag = gtk::TextTag::new(Some("placeholder"));
+
+        placeholder_tag.set_property_foreground_rgba(Some(&gdk::RGBA {
+            red: 1.0,
+            green: 1.0,
+            blue: 1.0,
+            alpha: 0.5,
+        }));
+
+        if let Some(tag_table) = buffer.get_tag_table() {
+            tag_table.add(&placeholder_tag);
+        }
+    }
+
+    // this is used to cancel the timeout and not search for every key input. We'll wait 500ms
+    // without key release event to launch the search
+    let source_id: Arc<Mutex<Option<glib::source::SourceId>>> = Arc::new(Mutex::new(None));
+    to_chat_entry.connect_key_release_event(clone!(@strong app_tx => move |entry, _| {
+        {
+            let mut id = source_id.lock().unwrap();
+            if let Some(sid) = id.take() {
+                glib::source::source_remove(sid);
+            }
+        }
+
+        let sid = glib::timeout_add_local(
+            500,
+            clone!(
+            @strong entry,
+            @strong source_id,
+            @strong app_tx
+            => move || {
+                if let Some(buffer) = entry.get_buffer() {
+                    let start = buffer.get_start_iter();
+                    let end = buffer.get_end_iter();
+
+                    if let Some(text) =
+                        buffer.get_text(&start, &end, false).map(|gstr| gstr.to_string())
+                    {
+                        let _ = app_tx.send(Box::new(|op| op.search_invite_user(text)));
+                    }
+                }
+
+                *(source_id.lock().unwrap()) = None;
+                Continue(false)
+            }),
+        );
+
+        *(source_id.lock().unwrap()) = Some(sid);
+        glib::signal::Inhibit(false)
+    }));
+
+    to_chat_entry.connect_focus_in_event(
+        clone!(@strong to_chat_entry_box, @strong app_tx => move |_, _| {
+            to_chat_entry_box.get_style_context().add_class("message-input-focused");
+
+            let _ = app_tx.send(Box::new(|op| op.remove_invite_user_dialog_placeholder()));
+
+            Inhibit(false)
+        }),
+    );
+
+    to_chat_entry.connect_focus_out_event(
+        clone!(@strong to_chat_entry_box, @strong app_tx => move |_, _| {
+            to_chat_entry_box.get_style_context().remove_class("message-input-focused");
+
+            let _ = app_tx.send(Box::new(|op| op.set_invite_user_dialog_placeholder()));
+
+            Inhibit(false)
+        }),
+    );
+
+    if let Some(buffer) = to_chat_entry.get_buffer() {
+        buffer.connect_delete_range(clone!(@strong app_tx => move |_, _, _| {
+            glib::idle_add_local(clone!(@strong app_tx => move || {
+                let _ = app_tx.send(Box::new(|op| op.detect_removed_invite()));
+                Continue(false)
+            }));
+        }));
+    }
+
+    dialog.connect_delete_event(clone!(@strong app_tx => move |_, _| {
+        let _ = app_tx.send(Box::new(|op| op.close_direct_chat_dialog()));
+        glib::signal::Inhibit(true)
+    }));
+    cancel.connect_clicked(clone!(@strong app_tx => move |_| {
+        let _ = app_tx.send(Box::new(|op| op.close_direct_chat_dialog()));
+    }));
+    invite.set_sensitive(false);
+    invite.connect_clicked(move |_| {
+        let _ = app_tx.send(Box::new(|op| op.start_chat()));
+    });
+}
diff --git a/fractal-gtk/src/appop/connect/directory.rs b/fractal-gtk/src/appop/connect/directory.rs
new file mode 100644
index 00000000..b987ccc5
--- /dev/null
+++ b/fractal-gtk/src/appop/connect/directory.rs
@@ -0,0 +1,192 @@
+use glib::clone;
+
+use crate::util::i18n::i18n;
+
+use gtk::prelude::*;
+use libhandy::prelude::*;
+
+use crate::appop::{AppOp, RoomSearchPagination};
+
+pub fn connect(appop: &AppOp) {
+    let app_tx = appop.app_tx.clone();
+    let q = appop
+        .ui
+        .builder
+        .get_object::<gtk::Entry>("directory_search_entry")
+        .expect("Can't find directory_search_entry in ui file.");
+
+    let directory_stack = appop
+        .ui
+        .builder
+        .get_object::<gtk::Stack>("directory_stack")
+        .expect("Can't find directory_stack in ui file.");
+
+    let clamp = libhandy::Clamp::new();
+    let listbox = gtk::ListBox::new();
+
+    clamp.set_maximum_size(800);
+    clamp.set_hexpand(true);
+    clamp.set_vexpand(true);
+    clamp.set_margin_top(24);
+    clamp.set_margin_start(12);
+    clamp.set_margin_end(12);
+
+    let frame = gtk::Frame::new(None);
+    frame.set_shadow_type(gtk::ShadowType::In);
+    frame.add(&listbox);
+    frame.get_style_context().add_class("room-directory");
+    clamp.add(&frame);
+    listbox.show();
+    frame.show();
+    clamp.show();
+    directory_stack.add_named(&clamp, "directory_clamp");
+
+    appop
+        .ui
+        .builder
+        .expose_object::<gtk::ListBox>("directory_room_list", &listbox);
+    appop
+        .ui
+        .builder
+        .expose_object::<libhandy::Clamp>("directory_clamp", &clamp);
+
+    let directory_choice_label = appop
+        .ui
+        .builder
+        .get_object::<gtk::Label>("directory_choice_label")
+        .expect("Can't find directory_choice_label in ui file.");
+
+    let default_matrix_server_radio = appop
+        .ui
+        .builder
+        .get_object::<gtk::RadioButton>("default_matrix_server_radio")
+        .expect("Can't find default_matrix_server_radio in ui file.");
+
+    let other_protocol_radio = appop
+        .ui
+        .builder
+        .get_object::<gtk::RadioButton>("other_protocol_radio")
+        .expect("Can't find other_protocol_radio in ui file.");
+
+    let protocol_combo = appop
+        .ui
+        .builder
+        .get_object::<gtk::ComboBox>("protocol_combo")
+        .expect("Can't find protocol_combo in ui file.");
+
+    let protocol_model = appop
+        .ui
+        .builder
+        .get_object::<gtk::ListStore>("protocol_model")
+        .expect("Can't find protocol_model in ui file.");
+
+    let other_homeserver_radio = appop
+        .ui
+        .builder
+        .get_object::<gtk::RadioButton>("other_homeserver_radio")
+        .expect("Can't find other_homeserver_radio in ui file.");
+
+    let other_homeserver_url_entry = appop
+        .ui
+        .builder
+        .get_object::<gtk::Entry>("other_homeserver_url_entry")
+        .expect("Can't find other_homeserver_url_entry in ui file.");
+
+    let other_homeserver_url = appop
+        .ui
+        .builder
+        .get_object::<gtk::EntryBuffer>("other_homeserver_url")
+        .expect("Can't find other_homeserver_url in ui file.");
+
+    let scroll = appop
+        .ui
+        .builder
+        .get_object::<gtk::ScrolledWindow>("directory_scroll")
+        .expect("Can't find directory_scroll in ui file.");
+
+    scroll.connect_edge_reached(clone!(@strong app_tx => move |_, dir| {
+        if dir == gtk::PositionType::Bottom {
+            let _ = app_tx.send(Box::new(|op| op.load_more_rooms()));
+        }
+    }));
+
+    q.connect_activate(move |_| {
+        let _ = app_tx.send(Box::new(|op| {
+            op.directory_pagination = RoomSearchPagination::Initial;
+            op.search_rooms();
+        }));
+    });
+
+    default_matrix_server_radio.connect_toggled(clone!(
+    @strong directory_choice_label,
+    @strong default_matrix_server_radio,
+    @strong protocol_combo,
+    @strong other_homeserver_url_entry
+    => move |_| {
+        if default_matrix_server_radio.get_active() {
+            protocol_combo.set_sensitive(false);
+            other_homeserver_url_entry.set_sensitive(false);
+        }
+
+        directory_choice_label.set_text(&i18n("Default Matrix Server"));
+    }));
+
+    other_protocol_radio.connect_toggled(clone!(
+    @strong directory_choice_label,
+    @strong other_protocol_radio,
+    @strong protocol_combo,
+    @strong protocol_model,
+    @strong other_homeserver_url_entry
+    => move |_| {
+        if other_protocol_radio.get_active() {
+            protocol_combo.set_sensitive(true);
+            other_homeserver_url_entry.set_sensitive(false);
+        }
+
+        let active = protocol_combo.get_active().map_or(-1, |uint| uint as i32);
+        let protocol: String = match protocol_model.iter_nth_child(None, active) {
+            Some(it) => {
+                let v = protocol_model.get_value(&it, 0);
+                v.get().unwrap().unwrap()
+            }
+            None => String::new(),
+        };
+
+        directory_choice_label.set_text(&protocol);
+    }));
+
+    protocol_combo.connect_changed(clone!(
+    @strong directory_choice_label,
+    @strong protocol_combo,
+    @strong protocol_model
+    => move |_| {
+        let active = protocol_combo.get_active().map_or(-1, |uint| uint as i32);
+        let protocol: String = match protocol_model.iter_nth_child(None, active) {
+            Some(it) => {
+                let v = protocol_model.get_value(&it, 0);
+                v.get().unwrap().unwrap()
+            }
+            None => String::new(),
+        };
+
+        directory_choice_label.set_text(&protocol);
+    }));
+
+    other_homeserver_radio.connect_toggled(clone!(
+    @strong other_homeserver_radio,
+    @strong protocol_combo,
+    @strong other_homeserver_url_entry
+    => move |_| {
+        if other_homeserver_radio.get_active() {
+            protocol_combo.set_sensitive(false);
+            other_homeserver_url_entry.set_sensitive(true);
+        }
+    }));
+
+    other_homeserver_url_entry.connect_changed(clone!(
+    @strong directory_choice_label,
+    @strong other_homeserver_url
+    => move |_| {
+        directory_choice_label.set_text(&other_homeserver_url.get_text());
+    }));
+}
diff --git a/fractal-gtk/src/appop/connect/headerbar.rs b/fractal-gtk/src/appop/connect/headerbar.rs
new file mode 100644
index 00000000..f09c47da
--- /dev/null
+++ b/fractal-gtk/src/appop/connect/headerbar.rs
@@ -0,0 +1,52 @@
+use glib::clone;
+use gtk::prelude::*;
+use libhandy::HeaderBarExt;
+
+use crate::appop::AppOp;
+
+pub fn connect(appop: &AppOp) {
+    if let Some(set) = gtk::Settings::get_default() {
+        let left_header: libhandy::HeaderBar = appop
+            .ui
+            .builder
+            .get_object("left-header")
+            .expect("Can't find left-header in ui file.");
+
+        let right_header: libhandy::HeaderBar = appop
+            .ui
+            .builder
+            .get_object("room_header_bar")
+            .expect("Can't find room_header_bar in ui file.");
+
+        if let Some(decor) = set.get_property_gtk_decoration_layout() {
+            let decor = decor.to_string();
+            let decor_split: Vec<String> = decor.splitn(2, ':').map(|s| s.to_string()).collect();
+            // Check if the close button is to the right; If not,
+            // change the headerbar controls
+            if decor_split.len() > 1 && !decor_split[1].contains("close") {
+                right_header.set_show_close_button(false);
+                left_header.set_show_close_button(true);
+            }
+        };
+
+        set.connect_property_gtk_decoration_layout_notify(clone!(
+        @strong right_header,
+        @strong left_header,
+        @strong set
+        => move |_| {
+            if let Some(decor) = set.get_property_gtk_decoration_layout() {
+                let decor = decor.to_string();
+                let decor_split: Vec<String> = decor.splitn(2,':').map(|s| s.to_string()).collect();
+                // Change the headerbar controls depending on position
+                // of close
+                if decor_split.len() > 1 && decor_split[1].contains("close") {
+                    left_header.set_show_close_button(false);
+                    right_header.set_show_close_button(true);
+                } else {
+                    right_header.set_show_close_button(false);
+                    left_header.set_show_close_button(true);
+                }
+            };
+        }));
+    };
+}
diff --git a/fractal-gtk/src/appop/connect/invite.rs b/fractal-gtk/src/appop/connect/invite.rs
new file mode 100644
index 00000000..b3d23e06
--- /dev/null
+++ b/fractal-gtk/src/appop/connect/invite.rs
@@ -0,0 +1,158 @@
+use glib::clone;
+use gtk::prelude::*;
+
+use glib::source::Continue;
+use std::sync::{Arc, Mutex};
+
+use crate::appop::AppOp;
+
+pub fn connect_dialog(appop: &AppOp) {
+    let app_tx = appop.app_tx.clone();
+    let dialog = appop
+        .ui
+        .builder
+        .get_object::<gtk::MessageDialog>("invite_dialog")
+        .expect("Can't find invite_dialog in ui file.");
+    let accept = appop
+        .ui
+        .builder
+        .get_object::<gtk::Button>("invite_accept")
+        .expect("Can't find invite_accept in ui file.");
+    let reject = appop
+        .ui
+        .builder
+        .get_object::<gtk::Button>("invite_reject")
+        .expect("Can't find invite_reject in ui file.");
+
+    reject.connect_clicked(clone!(@strong dialog, @strong app_tx => move |_| {
+        let _ = app_tx.send(Box::new(|op| op.accept_inv(false)));
+        dialog.hide();
+    }));
+    dialog.connect_delete_event(clone!(@strong dialog, @strong app_tx => move |_, _| {
+        let _ = app_tx.send(Box::new(|op| op.accept_inv(false)));
+        dialog.hide();
+        glib::signal::Inhibit(true)
+    }));
+
+    accept.connect_clicked(clone!(@strong dialog => move |_| {
+        let _ = app_tx.send(Box::new(|op| op.accept_inv(true)));
+        dialog.hide();
+    }));
+}
+
+pub fn connect_user(appop: &AppOp) {
+    let app_tx = appop.app_tx.clone();
+    let cancel = appop
+        .ui
+        .builder
+        .get_object::<gtk::Button>("cancel_invite")
+        .expect("Can't find cancel_invite in ui file.");
+    let invite = appop
+        .ui
+        .builder
+        .get_object::<gtk::Button>("invite_button")
+        .expect("Can't find invite_button in ui file.");
+    let invite_entry_box = appop
+        .ui
+        .builder
+        .get_object::<gtk::Box>("invite_entry_box")
+        .expect("Can't find invite_entry_box in ui file.");
+    let invite_entry = appop
+        .ui
+        .builder
+        .get_object::<gtk::TextView>("invite_entry")
+        .expect("Can't find invite_entry in ui file.");
+    let dialog = appop
+        .ui
+        .builder
+        .get_object::<gtk::Dialog>("invite_user_dialog")
+        .expect("Can't find invite_user_dialog in ui file.");
+
+    if let Some(buffer) = invite_entry.get_buffer() {
+        let placeholder_tag = gtk::TextTag::new(Some("placeholder"));
+
+        placeholder_tag.set_property_foreground_rgba(Some(&gdk::RGBA {
+            red: 1.0,
+            green: 1.0,
+            blue: 1.0,
+            alpha: 0.5,
+        }));
+
+        if let Some(tag_table) = buffer.get_tag_table() {
+            tag_table.add(&placeholder_tag);
+        }
+    }
+
+    // this is used to cancel the timeout and not search for every key input. We'll wait 500ms
+    // without key release event to launch the search
+    let source_id: Arc<Mutex<Option<glib::source::SourceId>>> = Arc::new(Mutex::new(None));
+    invite_entry.connect_key_release_event(clone!(@strong app_tx => move |entry, _| {
+            {
+                let mut id = source_id.lock().unwrap();
+                if let Some(sid) = id.take() {
+                    glib::source::source_remove(sid);
+                }
+            }
+
+            let sid = glib::timeout_add_local(
+                500,
+                clone!(@strong entry, @strong source_id, @strong app_tx => move || {
+                    if let Some(buffer) = entry.get_buffer() {
+                        let start = buffer.get_start_iter();
+                        let end = buffer.get_end_iter();
+
+                        if let Some(text) = buffer.get_text(&start, &end, false).map(|gstr| 
gstr.to_string()) {
+                            let _ = app_tx.send(Box::new(|op| op.search_invite_user(text)));
+                        }
+                    }
+
+                    *(source_id.lock().unwrap()) = None;
+                    Continue(false)
+                }),
+            );
+
+            *(source_id.lock().unwrap()) = Some(sid);
+            glib::signal::Inhibit(false)
+        }));
+
+    invite_entry.connect_focus_in_event(
+        clone!(@strong invite_entry_box, @strong app_tx => move |_, _| {
+            invite_entry_box.get_style_context().add_class("message-input-focused");
+
+            let _ = app_tx.send(Box::new(|op| op.remove_invite_user_dialog_placeholder()));
+
+            Inhibit(false)
+        }),
+    );
+
+    invite_entry.connect_focus_out_event(
+        clone!(@strong invite_entry_box, @strong app_tx => move |_, _| {
+            invite_entry_box.get_style_context().remove_class("message-input-focused");
+
+            let _ = app_tx.send(Box::new(|op| op.set_invite_user_dialog_placeholder()));
+
+            Inhibit(false)
+        }),
+    );
+
+    if let Some(buffer) = invite_entry.get_buffer() {
+        buffer.connect_delete_range(clone!(@strong app_tx => move |_, _, _| {
+            glib::idle_add_local(clone!(@strong app_tx => move || {
+                let _ = app_tx.send(Box::new(|op| op.detect_removed_invite()));
+                Continue(false)
+            }));
+        }));
+    }
+
+    dialog.connect_delete_event(clone!(@strong app_tx => move |_, _| {
+        let _ = app_tx.send(Box::new(|op| op.close_invite_dialog()));
+        glib::signal::Inhibit(true)
+    }));
+    cancel.connect_clicked(clone!(@strong app_tx => move |_| {
+        let _ = app_tx.send(Box::new(|op| op.close_invite_dialog()));
+    }));
+    invite.set_sensitive(false);
+    invite.connect_clicked(move |_| {
+        let _ = app_tx.send(Box::new(|op| op.invite()));
+    });
+}
diff --git a/fractal-gtk/src/appop/connect/join_room.rs b/fractal-gtk/src/appop/connect/join_room.rs
new file mode 100644
index 00000000..87306933
--- /dev/null
+++ b/fractal-gtk/src/appop/connect/join_room.rs
@@ -0,0 +1,55 @@
+use glib::clone;
+use gtk::prelude::*;
+
+use crate::appop::AppOp;
+
+pub fn connect(appop: &AppOp) {
+    let app_tx = appop.app_tx.clone();
+    let dialog = appop
+        .ui
+        .builder
+        .get_object::<gtk::Dialog>("join_room_dialog")
+        .expect("Can't find join_room_dialog in ui file.");
+    let cancel = appop
+        .ui
+        .builder
+        .get_object::<gtk::Button>("cancel_join_room")
+        .expect("Can't find cancel_join_room in ui file.");
+    let confirm = appop
+        .ui
+        .builder
+        .get_object::<gtk::Button>("join_room_button")
+        .expect("Can't find join_room_button in ui file.");
+    let entry = appop
+        .ui
+        .builder
+        .get_object::<gtk::Entry>("join_room_name")
+        .expect("Can't find join_room_name in ui file.");
+
+    cancel.connect_clicked(clone!(@strong entry, @strong dialog => move |_| {
+        dialog.hide();
+        entry.set_text("");
+    }));
+    dialog.connect_delete_event(clone!(@strong entry, @strong dialog => move |_, _| {
+        dialog.hide();
+        entry.set_text("");
+        glib::signal::Inhibit(true)
+    }));
+
+    confirm.connect_clicked(
+        clone!(@strong entry, @strong dialog, @strong app_tx => move |_| {
+            dialog.hide();
+            let _ = app_tx.send(Box::new(|op| op.join_to_room()));
+            entry.set_text("");
+        }),
+    );
+
+    entry.connect_activate(clone!(@strong dialog => move |entry| {
+        dialog.hide();
+        let _ = app_tx.send(Box::new(|op| op.join_to_room()));
+        entry.set_text("");
+    }));
+    entry.connect_changed(clone!(@strong confirm => move |entry| {
+            confirm.set_sensitive(entry.get_buffer().get_length() > 0);
+    }));
+}
diff --git a/fractal-gtk/src/appop/connect/language.rs b/fractal-gtk/src/appop/connect/language.rs
new file mode 100644
index 00000000..e7f90ad1
--- /dev/null
+++ b/fractal-gtk/src/appop/connect/language.rs
@@ -0,0 +1,39 @@
+use crate::app::RUNTIME;
+use crate::appop::AppOp;
+use crate::backend::{room, HandleError};
+use glib::object::Cast;
+use gtk::prelude::*;
+
+// The TextBufferExt alias is necessary to avoid conflict with gtk's TextBufferExt
+use gspell::{CheckerExt, TextBuffer, TextBufferExt as GspellTextBufferExt};
+
+pub fn connect(appop: &AppOp) {
+    let app_tx = appop.app_tx.clone();
+    let textview = appop.ui.sventry.view.upcast_ref::<gtk::TextView>();
+    if let Some(checker) = textview
+        .get_buffer()
+        .and_then(|gtk_buffer| TextBuffer::get_from_gtk_text_buffer(&gtk_buffer))
+        .and_then(|gs_buffer| gs_buffer.get_spell_checker())
+    {
+        let _signal_handler = checker.connect_property_language_notify(move |checker| {
+                let _ = app_tx.send(Box::new(clone!(@weak checker => move |op| {
+                    if let Some(lang_code) = checker
+                        .get_language()
+                        .and_then(|lang| lang.get_code())
+                        .map(String::from)
+                    {
+                            if let (Some(active_room), Some(login_data)) = (op.active_room.clone(), 
op.login_data.as_ref()) {
+                                let session_client = login_data.session_client.clone();
+                                let uid = login_data.uid.clone();
+                                RUNTIME.spawn(async move {
+                                    let query = room::set_language(session_client, &uid, &active_room, 
lang_code).await;
+                                    if let Err(err) = query {
+                                        err.handle_error();
+                                    }
+                                });
+                            }
+                    }
+                })));
+            });
+    }
+}
diff --git a/fractal-gtk/src/appop/connect/leave_room.rs b/fractal-gtk/src/appop/connect/leave_room.rs
new file mode 100644
index 00000000..ae528b74
--- /dev/null
+++ b/fractal-gtk/src/appop/connect/leave_room.rs
@@ -0,0 +1,36 @@
+use glib::clone;
+use gtk::prelude::*;
+
+use crate::appop::AppOp;
+
+pub fn connect(appop: &AppOp) {
+    let app_tx = appop.app_tx.clone();
+    let dialog = appop
+        .ui
+        .builder
+        .get_object::<gtk::Dialog>("leave_room_dialog")
+        .expect("Can't find leave_room_dialog in ui file.");
+    let cancel = appop
+        .ui
+        .builder
+        .get_object::<gtk::Button>("leave_room_cancel")
+        .expect("Can't find leave_room_cancel in ui file.");
+    let confirm = appop
+        .ui
+        .builder
+        .get_object::<gtk::Button>("leave_room_confirm")
+        .expect("Can't find leave_room_confirm in ui file.");
+
+    cancel.connect_clicked(clone!(@strong dialog => move |_| {
+        dialog.hide();
+    }));
+    dialog.connect_delete_event(clone!(@strong dialog => move |_, _| {
+        dialog.hide();
+        glib::signal::Inhibit(true)
+    }));
+
+    confirm.connect_clicked(clone!(@strong dialog => move |_| {
+        dialog.hide();
+        let _ = app_tx.send(Box::new(|op| op.really_leave_active_room()));
+    }));
+}
diff --git a/fractal-gtk/src/appop/connect/markdown.rs b/fractal-gtk/src/appop/connect/markdown.rs
new file mode 100644
index 00000000..dbcbd9fd
--- /dev/null
+++ b/fractal-gtk/src/appop/connect/markdown.rs
@@ -0,0 +1,87 @@
+use glib::clone;
+use gtk::prelude::*;
+use sourceview4::prelude::*;
+
+use crate::util;
+
+use crate::appop::AppOp;
+
+pub fn connect(appop: &AppOp) {
+    let app_tx = appop.app_tx.clone();
+    let md_popover_btn = &appop.ui.sventry.markdown;
+    let md_img = appop.ui.sventry.markdown_img.clone();
+    let buffer = appop.ui.sventry.buffer.clone();
+
+    let popover: gtk::Popover = appop
+        .ui
+        .builder
+        .get_object("markdown_popover")
+        .expect("Couldn't find markdown_popover in ui file.");
+
+    let markdown_switch: gtk::Switch = appop
+        .ui
+        .builder
+        .get_object("markdown_switch")
+        .expect("Couldn't find markdown_switch in ui file.");
+
+    let txt: gtk::Grid = appop
+        .ui
+        .builder
+        .get_object("tutorial_text_box")
+        .expect("Couldn't find tutorial_text_box in ui file.");
+
+    let md_lang =
+        sourceview4::LanguageManager::get_default().and_then(|lm| lm.get_language("markdown"));
+
+    md_popover_btn.set_popover(Some(&popover));
+
+    let md_active = util::get_markdown_schema();
+    if md_active {
+        let _ = app_tx.send(Box::new(|op| {
+            op.md_enabled = true;
+        }));
+        markdown_switch.set_active(true);
+        md_img.set_from_icon_name(Some("format-indent-more-symbolic"), gtk::IconSize::Menu);
+        txt.get_style_context().remove_class("dim-label");
+
+        if let Some(md_lang) = md_lang.clone() {
+            buffer.set_highlight_matching_brackets(true);
+            buffer.set_language(Some(&md_lang));
+            buffer.set_highlight_syntax(true);
+        }
+    }
+
+    markdown_switch.connect_property_active_notify(clone!(@strong markdown_switch => move |_| {
+        let md_active = markdown_switch.get_active();
+        let _ = app_tx.send(Box::new(move |op| {
+            op.md_enabled = md_active;
+        }));
+
+        if markdown_switch.get_active() {
+            md_img.set_from_icon_name(
+                Some("format-indent-more-symbolic"),
+                gtk::IconSize::Menu,
+            );
+            txt.get_style_context().remove_class("dim-label");
+            util::set_markdown_schema(true);
+
+            if let Some(md_lang) = md_lang.clone() {
+                buffer.set_highlight_matching_brackets(true);
+                buffer.set_language(Some(&md_lang));
+                buffer.set_highlight_syntax(true);
+            }
+        } else {
+            md_img.set_from_icon_name(
+                Some("format-justify-left-symbolic"),
+                gtk::IconSize::Menu,
+            );
+            txt.get_style_context().add_class("dim-label");
+            util::set_markdown_schema(false);
+
+            let lang: Option<&sourceview4::Language> = None;
+            buffer.set_highlight_matching_brackets(false);
+            buffer.set_language(lang);
+            buffer.set_highlight_syntax(false);
+        }
+    }));
+}
diff --git a/fractal-gtk/src/appop/connect/mod.rs b/fractal-gtk/src/appop/connect/mod.rs
new file mode 100644
index 00000000..144d1c3a
--- /dev/null
+++ b/fractal-gtk/src/appop/connect/mod.rs
@@ -0,0 +1,36 @@
+mod account;
+mod autocomplete;
+mod direct;
+mod directory;
+mod headerbar;
+mod invite;
+mod join_room;
+mod language;
+mod leave_room;
+mod markdown;
+mod new_room;
+mod roomlist_search;
+mod send;
+mod swipeable_widgets;
+
+use crate::appop::AppOp;
+
+impl AppOp {
+    pub fn connect_gtk(&self) {
+        headerbar::connect(self);
+        send::connect(self);
+        markdown::connect(self);
+        autocomplete::connect(self);
+        language::connect(self);
+        directory::connect(self);
+        leave_room::connect(self);
+        new_room::connect(self);
+        join_room::connect(self);
+        account::connect(self);
+        invite::connect_dialog(self);
+        invite::connect_user(self);
+        direct::connect(self);
+        roomlist_search::connect(self);
+        swipeable_widgets::connect(self);
+    }
+}
diff --git a/fractal-gtk/src/appop/connect/new_room.rs b/fractal-gtk/src/appop/connect/new_room.rs
new file mode 100644
index 00000000..aff74984
--- /dev/null
+++ b/fractal-gtk/src/appop/connect/new_room.rs
@@ -0,0 +1,69 @@
+use glib::clone;
+use gtk::prelude::*;
+
+use crate::appop::AppOp;
+
+pub fn connect(appop: &AppOp) {
+    let app_tx = appop.app_tx.clone();
+    let dialog = appop
+        .ui
+        .builder
+        .get_object::<gtk::Dialog>("new_room_dialog")
+        .expect("Can't find new_room_dialog in ui file.");
+    let cancel = appop
+        .ui
+        .builder
+        .get_object::<gtk::Button>("cancel_new_room")
+        .expect("Can't find cancel_new_room in ui file.");
+    let confirm = appop
+        .ui
+        .builder
+        .get_object::<gtk::Button>("new_room_button")
+        .expect("Can't find new_room_button in ui file.");
+    let entry = appop
+        .ui
+        .builder
+        .get_object::<gtk::Entry>("new_room_name")
+        .expect("Can't find new_room_name in ui file.");
+    let private = appop
+        .ui
+        .builder
+        .get_object::<gtk::ToggleButton>("private_visibility_button")
+        .expect("Can't find private_visibility_button in ui file.");
+
+    private.set_active(true);
+    cancel.connect_clicked(
+        clone!(@strong entry, @strong dialog, @strong private => move |_| {
+            dialog.hide();
+            entry.set_text("");
+            private.set_active(true);
+        }),
+    );
+    dialog.connect_delete_event(
+        clone!(@strong entry, @strong dialog, @strong private => move |_, _| {
+            dialog.hide();
+            entry.set_text("");
+            private.set_active(true);
+            glib::signal::Inhibit(true)
+        }),
+    );
+
+    confirm.connect_clicked(
+        clone!(@strong entry, @strong dialog, @strong private, @strong app_tx => move |_| {
+            dialog.hide();
+            let _ = app_tx.send(Box::new(|op| op.create_new_room()));
+            entry.set_text("");
+            private.set_active(true);
+        }),
+    );
+
+    entry.connect_activate(clone!(@strong dialog => move |entry| {
+        dialog.hide();
+        let _ = app_tx.send(Box::new(|op| op.create_new_room()));
+        entry.set_text("");
+        private.set_active(true);
+    }));
+    entry.connect_changed(clone!(@strong confirm => move |entry| {
+            confirm.set_sensitive(entry.get_buffer().get_length() > 0);
+    }));
+}
diff --git a/fractal-gtk/src/appop/connect/roomlist_search.rs 
b/fractal-gtk/src/appop/connect/roomlist_search.rs
new file mode 100644
index 00000000..774eecc4
--- /dev/null
+++ b/fractal-gtk/src/appop/connect/roomlist_search.rs
@@ -0,0 +1,60 @@
+use glib::clone;
+use gtk::prelude::*;
+
+use crate::appop::AppOp;
+
+pub fn connect(appop: &AppOp) {
+    let app_tx = appop.app_tx.clone();
+    let search_btn = appop
+        .ui
+        .builder
+        .get_object::<gtk::ToggleButton>("room_search_button")
+        .expect("Can't find room_search_button in ui file.");
+    let search_bar = appop
+        .ui
+        .builder
+        .get_object::<gtk::SearchBar>("room_list_searchbar")
+        .expect("Can't find room_list_searchbar in ui file.");
+    let search_entry = appop
+        .ui
+        .builder
+        .get_object::<gtk::SearchEntry>("room_list_search")
+        .expect("Can't find room_list_search in ui file.");
+
+    search_btn.connect_toggled(clone!(@strong search_bar => move |btn| {
+        search_bar.set_search_mode(btn.get_active());
+    }));
+
+    search_bar.connect_property_search_mode_enabled_notify(
+        clone!(@strong search_btn => move |headerbar| {
+            search_btn.set_active(headerbar.get_search_mode());
+        }),
+    );
+
+    search_entry.connect_search_changed(move |entry| {
+        let search_text = Some(entry.get_text().to_string());
+        let _ = app_tx.send(Box::new(|op| op.filter_rooms(search_text)));
+    });
+
+    // hidding left and right boxes to align with top buttons
+    let boxes = search_bar.get_children()[0]
+            .clone()
+            .downcast::<gtk::Revealer>()
+            .unwrap() // revealer
+            .get_children()[0]
+        .clone()
+        .downcast::<gtk::Box>()
+        .unwrap(); // box
+    boxes.get_children()[0]
+        .clone()
+        .downcast::<gtk::Box>()
+        .unwrap()
+        .hide();
+    boxes.get_children()[1].clone().set_hexpand(true);
+    boxes.get_children()[1].clone().set_halign(gtk::Align::Fill);
+    boxes.get_children()[2]
+        .clone()
+        .downcast::<gtk::Box>()
+        .unwrap()
+        .hide();
+}
diff --git a/fractal-gtk/src/appop/connect/send.rs b/fractal-gtk/src/appop/connect/send.rs
new file mode 100644
index 00000000..2682a4f0
--- /dev/null
+++ b/fractal-gtk/src/appop/connect/send.rs
@@ -0,0 +1,69 @@
+use glib::clone;
+use gtk::prelude::*;
+use sourceview4::BufferExt;
+
+use crate::actions::activate_action;
+use crate::appop::AppOp;
+
+const MAX_INPUT_HEIGHT: i32 = 100;
+
+pub fn connect(appop: &AppOp) {
+    let app_tx = appop.app_tx.clone();
+    appop.ui.sventry.container.set_redraw_on_allocate(true);
+    let msg_entry = appop.ui.sventry.view.clone();
+    let buffer = &appop.ui.sventry.buffer;
+    buffer.set_highlight_matching_brackets(false);
+
+    let msg_entry_box = appop.ui.sventry.entry_box.clone();
+    msg_entry_box.set_redraw_on_allocate(true);
+
+    if let Some(adjustment) = appop.ui.sventry.scroll.get_vadjustment() {
+        adjustment.connect_value_changed(clone!(@strong msg_entry => move |adj| {
+            if msg_entry.get_allocated_height() < MAX_INPUT_HEIGHT {
+                adj.set_value(0.0);
+            }
+        }));
+    }
+
+    let autocomplete_popover = appop
+        .ui
+        .builder
+        .get_object::<gtk::Popover>("autocomplete_popover")
+        .expect("Can't find autocomplete_popover in ui file.");
+
+    msg_entry.connect_key_press_event(
+        clone!(@strong app_tx => move |_, key| match key.get_keyval() {
+            gdk::keys::constants::Return | gdk::keys::constants::KP_Enter
+                if !key.get_state().contains(gdk::ModifierType::SHIFT_MASK)
+                    && !autocomplete_popover.is_visible() =>
+            {
+                activate_action(&app_tx, "app", "send-message");
+                Inhibit(true)
+            }
+            _ => Inhibit(false),
+        }),
+    );
+
+    msg_entry.connect_key_release_event(clone!(@strong app_tx => move |_, ev| {
+        if ev.get_keyval().to_unicode().is_some() {
+            let _ = app_tx.send(Box::new(|op| op.send_typing()));
+        }
+        Inhibit(false)
+    }));
+
+    msg_entry.connect_paste_clipboard(move |_| {
+        let _ = app_tx.send(Box::new(|op| op.paste()));
+    });
+
+    msg_entry.connect_focus_in_event(clone!(@strong msg_entry_box => move |_, _| {
+        msg_entry_box.get_style_context().add_class("message-input-focused");
+
+        Inhibit(false)
+    }));
+
+    msg_entry.connect_focus_out_event(clone!(@strong msg_entry_box => move |_, _| {
+        msg_entry_box.get_style_context().remove_class("message-input-focused");
+
+        Inhibit(false)
+    }));
+}
diff --git a/fractal-gtk/src/appop/connect/swipeable_widgets.rs 
b/fractal-gtk/src/appop/connect/swipeable_widgets.rs
new file mode 100644
index 00000000..16315db8
--- /dev/null
+++ b/fractal-gtk/src/appop/connect/swipeable_widgets.rs
@@ -0,0 +1,63 @@
+use gio::prelude::*;
+use gtk::prelude::*;
+use libhandy::prelude::*;
+
+use crate::appop::AppOp;
+
+// Set up HdyDeck and HdyLeaflet so that swipes trigger the
+// same behaviour as the back button.
+pub fn connect(appop: &AppOp) {
+    let deck: libhandy::Deck = appop
+        .ui
+        .builder
+        .get_object("main_deck")
+        .expect("Can't find main_deck in UI file");
+    let leaflet: libhandy::Leaflet = appop
+        .ui
+        .builder
+        .get_object("chat_page")
+        .expect("Can't find chat_page in UI file");
+
+    let app = gio::Application::get_default()
+        .expect("Could not get default application")
+        .downcast::<gtk::Application>()
+        .unwrap();
+    let global_back = app
+        .lookup_action("back")
+        .expect("Could not get back action");
+
+    deck.connect_property_transition_running_notify(
+        clone!(@weak app, @weak global_back => move |deck| {
+            let child: Option<String> = deck.get_visible_child_name().map(|g| g.to_string());
+            if !deck.get_transition_running() && child == Some("chat".to_string()) {
+                // Re-enable global back when returning to main view
+                let _ = global_back.set_property("enabled", &true);
+                app.activate_action("back", None);
+            }
+        }),
+    );
+
+    deck.connect_property_visible_child_notify(
+        clone!(@weak app, @weak global_back => move |deck| {
+            let child: Option<String> = deck.get_visible_child_name().map(|g| g.to_string());
+            if !deck.get_transition_running() && child == Some("chat".to_string()) {
+                let _ = global_back.set_property("enabled", &true);
+                app.activate_action("back", None);
+            }
+        }),
+    );
+
+    leaflet.connect_property_child_transition_running_notify(clone!(@weak app => move |leaflet| {
+        let child: Option<String> = leaflet.get_visible_child_name().map(|g| g.to_string());
+        if !leaflet.get_child_transition_running() && child == Some("sidebar".to_string()) {
+            app.activate_action("back", None);
+        }
+    }));
+
+    leaflet.connect_property_visible_child_notify(clone!(@weak app => move |leaflet| {
+        let child: Option<String> = deck.get_visible_child_name().map(|g| g.to_string());
+        if !leaflet.get_child_transition_running() && child == Some("sidebar".to_string()) {
+            app.activate_action("back", None);
+        }
+    }));
+}
diff --git a/fractal-gtk/src/appop/mod.rs b/fractal-gtk/src/appop/mod.rs
index 8d00827a..46e66c78 100644
--- a/fractal-gtk/src/appop/mod.rs
+++ b/fractal-gtk/src/appop/mod.rs
@@ -30,6 +30,7 @@ use crate::widgets;
 mod about;
 mod account;
 pub mod attach;
+mod connect;
 mod directory;
 mod invite;
 mod login;
diff --git a/fractal-gtk/src/meson.build b/fractal-gtk/src/meson.build
index 994cf799..1ad69ed6 100644
--- a/fractal-gtk/src/meson.build
+++ b/fractal-gtk/src/meson.build
@@ -52,23 +52,23 @@ app_sources = files(
   'api/identity.rs',
   'api/mod.rs',
   'api/r0.rs',
-  'app/connect/account.rs',
-  'app/connect/autocomplete.rs',
-  'app/connect/directory.rs',
-  'app/connect/direct.rs',
-  'app/connect/headerbar.rs',
-  'app/connect/invite.rs',
-  'app/connect/join_room.rs',
-  'app/connect/language.rs',
-  'app/connect/leave_room.rs',
-  'app/connect/markdown.rs',
-  'app/connect/mod.rs',
-  'app/connect/new_room.rs',
-  'app/connect/roomlist_search.rs',
-  'app/connect/send.rs',
-  'app/connect/swipeable_widgets.rs',
   'app/mod.rs',
   'app/windowstate.rs',
+  'appop/connect/account.rs',
+  'appop/connect/autocomplete.rs',
+  'appop/connect/directory.rs',
+  'appop/connect/direct.rs',
+  'appop/connect/headerbar.rs',
+  'appop/connect/invite.rs',
+  'appop/connect/join_room.rs',
+  'appop/connect/language.rs',
+  'appop/connect/leave_room.rs',
+  'appop/connect/markdown.rs',
+  'appop/connect/mod.rs',
+  'appop/connect/new_room.rs',
+  'appop/connect/roomlist_search.rs',
+  'appop/connect/send.rs',
+  'appop/connect/swipeable_widgets.rs',
   'appop/about.rs',
   'appop/account.rs',
   'appop/attach.rs',
diff --git a/fractal-gtk/src/widgets/autocomplete.rs b/fractal-gtk/src/widgets/autocomplete.rs
index ca713824..7244e314 100644
--- a/fractal-gtk/src/widgets/autocomplete.rs
+++ b/fractal-gtk/src/widgets/autocomplete.rs
@@ -10,6 +10,7 @@ use gtk::TextTag;
 
 use crate::model::member::Member;
 
+use crate::app;
 use crate::appop::AppOp;
 use crate::widgets;
 
@@ -27,7 +28,6 @@ pub struct Autocomplete {
 
 impl Autocomplete {
     pub fn new(
-        op: Arc<Mutex<AppOp>>,
         window: gtk::Window,
         msg_entry: sourceview4::View,
         popover: gtk::Popover,
@@ -42,7 +42,7 @@ impl Autocomplete {
             popover_position: None,
             popover_search: None,
             popover_closing: false,
-            op,
+            op: app::get_op().clone(),
         }
     }
 


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]