diff --git a/fractal-api/src/backend/mod.rs b/fractal-api/src/backend/mod.rs index f56ca454..966d1e17 100644 --- a/fractal-api/src/backend/mod.rs +++ b/fractal-api/src/backend/mod.rs @@ -120,6 +120,10 @@ impl Backend { let r = user::get_user_info_async(self, &sender, ctx); bkerror!(r, tx, BKResponse::CommandError); } + Ok(BKCommand::UserSearch(term)) => { + let r = user::search(self, term); + bkerror!(r, tx, BKResponse::CommandError); + } // Sync module @@ -206,6 +210,10 @@ impl Backend { let r = room::leave_room(self, roomid); bkerror!(r, tx, BKResponse::RejectInvError); } + Ok(BKCommand::Invite(room, userid)) => { + let r = room::invite(self, room, userid); + bkerror!(r, tx, BKResponse::InviteError); + } // Media module diff --git a/fractal-api/src/backend/room.rs b/fractal-api/src/backend/room.rs index a4bf823c..a04b49ea 100644 --- a/fractal-api/src/backend/room.rs +++ b/fractal-api/src/backend/room.rs @@ -525,3 +525,19 @@ pub fn add_to_fav(bk: &Backend, roomid: String, tofav: bool) -> Result<(), Error Ok(()) } + +pub fn invite(bk: &Backend, roomid: String, userid: String) -> Result<(), Error> { + let url = bk.url(&format!("rooms/{}/invite", roomid), vec![])?; + + let attrs = json!({ + "user_id": userid, + }); + + let tx = bk.tx.clone(); + post!(&url, &attrs, + |_| { }, + |err| { tx.send(BKResponse::InviteError(err)).unwrap(); } + ); + + Ok(()) +} diff --git a/fractal-api/src/backend/types.rs b/fractal-api/src/backend/types.rs index 47689786..f05841d5 100644 --- a/fractal-api/src/backend/types.rs +++ b/fractal-api/src/backend/types.rs @@ -49,6 +49,8 @@ pub enum BKCommand { AddToFav(String, bool), AcceptInv(String), RejectInv(String), + UserSearch(String), + Invite(String, String), } #[derive(Debug)] @@ -85,6 +87,7 @@ pub enum BKResponse { NewRoom(Room), AddedToFav(String, bool), RoomNotifications(String, i32, i32), + UserSearch(Vec), //errors UserNameError(Error), @@ -115,6 +118,7 @@ pub enum BKResponse { AddToFavError(Error), AcceptInvError(Error), RejectInvError(Error), + InviteError(Error), } #[derive(Debug)] diff --git a/fractal-api/src/backend/user.rs b/fractal-api/src/backend/user.rs index e8932395..29f7186b 100644 --- a/fractal-api/src/backend/user.rs +++ b/fractal-api/src/backend/user.rs @@ -143,3 +143,41 @@ pub fn get_avatar_async(bk: &Backend, member: Option, tx: Sender Ok(()) } + +pub fn search(bk: &Backend, term: String) -> Result<(), Error> { + let url = bk.url(&format!("user_directory/search"), vec![])?; + + let attrs = json!({ + "search_term": term, + }); + + let tx = bk.tx.clone(); + post!(&url, &attrs, + |js: JsonValue| { + let mut users: Vec = vec![]; + if let Some(arr) = js["results"].as_array() { + for member in arr.iter() { + let alias = match member["display_name"].as_str() { + None => None, + Some(a) => Some(a.to_string()), + }; + let avatar = match member["avatar_url"].as_str() { + None => None, + Some(a) => Some(a.to_string()), + }; + + users.push(Member{ + alias: alias, + uid: member["user_id"].as_str().unwrap_or_default().to_string(), + avatar: avatar, + }); + } + } + tx.send(BKResponse::UserSearch(users)).unwrap(); + }, + |err| { + tx.send(BKResponse::CommandError(err)).unwrap(); } + ); + + Ok(()) +} diff --git a/fractal-gtk/res/app.css b/fractal-gtk/res/app.css index aed25e90..46e59587 100644 --- a/fractal-gtk/res/app.css +++ b/fractal-gtk/res/app.css @@ -63,3 +63,8 @@ padding: 10px; font-size: x-small; } + +.member-uid { + color: @insensitive_fg_color; + font-size: x-small; +} diff --git a/fractal-gtk/res/main_window.glade b/fractal-gtk/res/main_window.glade index 8da322e8..7930deb1 100644 --- a/fractal-gtk/res/main_window.glade +++ b/fractal-gtk/res/main_window.glade @@ -2,6 +2,30 @@ + + 300 + 200 + False + + + True + True + in + + + True + False + + + True + False + + + + + + + True False @@ -137,6 +161,20 @@ 6 vertical 6 + + + True + False + False + app.room_invite + Invite to this room + + + False + True + 0 + + True @@ -1370,6 +1408,138 @@ Join a room to start to chat + + 400 + 300 + False + True + center + True + dialog + False + center + main_window + main_window + + + False + vertical + + + False + end + + + + + + + + + False + False + 0 + + + + + True + False + vertical + 6 + + + True + True + True + Matrix username, email or phone number + + + False + True + 0 + + + + + True + False + none + + + False + True + 2 + + + + + False + True + 1 + + + + + + + True + False + + + True + False + True + + + Cancel + True + True + True + + + False + True + 0 + + + + + True + False + True + Invite + + + + + + False + True + 1 + + + + + Invite + True + True + True + + + + False + True + 2 + + + + + + + False False diff --git a/fractal-gtk/src/app.rs b/fractal-gtk/src/app.rs index 4f6f16a0..7ccbd170 100644 --- a/fractal-gtk/src/app.rs +++ b/fractal-gtk/src/app.rs @@ -680,7 +680,7 @@ impl AppOp { } } - pub fn show_inv_dialog(&self, r: &Room) { + pub fn show_inv_dialog(&mut self, r: &Room) { let dialog = self.gtk_builder .get_object::("invite_dialog") .expect("Can't find invite_dialog in ui file."); @@ -720,6 +720,93 @@ impl AppOp { self.roomlist.remove_room(roomid); } + pub fn search_invite_user(&self, term: Option) { + if let Some(t) = term { + self.backend.send(BKCommand::UserSearch(t)).unwrap(); + } + } + + pub fn search_invite_finished(&self, users: Vec) { + let listbox = self.gtk_builder + .get_object::("autocomplete_listbox") + .expect("Can't find autocomplete_listbox in ui file."); + let popover = self.gtk_builder + .get_object::("autocomplete_popover") + .expect("Can't find autocomplete_popover in ui file."); + let entry = self.gtk_builder + .get_object::("invite_entry") + .expect("Can't find invite_entry in ui file."); + let to_invite = self.gtk_builder + .get_object::("to_invite") + .expect("Can't find to_invite in ui file."); + + for ch in listbox.get_children().iter() { + listbox.remove(ch); + } + + for (i, u) in users.iter().enumerate() { + let w; + let w1; + { + let mb = widgets::MemberBox::new(u, &self); + w = mb.widget(true); + w1 = mb.widget(true); + } + + w.connect_button_press_event(clone!(to_invite, entry, u, w1, popover => move |_, _| { + entry.set_text(&u.uid); + + for ch in to_invite.get_children().iter() { + to_invite.remove(ch); + } + to_invite.insert(&w1, 0); + + popover.popdown(); + glib::signal::Inhibit(true) + })); + + listbox.insert(&w, i as i32); + } + + popover.set_relative_to(Some(&entry)); + popover.set_modal(false); + popover.popup(); + } + + pub fn close_invite_dialog(&self) { + let listbox = self.gtk_builder + .get_object::("autocomplete_listbox") + .expect("Can't find autocomplete_listbox in ui file."); + let to_invite = self.gtk_builder + .get_object::("to_invite") + .expect("Can't find to_invite in ui file."); + let entry = self.gtk_builder + .get_object::("invite_entry") + .expect("Can't find invite_entry in ui file."); + let dialog = self.gtk_builder + .get_object::("invite_user_dialog") + .expect("Can't find invite_user_dialog in ui file."); + + for ch in to_invite.get_children().iter() { + to_invite.remove(ch); + } + for ch in listbox.get_children().iter() { + listbox.remove(ch); + } + entry.set_text(""); + dialog.hide(); + } + + pub fn invite(&self) { + let entry = self.gtk_builder + .get_object::("invite_entry") + .expect("Can't find invite_entry in ui file."); + if let (Some(ref t), &Some(ref r)) = (entry.get_text(), &self.active_room) { + self.backend.send(BKCommand::Invite(r.clone(), t.to_string())).unwrap(); + } + self.close_invite_dialog(); + } + pub fn set_active_room(&mut self, room: &Room) { self.member_limit = 50; self.room_panel(RoomPanel::Loading); @@ -1345,6 +1432,13 @@ impl AppOp { dialog.present(); } + pub fn show_invite_user_dialog(&self) { + let dialog = self.gtk_builder + .get_object::("invite_user_dialog") + .expect("Can't find invite_user_dialog in ui file."); + dialog.present(); + } + pub fn really_leave_active_room(&mut self) { let r = self.active_room.clone().unwrap_or_default(); self.backend.send(BKCommand::LeaveRoom(r.clone())).unwrap(); @@ -1734,7 +1828,7 @@ impl AppOp { { let mb = widgets::MemberBox::new(&m, &self); - w = mb.widget(); + w = mb.widget(false); } let msg = msg_entry.clone(); @@ -1888,6 +1982,7 @@ impl App { self.connect_member_search(); self.connect_invite_dialog(); + self.connect_invite_user(); } fn create_actions(&self) { @@ -1898,6 +1993,7 @@ impl App { let logout = gio::SimpleAction::new("logout", None); let room = gio::SimpleAction::new("room_details", None); + let inv = gio::SimpleAction::new("room_invite", None); let search = gio::SimpleAction::new("search", None); let leave = gio::SimpleAction::new("leave_room", None); @@ -1908,6 +2004,7 @@ impl App { self.op.lock().unwrap().gtk_app.add_action(&logout); self.op.lock().unwrap().gtk_app.add_action(&room); + self.op.lock().unwrap().gtk_app.add_action(&inv); self.op.lock().unwrap().gtk_app.add_action(&search); self.op.lock().unwrap().gtk_app.add_action(&leave); @@ -1924,6 +2021,8 @@ impl App { let op = self.op.clone(); room.connect_activate(move |_, _| { op.lock().unwrap().show_room_dialog(); }); let op = self.op.clone(); + inv.connect_activate(move |_, _| { op.lock().unwrap().show_invite_user_dialog(); }); + let op = self.op.clone(); search.connect_activate(move |_, _| { op.lock().unwrap().toggle_search(); }); let op = self.op.clone(); leave.connect_activate(move |_, _| { op.lock().unwrap().leave_active_room(); }); @@ -2278,6 +2377,48 @@ impl App { })); } + fn connect_invite_user(&self) { + let op = &self.op; + + let cancel = self.gtk_builder + .get_object::("cancel_invite") + .expect("Can't find cancel_invite in ui file."); + let invite = self.gtk_builder + .get_object::("invite_button") + .expect("Can't find invite_button in ui file."); + let entry = self.gtk_builder + .get_object::("invite_entry") + .expect("Can't find invite_entry in ui file."); + + // 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>> = Arc::new(Mutex::new(None)); + entry.connect_key_release_event(clone!(op => move |entry, _| { + { + let mut id = source_id.lock().unwrap(); + if let Some(sid) = id.take() { + glib::source::source_remove(sid); + } + } + + let sid = gtk::timeout_add(500, clone!(op, entry, source_id => move || { + op.lock().unwrap().search_invite_user(entry.get_text()); + *(source_id.lock().unwrap()) = None; + gtk::Continue(false) + })); + + *(source_id.lock().unwrap()) = Some(sid); + glib::signal::Inhibit(false) + })); + + cancel.connect_clicked(clone!(op => move |_| { + op.lock().unwrap().close_invite_dialog(); + })); + invite.connect_clicked(clone!(op => move |_| { + op.lock().unwrap().invite(); + })); + } + pub fn run(&self) { self.op.lock().unwrap().init(); @@ -2400,6 +2541,9 @@ fn backend_loop(rx: Receiver) { Ok(BKResponse::AddedToFav(r, tofav)) => { APPOP!(added_to_fav, (r, tofav)); } + Ok(BKResponse::UserSearch(users)) => { + APPOP!(search_invite_finished, (users)); + } // errors Ok(BKResponse::NewRoomError(err)) => { diff --git a/fractal-gtk/src/widgets/member.rs b/fractal-gtk/src/widgets/member.rs index a92f70a7..64f1af67 100644 --- a/fractal-gtk/src/widgets/member.rs +++ b/fractal-gtk/src/widgets/member.rs @@ -33,16 +33,25 @@ impl<'a> MemberBox<'a> { } } - pub fn widget(&self) -> gtk::EventBox { + pub fn widget(&self, show_uid: bool) -> gtk::EventBox { let backend = self.op.backend.clone(); let username = gtk::Label::new(""); + let uid = gtk::Label::new(""); let event_box = gtk::EventBox::new(); let w = gtk::Box::new(gtk::Orientation::Horizontal, 5); + let v = gtk::Box::new(gtk::Orientation::Vertical, 0); + + uid.set_text(&self.member.uid); + uid.set_alignment(0.0, 0.0); + if let Some(style) = uid.get_style_context() { + style.add_class("member-uid"); + } username.set_text(&self.member.get_alias().unwrap_or_default()); username.set_tooltip_text(&self.member.get_alias().unwrap_or_default()[..]); username.set_margin_end(5); username.set_ellipsize(pango::EllipsizeMode::End); + username.set_alignment(0.0, 0.5); if let Some(style) = username.get_style_context() { style.add_class("member"); } @@ -53,8 +62,13 @@ impl<'a> MemberBox<'a> { get_member_info(backend.clone(), avatar.clone(), username.clone(), self.member.uid.clone(), globals::USERLIST_ICON_SIZE, 10); avatar.set_margin_start(5); + v.pack_start(&username, true, true, 0); + if show_uid { + v.pack_start(&uid, true, true, 0); + } + w.add(&avatar); - w.add(&username); + w.add(&v); event_box.add(&w); event_box.show_all();