// 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::{AtomicBool, AtomicU64}; use std::sync::Mutex; use std::sync::{atomic::Ordering, mpsc::channel, Arc, RwLock}; use std::thread; use std::time::{Duration, SystemTime}; use chrono::Local; use gtk4::gdk_pixbuf::InterpType; use gtk4 as gtk; use gtk::gdk::{Cursor, Display, Texture}; use gtk::gdk_pixbuf::{Pixbuf, PixbufAnimation, PixbufLoader}; use gtk::gio::{self, ActionEntry, ApplicationFlags, MemoryInputStream, Menu}; use gtk::glib::clone; use gtk::glib::{ self, clone::Downgrade, source::timeout_add_local_once, timeout_add_local, timeout_add_once, ControlFlow, }; use gtk::pango::WrapMode; use gtk::prelude::*; use gtk::{ AboutDialog, Align, Application, ApplicationWindow, Box as GtkBox, Button, Calendar, CheckButton, CssProvider, Entry, Fixed, GestureClick, Justification, Label, ListBox, 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, get_config_path, save_config, Config, }, ctx::Context, on_send_message, parse_message, print_message, recv_tick, sanitize_message, SERVER_LIST, }; struct UiModel { is_dark_theme: bool, chat_box: GtkBox, chat_scrolled: ScrolledWindow, app: Application, window: ApplicationWindow, #[cfg(feature = "libnotify")] notifications: Arc>>, #[cfg(all(not(feature = "libnotify"), not(feature = "notify-rust")))] notifications: Arc>>, default_avatar: Pixbuf, avatars: Arc>>>, latest_sign: Arc } thread_local!( static GLOBAL: RefCell> = RefCell::new(None); ); pub fn clear_chat_messages(ctx: Arc, messages: Vec) { let _ = ctx .sender .read() .unwrap() .clone() .unwrap() .send((messages, true)); } pub fn add_chat_messages(ctx: Arc, messages: Vec) { println!("add chat messages: {}", messages.len()); let _ = ctx .sender .read() .unwrap() .clone() .unwrap() .send((messages, false)); } fn load_pixbuf(data: &[u8]) -> Result> { let loader = PixbufLoader::new(); loader.write(data)?; loader.close()?; Ok(loader.pixbuf().ok_or("laod pixbuf error")?) } macro_rules! gui_entry_setting { ($e:expr, $i:ident, $ctx:ident, $vbox:ident) => {{ let hbox = GtkBox::new(Orientation::Horizontal, 5); hbox.append(&Label::builder().label($e).build()); let entry = Entry::builder() .text(&$ctx.config(|o| o.$i.clone())) .build(); hbox.append(&entry); $vbox.append(&hbox); entry }}; } macro_rules! gui_usize_entry_setting { ($e:expr, $i:ident, $ctx:ident, $vbox:ident) => {{ let hbox = GtkBox::new(Orientation::Horizontal, 5); hbox.append(&Label::builder().label($e).build()); let entry = Entry::builder() .text(&$ctx.config(|o| o.$i.to_string())) .build(); hbox.append(&entry); $vbox.append(&hbox); entry }}; } macro_rules! gui_option_entry_setting { ($e:expr, $i:ident, $ctx:ident, $vbox:ident) => {{ let hbox = GtkBox::new(Orientation::Horizontal, 5); hbox.append(&Label::builder().label($e).build()); let entry = Entry::builder() .text(&$ctx.config(|o| o.$i.clone()).unwrap_or_default()) .build(); hbox.append(&entry); $vbox.append(&hbox); entry }}; } macro_rules! gui_checkbox_setting { ($e:expr, $i:ident, $ctx:ident, $vbox:ident) => {{ let hbox = GtkBox::new(Orientation::Horizontal, 5); hbox.append(&Label::builder().label($e).build()); let entry = CheckButton::builder().active($ctx.config(|o| o.$i)).build(); hbox.append(&entry); $vbox.append(&hbox); entry }}; } fn update_window_title(ctx: Arc) { GLOBAL.with(|global| { if let Some(ui) = &*global.borrow() { ui.window.set_title(Some(&format!( "bRAC - Connected to {} as {}", ctx.config(|o| o.host.clone()), &ctx.name() ))) } }) } fn open_settings(ctx: Arc, app: &Application) { let vbox = GtkBox::new(Orientation::Vertical, 10); vbox.set_margin_bottom(15); vbox.set_margin_top(15); vbox.set_margin_start(15); vbox.set_margin_end(15); let settings_vbox = GtkBox::new(Orientation::Vertical, 10); let host_entry = gui_entry_setting!("Host", host, ctx, settings_vbox); let name_entry = gui_option_entry_setting!("Name", name, ctx, settings_vbox); 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!( "Out-of-focus Update Time", oof_update_time, ctx, settings_vbox ); let max_messages_entry = gui_usize_entry_setting!("Max Messages", max_messages, ctx, settings_vbox); let hide_my_ip_entry = gui_checkbox_setting!("Hide My IP", hide_my_ip, ctx, settings_vbox); let show_other_ip_entry = gui_checkbox_setting!("Show Other IP", show_other_ip, ctx, settings_vbox); let chunked_enabled_entry = gui_checkbox_setting!("Chunked Enabled", chunked_enabled, ctx, settings_vbox); let formatting_enabled_entry = gui_checkbox_setting!("Formatting Enabled", formatting_enabled, ctx, settings_vbox); let commands_enabled_entry = gui_checkbox_setting!("Commands Enabled", commands_enabled, ctx, settings_vbox); let notifications_enabled_entry = gui_checkbox_setting!( "Notifications Enabled", notifications_enabled, ctx, settings_vbox ); let debug_logs_entry = gui_checkbox_setting!("Debug Logs", debug_logs, ctx, settings_vbox); let konata_size_entry = gui_usize_entry_setting!("Konata Size", konata_size, ctx, settings_vbox); let remove_gui_shit_entry = gui_checkbox_setting!("Remove Gui Shit", remove_gui_shit, ctx, settings_vbox); let new_ui_enabled_entry = gui_checkbox_setting!("New UI", new_ui_enabled, ctx, settings_vbox); let scrollable = ScrolledWindow::builder() .child(&settings_vbox) .vexpand(true) .hexpand(true) .build(); vbox.append(&scrollable); let save_button = Button::builder().label("Save").build(); vbox.append(&save_button); save_button.connect_clicked(clone!( #[weak] ctx, #[weak] host_entry, #[weak] name_entry, #[weak] message_format_entry, #[weak] update_time_entry, #[weak] max_messages_entry, #[weak] hide_my_ip_entry, #[weak] show_other_ip_entry, #[weak] chunked_enabled_entry, #[weak] formatting_enabled_entry, #[weak] commands_enabled_entry, #[weak] notifications_enabled_entry, #[weak] proxy_entry, #[weak] debug_logs_entry, #[weak] oof_update_time_entry, #[weak] konata_size_entry, #[weak] remove_gui_shit_entry, #[weak] new_ui_enabled_entry, #[weak] avatar_entry, move |_| { let config = Config { host: host_entry.text().to_string(), name: { let name = name_entry.text().to_string(); if name.is_empty() { None } else { 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(); if let Ok(update_time) = update_time.parse::() { update_time } else { let update_time = default_update_time(); update_time_entry.set_text(&update_time.to_string()); update_time } }, oof_update_time: { let oof_update_time = oof_update_time_entry.text(); if let Ok(oof_update_time) = oof_update_time.parse::() { oof_update_time } else { let oof_update_time = default_oof_update_time(); oof_update_time_entry.set_text(&oof_update_time.to_string()); oof_update_time } }, konata_size: { let konata_size = konata_size_entry.text(); if let Ok(konata_size) = konata_size.parse::() { konata_size.max(0).min(200) } else { let konata_size = default_konata_size(); konata_size_entry.set_text(&konata_size.to_string()); konata_size } }, max_messages: { let max_messages = max_messages_entry.text(); if let Ok(max_messages) = max_messages.parse::() { max_messages } else { let max_messages = default_max_messages(); max_messages_entry.set_text(&max_messages.to_string()); max_messages } }, hide_my_ip: hide_my_ip_entry.is_active(), remove_gui_shit: remove_gui_shit_entry.is_active(), show_other_ip: show_other_ip_entry.is_active(), chunked_enabled: chunked_enabled_entry.is_active(), formatting_enabled: formatting_enabled_entry.is_active(), commands_enabled: commands_enabled_entry.is_active(), notifications_enabled: notifications_enabled_entry.is_active(), new_ui_enabled: new_ui_enabled_entry.is_active(), debug_logs: debug_logs_entry.is_active(), proxy: { let proxy = proxy_entry.text().to_string(); if proxy.is_empty() { None } else { Some(proxy) } }, }; ctx.set_config(&config); save_config(get_config_path(), &config); update_window_title(ctx.clone()); } )); let reset_button = Button::builder().label("Reset all").build(); vbox.append(&reset_button); reset_button.connect_clicked(clone!( #[weak] ctx, #[weak] host_entry, #[weak] name_entry, #[weak] message_format_entry, #[weak] update_time_entry, #[weak] max_messages_entry, #[weak] hide_my_ip_entry, #[weak] show_other_ip_entry, #[weak] chunked_enabled_entry, #[weak] formatting_enabled_entry, #[weak] commands_enabled_entry, #[weak] notifications_enabled_entry, #[weak] proxy_entry, #[weak] debug_logs_entry, #[weak] oof_update_time_entry, #[weak] konata_size_entry, #[weak] 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()); max_messages_entry.set_text(&config.max_messages.to_string()); hide_my_ip_entry.set_active(config.hide_my_ip); show_other_ip_entry.set_active(config.show_other_ip); chunked_enabled_entry.set_active(config.chunked_enabled); formatting_enabled_entry.set_active(config.formatting_enabled); commands_enabled_entry.set_active(config.commands_enabled); notifications_enabled_entry.set_active(config.notifications_enabled); debug_logs_entry.set_active(config.debug_logs); oof_update_time_entry.set_text(&config.oof_update_time.to_string()); konata_size_entry.set_text(&config.konata_size.to_string()); remove_gui_shit_entry.set_active(config.remove_gui_shit); new_ui_enabled_entry.set_active(config.new_ui_enabled); } )); let window = Window::builder() .application(app) .title("Settings") .default_width(400) .default_height(500) .resizable(true) .decorated(true) .child(&vbox) .build(); let controller = gtk::EventControllerKey::new(); controller.connect_key_pressed({ let window = window.clone(); move |_, key, _, _| { if key == gtk::gdk::Key::Escape { window.close(); gtk::glib::Propagation::Proceed } else { gtk::glib::Propagation::Stop } } }); window.add_controller(controller); window.present(); } fn build_menu(ctx: Arc, app: &Application) { let menu = Menu::new(); let file_menu = Menu::new(); file_menu.append(Some("About"), Some("app.about")); file_menu.append(Some("Close"), Some("app.close")); let edit_menu = Menu::new(); edit_menu.append(Some("Settings"), Some("app.settings")); menu.append_submenu(Some("File"), &file_menu); menu.append_submenu(Some("Edit"), &edit_menu); app.set_menubar(Some((&menu).into())); app.add_action_entries([ ActionEntry::builder("settings") .activate(clone!( #[weak] ctx, move |a: &Application, _, _| { open_settings(ctx, a); } )) .build(), ActionEntry::builder("close") .activate(move |a: &Application, _, _| { a.quit(); }) .build(), ActionEntry::builder("about") .activate(clone!( #[weak] app, move |_, _, _| { AboutDialog::builder() .application(&app) .authors(["MeexReay"]) .license(include_str!("../../LICENSE")) .comments("better RAC client") .website("https://github.com/MeexReay/bRAC") .website_label("source code") .logo(&Texture::for_pixbuf( &load_pixbuf(include_bytes!("images/icon.png")).unwrap(), )) .build() .present(); } )) .build(), ]); } fn build_ui(ctx: Arc, app: &Application) -> UiModel { let is_dark_theme = if let Some(settings) = Settings::default() { settings.is_gtk_application_prefer_dark_theme() || settings .gtk_theme_name() .map(|o| o.to_lowercase().contains("dark")) .unwrap_or_default() } else { false }; #[cfg(target_os = "windows")] let is_dark_theme = true; let main_box = GtkBox::new(Orientation::Vertical, 5); main_box.set_css_classes(&["main-box"]); let widget_box_overlay = Overlay::new(); let widget_box = GtkBox::new(Orientation::Horizontal, 5); widget_box.set_css_classes(&["widget_box"]); let remove_gui_shit = ctx.config(|c| c.remove_gui_shit); if !remove_gui_shit { widget_box.append( &Calendar::builder() .css_classes(["calendar"]) .show_heading(false) .can_target(false) .build(), ); } let server_list_vbox = GtkBox::new(Orientation::Vertical, 5); let server_list = ListBox::new(); for url in SERVER_LIST.iter() { let url = url.to_string(); let label = Label::builder().label(&url).halign(Align::Start).build(); let click = GestureClick::new(); click.connect_pressed(clone!( #[weak] ctx, move |_, _, _, _| { let mut config = ctx.config.read().unwrap().clone(); config.host = url.clone(); ctx.set_config(&config); save_config(get_config_path(), &config); update_window_title(ctx.clone()); } )); label.add_controller(click); server_list.append(&label); } server_list_vbox.append(&Label::builder().label("Server List:").build()); server_list_vbox.append(&server_list); widget_box.append(&server_list_vbox); if !remove_gui_shit { let fixed = Fixed::new(); fixed.set_can_target(false); let konata_size = ctx.config(|c| c.konata_size) as i32; let konata = Picture::for_pixbuf(&load_pixbuf(include_bytes!("images/konata.png")).unwrap()); konata.set_size_request(174 * konata_size / 100, 127 * konata_size / 100); fixed.put( &konata, (499 - 174 * konata_size / 100) as f64, (131 - 127 * konata_size / 100) as f64, ); let logo_gif = include_bytes!("images/logo.gif"); let logo = Picture::for_pixbuf(&load_pixbuf(logo_gif).unwrap()); logo.set_size_request(152 * konata_size / 100, 64 * konata_size / 100); let logo_anim = PixbufAnimation::from_stream( &MemoryInputStream::from_bytes(&glib::Bytes::from(logo_gif)), None::<&gio::Cancellable>, ) .unwrap() .iter(Some(SystemTime::now())); timeout_add_local(Duration::from_millis(30), { let logo = logo.clone(); let logo_anim = logo_anim.clone(); let ctx = ctx.clone(); move || { if ctx.is_focused.load(Ordering::SeqCst) { logo.set_pixbuf(Some(&logo_anim.pixbuf())); logo_anim.advance(SystemTime::now()); } ControlFlow::Continue } }); // 262, 4 fixed.put( &logo, (436 - 174 * konata_size / 100) as f64, (131 - 127 * konata_size / 100) as f64, ); let time = Label::builder() .label(&Local::now().format("%H:%M").to_string()) .justify(Justification::Right) .css_classes(["time"]) .build(); timeout_add_local(Duration::from_secs(1), { let time = time.clone(); move || { time.set_label(&Local::now().format("%H:%M").to_string()); ControlFlow::Continue } }); fixed.put(&time, 432.0, 4.0); fixed.set_halign(Align::End); widget_box_overlay.add_overlay(&fixed); } widget_box_overlay.set_child(Some(&widget_box)); main_box.append(&widget_box_overlay); let chat_box = GtkBox::new(Orientation::Vertical, 2); chat_box.set_css_classes(&["chat-box"]); let chat_scrolled = ScrolledWindow::builder() .child(&chat_box) .vexpand(true) .hexpand(true) .margin_bottom(5) .margin_end(5) .margin_start(5) .propagate_natural_height(true) .build(); main_box.append(&chat_scrolled); let send_box = GtkBox::new(Orientation::Horizontal, 5); send_box.set_margin_bottom(5); send_box.set_margin_end(5); send_box.set_margin_start(5); let text_entry = Entry::builder() .placeholder_text("Message") .css_classes(["send-button"]) .hexpand(true) .build(); send_box.append(&text_entry); let send_btn = Button::builder() .label("Send") .css_classes(["send-text"]) .cursor(&Cursor::from_name("pointer", None).unwrap()) .build(); send_btn.connect_clicked(clone!( #[weak] text_entry, #[weak] ctx, move |_| { let text = text_entry.text().clone(); if text.is_empty() { return; } text_entry.set_text(""); thread::spawn({ move || { if let Err(e) = on_send_message(ctx.clone(), &text) { if ctx.config(|o| o.debug_logs) { let msg = format!("Send message error: {}", e.to_string()).to_string(); add_chat_messages(ctx.clone(), vec![msg]); } } } }); } )); text_entry.connect_activate(clone!( #[weak] text_entry, #[weak] ctx, move |_| { let text = text_entry.text().clone(); if text.is_empty() { return; } text_entry.set_text(""); thread::spawn({ move || { if let Err(e) = on_send_message(ctx.clone(), &text) { if ctx.config(|o| o.debug_logs) { let msg = format!("Send message error: {}", e.to_string()).to_string(); add_chat_messages(ctx.clone(), vec![msg]); } } } }); } )); send_box.append(&send_btn); main_box.append(&send_box); let scrolled_window_weak = Downgrade::downgrade(&chat_scrolled); timeout_add_local_once(Duration::ZERO, { let scrolled_window_weak = scrolled_window_weak.clone(); move || { if let Some(o) = scrolled_window_weak.upgrade() { o.vadjustment() .set_value(o.vadjustment().upper() - o.vadjustment().page_size()); } } }); let window = ApplicationWindow::builder() .application(app) .title(format!( "bRAC - Connected to {} as {}", ctx.config(|o| o.host.clone()), &ctx.name() )) .default_width(500) .default_height(500) .resizable(true) .decorated(true) .show_menubar(true) .child(&main_box) .build(); window.connect_default_width_notify({ let scrolled_window_weak = scrolled_window_weak.clone(); move |_| { let scrolled_window_weak = scrolled_window_weak.clone(); timeout_add_local_once(Duration::ZERO, move || { if let Some(o) = scrolled_window_weak.upgrade() { o.vadjustment() .set_value(o.vadjustment().upper() - o.vadjustment().page_size()); } }); } }); window.present(); UiModel { is_dark_theme, chat_scrolled, chat_box, app: app.clone(), window: window.clone(), #[cfg(feature = "libnotify")] notifications: Arc::new(RwLock::new(Vec::::new())), #[cfg(all(not(feature = "libnotify"), not(feature = "notify-rust")))] notifications: Arc::new(RwLock::new(Vec::::new())), default_avatar: load_pixbuf(include_bytes!("images/avatar.png")).unwrap(), avatars: Arc::new(Mutex::new(HashMap::new())), latest_sign: Arc::new(AtomicU64::new(0)) } } fn setup(_: &Application, ctx: Arc, ui: UiModel) { let (sender, receiver) = channel(); *ctx.sender.write().unwrap() = Some(Arc::new(sender)); run_recv_loop(ctx.clone()); ui.window.connect_notify(Some("is-active"), { let ctx = ctx.clone(); move |a, _| { let is_focused = a.is_active(); ctx.is_focused.store(is_focused, Ordering::SeqCst); if is_focused { thread::spawn({ let ctx = ctx.clone(); move || { make_recv_tick(ctx.clone()); } }); #[cfg(not(feature = "notify-rust"))] GLOBAL.with(|global| { if let Some(ui) = &*global.borrow() { #[cfg(feature = "libnotify")] for i in ui.notifications.read().unwrap().clone() { i.close().expect("libnotify close error"); } #[cfg(not(feature = "libnotify"))] for i in ui.notifications.read().unwrap().clone() { ui.app.withdraw_notification(&i); } } }); } } }); GLOBAL.with(|global| { *global.borrow_mut() = Some(ui); }); thread::spawn({ let ctx = ctx.clone(); move || { while let Ok((messages, clear)) = receiver.recv() { println!("got chat messages: {}", messages.len()); let ctx = ctx.clone(); let messages = Arc::new(messages); timeout_add_once(Duration::ZERO, { let messages = messages.clone(); move || { GLOBAL.with(|global| { if let Some(ui) = &*global.borrow() { if clear { while let Some(row) = ui.chat_box.last_child() { ui.chat_box.remove(&row); } } for message in messages.iter() { on_add_message(ctx.clone(), &ui, message.to_string(), !clear); } } }); if ctx.config(|o| !o.new_ui_enabled) { return; } thread::spawn(move || { for message in messages.iter() { let Some(avatar_url) = grab_avatar(message) else { continue }; let avatar_id = get_avatar_id(&avatar_url); let Some(avatar) = load_avatar(&avatar_url) .and_then(|avatar| load_pixbuf(&avatar).ok()) .and_then(|pixbuf| pixbuf.scale_simple(32, 32, InterpType::Bilinear )) .and_then(|pixbuf| Some(( pixbuf.pixel_bytes()?, pixbuf.colorspace(), pixbuf.has_alpha(), pixbuf.bits_per_sample(), pixbuf.width(), pixbuf.height(), pixbuf.rowstride() ))) else { continue }; timeout_add_once(Duration::ZERO, { move || { GLOBAL.with(|global| { if let Some(ui) = &*global.borrow() { let pixbuf = Pixbuf::from_bytes( &avatar.0, avatar.1, avatar.2, avatar.3, avatar.4, avatar.5, avatar.6 ); if let Some(pics) = ui.avatars.lock().unwrap().remove(&avatar_id) { for pic in pics { pic.set_pixbuf(Some(&pixbuf)); } } } }); } }); } }); } }); } } }); } fn load_css(is_dark_theme: bool) { let provider = CssProvider::new(); provider.load_from_data(&format!( "{}\n{}", if is_dark_theme { include_str!("styles/dark.css") } else { include_str!("styles/light.css") }, include_str!("styles/style.css") )); gtk::style_context_add_provider_for_display( &Display::default().expect("Could not connect to a display."), &provider, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, ); } #[cfg(feature = "notify-rust")] fn send_notification(_: Arc, _: &UiModel, title: &str, message: &str) { use notify_rust::{Notification, Timeout}; Notification::new() .summary(title) .body(message) .auto_icon() .appname("bRAC") .timeout(Timeout::Default) // this however is .show() .expect("notify-rust send error"); } #[cfg(feature = "libnotify")] fn send_notification(_: Arc, ui: &UiModel, title: &str, message: &str) { use libnotify::Notification; let notification = Notification::new(title, message, None); notification.set_app_name("bRAC"); let pixbuf_loader = gdk_pixbuf::PixbufLoader::new(); pixbuf_loader .loader_write(include_bytes!("images/icon.png")) .unwrap(); pixbuf_loader.close().unwrap(); notification.set_image_from_pixbuf(&pixbuf_loader.get_pixbuf().unwrap()); notification.show().expect("libnotify send error"); ui.notifications.write().unwrap().push(notification); } #[cfg(all(not(feature = "libnotify"), not(feature = "notify-rust")))] fn send_notification(_: Arc, ui: &UiModel, title: &str, message: &str) { use std::{ hash::{DefaultHasher, Hasher}, time::UNIX_EPOCH, }; use gtk4::gio::Notification; let mut hash = DefaultHasher::new(); hash.write(title.as_bytes()); hash.write(message.as_bytes()); let id = format!( "bRAC-{}-{}", SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_millis(), hash.finish() ); let notif = Notification::new(title); notif.set_body(Some(&message)); ui.app.send_notification(Some(&id), ¬if); ui.notifications.write().unwrap().push(id); } fn get_message_box( ctx: Arc, ui: &UiModel, message: String, notify: bool, formatting_enabled: bool, ) -> GtkBox { // TODO: softcode these colors let (ip_color, date_color, text_color) = if ui.is_dark_theme { ("#494949", "#929292", "#FFFFFF") } else { ("#585858", "#292929", "#000000") }; let mut label = String::new(); if let (true, Some((date, ip, content, nick, _))) = (formatting_enabled, parse_message(message.clone())) { if let Some(ip) = ip { if ctx.config(|o| o.show_other_ip) { label.push_str(&format!( "{} ", glib::markup_escape_text(&ip) )); } } label.push_str(&format!( "[{}] ", glib::markup_escape_text(&date) )); if let Some((name, color)) = nick { label.push_str(&format!( "<{}> ", color.to_uppercase(), glib::markup_escape_text(&name) )); if notify && !ui.window.is_active() { if ctx.config(|o| o.chunked_enabled) { send_notification( ctx.clone(), ui, &format!("{}'s Message", &name), &glib::markup_escape_text(&content), ); } } } else { if notify && !ui.window.is_active() { if ctx.config(|o| o.chunked_enabled) { send_notification(ctx.clone(), ui, "System Message", &content); } } } label.push_str(&format!( "{}", glib::markup_escape_text(&content) )); } else { label.push_str(&format!( "{}", glib::markup_escape_text(&message) )); if notify && !ui.window.is_active() { if ctx.config(|o| o.chunked_enabled) { send_notification(ctx.clone(), ui, "Chat Message", &message); } } } let hbox = GtkBox::new(Orientation::Horizontal, 2); hbox.append( &Label::builder() .label(&label) .halign(Align::Start) .valign(Align::Start) .selectable(true) .wrap(true) .wrap_mode(WrapMode::WordChar) .use_markup(true) .build(), ); hbox.set_hexpand(true); hbox } fn get_avatar_id(url: &str) -> u64 { let mut hasher = DefaultHasher::new(); hasher.write(url.as_bytes()); hasher.finish() } fn load_avatar(url: &str) -> Option> { reqwest::blocking::get(url).ok() .and_then(|resp| resp.bytes().ok()) .map(|bytes| bytes.to_vec()) } fn get_new_message_box( ctx: Arc, ui: &UiModel, message: String, notify: bool, formatting_enabled: bool ) -> Overlay { // TODO: softcode these colors let (ip_color, date_color, text_color) = if ui.is_dark_theme { ("#494949", "#929292", "#FFFFFF") } else { ("#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, content, nick.as_ref() .map(|o| o.0.to_string()) .unwrap_or("System".to_string()), nick.as_ref() .map(|o| o.1.to_string()) .unwrap_or("#DDDDDD".to_string()), avatar.map(|o| get_avatar_id(&o)).unwrap_or_default() ) } else { ( Local::now().format("%d.%m.%Y %H:%M").to_string(), None, message, "System".to_string(), "#DDDDDD".to_string(), 0 ) }; 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(&ui.default_avatar.clone()); avatar_picture.set_css_classes(&["message-avatar"]); avatar_picture.set_vexpand(false); avatar_picture.set_hexpand(false); avatar_picture.set_valign(Align::Start); avatar_picture.set_halign(Align::Start); avatar_picture.set_size_request(32, 32); if avatar != 0 { let mut lock = ui.avatars.lock().unwrap(); if let Some(pics) = lock.get_mut(&avatar) { pics.push(avatar_picture.clone()); } else { lock.insert(avatar, vec![avatar_picture.clone()]); } } 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!( "{} {} {}", glib::markup_escape_text(&name), glib::markup_escape_text(&date), glib::markup_escape_text(&ip.unwrap_or_default()), )) .halign(Align::Start) .valign(Align::Start) .selectable(true) .wrap(true) .wrap_mode(WrapMode::WordChar) .use_markup(true) .build()); } vbox.append(&Label::builder() .label(format!( "{}", glib::markup_escape_text(&content) )) .halign(Align::Start) .hexpand(true) .selectable(true) .wrap(true) .wrap_mode(WrapMode::WordChar) .use_markup(true) .build()); 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, ui: &UiModel, message: String, notify: bool) { let notify = notify && ctx.config(|c| c.notifications_enabled); let formatting_enabled = ctx.config(|c| c.formatting_enabled); let Some(sanitized) = (if formatting_enabled { sanitize_message(message.clone()) } else { Some(message.clone()) }) else { return; }; if sanitized.is_empty() { return; } if ctx.config(|o| o.new_ui_enabled) { 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| { if let Some(ui) = &*global.borrow() { let o = &ui.chat_scrolled; o.vadjustment() .set_value(o.vadjustment().upper() - o.vadjustment().page_size()); } }); }); } fn make_recv_tick(ctx: Arc) { if let Err(e) = recv_tick(ctx.clone()) { if ctx.config(|o| o.debug_logs) { let _ = print_message( ctx.clone(), format!("Print messages error: {}", e.to_string()).to_string(), ); } thread::sleep(Duration::from_secs(1)); } } fn run_recv_loop(ctx: Arc) { thread::spawn(move || loop { make_recv_tick(ctx.clone()); thread::sleep(Duration::from_millis( if ctx.is_focused.load(Ordering::SeqCst) { ctx.config(|o| o.update_time) as u64 } else { ctx.config(|o| o.oof_update_time) as u64 }, )); }); } pub fn run_main_loop(ctx: Arc) { #[cfg(feature = "libnotify")] { libnotify::init("ru.themixray.bRAC").expect("libnotify init error"); } #[cfg(target_os = "windows")] { use std::env; env::set_var("GTK_THEME", "Adwaita:dark"); } let application = Application::builder() .application_id("ru.themixray.bRAC") .flags(ApplicationFlags::FLAGS_NONE) .build(); application.connect_activate({ let ctx = ctx.clone(); move |app| { let ui = build_ui(ctx.clone(), app); load_css(ui.is_dark_theme); setup(app, ctx.clone(), ui); } }); application.connect_startup({ let ctx = ctx.clone(); move |app| { build_menu(ctx.clone(), app); } }); application.run_with_args::<&str>(&[]); #[cfg(feature = "libnotify")] { libnotify::uninit(); } }