diff --git a/Cargo.lock b/Cargo.lock index a0ec4fee..dcf530d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -381,9 +381,11 @@ dependencies = [ "gstreamer-player 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)", "gtk 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "html2pango 0.1.0 (git+https://gitlab.gnome.org/World/html2pango)", + "letter-avatar 0.1.0 (git+https://gitlab.gnome.org/jsparber/letter-avatar)", "log 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "notify-rust 3.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "pango 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "pangocairo 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "secret-service 0.4.0 (git+https://github.com/jhaye/secret-service-rs?rev=3c265527e43376fe8e00ddfa645a70813c35f449)", @@ -391,6 +393,7 @@ dependencies = [ "serde_derive 1.0.43 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", "tree_magic 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-segmentation 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -400,13 +403,9 @@ version = "3.29.1" dependencies = [ "cairo-rs 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "gdk 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", - "gdk-pixbuf 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "glib 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "md5 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", "mime 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", - "pango 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "pangocairo 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "regex 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", "reqwest 0.8.5 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.43 (registry+https://github.com/rust-lang/crates.io-index)", @@ -414,7 +413,6 @@ dependencies = [ "serde_json 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", "time 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)", "tree_magic 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-segmentation 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)", "urlencoding 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -924,6 +922,17 @@ name = "lazycell" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "letter-avatar" +version = "0.1.0" +source = "git+https://gitlab.gnome.org/jsparber/letter-avatar#7a58d07442654e3e2dda9960e1ec3f14e2dba25a" +dependencies = [ + "cairo-rs 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "pango 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "pangocairo 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-segmentation 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "libc" version = "0.2.40" @@ -2242,6 +2251,7 @@ dependencies = [ "checksum lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73" "checksum lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c8f31047daa365f19be14b47c29df4f7c3b581832407daabe6ae77397619237d" "checksum lazycell 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a6f08839bc70ef4a3fe1d566d5350f519c5912ea86be0df1740a7d247c7fc0ef" +"checksum letter-avatar 0.1.0 (git+https://gitlab.gnome.org/jsparber/letter-avatar)" = "" "checksum libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)" = "6fd41f331ac7c5b8ac259b8bf82c75c0fb2e469bbf37d2becbba9a6a2221965b" "checksum libflate 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "1a429b86418868c7ea91ee50e9170683f47fd9d94f5375438ec86ec3adb74e8e" "checksum linkify 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9ce9439c6f4a1092dc1861272bef01034891da39f13aa1cdcf40ca3e4081de5f" diff --git a/fractal-gtk/Cargo.toml b/fractal-gtk/Cargo.toml index 3c8fd1b2..6cd634fb 100644 --- a/fractal-gtk/Cargo.toml +++ b/fractal-gtk/Cargo.toml @@ -16,6 +16,7 @@ gstreamer = "0.11.3" gstreamer-player = "0.11.3" notify-rust = "3.4.2" pango = "0.4.0" +pangocairo = "0.5.0" secret-service = "0.4.0" serde = "1.0.43" serde_derive = "1.0.43" @@ -25,6 +26,8 @@ rand = "0.4.2" html2pango = { git = "https://gitlab.gnome.org/World/html2pango" } comrak = "0.2.9" gettext-rs = { git = "https://github.com/danigm/gettext-rs", branch = "no-gettext", features = ["gettext-system"] } +letter-avatar = { git = "https://gitlab.gnome.org/jsparber/letter-avatar" } +unicode-segmentation = "1.2.0" regex = "1.0.0" tree_magic = "0.2.0" log = "0.4.2" diff --git a/fractal-gtk/src/appop/account.rs b/fractal-gtk/src/appop/account.rs index e743da60..ff0aa180 100644 --- a/fractal-gtk/src/appop/account.rs +++ b/fractal-gtk/src/appop/account.rs @@ -9,6 +9,7 @@ use backend::BKCommand; use widgets; use widgets::AvatarExt; +use cache::download_to_cache; use fractal_api::types::UserInfo; impl AppOp { @@ -211,7 +212,7 @@ impl AppOp { avatar_spinner.hide(); avatar_btn.set_sensitive(true); - self.show_avatar(self.avatar.clone()); + self.show_avatar(); name_btn.hide(); name.set_editable(true); @@ -338,10 +339,10 @@ impl AppOp { self.set_avatar(path.clone()); avatar_spinner.hide(); avatar_btn.set_sensitive(true); - self.show_avatar(self.avatar.clone()); + self.show_avatar(); } - pub fn show_avatar(&self, path: Option) { + pub fn show_avatar(&self) { let stack = self.ui.builder .get_object::("account_settings_stack") .expect("Can't find account_settings_delete_box in ui file."); @@ -358,7 +359,9 @@ impl AppOp { } } - let w = widgets::Avatar::circle_avatar(path.unwrap_or_default(), Some(100)); + download_to_cache(self.backend.clone(), self.uid.clone().unwrap_or_default()); + let w = widgets::Avatar::avatar_new(Some(100)); + w.circle(self.uid.clone().unwrap_or_default(), self.username.clone(), 100); avatar.add(&w); /* FIXME: hack to make the avatar drawing area clickable*/ @@ -380,7 +383,7 @@ impl AppOp { self.backend.send(command).unwrap(); avatar_btn.set_sensitive(false); avatar_spinner.show(); - self.show_avatar(Some(file)); + self.show_avatar(); } pub fn show_new_username(&mut self, name: Option) { diff --git a/fractal-gtk/src/appop/notify.rs b/fractal-gtk/src/appop/notify.rs index 6635b4c8..be88fd8b 100644 --- a/fractal-gtk/src/appop/notify.rs +++ b/fractal-gtk/src/appop/notify.rs @@ -44,7 +44,7 @@ impl AppOp { body.truncate(80); let (tx, rx): (Sender<(String, String)>, Receiver<(String, String)>) = channel(); - self.backend.send(BKCommand::GetUserInfoAsync(msg.sender.clone(), tx)).unwrap(); + self.backend.send(BKCommand::GetUserInfoAsync(msg.sender.clone(), Some(tx))).unwrap(); let bk = self.internal.clone(); let m = msg.clone(); gtk::timeout_add(50, move || match rx.try_recv() { diff --git a/fractal-gtk/src/appop/room.rs b/fractal-gtk/src/appop/room.rs index 747ba95d..73193424 100644 --- a/fractal-gtk/src/appop/room.rs +++ b/fractal-gtk/src/appop/room.rs @@ -1,5 +1,4 @@ extern crate gtk; -extern crate gdk_pixbuf; extern crate rand; use i18n::{i18n, i18n_k}; @@ -17,14 +16,12 @@ use backend::BKCommand; use globals; use cache; use widgets; -use widgets::AvatarExt; use types::Room; use types::Message; use util::markup_text; -use self::gdk_pixbuf::Pixbuf; use self::rand::{thread_rng, Rng}; @@ -419,7 +416,7 @@ impl AppOp { }; } - pub fn set_current_room_avatar(&self, avatar: Option, size: i32) { + pub fn set_current_room_avatar(&self, _avatar: Option, _size: i32) { let image = self.ui.builder .get_object::("room_image") .expect("Can't find room_image in ui file."); @@ -427,6 +424,8 @@ impl AppOp { image.remove(&ch); } + /* + * This will be removed soon. let config = self.ui.builder .get_object::("room_avatar_image") .expect("Can't find room_avatar_image in ui file."); @@ -442,6 +441,7 @@ impl AppOp { image.add(&w); config.set_from_icon_name("camera-photo-symbolic", 1); } + */ } pub fn filter_rooms(&self, term: Option) { diff --git a/fractal-gtk/src/appop/user.rs b/fractal-gtk/src/appop/user.rs index 2d08260e..d280900b 100644 --- a/fractal-gtk/src/appop/user.rs +++ b/fractal-gtk/src/appop/user.rs @@ -4,6 +4,8 @@ use self::gtk::prelude::*; use appop::AppOp; +use cache::download_to_cache; + use backend::BKCommand; use widgets; use widgets::AvatarExt; @@ -41,7 +43,9 @@ impl AppOp { avatar.remove(w); } - let w = widgets::Avatar::circle_avatar(self.avatar.clone().unwrap_or_default(), Some(40)); + download_to_cache(self.backend.clone(), self.uid.clone().unwrap_or_default()); + let w = widgets::Avatar::avatar_new(Some(40)); + w.circle(self.uid.clone().unwrap_or_default(), self.username.clone(), 40); avatar.add(&w); stack.set_visible_child_name("info"); } @@ -55,11 +59,13 @@ impl AppOp { .expect("Can't find user_menu_button in ui file."); let eb = gtk::EventBox::new(); - match self.avatar.clone() { - Some(s) => { - let w = widgets::Avatar::circle_avatar(s.clone(), Some(24)); - eb.add(&w); - } + match self.avatar.clone() { + Some(_) => { + download_to_cache(self.backend.clone(), self.uid.clone().unwrap_or_default()); + let w = widgets::Avatar::avatar_new(Some(24)); + w.circle(self.uid.clone().unwrap_or_default(), self.username.clone(), 24); + eb.add(&w); + } None => { let w = gtk::Spinner::new(); w.show(); diff --git a/fractal-gtk/src/cache.rs b/fractal-gtk/src/cache.rs index cff7e635..2cffc8d5 100644 --- a/fractal-gtk/src/cache.rs +++ b/fractal-gtk/src/cache.rs @@ -11,6 +11,10 @@ use error::Error; use fractal_api::util::cache_path; use globals; +/* includes for avatar download */ +use backend::BKCommand; +use std::sync::mpsc::Sender; + use types::Message; #[derive(Serialize, Deserialize)] @@ -71,3 +75,8 @@ pub fn destroy() -> Result<(), Error> { let fname = cache_path("")?; remove_dir_all(fname).or_else(|_| Err(Error::CacheError)) } + +/* this downloads a avatar and stores it in the cache folder */ +pub fn download_to_cache(backend: Sender, name: String) { + let _ = backend.send(BKCommand::GetUserInfoAsync(name.clone(), None)); +} diff --git a/fractal-gtk/src/widgets/avatar.rs b/fractal-gtk/src/widgets/avatar.rs index dfd06ba7..491fd49a 100644 --- a/fractal-gtk/src/widgets/avatar.rs +++ b/fractal-gtk/src/widgets/avatar.rs @@ -1,25 +1,27 @@ extern crate gtk; extern crate gdk; +extern crate glib; extern crate gdk_pixbuf; extern crate cairo; +extern crate letter_avatar; + +use std::cell::RefCell; +use std::rc::Rc; use self::gtk::prelude::*; pub use self::gtk::DrawingArea; use self::gdk_pixbuf::Pixbuf; use self::gdk_pixbuf::PixbufExt; use self::gdk::ContextExt; - +use fractal_api::util::cache_path; pub type Avatar = gtk::Box; pub trait AvatarExt { fn avatar_new(size: Option) -> gtk::Box; - fn circle_avatar(path: String, size: Option) -> gtk::Box; fn clean(&self); fn create_da(&self, size: Option) -> DrawingArea; - - fn circle(&self, path: String, size: Option); - fn default(&self, icon: String, size: Option); + fn circle(&self, uid: String, username: Option, size: i32); } impl AvatarExt for gtk::Box { @@ -51,102 +53,72 @@ impl AvatarExt for gtk::Box { b } - fn circle_avatar(path: String, size: Option) -> gtk::Box { - let b = gtk::Box::new(gtk::Orientation::Horizontal, 0); - b.create_da(size); - b.circle(path, size); - b.show_all(); - if let Some(style) = b.get_style_context() { - style.add_class("avatar"); + fn circle(&self, uid: String, username: Option, size: i32) { + struct Data { + cache : Result, + fallback: cairo::ImageSurface } - b - } - - fn default(&self, icon: String, size: Option) { self.clean(); - let da = self.create_da(size); - let s = size.unwrap_or(40); - - let pixbuf = match gtk::IconTheme::get_default() { - None => None, - Some(i1) => match i1.load_icon(&icon[..], s, gtk::IconLookupFlags::empty()) { - Err(_) => None, - Ok(i2) => i2, - } + let da = self.create_da(Some(size)); + let path = cache_path(&uid).unwrap_or(String::from("")); + let user_avatar = Pixbuf::new_from_file_at_scale(&path, size, -1, true); + /* remove IRC postfix from the username */ + let username = if let Some(u) = username { + Some(u.trim_right_matches(" (IRC)").to_owned()) + }else { + None }; + /* This function should never fail */ + let fallback = letter_avatar::generate::new(uid, username, size as f64).unwrap(); - da.connect_draw(move |da, g| { - use std::f64::consts::PI; - - let width = s as f64; - let height = s as f64; - - let context = da.get_style_context().unwrap(); - - gtk::render_background(&context, g, 0.0, 0.0, width, height); - - if let Some(ref pb) = pixbuf { - let hpos: f64 = (width - (pb.get_height()) as f64) / 2.0; - - g.arc(width / 2.0, height / 2.0, width.min(height) / 2.5, 0.0, 2.0 * PI); - g.clip(); - - g.set_source_pixbuf(&pb, 0.0, hpos); - g.rectangle(0.0, 0.0, width, height); - g.fill(); - } - - Inhibit(false) - }); - } - - fn circle(&self, path: String, size: Option) { - if path.starts_with("mxc:") { - self.default(String::from("image-loading-symbolic"), size); - return; - } - - self.clean(); - let da = self.create_da(size); - let s = size.unwrap_or(40); - - let pixbuf = Pixbuf::new_from_file_at_scale(&path, s, -1, true); + let user_cache: Rc> = Rc::new(RefCell::new(Data {cache: user_avatar, fallback: fallback})); + let user_cache = user_cache.clone(); da.connect_draw(move |da, g| { use std::f64::consts::PI; - g.set_antialias(cairo::Antialias::Best); - - let width = s as f64; - let height = s as f64; - - let context = da.get_style_context().unwrap(); - - gtk::render_background(&context, g, 0.0, 0.0, width, height); - - if let Ok(ref pb) = pixbuf { - let hpos: f64 = (width - (pb.get_height()) as f64) / 2.0; + let width = size as f64; + let height = size as f64; - g.arc(width / 2.0, height / 2.0, width.min(height) / 2.0, 0.0, 2.0 * PI); - g.clip(); + g.set_antialias(cairo::Antialias::Best); - g.set_source_pixbuf(&pb, 0.0, hpos); - g.rectangle(0.0, 0.0, width, height); - g.fill(); + { + let data = user_cache.borrow(); + if let Ok(ref pb) = data.cache { + let context = da.get_style_context().unwrap(); + gtk::render_background(&context, g, 0.0, 0.0, width, height); + + g.arc(width / 2.0, height / 2.0, width.min(height) / 2.0, 0.0, 2.0 * PI); + g.clip(); + + let hpos: f64 = (width - (pb.get_height()) as f64) / 2.0; + g.set_source_pixbuf(&pb, 0.0, hpos); + } else { + /* use fallback */ + g.set_source_surface(&data.fallback, 0f64, 0f64); + } + } + /* we should look into the cache only if the cache has changed, but this information is + * not yet available, we could also create our own signal and not use the draw signal*/ + { + let mut data = user_cache.borrow_mut(); + let new_avatar = Pixbuf::new_from_file_at_scale(&path, size, -1, true); + data.cache = new_avatar; } + g.rectangle(0.0, 0.0, width, height); + g.fill(); + Inhibit(false) }); } } - pub enum AdminColor { Gold, Silver, } - pub fn admin_badge(kind: AdminColor, size: Option) -> gtk::DrawingArea { let s = size.unwrap_or(10); diff --git a/fractal-gtk/src/widgets/member.rs b/fractal-gtk/src/widgets/member.rs index ae6d0154..0c6cc9ee 100644 --- a/fractal-gtk/src/widgets/member.rs +++ b/fractal-gtk/src/widgets/member.rs @@ -1,21 +1,14 @@ extern crate pango; extern crate gtk; -extern crate gdk_pixbuf; -use self::gdk_pixbuf::Pixbuf; use self::gtk::prelude::*; use types::Member; -use backend::BKCommand; - -use std::sync::mpsc::channel; -use std::sync::mpsc::{Sender, Receiver}; -use std::sync::mpsc::TryRecvError; - use appop::AppOp; use globals; +use cache::download_to_cache; use widgets; use widgets::AvatarExt; @@ -61,10 +54,10 @@ impl<'a> MemberBox<'a> { style.add_class("member"); } + download_to_cache(backend.clone(), self.member.uid.clone()); let avatar = widgets::Avatar::avatar_new(Some(globals::USERLIST_ICON_SIZE)); - avatar.default(String::from("avatar-default-symbolic"), - Some(globals::USERLIST_ICON_SIZE)); - get_member_info(backend.clone(), avatar.clone(), username.clone(), self.member.uid.clone(), globals::USERLIST_ICON_SIZE, 10); + avatar.circle(self.member.uid.clone(), Some(alias.clone()), globals::USERLIST_ICON_SIZE); + //get_member_info(backend.clone(), avatar.clone(), username.clone(), self.member.uid.clone(), globals::USERLIST_ICON_SIZE, 10); avatar.set_margin_start(3); avatar.set_valign(gtk::Align::Center); @@ -99,69 +92,3 @@ impl<'a> MemberBox<'a> { event_box } } - -#[allow(dead_code)] -pub fn get_member_avatar(backend: Sender, - img: widgets::Avatar, - m: Option, - size: i32, tries: i32) { - if tries <= 0 { - return; - } - - let (tx, rx): (Sender, Receiver) = channel(); - backend.send(BKCommand::GetAvatarAsync(m.clone(), tx)).unwrap(); - gtk::timeout_add(100, move || match rx.try_recv() { - Err(TryRecvError::Empty) => gtk::Continue(true), - Err(TryRecvError::Disconnected) => gtk::Continue(false), - Ok(avatar) => { - if let Ok(_) = Pixbuf::new_from_file_at_scale(&avatar, size, size, false) { - img.circle(avatar, Some(size)); - } else { - // trying again if fail - img.default(String::from("avatar-default-symbolic"), Some(size)); - get_member_avatar(backend.clone(), img.clone(), m.clone(), size, tries - 1); - } - - gtk::Continue(false) - } - }); -} - - - -pub fn get_member_info(backend: Sender, - img: widgets::Avatar, - username: gtk::Label, - sender: String, - size: i32, tries: i32) { - - if tries <= 0 { - return; - } - - let (tx, rx): (Sender<(String, String)>, Receiver<(String, String)>) = channel(); - backend.send(BKCommand::GetUserInfoAsync(sender.clone(), tx)).unwrap(); - gtk::timeout_add(100, move || match rx.try_recv() { - Err(TryRecvError::Empty) => gtk::Continue(true), - Err(TryRecvError::Disconnected) => gtk::Continue(false), - Ok((name, avatar)) => { - if let Ok(_) = Pixbuf::new_from_file_at_scale(&avatar, size, size, false) { - img.circle(avatar, Some(size)); - } else { - // trying again if fail - img.default(String::from("avatar-default-symbolic"), Some(size)); - get_member_info(backend.clone(), img.clone(), username.clone(), sender.clone(), size, tries - 1); - return gtk::Continue(false); - } - - if !name.is_empty() { - username.set_text(&name); - } else { - get_member_info(backend.clone(), img.clone(), username.clone(), sender.clone(), size, tries - 1); - } - - gtk::Continue(false) - } - }); -} diff --git a/fractal-gtk/src/widgets/message.rs b/fractal-gtk/src/widgets/message.rs index 7ca7f993..7b520d0c 100644 --- a/fractal-gtk/src/widgets/message.rs +++ b/fractal-gtk/src/widgets/message.rs @@ -16,19 +16,18 @@ use self::chrono::prelude::*; use backend::BKCommand; -use fractal_api as api; use util::markup_text; -use std::path::Path; use std::sync::mpsc::channel; use std::sync::mpsc::{Sender, Receiver}; use std::sync::mpsc::TryRecvError; +use cache::download_to_cache; + use appop::AppOp; use globals; use widgets; use widgets::AvatarExt; -use widgets::member::get_member_info; // Room Message item pub struct MessageBox<'a> { @@ -136,34 +135,25 @@ impl<'a> MessageBox<'a> { } fn build_room_msg_avatar(&self) -> widgets::Avatar { - let sender = self.msg.sender.clone(); - let backend = self.op.backend.clone(); + let uid = self.msg.sender.clone(); let avatar = widgets::Avatar::avatar_new(Some(globals::MSG_ICON_SIZE)); - let fname = api::util::cache_path(&sender).unwrap_or(strn!("")); + let m = self.room.members.get(&uid); - let pathname = fname.clone(); - let p = Path::new(&pathname); - if p.is_file() { - avatar.circle(fname, Some(globals::MSG_ICON_SIZE)); - } else { - avatar.default(String::from("avatar-default-symbolic"), - Some(globals::MSG_ICON_SIZE)); - } - - let m = self.room.members.get(&sender); - - match m { + let username = match m { Some(member) => { self.username.set_text(&member.get_alias()); - get_member_info(backend.clone(), avatar.clone(), self.username.clone(), sender.clone(), globals::MSG_ICON_SIZE, 10); + Some(member.get_alias()) } None => { - self.username.set_text(&sender); - get_member_info(backend.clone(), avatar.clone(), self.username.clone(), sender.clone(), globals::MSG_ICON_SIZE, 10); + self.username.set_text(&uid); + None } }; + download_to_cache(self.op.backend.clone(), uid.clone()); + avatar.circle(uid, username, globals::MSG_ICON_SIZE); + avatar } diff --git a/fractal-gtk/src/widgets/room.rs b/fractal-gtk/src/widgets/room.rs index e89c7ac3..3dedc643 100644 --- a/fractal-gtk/src/widgets/room.rs +++ b/fractal-gtk/src/widgets/room.rs @@ -1,29 +1,20 @@ extern crate gtk; -extern crate gdk; -extern crate gdk_pixbuf; -extern crate cairo; extern crate pango; use i18n::i18n; use self::gtk::prelude::*; -use self::gdk_pixbuf::Pixbuf; -use self::gdk_pixbuf::PixbufExt; -use self::gdk::ContextExt; - -use fractal_api::util::AvatarMode; -use fractal_api::util::draw_identicon; use types::Room; use backend::BKCommand; use util::markup_text; -use util::glib_thread_prelude::*; use appop::AppOp; -use widgets::image::Image; +use widgets; +use widgets::AvatarExt; use self::gtk::WidgetExt; const AVATAR_SIZE: i32 = 60; @@ -48,19 +39,8 @@ impl<'a> RoomBox<'a> { let widget_box = gtk::Box::new(gtk::Orientation::Horizontal, 0); - let mut avatar = gtk::DrawingArea::new(); - if room.avatar.clone().unwrap_or_default().is_empty() { - make_identicon(&avatar, AVATAR_SIZE, room.id.clone(), room.name.clone().unwrap_or_default()); - } else { - let mut avatar_widget = Image::new(&self.op.backend, - &room.avatar.clone().unwrap_or_default()) - .size(Some((AVATAR_SIZE, AVATAR_SIZE))) - .thumb(true).circle(true) - .fixed(true).build(); - avatar_widget.fixed_size = true; - avatar = avatar_widget.widget; - } - + let avatar = widgets::Avatar::avatar_new(Some(AVATAR_SIZE)); + avatar.circle(room.id.clone(), room.name.clone(), AVATAR_SIZE); widget_box.pack_start(&avatar, false, false, 18); let details_box = gtk::Box::new(gtk::Orientation::Vertical, 6); @@ -135,44 +115,3 @@ impl<'a> RoomBox<'a> { widget_box } } - -fn make_identicon(da: >k::DrawingArea, size: i32, rid: String, name: String) { - let da = da.clone(); - glib_thread!(Result, - || { - identicon!(&rid, name) - }, - |rc: Result| { - if let Ok(path) = rc { - da.set_size_request(size, size); - - let pixbuf = Pixbuf::new_from_file_at_scale(&path, size, size, true); - - da.connect_draw(move |da, g| { - use std::f64::consts::PI; - g.set_antialias(cairo::Antialias::Best); - - let width = size as f64; - let height = size as f64; - - let context = da.get_style_context().unwrap(); - - gtk::render_background(&context, g, 0.0, 0.0, width, height); - - if let Ok(ref pb) = pixbuf { - let hpos: f64 = (width - (pb.get_height()) as f64) / 2.0; - - g.arc(width / 2.0, height / 2.0, width.min(height) / 2.0, 0.0, 2.0 * PI); - g.clip(); - - g.set_source_pixbuf(&pb, 0.0, hpos); - g.rectangle(0.0, 0.0, width, height); - g.fill(); - } - - Inhibit(false) - }); - } - } - ); -} diff --git a/fractal-gtk/src/widgets/roomlist.rs b/fractal-gtk/src/widgets/roomlist.rs index 2f84f9b0..1723e589 100644 --- a/fractal-gtk/src/widgets/roomlist.rs +++ b/fractal-gtk/src/widgets/roomlist.rs @@ -162,7 +162,7 @@ impl RoomListGroup { let rid = r.id.clone(); self.roomvec.lock().unwrap().push(RoomUpdated::new(r.clone())); - let row = RoomRow::new(r, &self.baseu); + let row = RoomRow::new(r); self.list.add(&row.widget()); self.rooms.insert(rid, row); @@ -187,7 +187,7 @@ impl RoomListGroup { rv.insert(pos, RoomUpdated::new(r.room.clone())); - let row = RoomRow::new(r.room, &self.baseu); + let row = RoomRow::new(r.room); self.list.insert(&row.widget(), pos as i32); self.rooms.insert(rid, row); diff --git a/fractal-gtk/src/widgets/roomrow.rs b/fractal-gtk/src/widgets/roomrow.rs index c1eea801..db73a95d 100644 --- a/fractal-gtk/src/widgets/roomrow.rs +++ b/fractal-gtk/src/widgets/roomrow.rs @@ -1,20 +1,12 @@ extern crate pango; -extern crate url; extern crate gdk; extern crate gtk; extern crate cairo; -use self::url::Url; use self::gtk::prelude::*; -use fractal_api; -use fractal_api::util::AvatarMode; -use fractal_api::util::draw_identicon; - use types::Room; -use util::glib_thread_prelude::*; - use widgets; use widgets::AvatarExt; @@ -28,7 +20,6 @@ const ICON_SIZE: i32 = 24; // | IMG | Fractal | 32 | // +-----+--------------------------+------+ pub struct RoomRow { - baseu: Url, pub room: Room, pub icon: widgets::Avatar, pub direct: gtk::Image, @@ -38,10 +29,10 @@ pub struct RoomRow { } impl RoomRow { - pub fn new(room: Room, url: &Url) -> RoomRow { + pub fn new(room: Room) -> RoomRow { let widget = gtk::EventBox::new(); let name = room.name.clone().unwrap_or("...".to_string()); - let avatar = room.avatar.clone().unwrap_or_default(); + let icon = widgets::Avatar::avatar_new(Some(ICON_SIZE)); let direct = gtk::Image::new_from_icon_name("avatar-default-symbolic", 1); if let Some(style) = direct.get_style_context() { @@ -49,7 +40,6 @@ impl RoomRow { } let text = gtk::Label::new(name.clone().as_str()); - let baseu = url.clone(); text.set_valign(gtk::Align::Start); text.set_halign(gtk::Align::Start); text.set_ellipsize(pango::EllipsizeMode::End); @@ -77,19 +67,13 @@ impl RoomRow { notifications.hide(); } - icon.default(String::from("avatar-default-symbolic"), Some(ICON_SIZE)); - if avatar.starts_with("mxc") || avatar.is_empty() { - download_avatar(&baseu, room.id.clone(), name, avatar, &icon); - } else { - icon.circle(avatar, Some(ICON_SIZE)); - } + icon.circle(room.id.clone(), Some(name), ICON_SIZE); let rr = RoomRow { room, icon, text, notifications, - baseu, widget, direct, }; @@ -146,13 +130,7 @@ impl RoomRow { let name = self.room.name.clone().unwrap_or("...".to_string()); - self.icon.default(String::from("avatar-default-symbolic"), Some(ICON_SIZE)); - let av = avatar.unwrap_or_default(); - if av.starts_with("mxc") || av.is_empty() { - download_avatar(&self.baseu, self.room.id.clone(), name, av, &self.icon); - } else { - self.icon.circle(av, Some(ICON_SIZE)); - } + self.icon.circle(self.room.id.clone(), Some(name), ICON_SIZE); } pub fn widget(&self) -> gtk::EventBox { @@ -214,26 +192,3 @@ impl RoomRow { }); } } - -fn download_avatar(baseu: &Url, - rid: String, - name: String, - avatar: String, - image: &widgets::Avatar) { - - let url = baseu.clone(); - let img = image.clone(); - glib_thread!(Result, - || { - match avatar { - ref s if s.is_empty() => identicon!(&rid, name), - _ => fractal_api::util::dw_media(&url, &avatar, true, None, 40, 40), - } - }, - |rc: Result| { - if let Ok(c) = rc { - img.circle(c, Some(ICON_SIZE)); - } - } - ); -} diff --git a/fractal-matrix-api/Cargo.toml b/fractal-matrix-api/Cargo.toml index bbda41a0..12ee48b9 100644 --- a/fractal-matrix-api/Cargo.toml +++ b/fractal-matrix-api/Cargo.toml @@ -15,13 +15,8 @@ repository = "https://https://gitlab.gnome.org/World/fractal" documentation = "https://world.pages.gitlab.gnome.org/fractal/fractal_matrix_api/index.html" [dependencies] -gdk = "0.8.0" -gdk-pixbuf = "0.4.0" -#gdk-pixbuf-sys = "0.5.0" glib = "0.5.0" mime = "0.3.5" -pango = "0.4.0" -pangocairo = "0.5.0" regex = "0.2.10" reqwest = "0.8.5" serde = "1.0.43" @@ -30,7 +25,6 @@ serde_json = "1.0.16" time = "0.1.39" tree_magic = "0.2.0" url = "1.7.0" -unicode-segmentation = "1.2.0" urlencoding = "1.0.0" md5 = "0.3.7" diff --git a/fractal-matrix-api/src/backend/directory.rs b/fractal-matrix-api/src/backend/directory.rs index 15b16f65..4f2ebd40 100644 --- a/fractal-matrix-api/src/backend/directory.rs +++ b/fractal-matrix-api/src/backend/directory.rs @@ -12,6 +12,8 @@ use backend::types::BKResponse; use backend::types::Backend; use util::json_q; +use util::dw_media; +use util::cache_path; use types::Room; use types::Protocol; @@ -74,6 +76,7 @@ pub fn room_search(bk: &Backend, } let url = bk.url("publicRooms", params)?; + let base = bk.get_base_url()?; let mut attrs = json!({"limit": globals::ROOM_DIRECTORY_LIMIT}); @@ -104,13 +107,19 @@ pub fn room_search(bk: &Backend, let alias = String::from(room["canonical_alias"].as_str().unwrap_or("")); let id = String::from(room["room_id"].as_str().unwrap_or("")); let name = String::from(room["name"].as_str().unwrap_or("")); - let mut r = Room::new(id, Some(name)); + let mut r = Room::new(id.clone(), Some(name)); r.alias = Some(alias); r.avatar = Some(String::from(room["avatar_url"].as_str().unwrap_or(""))); r.topic = Some(String::from(room["topic"].as_str().unwrap_or(""))); r.n_members = room["num_joined_members"].as_i64().unwrap_or(0) as i32; r.world_readable = room["world_readable"].as_bool().unwrap_or(false); r.guest_can_join = room["guest_can_join"].as_bool().unwrap_or(false); + /* download the avatar */ + if let Some(avatar) = r.avatar.clone() { + if let Ok(dest) = cache_path(&id) { + media!(&base.clone(), &avatar, Some(&dest)).unwrap_or_default(); + } + } rooms.push(r); } diff --git a/fractal-matrix-api/src/backend/room.rs b/fractal-matrix-api/src/backend/room.rs index 7cacba4e..4560276f 100644 --- a/fractal-matrix-api/src/backend/room.rs +++ b/fractal-matrix-api/src/backend/room.rs @@ -17,6 +17,7 @@ use util::get_initial_room_messages; use util::build_url; use util::put_media; use util; +use util::cache_path; use backend::types::Backend; use backend::types::BKResponse; @@ -73,7 +74,11 @@ pub fn get_room_avatar(bk: &Backend, roomid: String) -> Result<(), Error> { match r["url"].as_str() { Some(u) => { - avatar = thumb!(&baseu, u).unwrap_or_default(); + if let Ok(dest) = cache_path(&roomid) { + avatar = media!(&baseu, u, Some(&dest)).unwrap_or_default(); + } else { + avatar = String::from(""); + } }, None => { avatar = util::get_room_avatar(&baseu, &tk, &userid, &roomid) diff --git a/fractal-matrix-api/src/backend/types.rs b/fractal-matrix-api/src/backend/types.rs index 8c106bd1..2dcfb015 100644 --- a/fractal-matrix-api/src/backend/types.rs +++ b/fractal-matrix-api/src/backend/types.rs @@ -48,7 +48,7 @@ pub enum BKCommand { GetAvatarAsync(Option, Sender), GetMedia(String), GetMediaUrl(String, Sender), - GetUserInfoAsync(String, Sender<(String, String)>), + GetUserInfoAsync(String, Option>), SendMsg(Message), SetRoom(Room), ShutDown, diff --git a/fractal-matrix-api/src/backend/user.rs b/fractal-matrix-api/src/backend/user.rs index f478dfb7..420939f1 100644 --- a/fractal-matrix-api/src/backend/user.rs +++ b/fractal-matrix-api/src/backend/user.rs @@ -300,19 +300,20 @@ pub fn get_avatar(bk: &Backend) -> Result<(), Error> { pub fn get_user_info_async(bk: &mut Backend, uid: &str, - tx: Sender<(String, String)>) + tx: Option>) -> Result<(), Error> { let baseu = bk.get_base_url()?; let u = String::from(uid); if let Some(info) = bk.user_info_cache.get(&u) { - let tx = tx.clone(); - let info = info.clone(); - thread::spawn(move || { - let i = info.lock().unwrap().clone(); - tx.send(i).unwrap(); - }); + if let Some(tx) = tx.clone() { + let info = info.clone(); + thread::spawn(move || { + let i = info.lock().unwrap().clone(); + tx.send(i).unwrap(); + }); + } return Ok(()) } @@ -324,13 +325,17 @@ pub fn get_user_info_async(bk: &mut Backend, let i0 = info.lock(); match get_user_avatar(&baseu, &u) { Ok(info) => { - tx.send(info.clone()).unwrap(); - let mut i = i0.unwrap(); - i.0 = info.0; - i.1 = info.1; + if let Some(tx) = tx.clone() { + tx.send(info.clone()).unwrap(); + let mut i = i0.unwrap(); + i.0 = info.0; + i.1 = info.1; + } } Err(_) => { - tx.send((String::new(), String::new())).unwrap(); + if let Some(tx) = tx.clone() { + tx.send((String::new(), String::new())).unwrap(); + } } }; }); @@ -351,12 +356,10 @@ pub fn get_avatar_async(bk: &Backend, member: Option, tx: Sender let m = member.unwrap(); let uid = m.uid.clone(); - let alias = m.get_alias(); let avatar = m.avatar.clone(); semaphore!(bk.limit_threads, { match get_user_avatar_img(&baseu, uid, - alias, avatar.unwrap_or_default()) { Ok(fname) => { tx.send(fname.clone()).unwrap(); } Err(_) => { tx.send(String::new()).unwrap(); } diff --git a/fractal-matrix-api/src/util.rs b/fractal-matrix-api/src/util.rs index e8ecd6d6..9dd9f574 100644 --- a/fractal-matrix-api/src/util.rs +++ b/fractal-matrix-api/src/util.rs @@ -3,22 +3,8 @@ extern crate url; extern crate reqwest; extern crate regex; extern crate serde_json; -extern crate cairo; -extern crate pango; -extern crate pangocairo; -extern crate gdk; -extern crate gdk_pixbuf; extern crate mime; extern crate tree_magic; -extern crate unicode_segmentation; - -use self::unicode_segmentation::UnicodeSegmentation; - -use self::pango::LayoutExt; - -use self::gdk_pixbuf::Pixbuf; -use self::gdk_pixbuf::PixbufExt; -use self::gdk::ContextExt; use self::regex::Regex; @@ -35,8 +21,6 @@ use std::fs::create_dir_all; use std::io::prelude::*; use std::collections::HashSet; -use std::collections::hash_map::DefaultHasher; -use std::hash::{Hash, Hasher}; use std::time::Duration as StdDuration; @@ -51,14 +35,6 @@ use self::mime::Mime; use globals; - -#[allow(dead_code)] -pub enum AvatarMode { - Rect, - Circle, -} - - macro_rules! semaphore { ($cv: expr, $blk: block) => {{ let thread_count = $cv.clone(); @@ -88,13 +64,6 @@ macro_rules! semaphore { }} } - -#[macro_export] -macro_rules! identicon { - ($userid: expr, $name: expr) => { draw_identicon($userid, $name, AvatarMode::Circle) } -} - - // from https://stackoverflow.com/a/43992218/1592377 #[macro_export] macro_rules! clone { @@ -649,10 +618,10 @@ pub fn get_user_avatar(baseu: &Url, userid: &str) -> Result<(String, String), Er let img = dw_media(baseu, &url, true, Some(&dest), 64, 64)?; Ok((name.clone(), img)) }, - None => Ok((name.clone(), identicon!(userid, name)?)), + None => Ok((name.clone(), String::from(""))), } } - Err(_) => Ok((String::from(userid), identicon!(userid, String::from(&userid[1..2]))?)), + Err(_) => Ok((String::from(userid), String::from(""))), } } @@ -682,106 +651,23 @@ pub fn get_room_avatar(base: &Url, tk: &str, userid: &str, roomid: &str) -> Resu }; let mut fname = match members.count() { - 1 => thumb!(&base, m1).unwrap_or_default(), + 1 => { + if let Ok(dest) = cache_path(&roomid) { + media!(&base, m1, Some(&dest)).unwrap_or_default() + } else { + String::new() + } + }, _ => String::new(), }; if fname.is_empty() { - let roomname = match calculate_room_name(&st, userid)?{ - Some(ref name) => { name.clone() }, - None => { "X".to_string() }, - }; - fname = identicon!(roomid, roomname)?; + fname = String::from(""); } Ok(fname) } -struct Color { - r: i32, - g: i32, - b: i32, -} - -pub fn calculate_hash(t: &T) -> u64 { - let mut s = DefaultHasher::new(); - t.hash(&mut s); - s.finish() -} - -pub fn get_initials(name: String) -> Result { - let name = name.trim_right_matches(" (IRC)"); - let mut words = name.unicode_words(); - let first = words - .next() - .and_then(|w| UnicodeSegmentation::graphemes(w, true).next()) - .unwrap_or_default(); - let second = words - .next() - .and_then(|w| UnicodeSegmentation::graphemes(w, true).next()) - .unwrap_or_default(); - let initials = format!( "{}{}", first, second); - - Ok(initials) -} - -pub fn draw_identicon(fname: &str, name: String, mode: AvatarMode) -> Result { - // Our color palette with a darker and a muted variant for each one - let colors = [ - [Color { r: 206, g: 77, b: 205, }, Color { r: 251, g: 224, b: 251, }], - [Color { r: 121, g: 81, b: 192, }, Color { r: 231, g: 218, b: 251, }], - [Color { r: 78, g: 99, b: 201, }, Color { r: 207, g: 215, b: 248, }], - [Color { r: 66, g: 160, b: 243, }, Color { r: 214, g: 234, b: 252, }], - [Color { r: 70, g: 189, b: 158, }, Color { r: 212, g: 248, b: 239, }], - [Color { r: 117, g: 184, b: 45, }, Color { r: 220, g: 247, b: 191, }], - [Color { r: 235, g: 121, b: 10, }, Color { r: 254, g: 235, b: 218, }], - [Color { r: 227, g: 61, b: 34, }, Color { r: 251, g: 219, b: 211, }], - [Color { r: 109, g: 109, b: 109, }, Color { r: 219, g: 219, b: 219 }], - ]; - - let fname = cache_path(fname)?; - - let image = cairo::ImageSurface::create(cairo::Format::ARgb32, 60, 60)?; - let g = cairo::Context::new(&image); - - let color_index = calculate_hash(&fname) as usize % colors.len() as usize; - let bg_c = &colors[color_index][0]; - g.set_source_rgba(bg_c.r as f64 / 256., bg_c.g as f64 / 256., bg_c.b as f64 / 256., 1.); - - match mode { - AvatarMode::Rect => g.rectangle(0., 0., 60., 60.), - AvatarMode::Circle => { - g.arc(30.0, 30.0, 30.0, 0.0, 2.0 * 3.14159); - g.fill(); - } - }; - - let fg_c = &colors[color_index][1]; - g.set_source_rgba(fg_c.r as f64 / 256., fg_c.g as f64 / 256., fg_c.b as f64 / 256., 1.); - - if !name.is_empty() { - let initials = get_initials(name)?.to_uppercase(); - - let layout = pangocairo::functions::create_layout(&g).unwrap(); - let fontdesc = pango::FontDescription::from_string("Cantarell Ultra-Bold 26"); - layout.set_font_description(&fontdesc); - layout.set_text(&initials); - // Move to center of the background shape we drew, - // offset by half the size of the glyph - let bx = image.get_width(); - let by = image.get_height(); - let (ox, oy) = layout.get_pixel_size(); - g.translate((bx - ox) as f64/2., (by - oy) as f64/2.); - // Finally draw the glyph - pangocairo::functions::show_layout(&g, &layout); - } - - let mut buffer = File::create(&fname)?; - image.write_to_png(&mut buffer)?; - - Ok(fname) -} - pub fn calculate_room_name(roomst: &JsonValue, userid: &str) -> Result, Error> { // looking for "m.room.name" event @@ -963,27 +849,6 @@ pub fn build_url(base: &Url, path: &str, params: Vec<(&str, String)>) -> Result< Ok(url) } -pub fn circle_image(fname: String) -> Result { - use std::f64::consts::PI; - - let pb = Pixbuf::new_from_file_at_scale(&fname, 40, -1, true)?; - let image = cairo::ImageSurface::create(cairo::Format::ARgb32, 40, 40)?; - let g = cairo::Context::new(&image); - g.set_antialias(cairo::Antialias::Best); - let hpos: f64 = (40.0 - (pb.get_height()) as f64) / 2.0; - g.set_source_pixbuf(&pb, 0.0, hpos); - - g.arc(20.0, 20.0, 20.0, 0.0, 2.0 * PI); - g.clip(); - - g.paint(); - - let mut buffer = File::create(&fname)?; - image.write_to_png(&mut buffer)?; - - Ok(fname) -} - pub fn cache_path(name: &str) -> Result { let mut path = match glib::get_user_cache_dir() { Some(path) => path, @@ -1019,9 +884,9 @@ pub fn cache_dir_path(dir: &str, name: &str) -> Result { Ok(path.into_os_string().into_string()?) } -pub fn get_user_avatar_img(baseu: &Url, userid: String, alias: String, avatar: String) -> Result { +pub fn get_user_avatar_img(baseu: &Url, userid: String, avatar: String) -> Result { if avatar.is_empty() { - return identicon!(&userid, alias); + return Ok(String::from("")); } let dest = cache_path(&userid)?;