Browse Source

avatar: refactor avatar loading and caching

* use the same method everywhere to display an avatar
* move code to generate letter avatars to a separate crate
* use the generated avatar as a fallback till the real avatar is
avaibile
* remove gtk dependecies from fractal-matrix-api

Note: The avatar in room details dialog is borken but the dialog will be
replaced soon. Also updating the username in the room history doesn't
work, the room history needs a refactor to resolve some issues.
environments/review-jsparber-h-cxnwl8/deployments/1
Julian Sparber 8 years ago
parent
commit
8993f5fdfd
  1. 20
      Cargo.lock
  2. 3
      fractal-gtk/Cargo.toml
  3. 13
      fractal-gtk/src/appop/account.rs
  4. 2
      fractal-gtk/src/appop/notify.rs
  5. 8
      fractal-gtk/src/appop/room.rs
  6. 18
      fractal-gtk/src/appop/user.rs
  7. 9
      fractal-gtk/src/cache.rs
  8. 130
      fractal-gtk/src/widgets/avatar.rs
  9. 81
      fractal-gtk/src/widgets/member.rs
  10. 32
      fractal-gtk/src/widgets/message.rs
  11. 69
      fractal-gtk/src/widgets/room.rs
  12. 4
      fractal-gtk/src/widgets/roomlist.rs
  13. 53
      fractal-gtk/src/widgets/roomrow.rs
  14. 6
      fractal-matrix-api/Cargo.toml
  15. 11
      fractal-matrix-api/src/backend/directory.rs
  16. 7
      fractal-matrix-api/src/backend/room.rs
  17. 2
      fractal-matrix-api/src/backend/types.rs
  18. 31
      fractal-matrix-api/src/backend/user.rs
  19. 159
      fractal-matrix-api/src/util.rs

20
Cargo.lock generated

@ -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)" = "<none>"
"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"

3
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"

13
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<String>) {
pub fn show_avatar(&self) {
let stack = self.ui.builder
.get_object::<gtk::Stack>("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<String>) {

2
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() {

8
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<String>, size: i32) {
pub fn set_current_room_avatar(&self, _avatar: Option<String>, _size: i32) {
let image = self.ui.builder
.get_object::<gtk::Box>("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::<gtk::Image>("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<String>) {

18
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();

9
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<BKCommand>, name: String) {
let _ = backend.send(BKCommand::GetUserInfoAsync(name.clone(), None));
}

130
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<i32>) -> gtk::Box;
fn circle_avatar(path: String, size: Option<i32>) -> gtk::Box;
fn clean(&self);
fn create_da(&self, size: Option<i32>) -> DrawingArea;
fn circle(&self, path: String, size: Option<i32>);
fn default(&self, icon: String, size: Option<i32>);
fn circle(&self, uid: String, username: Option<String>, size: i32);
}
impl AvatarExt for gtk::Box {
@ -51,102 +53,72 @@ impl AvatarExt for gtk::Box {
b
}
fn circle_avatar(path: String, size: Option<i32>) -> 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<String>, size: i32) {
struct Data {
cache : Result<Pixbuf, glib::Error>,
fallback: cairo::ImageSurface
}
b
}
fn default(&self, icon: String, size: Option<i32>) {
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<i32>) {
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<RefCell<Data>> = 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<i32>) -> gtk::DrawingArea {
let s = size.unwrap_or(10);

81
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<BKCommand>,
img: widgets::Avatar,
m: Option<Member>,
size: i32, tries: i32) {
if tries <= 0 {
return;
}
let (tx, rx): (Sender<String>, Receiver<String>) = 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<BKCommand>,
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)
}
});
}

32
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
}

69
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: &gtk::DrawingArea, size: i32, rid: String, name: String) {
let da = da.clone();
glib_thread!(Result<String, Error>,
|| {
identicon!(&rid, name)
},
|rc: Result<String, Error>| {
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)
});
}
}
);
}

4
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);

53
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<String, Error>,
|| {
match avatar {
ref s if s.is_empty() => identicon!(&rid, name),
_ => fractal_api::util::dw_media(&url, &avatar, true, None, 40, 40),
}
},
|rc: Result<String, Error>| {
if let Ok(c) = rc {
img.circle(c, Some(ICON_SIZE));
}
}
);
}

6
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"

11
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);
}

7
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)

2
fractal-matrix-api/src/backend/types.rs

@ -48,7 +48,7 @@ pub enum BKCommand {
GetAvatarAsync(Option<Member>, Sender<String>),
GetMedia(String),
GetMediaUrl(String, Sender<String>),
GetUserInfoAsync(String, Sender<(String, String)>),
GetUserInfoAsync(String, Option<Sender<(String, String)>>),
SendMsg(Message),
SetRoom(Room),
ShutDown,

31
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<Sender<(String, String)>>)
-> 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<Member>, tx: Sender<String>
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(); }

159
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: Hash>(t: &T) -> u64 {
let mut s = DefaultHasher::new();
t.hash(&mut s);
s.finish()
}
pub fn get_initials(name: String) -> Result<String, Error> {
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<String, Error> {
// 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<Option<String>, 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<String, Error> {
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<String, Error> {
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<String, Error> {
Ok(path.into_os_string().into_string()?)
}
pub fn get_user_avatar_img(baseu: &Url, userid: String, alias: String, avatar: String) -> Result<String, Error> {
pub fn get_user_avatar_img(baseu: &Url, userid: String, avatar: String) -> Result<String, Error> {
if avatar.is_empty() {
return identicon!(&userid, alias);
return Ok(String::from(""));
}
let dest = cache_path(&userid)?;

Loading…
Cancel
Save