Commit 2c9557f5 authored by Daniel Garcia Moreno's avatar Daniel Garcia Moreno

Merge branch 'invite-dialog' into 'master'

Redesign of the invite dialog

See merge request World/fractal!242
parents 4e110716 ec9ea6ee
Pipeline #34529 passed with stages
in 45 minutes and 32 seconds
......@@ -42,11 +42,31 @@
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkListBox" id="to_chat">
<object class="GtkBox" id="to_chat_entry_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hscrollbar_policy">external</property>
<property name="vscrollbar_policy">never</property>
<child>
<object class="GtkTextView" id="to_chat_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<style>
<class name="invite-selected"/>
<class name="message-input"/>
</style>
</object>
<packing>
......@@ -55,19 +75,6 @@
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="to_chat_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<property name="placeholder_text" translatable="yes">Matrix username, email or phone number</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="direct_chat_search_scroll">
<property name="height_request">150</property>
......@@ -94,7 +101,7 @@
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
<property name="position">1</property>
</packing>
</child>
</object>
......
......@@ -42,11 +42,31 @@
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkListBox" id="to_invite">
<object class="GtkBox" id="invite_entry_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hscrollbar_policy">external</property>
<property name="vscrollbar_policy">never</property>
<child>
<object class="GtkTextView" id="invite_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<style>
<class name="invite-selected"/>
<class name="message-input"/>
</style>
</object>
<packing>
......@@ -55,19 +75,6 @@
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="invite_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<property name="placeholder_text" translatable="yes">Matrix username, email or phone number</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="user_search_scroll">
<property name="height_request">150</property>
......@@ -94,7 +101,7 @@
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
<property name="position">1</property>
</packing>
</child>
</object>
......
extern crate gdk;
extern crate gtk;
use self::gtk::prelude::*;
......@@ -16,17 +17,35 @@ impl App {
let invite = self.ui.builder
.get_object::<gtk::Button>("direct_chat_button")
.expect("Can't find direct_chat_button in ui file.");
let entry = self.ui.builder
.get_object::<gtk::Entry>("to_chat_entry")
let to_chat_entry_box = self.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 = self.ui.builder
.get_object::<gtk::TextView>("to_chat_entry")
.expect("Can't find to_chat_entry in ui file.");
let dialog = self.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));
entry.connect_key_release_event(clone!(op => move |entry, _| {
to_chat_entry.connect_key_release_event(clone!(op => move |entry, _| {
{
let mut id = source_id.lock().unwrap();
if let Some(sid) = id.take() {
......@@ -35,7 +54,15 @@ impl App {
}
let sid = gtk::timeout_add(500, clone!(op, entry, source_id => move || {
op.lock().unwrap().search_invite_user(entry.get_text());
if let Some(buffer) = entry.get_buffer() {
let start = buffer.get_start_iter();
let end = buffer.get_end_iter();
let text = buffer.get_text(&start, &end, false);
op.lock().unwrap().search_invite_user(text);
}
*(source_id.lock().unwrap()) = None;
gtk::Continue(false)
}));
......@@ -44,6 +71,35 @@ impl App {
glib::signal::Inhibit(false)
}));
to_chat_entry.connect_focus_in_event(clone!(op, to_chat_entry_box => move |_, _| {
if let Some(style) = to_chat_entry_box.get_style_context() {
style.add_class("message-input-focused");
}
op.lock().unwrap().remove_invite_user_dialog_placeholder();
Inhibit(false)
}));
to_chat_entry.connect_focus_out_event(clone!(op, to_chat_entry_box => move |_, _| {
if let Some(style) = to_chat_entry_box.get_style_context() {
style.remove_class("message-input-focused");
}
op.lock().unwrap().set_invite_user_dialog_placeholder();
Inhibit(false)
}));
if let Some(buffer) = to_chat_entry.get_buffer() {
buffer.connect_delete_range(clone!( op => move |_, _, _| {
gtk::idle_add(clone!(op => move || {
op.lock().unwrap().detect_removed_invite();
Continue(false)
}));
}));
}
dialog.connect_delete_event(clone!(op => move |_, _| {
op.lock().unwrap().close_direct_chat_dialog();
glib::signal::Inhibit(true)
......
extern crate gdk;
extern crate gtk;
use self::gtk::prelude::*;
......@@ -44,17 +45,35 @@ impl App {
let invite = self.ui.builder
.get_object::<gtk::Button>("invite_button")
.expect("Can't find invite_button in ui file.");
let entry = self.ui.builder
.get_object::<gtk::Entry>("invite_entry")
let invite_entry_box = self.ui.builder
.get_object::<gtk::Box>("invite_entry_box")
.expect("Can't find invite_entry_box in ui file.");
let invite_entry = self.ui.builder
.get_object::<gtk::TextView>("invite_entry")
.expect("Can't find invite_entry in ui file.");
let dialog = self.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));
entry.connect_key_release_event(clone!(op => move |entry, _| {
invite_entry.connect_key_release_event(clone!(op => move |entry, _| {
{
let mut id = source_id.lock().unwrap();
if let Some(sid) = id.take() {
......@@ -63,7 +82,15 @@ impl App {
}
let sid = gtk::timeout_add(500, clone!(op, entry, source_id => move || {
op.lock().unwrap().search_invite_user(entry.get_text());
if let Some(buffer) = entry.get_buffer() {
let start = buffer.get_start_iter();
let end = buffer.get_end_iter();
let text = buffer.get_text(&start, &end, false);
op.lock().unwrap().search_invite_user(text);
}
*(source_id.lock().unwrap()) = None;
gtk::Continue(false)
}));
......@@ -72,6 +99,35 @@ impl App {
glib::signal::Inhibit(false)
}));
invite_entry.connect_focus_in_event(clone!(op, invite_entry_box => move |_, _| {
if let Some(style) = invite_entry_box.get_style_context() {
style.add_class("message-input-focused");
}
op.lock().unwrap().remove_invite_user_dialog_placeholder();
Inhibit(false)
}));
invite_entry.connect_focus_out_event(clone!(op, invite_entry_box => move |_, _| {
if let Some(style) = invite_entry_box.get_style_context() {
style.remove_class("message-input-focused");
}
op.lock().unwrap().set_invite_user_dialog_placeholder();
Inhibit(false)
}));
if let Some(buffer) = invite_entry.get_buffer() {
buffer.connect_delete_range(clone!( op => move |_, _, _| {
gtk::idle_add(clone!(op => move || {
op.lock().unwrap().detect_removed_invite();
Continue(false)
}));
}));
}
dialog.connect_delete_event(clone!(op => move |_, _| {
op.lock().unwrap().close_invite_dialog();
glib::signal::Inhibit(true)
......
......@@ -10,6 +10,8 @@ use appop::member::SearchType;
use app::InternalCommand;
use backend::BKCommand;
use globals;
use widgets;
use types::Member;
......@@ -18,28 +20,30 @@ use types::Room;
impl AppOp {
pub fn add_to_invite(&mut self, u: Member) {
let listboxid = match self.search_type {
SearchType::Invite => "to_invite",
SearchType::DirectChat => "to_chat",
};
let to_invite = self.ui.builder
.get_object::<gtk::ListBox>(listboxid)
.expect("Can't find to_invite in ui file.");
if self.invite_list.contains(&u) {
if self.invite_list.iter().any(|(mem, _)| *mem == u) {
return;
}
let textviewid = match self.search_type {
SearchType::Invite => "invite_entry",
SearchType::DirectChat => "to_chat_entry",
};
let invite_entry = self.ui.builder
.get_object::<gtk::TextView>(textviewid)
.expect("Can't find invite_entry in ui file.");
if let SearchType::DirectChat = self.search_type {
self.invite_list = vec![];
for ch in to_invite.get_children().iter() {
to_invite.remove(ch);
if let Some(buffer) = invite_entry.get_buffer() {
let mut start = buffer.get_start_iter();
let mut end = buffer.get_end_iter();
buffer.delete(&mut start, &mut end);
}
}
self.invite_list.push(u.clone());
self.ui.builder
.get_object::<gtk::Button>("direct_chat_button")
.map(|btn| btn.set_sensitive(true));
......@@ -48,62 +52,37 @@ impl AppOp {
.get_object::<gtk::Button>("invite_button")
.map(|btn| btn.set_sensitive(true));
let w;
{
let mb = widgets::MemberBox::new(&u, &self);
w = mb.widget(true);
}
if let Some(buffer) = invite_entry.get_buffer() {
let mut start_word = buffer.get_iter_at_offset(buffer.get_property_cursor_position());
let mut end_word = buffer.get_iter_at_offset(buffer.get_property_cursor_position());
let mbox;
mbox = gtk::Box::new(gtk::Orientation::Horizontal, 0);
let btn = gtk::Button::new();
let img = gtk::Image::new_from_icon_name("window-close-symbolic", 2);
btn.get_style_context().unwrap().add_class("circular");
btn.set_image(&img);
// Remove the search input in the entry before inserting the member's pill
if !start_word.starts_word() {
start_word.backward_word_start();
}
if !end_word.ends_word() {
end_word.forward_word_end();
}
buffer.delete(&mut start_word, &mut end_word);
mbox.pack_start(&w, true, true, 0);
mbox.pack_start(&btn, false, false, 0);
mbox.show_all();
if let Some(anchor) = buffer.create_child_anchor(&mut end_word) {
let w;
{
let mb = widgets::MemberBox::new(&u, &self);
w = mb.pill();
}
let tx = self.internal.clone();
let uid = u.uid.clone();
btn.connect_clicked(move |_| {
tx.send(InternalCommand::RmInvite(uid.clone())).unwrap();
});
invite_entry.add_child_at_anchor(&w, &anchor);
let size = (self.invite_list.len() - 1) as i32;
to_invite.insert(&mbox, size);
self.invite_list.push((u.clone(), anchor));
}
}
}
pub fn rm_from_invite(&mut self, uid: String) {
let invid;
let dialogid;
match self.search_type {
SearchType::Invite => {
invid = "to_invite";
dialogid = "invite_user_dialog";
}
SearchType::DirectChat => {
invid = "to_chat";
dialogid = "direct_chat_dialog";
}
};
let to_invite = self.ui.builder
.get_object::<gtk::ListBox>(invid)
.expect("Can’t find to_invite in ui file.");
let dialog = self.ui.builder
.get_object::<gtk::Dialog>(dialogid)
.expect("Can’t find invite_user_dialog in ui file.");
let idx = self.invite_list.iter().position(|x| x.uid == uid);
let idx = self.invite_list.iter().position(|x| x.0.uid == uid);
if let Some(i) = idx {
self.invite_list.remove(i);
if let Some(r) = to_invite.get_row_at_index(i as i32) {
to_invite.remove(&r);
}
}
if self.invite_list.is_empty() {
......@@ -116,9 +95,33 @@ impl AppOp {
.map(|btn| btn.set_sensitive(false));
}
let dialogid = match self.search_type {
SearchType::Invite => {
"invite_user_dialog"
}
SearchType::DirectChat => {
"direct_chat_dialog"
}
};
let dialog = self.ui.builder
.get_object::<gtk::Dialog>(dialogid)
.expect("Can’t find invite_user_dialog in ui file.");
dialog.resize(300, 200);
}
pub fn detect_removed_invite(&self) {
for (member, anchor) in self.invite_list.clone() {
if anchor.get_deleted() {
let tx = self.internal.clone();
let uid = member.uid.clone();
tx.send(InternalCommand::RmInvite(uid)).unwrap();
}
}
}
pub fn show_invite_user_dialog(&mut self) {
let dialog = self.ui.builder
.get_object::<gtk::Dialog>("invite_user_dialog")
......@@ -140,6 +143,9 @@ impl AppOp {
}
}
}
self.set_invite_user_dialog_placeholder();
dialog.present();
scroll.hide();
}
......@@ -147,7 +153,7 @@ impl AppOp {
pub fn invite(&mut self) {
if let &Some(ref r) = &self.active_room {
for user in &self.invite_list {
self.backend.send(BKCommand::Invite(r.clone(), user.uid.clone())).unwrap();
self.backend.send(BKCommand::Invite(r.clone(), user.0.uid.clone())).unwrap();
}
}
self.close_invite_dialog();
......@@ -160,25 +166,24 @@ impl AppOp {
let scroll = self.ui.builder
.get_object::<gtk::Widget>("user_search_scroll")
.expect("Can't find user_search_scroll in ui file.");
let to_invite = self.ui.builder
.get_object::<gtk::ListBox>("to_invite")
.expect("Can't find to_invite in ui file.");
let entry = self.ui.builder
.get_object::<gtk::Entry>("invite_entry")
let invite_entry = self.ui.builder
.get_object::<gtk::TextView>("invite_entry")
.expect("Can't find invite_entry in ui file.");
let dialog = self.ui.builder
.get_object::<gtk::Dialog>("invite_user_dialog")
.expect("Can't find invite_user_dialog in ui file.");
self.invite_list = vec![];
for ch in to_invite.get_children().iter() {
to_invite.remove(ch);
if let Some(buffer) = invite_entry.get_buffer() {
let mut start = buffer.get_start_iter();
let mut end = buffer.get_end_iter();
buffer.delete(&mut start, &mut end);
}
for ch in listbox.get_children().iter() {
listbox.remove(ch);
}
scroll.hide();
entry.set_text("");
dialog.hide();
dialog.resize(300, 200);
}
......@@ -223,4 +228,58 @@ impl AppOp {
self.invitation_roomid = Some(r.id.clone());
dialog.present();
}
pub fn set_invite_user_dialog_placeholder(&mut self) {
let textviewid = match self.search_type {
SearchType::Invite => "invite_entry",
SearchType::DirectChat => "to_chat_entry",
};
let invite_entry = self.ui.builder
.get_object::<gtk::TextView>(textviewid)
.expect("Can't find invite_entry in ui file.");
if let Some(buffer) = invite_entry.get_buffer() {
let start = buffer.get_start_iter();
let end = buffer.get_end_iter();
if let Some(text) = buffer.get_text(&start, &end, true) {
if text.is_empty() && self.invite_list.is_empty() {
buffer.set_text(globals::PLACEHOLDER_TEXT);
let start = buffer.get_start_iter();
let end = buffer.get_end_iter();
buffer.apply_tag_by_name("placeholder", &start, &end);
}
}
}
}
pub fn remove_invite_user_dialog_placeholder(&mut self) {
let textviewid = match self.search_type {
SearchType::Invite => "invite_entry",
SearchType::DirectChat => "to_chat_entry",
};
let invite_entry = self.ui.builder
.get_object::<gtk::TextView>(textviewid)
.expect("Can't find invite_entry in ui file.");
if let Some(buffer) = invite_entry.get_buffer() {
let mut start = buffer.get_start_iter();
let mut end = buffer.get_end_iter();
if let Some(text) = buffer.get_text(&start, &end, true) {
if text == globals::PLACEHOLDER_TEXT && self.invite_list.is_empty() {
buffer.set_text("");
let start = buffer.get_start_iter();
let end = buffer.get_end_iter();
buffer.remove_tag_by_name("placeholder", &start, &end);
}
}
}
}
}
......@@ -77,7 +77,7 @@ impl AppOp {
match self.search_type {
SearchType::Invite => {
let entry = self.ui.builder
.get_object::<gtk::Entry>("invite_entry")
.get_object::<gtk::TextView>("invite_entry")
.expect("Can't find invite_entry in ui file.");
let listbox = self.ui.builder
.get_object::<gtk::ListBox>("user_search_box")
......@@ -85,11 +85,17 @@ impl AppOp {
let scroll = self.ui.builder
.get_object::<gtk::Widget>("user_search_scroll")
.expect("Can't find user_search_scroll in ui file.");
self.search_finished(users, listbox, scroll, entry.get_text());
if let Some(buffer) = entry.get_buffer() {
let start = buffer.get_start_iter();
let end = buffer.get_end_iter();
self.search_finished(users, listbox, scroll, buffer.get_text(&start, &end, false));
}
},
SearchType::DirectChat => {
let entry = self.ui.builder
.get_object::<gtk::Entry>("to_chat_entry")
.get_object::<gtk::TextView>("to_chat_entry")
.expect("Can't find to_chat_entry in ui file.");
let listbox = self.ui.builder
.get_object::<gtk::ListBox>("direct_chat_search_box")
......@@ -97,7 +103,13 @@ impl AppOp {
let scroll = self.ui.builder
.get_object::<gtk::Widget>("direct_chat_search_scroll")
.expect("Can't find direct_chat_search_scroll in ui file.");
self.search_finished(users, listbox, scroll, entry.get_text());
if let Some(buffer) = entry.get_buffer() {
let start = buffer.get_start_iter();
let end = buffer.get_end_iter();
self.search_finished(users, listbox, scroll, buffer.get_text(&start, &end, false));
}
}
}
}
......
......@@ -91,7 +91,7 @@ pub struct AppOp {
pub invitation_roomid: Option<String>,
pub md_enabled: bool,
invite_list: Vec<Member>,
pub invite_list: Vec<(Member, gtk::TextChildAnchor)>,
search_type: SearchType,
pub stickers: Vec<StickerGroup>,
......
......@@ -22,10 +22,10 @@ impl AppOp {
let user = self.invite_list[0].clone();
let internal_id: String = thread_rng().sample_iter(&Alphanumeric).take(10).collect();
self.backend.send(BKCommand::DirectChat(user.clone(), internal_id.clone())).unwrap();
self.backend.send(BKCommand::DirectChat(user.0.clone(), internal_id.clone())).unwrap();
self.close_direct_chat_dialog();
let mut fakeroom = Room::new(internal_id.clone(), user.alias.clone());
let mut fakeroom = Room::new(internal_id.clone(), user.0.alias.clone());
fakeroom.direct = true;
self.new_room(fakeroom, None);
......@@ -56,25 +56,30 @@ impl AppOp {
let scroll = self.ui.builder
.get_object::<gtk::Widget>("direct_chat_search_scroll")
.expect("Can't find direct_chat_search_scroll in ui file.");
let to_invite = self.ui.builder
.get_object::<gtk::ListBox>("to_chat")
.expect("Can't find to_chat in ui file.");
let to_chat_entry = self.ui.builder
.get_object::<gtk::TextView>("to_chat_entry")
.expect("Can't find to_chat_entry in ui file.");
let entry = self.ui.builder
.get_object::<gtk::Entry>("to_chat_entry")
.get_object::<gtk::TextView>("to_chat_entry")
.expect("Can't find to_chat_entry in ui file.");
let dialog = self.ui.builder
.get_object::<gtk::Dialog>("direct_chat_dialog")
.expect("Can't find direct_chat_dialog in ui file.");
self.invite_list = vec![];
for ch in to_invite.get_children().iter() {
to_invite.remove(ch);
if let Some(buffer) = to_chat_entry.get_buffer() {
let mut start = buffer.get_start_iter();
let mut end = buffer.get_end_iter();
buffer.delete(&mut start, &mut end);
}
for ch in listbox.get_children().iter() {
listbox.remove(ch);
}
scroll.hide();
entry.set_text("");
if let Some(buffer) = entry.get_buffer() {
buffer.set_text("");
}
dialog.hide();
dialog.resize(300, 200);
}
......
......@@ -2,9 +2,11 @@ pub static INITIAL_MESSAGES: usize = 40;
pub static CACHE_SIZE: usize = 40;
pub static MSG_ICON_SIZE: i32 = 40;
pub static USERLIST_ICON_SIZE: i32 = 30;
pub static PILL_ICON_SIZE: i32 = 18;
pub static MINUTES_TO_SPLIT_MSGS: i64 = 30;