power_level badges for users in room settings

Used Avatar::circle badge parameter in MemberBox

rustfmt

Show power level next to the matrix user ID

Fix mxid & power level forcing window width

new design closer to @bertob's proposal

Gap around avatar's badge

Power level roles translation

css-themed avatar's colored dot

Reduced badge height
parent ef8f3c8e
Pipeline #69457 failed with stages
in 5 minutes and 59 seconds
......@@ -299,3 +299,22 @@ stack.titlebar:not(headerbar) > box > separator {
margin-bottom: 8px;
color: @theme_selected_bg_color;
}
.badge {
border-radius: 5px;
padding: 0.1em 5px;
font-size: 0.8em;
}
.badge-circle {
border-radius: 99999px;
}
.badge-gold {
background-color: #E5A50A;
}
.badge-silver {
background-color: #99A8B0;
}
.badge-grey {
background-color: #D9D9D9;
}
\ No newline at end of file
......@@ -456,7 +456,7 @@ impl AppOp {
avatar.add(&w);
let uid = self.uid.clone().unwrap_or_default();
let data = w.circle(uid.clone(), self.username.clone(), 100);
let data = w.circle(uid.clone(), self.username.clone(), 100, None, None);
download_to_cache(self.backend.clone(), uid.clone(), data.clone());
/* FIXME: hack to make the avatar drawing area clickable*/
......
......@@ -52,7 +52,7 @@ impl AppOp {
let w = widgets::Avatar::avatar_new(Some(40));
let uid = self.uid.clone().unwrap_or_default();
let data = w.circle(uid.clone(), self.username.clone(), 40);
let data = w.circle(uid.clone(), self.username.clone(), 40, None, None);
download_to_cache(self.backend.clone(), uid.clone(), data.clone());
avatar.add(&w);
......@@ -66,7 +66,7 @@ impl AppOp {
Some(_) => {
let w = widgets::Avatar::avatar_new(Some(24));
let uid = self.uid.clone().unwrap_or_default();
let data = w.circle(uid.clone(), self.username.clone(), 24);
let data = w.circle(uid.clone(), self.username.clone(), 24, None, None);
download_to_cache(self.backend.clone(), uid.clone(), data.clone());
eb.add(&w);
......
......@@ -11,7 +11,13 @@ use gtk;
use gtk::prelude::*;
pub use gtk::DrawingArea;
pub type Avatar = gtk::Box;
pub enum AvatarBadgeColor {
Gold,
Silver,
Grey,
}
pub type Avatar = gtk::Overlay;
pub struct AvatarData {
uid: String,
......@@ -39,13 +45,20 @@ impl AvatarData {
}
pub trait AvatarExt {
fn avatar_new(size: Option<i32>) -> gtk::Box;
fn avatar_new(size: Option<i32>) -> gtk::Overlay;
fn clean(&self);
fn create_da(&self, size: Option<i32>) -> DrawingArea;
fn circle(&self, uid: String, username: Option<String>, size: i32) -> Rc<RefCell<AvatarData>>;
fn circle(
&self,
uid: String,
username: Option<String>,
size: i32,
badge: Option<AvatarBadgeColor>,
badge_size: Option<i32>,
) -> Rc<RefCell<AvatarData>>;
}
impl AvatarExt for gtk::Box {
impl AvatarExt for gtk::Overlay {
fn clean(&self) {
for ch in self.get_children().iter() {
self.remove(ch);
......@@ -57,14 +70,14 @@ impl AvatarExt for gtk::Box {
let s = size.unwrap_or(40);
da.set_size_request(s, s);
self.pack_start(&da, true, true, 0);
self.add(&da);
self.show_all();
da
}
fn avatar_new(size: Option<i32>) -> gtk::Box {
let b = gtk::Box::new(gtk::Orientation::Horizontal, 0);
fn avatar_new(size: Option<i32>) -> gtk::Overlay {
let b = gtk::Overlay::new();
b.create_da(size);
b.show_all();
if let Some(style) = b.get_style_context() {
......@@ -73,8 +86,20 @@ impl AvatarExt for gtk::Box {
b
}
fn circle(&self, uid: String, username: Option<String>, size: i32) -> Rc<RefCell<AvatarData>> {
/// # Arguments
/// * `uid` - Matrix ID
/// * `username` - Full name
/// * `size` - Size of the avatar
/// * `badge_color` - Badge color. None for no badge
/// * `badge_size` - Badge size. None for size / 3
fn circle(
&self,
uid: String,
username: Option<String>,
size: i32,
badge_color: Option<AvatarBadgeColor>,
badge_size: Option<i32>,
) -> Rc<RefCell<AvatarData>> {
self.clean();
let da = self.create_da(Some(size));
let path = cache_path(&uid).unwrap_or_default();
......@@ -90,6 +115,25 @@ impl AvatarExt for gtk::Box {
let fallback = letter_avatar::generate::new(uid.clone(), username, size as f64)
.expect("this function should never fail");
// Power level badge setup
let has_badge = badge_color.is_some();
let badge_size = badge_size.unwrap_or(size / 3);
if let Some(color) = badge_color {
let badge = gtk::Box::new(gtk::Orientation::Vertical, 0);
badge.set_size_request(badge_size, badge_size);
badge.set_valign(gtk::Align::Start);
badge.set_halign(gtk::Align::End);
if let Some(style) = badge.get_style_context() {
style.add_class("badge-circle");
style.add_class(match color {
AvatarBadgeColor::Gold => "badge-gold",
AvatarBadgeColor::Silver => "badge-silver",
AvatarBadgeColor::Grey => "badge-grey",
});
}
self.add_overlay(&badge);
}
let data = AvatarData {
uid: uid.clone(),
username: uname,
......@@ -109,19 +153,32 @@ impl AvatarExt for gtk::Box {
g.set_antialias(cairo::Antialias::Best);
{
let data = user_cache.borrow();
if let Some(ref pb) = data.cache {
let context = da.get_style_context().unwrap();
gtk::render_background(&context, g, 0.0, 0.0, width, height);
g.set_fill_rule(cairo::FillRule::EvenOdd);
g.arc(
width / 2.0,
height / 2.0,
width.min(height) / 2.0,
0.0,
2.0 * PI,
);
if has_badge {
g.clip_preserve();
g.new_sub_path();
let badge_radius = badge_size as f64 / 2.0;
g.arc(
width / 2.0,
height / 2.0,
width.min(height) / 2.0,
width - badge_radius,
badge_radius,
badge_radius * 1.4,
0.0,
2.0 * PI,
);
g.clip();
}
g.clip();
let data = user_cache.borrow();
if let Some(ref pb) = data.cache {
let context = da.get_style_context().unwrap();
gtk::render_background(&context, g, 0.0, 0.0, width, height);
let hpos: f64 = (width - (pb.get_height()) as f64) / 2.0;
g.set_source_pixbuf(&pb, 0.0, hpos);
......@@ -155,60 +212,3 @@ fn load_pixbuf(path: &str, size: i32) -> Option<Pixbuf> {
None
}
}
pub enum AdminColor {
Gold,
Silver,
}
pub fn admin_badge(kind: AdminColor, size: Option<i32>) -> gtk::DrawingArea {
let s = size.unwrap_or(10);
let da = DrawingArea::new();
da.set_size_request(s, s);
let color = match kind {
AdminColor::Gold => (237.0, 212.0, 0.0),
AdminColor::Silver => (186.0, 186.0, 186.0),
};
let border = match kind {
AdminColor::Gold => (107.0, 114.0, 0.0),
AdminColor::Silver => (137.0, 137.0, 137.0),
};
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);
g.set_source_rgba(color.0 / 256.0, color.1 / 256.0, color.2 / 256.0, 1.);
g.arc(
width / 2.0,
height / 2.0,
width.min(height) / 2.5,
0.0,
2.0 * PI,
);
g.fill();
g.set_source_rgba(border.0 / 256.0, border.1 / 256.0, border.2 / 256.0, 0.5);
g.arc(
width / 2.0,
height / 2.0,
width.min(height) / 2.5,
0.0,
2.0 * PI,
);
g.stroke();
Inhibit(false)
});
da
}
......@@ -54,10 +54,17 @@ impl<'a> MemberBox<'a> {
}
let avatar = widgets::Avatar::avatar_new(Some(globals::USERLIST_ICON_SIZE));
let badge = match self.op.member_level(self.member) {
100 => Some(widgets::AvatarBadgeColor::Gold),
50...100 => Some(widgets::AvatarBadgeColor::Silver),
_ => None,
};
let data = avatar.circle(
self.member.uid.clone(),
Some(alias.clone()),
globals::USERLIST_ICON_SIZE,
badge,
None,
);
let member_id = self.member.uid.clone();
download_to_cache(backend.clone(), member_id.clone(), data.clone());
......@@ -71,24 +78,7 @@ impl<'a> MemberBox<'a> {
v.pack_start(&uid, true, true, 0);
}
match self.op.member_level(self.member) {
100 => {
let overlay = gtk::Overlay::new();
overlay.add(&avatar);
overlay.add_overlay(&widgets::admin_badge(widgets::AdminColor::Gold, None));
w.add(&overlay);
}
50 => {
let overlay = gtk::Overlay::new();
overlay.add(&avatar);
overlay.add_overlay(&widgets::admin_badge(widgets::AdminColor::Silver, None));
w.add(&overlay);
}
_ => {
w.add(&avatar);
}
}
w.add(&avatar);
w.add(&v);
event_box.add(&w);
......@@ -113,6 +103,8 @@ impl<'a> MemberBox<'a> {
self.member.uid.clone(),
Some(self.member.get_alias()),
globals::PILL_ICON_SIZE,
None,
None,
);
let member_id = self.member.uid.clone();
download_to_cache(backend.clone(), member_id.clone(), data.clone());
......
use fractal_api::clone;
use std::cell::RefCell;
use std::collections::hash_map::HashMap;
use std::rc::Rc;
use glib::signal;
......@@ -9,7 +10,7 @@ use gtk::prelude::*;
use crate::i18n::i18n;
use crate::types::Member;
use crate::widgets;
use crate::widgets::avatar::AvatarExt;
use crate::widgets::avatar::{AvatarBadgeColor, AvatarExt};
#[derive(Debug, Clone)]
pub struct MembersList {
......@@ -17,15 +18,21 @@ pub struct MembersList {
search_entry: gtk::SearchEntry,
error: gtk::Label,
members: Vec<Member>,
power_levels: HashMap<String, i32>,
}
impl MembersList {
pub fn new(m: Vec<Member>, entry: gtk::SearchEntry) -> MembersList {
pub fn new(
m: Vec<Member>,
power_levels: HashMap<String, i32>,
entry: gtk::SearchEntry,
) -> MembersList {
MembersList {
container: gtk::ListBox::new(),
error: gtk::Label::new(None),
members: m,
search_entry: entry,
power_levels: power_levels,
}
}
......@@ -35,7 +42,11 @@ impl MembersList {
let b = gtk::Box::new(gtk::Orientation::Vertical, 0);
b.set_hexpand(true);
b.pack_start(&self.container, true, true, 0);
add_rows(self.container.clone(), self.members.clone());
add_rows(
self.container.clone(),
self.members.clone(),
self.power_levels.clone(),
);
self.error
.get_style_context()?
.add_class("no_member_search");
......@@ -105,11 +116,11 @@ impl MembersList {
}
}
fn create_row(member: Member) -> Option<gtk::ListBoxRow> {
fn create_row(member: Member, power_level: Option<i32>) -> Option<gtk::ListBoxRow> {
let row = gtk::ListBoxRow::new();
row.connect_draw(clone!(member => move |w, _| {
if w.get_child().is_none() {
w.add(&load_row_content(member.clone()));
w.add(&load_row_content(member.clone(), power_level));
}
gtk::Inhibit(false)
}));
......@@ -120,40 +131,93 @@ fn create_row(member: Member) -> Option<gtk::ListBoxRow> {
}
/* creating the row is quite slow, therefore we have a small delay when scrolling the members list */
fn load_row_content(member: Member) -> gtk::Box {
fn load_row_content(member: Member, power_level: Option<i32>) -> gtk::Box {
let b = gtk::Box::new(gtk::Orientation::Horizontal, 12);
// Power level badge colour
let pl = power_level.unwrap_or_default();
let badge_color = match pl {
100 => Some(AvatarBadgeColor::Gold),
50...99 => Some(AvatarBadgeColor::Silver),
1...49 => Some(AvatarBadgeColor::Grey),
_ => None,
};
// Avatar
let avatar = widgets::Avatar::avatar_new(Some(40));
avatar.circle(member.uid.clone(), member.alias.clone(), 40);
let user_box = gtk::Box::new(gtk::Orientation::Vertical, 0);
avatar.circle(
member.uid.clone(),
member.alias.clone(),
40,
badge_color,
None,
);
let user_box = gtk::Box::new(gtk::Orientation::Vertical, 0); // Name & badge + Matrix ID
let username_box = gtk::Box::new(gtk::Orientation::Horizontal, 0); // Name + badge
let username = gtk::Label::new(Some(member.get_alias().as_str()));
let uid = gtk::Label::new(Some(member.uid.as_str()));
username.set_xalign(0.);
username.set_margin_end(5);
username.set_ellipsize(pango::EllipsizeMode::End);
username_box.pack_start(&username, false, false, 0);
// Power level badge colour
let pl = power_level.unwrap_or_default();
if pl > 0 && pl <= 100 {
let badge_data = match pl {
100 => (i18n("Admin"), "badge-gold"),
50...99 => (i18n("Moderator"), "badge-silver"),
1...49 => (i18n("Privileged"), "badge-grey"),
_ => panic!(),
};
let badge_wid = gtk::Label::new(Some(format!("{} ({})", badge_data.0, pl).as_str()));
badge_wid.set_valign(gtk::Align::Center);
if let Some(style) = badge_wid.get_style_context() {
style.add_class("badge");
style.add_class(badge_data.1);
}
username_box.pack_start(&badge_wid, false, false, 0);
}
// matrix ID + power level
let uid = gtk::Label::new(Some(member.uid.as_str()));
uid.set_xalign(0.);
uid.set_line_wrap(true);
uid.set_line_wrap_mode(pango::WrapMode::Char);
if let Some(style) = uid.get_style_context() {
style.add_class("small-font");
style.add_class("dim-label");
}
b.set_margin_start(12);
b.set_margin_end(12);
b.set_margin_top(6);
b.set_margin_bottom(6);
user_box.pack_start(&username, true, true, 0);
user_box.pack_start(&uid, false, false, 0);
user_box.pack_start(&username_box, true, true, 0);
user_box.pack_start(&uid, true, true, 0);
/* we don't have this state yet
* let state = gtk::Label::new();
* user_box.pack_end(&state, true, true, 0); */
b.pack_start(&avatar, false, true, 0);
b.pack_start(&user_box, false, true, 0);
b.pack_start(&user_box, true, true, 0);
b.show_all();
b
}
fn add_rows(container: gtk::ListBox, members: Vec<Member>) -> Option<usize> {
fn add_rows(
container: gtk::ListBox,
members: Vec<Member>,
power_levels: HashMap<String, i32>,
) -> Option<usize> {
/* Load just enough members to fill atleast the visible list */
for member in members.iter() {
container.insert(&create_row(member.clone())?, -1);
let power_level = match power_levels.get(&member.uid) {
Some(pl) => Some(*pl),
None => None,
};
container.insert(&create_row(member.clone(), power_level)?, -1);
}
None
}
......
......@@ -166,7 +166,13 @@ impl MessageBox {
let alias = msg.sender_name.clone();
let avatar = widgets::Avatar::avatar_new(Some(globals::MSG_ICON_SIZE));
let data = avatar.circle(uid.clone(), alias.clone(), globals::MSG_ICON_SIZE);
let data = avatar.circle(
uid.clone(),
alias.clone(),
globals::MSG_ICON_SIZE,
None,
None,
);
if let Some(name) = alias {
self.username.set_text(&name);
} else {
......
......@@ -23,9 +23,8 @@ mod sourceview_entry;
pub use self::address::Address;
pub use self::address::AddressType;
pub use self::autocomplete::Autocomplete;
pub use self::avatar::admin_badge;
pub use self::avatar::AdminColor;
pub use self::avatar::Avatar;
pub use self::avatar::AvatarBadgeColor;
pub use self::avatar::AvatarData;
pub use self::avatar::AvatarExt;
pub use self::divider::NewMessageDivider;
......
......@@ -47,7 +47,7 @@ impl<'a> RoomBox<'a> {
let widget_box = gtk::Box::new(gtk::Orientation::Horizontal, 0);
let avatar = widgets::Avatar::avatar_new(Some(AVATAR_SIZE));
avatar.circle(room.id.clone(), room.name.clone(), AVATAR_SIZE);
avatar.circle(room.id.clone(), room.name.clone(), AVATAR_SIZE, None, None);
widget_box.pack_start(&avatar, false, false, 18);
let details_box = gtk::Box::new(gtk::Orientation::Vertical, 6);
......
......@@ -415,7 +415,13 @@ impl RoomSettings {
}
let image = widgets::Avatar::avatar_new(Some(100));
let data = image.circle(self.room.id.clone(), self.room.name.clone(), 100);
let data = image.circle(
self.room.id.clone(),
self.room.name.clone(),
100,
None,
None,
);
download_to_cache(self.backend.clone(), self.room.id.clone(), data);
if edit {
......@@ -616,7 +622,8 @@ impl RoomSettings {
)
.as_str(),
);
let list = widgets::MembersList::new(members.clone(), entry);
let list =
widgets::MembersList::new(members.clone(), self.room.power_levels.clone(), entry);
let w = list.create()?;
b.add(&w);
self.members_list = Some(list);
......
......@@ -65,7 +65,7 @@ impl RoomRow {
notifications.hide();
}
icon.circle(room.id.clone(), Some(name), ICON_SIZE);
icon.circle(room.id.clone(), Some(name), ICON_SIZE, None, None);
let rr = RoomRow {
room,
......@@ -130,7 +130,7 @@ impl RoomRow {
let name = self.room.name.clone().unwrap_or("...".to_string());
self.icon
.circle(self.room.id.clone(), Some(name), ICON_SIZE);
.circle(self.room.id.clone(), Some(name), ICON_SIZE, None, None);
}
pub fn widget(&self) -> gtk::ListBoxRow {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment