diff --git a/res/main_window.glade b/res/main_window.glade index f3dcea66..866bec48 100644 --- a/res/main_window.glade +++ b/res/main_window.glade @@ -2,6 +2,14 @@ + + + + + + + + @@ -185,46 +193,47 @@ 2 0 - - - - - - 200 - 300 - True - False - 200 - 300 - True - - - - 480 - 360 - False - True - True - - - - - - - 0 - 2 - - - 200 False - - False - <Members> + + True + True + in + + + True + False + + + True + True + members_store + False + vertical + + + + + + name + + + end + + + 0 + + + + + + + + room_member_list @@ -362,6 +371,34 @@ 4 + + + 300 + 300 + True + True + never + in + + + True + False + + + 300 + True + False + True + + + + + + + 0 + 2 + + room_page diff --git a/src/app.rs b/src/app.rs index 68a56681..f8b30c4e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -34,8 +34,8 @@ const APP_ID: &'static str = "org.gnome.guillotine"; struct AppOp { gtk_builder: gtk::Builder, backend: Backend, - rooms: Option>, active_room: String, + members: HashMap, } impl AppOp { @@ -105,7 +105,7 @@ impl AppOp { if let Ok(pixbuf) = Pixbuf::new_from_file_at_size(fname, 20, 20) { image.set_from_pixbuf(&pixbuf); } else { - image.set_from_stock("image-missing", 20); + image.set_from_icon_name("image-missing", 2); } self.show_username(); @@ -207,27 +207,38 @@ impl AppOp { } pub fn set_rooms(&mut self, rooms: HashMap) { - self.rooms = Some(rooms); let store: gtk::TreeStore = self.gtk_builder.get_object("rooms_tree_store") .expect("Couldn't find rooms_tree_store in ui file."); - if let Some(ref rs) = self.rooms { - for (id, name) in rs { - let iter = store.insert_with_values(None, None, - &[0, 1], - &[&name, &id]); - } + for (id, name) in rooms { + let iter = store.insert_with_values(None, None, + &[0, 1], + &[&name, &id]); } } pub fn set_active_room(&mut self, room: String) { self.active_room = room; + let messages = self.gtk_builder + .get_object::("message_list") + .expect("Can't find message_list in ui file."); + for ch in messages.get_children() { + messages.remove(&ch); + } + + self.members.clear(); + let members = self.gtk_builder + .get_object::("members_store") + .expect("Can't find members_store in ui file."); + members.clear(); + // getting room details self.backend.get_room_detail(self.active_room.clone(), String::from("m.room.name")).unwrap(); self.backend.get_room_detail(self.active_room.clone(), String::from("m.room.topic")).unwrap(); self.backend.get_room_avatar(self.active_room.clone()).unwrap(); self.backend.get_room_messages(self.active_room.clone()).unwrap(); + self.backend.get_room_members(self.active_room.clone()).unwrap(); } pub fn set_room_detail(&self, key: String, value: String) { @@ -260,29 +271,101 @@ impl AppOp { image.set_from_pixbuf(&pixbuf); } } else { - image.set_from_stock("image-missing", 40); + image.set_from_icon_name("image-missing", 5); } } - pub fn add_room_message(&self, msg: backend::Message) { + pub fn scroll_down(&self) { let s = self.gtk_builder .get_object::("messages_scroll") .expect("Can't find message_scroll in ui file."); + + if let Some(adj) = s.get_vadjustment() { + println!("adj: {:?}", adj); + adj.set_value(adj.get_upper()); + } + } + + pub fn add_room_message(&self, msg: backend::Message) { let messages = self.gtk_builder .get_object::("message_list") .expect("Can't find message_list in ui file."); let body = msg.b; - let mut msg = gtk::Label::new(&body[..]); - msg.set_line_wrap(true); - msg.set_justify(gtk::Justification::Left); - msg.set_halign(gtk::Align::Start); - msg.show(); + let sender = msg.s; + + let mut msg = gtk::Box::new(gtk::Orientation::Horizontal, 5); + + let mut vert = gtk::Box::new(gtk::Orientation::Vertical, 0); + let mut label = gtk::Label::new(&body[..]); + label.set_line_wrap(true); + label.set_justify(gtk::Justification::Left); + label.set_halign(gtk::Align::Start); + label.set_alignment(0 as f32, 0 as f32); + + let mut fname = sender.clone(); + let mut avatar_url = String::new(); + if let Some(m) = self.members.get(&sender) { + fname = m.get_alias(); + avatar_url = m.avatar.clone(); + } + + let avatar = gtk::Image::new_from_icon_name("image-missing", 5); + + if !avatar_url.is_empty() { + let a = avatar.clone(); + let fname = self.backend.get_media_async(avatar_url).unwrap(); + let mut tries = 0; + gtk::timeout_add(50, move || { + match Pixbuf::new_from_file_at_size(&fname, 32, 32) { + Ok(pixbuf) => { + a.set_from_pixbuf(&pixbuf); + gtk::Continue(false) + }, + Err(err) => { + match tries { + i if i < 200 => gtk::Continue(true), + _ => gtk::Continue(false), + } + } + } + }); + } + + let mut username = gtk::Label::new(""); + username.set_markup(&format!("{}", fname)); + username.set_justify(gtk::Justification::Left); + username.set_halign(gtk::Align::Start); + + vert.pack_start(&username, false, false, 0); + vert.pack_start(&label, true, true, 0); + + msg.pack_start(&avatar, false, true, 5); + msg.pack_start(&vert, true, true, 0); + msg.show_all(); + messages.add(&msg); + } - if let Some(adj) = s.get_vadjustment() { - adj.set_value(adj.get_upper()); + pub fn add_room_member(&mut self, m: backend::Member) { + if !m.avatar.is_empty() { + self.backend.get_member_avatar(m.uid.clone(), m.avatar.clone()).unwrap(); } + + let store: gtk::ListStore = self.gtk_builder.get_object("members_store") + .expect("Couldn't find members_store in ui file."); + + let name = m.get_alias(); + + let iter = store.insert_with_values(None, + &[0, 1], + &[&name, &(m.uid)]); + + self.members.insert(m.uid.clone(), m); + } + + pub fn member_clicked(&self, uid: String) { + println!("member clicked: {}, {:?}", uid, self.members.get(&uid)); } } @@ -313,13 +396,13 @@ impl App { AppOp{ gtk_builder: gtk_builder.clone(), backend: Backend::new(tx), - rooms: None, active_room: String::from(""), + members: HashMap::new(), } )); let theop = op.clone(); - gtk::timeout_add(50, move || { + gtk::timeout_add(300, move || { let recv = rx.try_recv(); match recv { Ok(backend::BKResponse::Token(uid, _)) => { @@ -348,6 +431,23 @@ impl App { }, Ok(backend::BKResponse::RoomMessage(msg)) => { theop.lock().unwrap().add_room_message(msg); + theop.lock().unwrap().scroll_down(); + }, + Ok(backend::BKResponse::RoomMessages(msgs)) => { + for msg in msgs { + theop.lock().unwrap().add_room_message(msg); + } + theop.lock().unwrap().scroll_down(); + }, + Ok(backend::BKResponse::RoomMember(member)) => { + theop.lock().unwrap().add_room_member(member); + }, + Ok(backend::BKResponse::RoomMembers(members)) => { + for m in members { + theop.lock().unwrap().add_room_member(m); + } + }, + Ok(backend::BKResponse::RoomMemberAvatar(uid, avatar)) => { }, Err(_) => { }, }; @@ -403,6 +503,18 @@ impl App { op_c.lock().unwrap().set_active_room(id.get().unwrap()); }); + // member selection + let members: gtk::TreeView = gtk_builder.get_object("members_treeview") + .expect("Couldn't find members_treeview in ui file."); + + op_c = op.clone(); + members.set_activate_on_single_click(true); + members.connect_row_activated(move |view, path, column| { + let iter = view.get_model().unwrap().get_iter(path).unwrap(); + let id = view.get_model().unwrap().get_value(&iter, 1); + op_c.lock().unwrap().member_clicked(id.get().unwrap()); + }); + // Login click let login_btn: gtk::Button = gtk_builder.get_object("login_button") .expect("Couldn't find login_button in ui file."); diff --git a/src/backend.rs b/src/backend.rs index 140ca0b5..11f0ab02 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -134,6 +134,10 @@ pub enum BKResponse { RoomDetail(String, String), RoomAvatar(String), RoomMessage(Message), + RoomMessages(Vec), + RoomMember(Member), + RoomMembers(Vec), + RoomMemberAvatar(String, String), } #[derive(Debug)] @@ -148,6 +152,22 @@ pub struct Message { pub a: i64, } +#[derive(Debug)] +pub struct Member { + pub alias: String, + pub uid: String, + pub avatar: String, +} + +impl Member { + pub fn get_alias(&self) -> String { + match self.alias { + ref a if a.is_empty() => self.uid.clone(), + ref a => a.clone(), + } + } +} + impl Backend { pub fn new(tx: Sender) -> Backend { @@ -352,20 +372,101 @@ impl Backend { let tx = self.tx.clone(); get!(url, map, |r: JsonValue| { + let mut ms: Vec = vec![]; for msg in r["chunk"].as_array().unwrap().iter().rev() { - println!("messages: {:#?}", msg); + //println!("messages: {:#?}", msg); let m = Message { s: String::from(msg["sender"].as_str().unwrap()), t: String::from(msg["content"]["msgtype"].as_str().unwrap()), b: String::from(msg["content"]["body"].as_str().unwrap()), a: msg["age"].as_i64().unwrap(), }; - tx.send(BKResponse::RoomMessage(m)).unwrap(); + ms.push(m); + } + tx.send(BKResponse::RoomMessages(ms)).unwrap(); + }); + + Ok(()) + } + + pub fn get_room_members(&self, roomid: String) -> Result<(), Error> { + let s = self.data.lock().unwrap().server_url.clone(); + let tk = self.data.lock().unwrap().access_token.clone(); + let baseu = Url::parse(&s)?; + let mut url = baseu.join("/_matrix/client/r0/rooms/")?.join(&(roomid + "/"))?.join("members")?; + url = url.join(&format!("?access_token={}", tk))?; + let map: HashMap = HashMap::new(); + + let tx = self.tx.clone(); + get!(url, map, + |r: JsonValue| { + //println!("{:#?}", r); + let mut ms: Vec = vec![]; + for member in r["chunk"].as_array().unwrap().iter().rev() { + if member["type"].as_str().unwrap() != "m.room.member" { + continue; + } + + let content = &member["content"]; + if content["membership"].as_str().unwrap() != "join" { + continue; + } + + let m = Member { + alias: String::from(content["displayname"].as_str().unwrap_or("")), + uid: String::from(member["sender"].as_str().unwrap()), + avatar: String::from(content["avatar_url"].as_str().unwrap_or("")), + }; + ms.push(m); + if (ms.len() > 20) { + tx.send(BKResponse::RoomMembers(ms)).unwrap(); + ms = vec![]; + } } + if (!ms.is_empty()) { + tx.send(BKResponse::RoomMembers(ms)).unwrap(); + } + }); + + Ok(()) + } + + pub fn get_member_avatar(&self, memberid: String, avatar_url: String) -> Result<(), Error> { + let s = self.data.lock().unwrap().server_url.clone(); + let baseu = Url::parse(&s)?; + + let tx = self.tx.clone(); + thread::spawn(move || { + let fname = thumb!(baseu, &avatar_url).unwrap(); + tx.send(BKResponse::RoomMemberAvatar(memberid, fname)).unwrap(); }); Ok(()) } + + pub fn get_base_url(&self) -> Result { + let s = self.data.lock().unwrap().server_url.clone(); + let url = Url::parse(&s)?; + Ok(url) + } + + pub fn get_media_async(&self, url: String) -> Result { + let base = self.get_base_url()?; + let xdg_dirs = xdg::BaseDirectories::with_prefix("guillotine").unwrap(); + + let re = Regex::new(r"mxc://(?P[^/]+)/(?P.+)")?; + let caps = re.captures(&url).ok_or(Error::BackendError)?; + let media = String::from(&caps["media"]); + + let fname = String::from(xdg_dirs.place_cache_file(&media)?.to_str().ok_or(Error::BackendError)?); + + let u = url.clone(); + thread::spawn(move || { + thumb!(base, &u).unwrap(); + }); + + Ok(fname) + } } fn get_rooms_from_json(r: JsonValue) -> Result, Error> { @@ -403,6 +504,8 @@ fn get_media(url: &str) -> Result, Error> { } fn dw_media(base: Url, url: &str, thumb: bool, dest: Option<&str>, w: i32, h: i32) -> Result { + // TODO, don't download if exists + let xdg_dirs = xdg::BaseDirectories::with_prefix("guillotine").unwrap(); let re = Regex::new(r"mxc://(?P[^/]+)/(?P.+)")?; diff --git a/src/main.rs b/src/main.rs index 2e614f0d..508445fe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,5 @@ #[macro_use] mod util; - mod backend; mod app;