refactor: more avatar-specific stuff

This commit is contained in:
MeexReay 2025-06-30 01:38:04 +03:00
parent 00cc5b2e86
commit 5266d1190f
5 changed files with 157 additions and 40 deletions

8
Cargo.lock generated
View File

@ -244,7 +244,7 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "bRAC"
version = "0.1.5+2.0"
version = "0.1.6+2.0"
dependencies = [
"chrono",
"clap",
@ -720,6 +720,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
@ -788,8 +789,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
@ -2030,7 +2034,9 @@ dependencies = [
"base64",
"bytes",
"encoding_rs",
"futures-channel",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",

View File

@ -1,6 +1,6 @@
[package]
name = "bRAC"
version = "0.1.5+2.0"
version = "0.1.6+2.0"
edition = "2021"
[dependencies]
@ -21,7 +21,7 @@ notify-rust = { version = "4.11.7", optional = true }
gdk-pixbuf = { version = "0.3.0", optional = true } # DO NOT UPDATE
winapi = { version = "0.3.9", optional = true, features = ["wincon", "winuser"] }
tungstenite = "0.27.0"
reqwest = "0.12.20"
reqwest = { version = "0.12.20", features = ["blocking"] }
[build-dependencies]
winresource = { version = "0.1.20", optional = true }

View File

@ -63,6 +63,8 @@ pub struct Config {
pub new_ui_enabled: bool,
#[serde(default)]
pub debug_logs: bool,
#[serde(default)]
pub avatar: Option<String>,
}
#[cfg(target_os = "windows")]
@ -154,6 +156,8 @@ pub struct Args {
#[arg(long)]
pub proxy: Option<String>,
#[arg(long)]
pub avatar: Option<String>,
#[arg(long)]
pub debug_logs: bool,
}
@ -207,6 +211,9 @@ impl Args {
if let Some(v) = self.new_ui_enabled {
config.new_ui_enabled = v
}
if let Some(v) = self.avatar.clone() {
config.avatar = Some(v)
}
if self.debug_logs {
config.debug_logs = true
}

View File

@ -1,6 +1,11 @@
// TODO: REFACTOR THIS SHIT!!!!!!!!!!!!!!!
use std::cell::RefCell;
use std::collections::HashMap;
use std::error::Error;
use std::hash::{DefaultHasher, Hasher};
use std::sync::atomic::AtomicU64;
use std::sync::RwLockWriteGuard;
use std::sync::{atomic::Ordering, mpsc::channel, Arc, RwLock};
use std::thread;
use std::time::{Duration, SystemTime};
@ -25,6 +30,8 @@ use gtk::{
Orientation, Overlay, Picture, ScrolledWindow, Settings, Window,
};
use crate::chat::grab_avatar;
use super::{
config::{
default_konata_size, default_max_messages, default_oof_update_time, default_update_time,
@ -46,6 +53,7 @@ struct UiModel {
notifications: Arc<RwLock<Vec<String>>>,
default_avatar: Pixbuf,
avatars: Arc<RwLock<HashMap<u64, Pixbuf>>>,
latest_sign: Arc<AtomicU64>
}
thread_local!(
@ -176,6 +184,7 @@ fn open_settings(ctx: Arc<Context>, app: &Application) {
let message_format_entry =
gui_entry_setting!("Message Format", message_format, ctx, settings_vbox);
let proxy_entry = gui_option_entry_setting!("Socks5 proxy", proxy, ctx, settings_vbox);
let avatar_entry = gui_option_entry_setting!("Avatar", avatar, ctx, settings_vbox);
let update_time_entry =
gui_usize_entry_setting!("Update Time", update_time, ctx, settings_vbox);
let oof_update_time_entry = gui_usize_entry_setting!(
@ -257,6 +266,8 @@ fn open_settings(ctx: Arc<Context>, app: &Application) {
remove_gui_shit_entry,
#[weak]
new_ui_enabled_entry,
#[weak]
avatar_entry,
move |_| {
let config = Config {
host: host_entry.text().to_string(),
@ -269,6 +280,15 @@ fn open_settings(ctx: Arc<Context>, app: &Application) {
Some(name)
}
},
avatar: {
let avatar = avatar_entry.text().to_string();
if avatar.is_empty() {
None
} else {
Some(avatar)
}
},
message_format: message_format_entry.text().to_string(),
update_time: {
let update_time = update_time_entry.text();
@ -380,12 +400,15 @@ fn open_settings(ctx: Arc<Context>, app: &Application) {
remove_gui_shit_entry,
#[weak]
new_ui_enabled_entry,
#[weak]
avatar_entry,
move |_| {
let config = Config::default();
ctx.set_config(&config);
save_config(get_config_path(), &config);
host_entry.set_text(&config.host);
name_entry.set_text(&config.name.unwrap_or_default());
avatar_entry.set_text(&config.avatar.unwrap_or_default());
proxy_entry.set_text(&config.proxy.unwrap_or_default());
message_format_entry.set_text(&config.message_format);
update_time_entry.set_text(&config.update_time.to_string());
@ -791,6 +814,7 @@ fn build_ui(ctx: Arc<Context>, app: &Application) -> UiModel {
notifications: Arc::new(RwLock::new(Vec::<String>::new())),
default_avatar: load_pixbuf(include_bytes!("images/avatar.png")).unwrap(),
avatars: Arc::new(RwLock::new(HashMap::new())),
latest_sign: Arc::new(AtomicU64::new(0))
}
}
@ -843,6 +867,7 @@ fn setup(_: &Application, ctx: Arc<Context>, ui: UiModel) {
move || {
while let Ok((messages, clear)) = receiver.recv() {
let ctx = ctx.clone();
timeout_add_once(Duration::ZERO, move || {
GLOBAL.with(|global| {
if let Some(ui) = &*global.borrow() {
@ -852,6 +877,7 @@ fn setup(_: &Application, ctx: Arc<Context>, ui: UiModel) {
}
}
for message in messages.iter() {
prepare_avatar(&mut ui.avatars.write().unwrap(), message); // TODO: fuck
on_add_message(ctx.clone(), &ui, message.to_string(), !clear);
}
}
@ -1036,8 +1062,34 @@ fn get_message_box(
hbox
}
fn load_avatar(ui: &UiModel, url: &str) -> Option<Pixbuf> {
Some(ui.default_avatar.clone())
fn prepare_avatar(avatars: &mut RwLockWriteGuard<'_, HashMap<u64, Pixbuf>>, message: &str) {
if let Some(url) = grab_avatar(message) {
let mut hasher = DefaultHasher::new();
hasher.write(url.as_bytes());
let id = hasher.finish();
if !avatars.contains_key(&id) {
let Ok(data) = reqwest::blocking::get(&url).and_then(|o| o.bytes()) else {
return
};
let Ok(pixbuf) = load_pixbuf(&data.to_vec()) else {
return
};
avatars.insert(id, pixbuf);
}
}
}
fn get_avatar_or_default(ui: &UiModel, url: &str) -> Pixbuf {
let mut hasher = DefaultHasher::new();
hasher.write(url.as_bytes());
let id = hasher.finish();
if let Some(pixbuf) = ui.avatars.read().unwrap().get(&id) {
pixbuf.clone() // FIXME: cloning pixbufs is a dangerous shit
} else {
ui.default_avatar.clone()
}
}
fn get_new_message_box(
@ -1045,7 +1097,7 @@ fn get_new_message_box(
ui: &UiModel,
message: String,
notify: bool,
formatting_enabled: bool,
formatting_enabled: bool
) -> Overlay {
// TODO: softcode these colors
@ -1055,10 +1107,13 @@ fn get_new_message_box(
("#585858", "#292929", "#000000")
};
let latest_sign = ui.latest_sign.load(Ordering::SeqCst);
let (date, ip, content, name, color, avatar) =
if let (true, Some((date, ip, content, nick, avatar))) =
(formatting_enabled, parse_message(message.clone()))
{
(
date,
ip,
@ -1070,7 +1125,7 @@ fn get_new_message_box(
.map(|o| o.1.to_string())
.unwrap_or("#DDDDDD".to_string()),
avatar
.and_then(|o| load_avatar(ui, &o))
.map(|o| get_avatar_or_default(ui, &o))
.unwrap_or(ui.default_avatar.clone()),
)
} else {
@ -1084,9 +1139,32 @@ fn get_new_message_box(
)
};
if notify && !ui.window.is_active() {
if ctx.config(|o| o.chunked_enabled) {
send_notification(
ctx.clone(),
ui,
&if name == "System" {
"System Message".to_string()
} else {
format!("{}'s Message", name)
},
&glib::markup_escape_text(&content),
);
}
}
let sign = get_message_sign(&name, &date);
let squashed = latest_sign == sign;
ui.latest_sign.store(sign, Ordering::SeqCst);
let overlay = Overlay::new();
if !squashed {
let fixed = Fixed::new();
fixed.set_can_target(false);
let avatar_picture = Picture::for_pixbuf(&avatar);
avatar_picture.set_css_classes(&["message-avatar"]);
@ -1099,9 +1177,11 @@ fn get_new_message_box(
fixed.put(&avatar_picture, 0.0, 4.0);
overlay.add_overlay(&fixed);
}
let vbox = GtkBox::new(Orientation::Vertical, 2);
if !squashed {
vbox.append(&Label::builder()
.label(format!(
"<span color=\"{color}\">{}</span> <span color=\"{date_color}\">{}</span> <span color=\"{ip_color}\">{}</span>",
@ -1115,8 +1195,8 @@ fn get_new_message_box(
.wrap(true)
.wrap_mode(WrapMode::WordChar)
.use_markup(true)
.vexpand(true)
.build());
}
vbox.append(&Label::builder()
.label(format!(
@ -1129,20 +1209,31 @@ fn get_new_message_box(
.wrap(true)
.wrap_mode(WrapMode::WordChar)
.use_markup(true)
.vexpand(true)
.build());
vbox.set_valign(Align::Fill);
vbox.set_vexpand(true);
vbox.set_margin_start(37);
vbox.set_hexpand(true);
overlay.set_child(Some(&vbox));
if !squashed {
overlay.set_margin_top(7);
} else {
overlay.set_margin_top(2);
}
overlay
}
// creates sign that expires in 0-20 minutes
fn get_message_sign(name: &str, date: &str) -> u64 {
let mut hasher = DefaultHasher::new();
hasher.write(name.as_bytes());
hasher.write(date[..date.len()-2].as_bytes());
hasher.finish()
}
/// returns message sign
fn on_add_message(ctx: Arc<Context>, ui: &UiModel, message: String, notify: bool) {
let notify = notify && ctx.config(|c| c.notifications_enabled);
@ -1164,7 +1255,7 @@ fn on_add_message(ctx: Arc<Context>, ui: &UiModel, message: String, notify: bool
ui.chat_box.append(&get_new_message_box(ctx.clone(), ui, message, notify, formatting_enabled));
} else {
ui.chat_box.append(&get_message_box(ctx.clone(), ui, message, notify, formatting_enabled));
}
};
timeout_add_local_once(Duration::from_millis(1000), move || {
GLOBAL.with(|global| {

View File

@ -234,13 +234,17 @@ pub fn on_send_message(ctx: Arc<Context>, message: &str) -> Result<(), Box<dyn E
if message.starts_with("/") && ctx.config(|o| o.commands_enabled) {
on_command(ctx.clone(), &message)?;
} else {
let message = prepare_message(
let mut message = prepare_message(
ctx.clone(),
&ctx.config(|o| o.message_format.clone())
.replace("{name}", &ctx.name())
.replace("{text}", &message),
);
if let Some(avatar) = ctx.config(|o| o.avatar.clone()) {
message = format!("{message}\x06!!AR!!{avatar}"); // TODO: softcode this shittttttt
}
if let Some(password) = ctx.registered.read().unwrap().clone() {
send_message_auth(connect_rac!(ctx), &ctx.name(), &password, &message)?;
} else {
@ -257,6 +261,15 @@ pub fn sanitize_message(message: String) -> Option<String> {
Some(message)
}
/// message -> avatar
pub fn grab_avatar(message: &str) -> Option<String> {
if let Some(message) = AVATAR_REGEX.captures(&message) {
Some(message.get(2)?.as_str().to_string())
} else {
None
}
}
/// message -> (date, ip, text, (name, color), avatar)
pub fn parse_message(
message: String,