mirror of
https://github.com/MeexReay/bRAC.git
synced 2025-09-13 15:37:40 +03:00
803 lines
23 KiB
Rust
803 lines
23 KiB
Rust
use std::cell::RefCell;
|
|
use std::collections::HashMap;
|
|
use std::error::Error;
|
|
use std::hash::{DefaultHasher, Hasher};
|
|
use std::io::Read;
|
|
use std::path::PathBuf;
|
|
use std::sync::atomic::AtomicU64;
|
|
use std::sync::Mutex;
|
|
use std::sync::{atomic::Ordering, mpsc::channel, Arc, RwLock};
|
|
use std::thread;
|
|
use std::time::{Duration, SystemTime};
|
|
|
|
use clap::crate_version;
|
|
|
|
use adw::gdk::Display;
|
|
use adw::gio::{ActionEntry, ApplicationFlags, Menu};
|
|
use adw::glib::clone;
|
|
use adw::glib::{self, source::timeout_add_local_once, timeout_add_once};
|
|
use adw::prelude::*;
|
|
use adw::{Application, ApplicationWindow};
|
|
use libadwaita::gdk::Texture;
|
|
use libadwaita::gtk::gdk_pixbuf::InterpType;
|
|
use libadwaita::gtk::{Button, Entry, Label, Picture};
|
|
use libadwaita::{self as adw, Avatar, Breakpoint, BreakpointCondition, Dialog, OverlaySplitView};
|
|
|
|
use adw::gtk;
|
|
use gtk::gdk_pixbuf::{Pixbuf, PixbufLoader};
|
|
use gtk::{Box as GtkBox, CssProvider, Orientation, ScrolledWindow, Settings};
|
|
|
|
use crate::chat::grab_avatar;
|
|
|
|
use super::config::get_config_path;
|
|
use super::{
|
|
config::{save_config, Config},
|
|
ctx::Context,
|
|
print_message, recv_tick, sanitize_message,
|
|
};
|
|
|
|
mod page;
|
|
mod preferences;
|
|
mod widgets;
|
|
|
|
use page::*;
|
|
use preferences::*;
|
|
|
|
pub fn try_save_config(path: PathBuf, config: &Config) {
|
|
match save_config(path, config) {
|
|
Ok(_) => {}
|
|
Err(e) => {
|
|
println!("save config error: {e}")
|
|
}
|
|
}
|
|
}
|
|
|
|
struct UiModel {
|
|
is_dark_theme: bool,
|
|
chat_box: GtkBox,
|
|
chat_scrolled: ScrolledWindow,
|
|
app: Application,
|
|
window: ApplicationWindow,
|
|
#[cfg(feature = "libnotify")]
|
|
notifications: Arc<RwLock<Vec<libnotify::Notification>>>,
|
|
#[cfg(all(not(feature = "libnotify"), not(feature = "notify-rust")))]
|
|
notifications: Arc<RwLock<Vec<String>>>,
|
|
avatars: Arc<Mutex<HashMap<u64, Vec<Avatar>>>>,
|
|
latest_sign: Arc<AtomicU64>,
|
|
}
|
|
|
|
thread_local!(
|
|
static GLOBAL: RefCell<Option<UiModel>> = RefCell::new(None);
|
|
);
|
|
|
|
pub fn clear_chat_messages(ctx: Arc<Context>, messages: Vec<String>) {
|
|
let _ = ctx
|
|
.sender
|
|
.read()
|
|
.unwrap()
|
|
.clone()
|
|
.unwrap()
|
|
.send((messages, true));
|
|
}
|
|
|
|
pub fn add_chat_messages(ctx: Arc<Context>, messages: Vec<String>) {
|
|
println!("add chat messages: {}", messages.len());
|
|
let _ = ctx
|
|
.sender
|
|
.read()
|
|
.unwrap()
|
|
.clone()
|
|
.unwrap()
|
|
.send((messages, false));
|
|
}
|
|
|
|
fn load_pixbuf(data: &[u8]) -> Result<Pixbuf, Box<dyn Error>> {
|
|
let loader = PixbufLoader::new();
|
|
loader.write(data)?;
|
|
loader.close()?;
|
|
Ok(loader.pixbuf().ok_or("laod pixbuf error")?)
|
|
}
|
|
|
|
fn update_window_title(ctx: Arc<Context>) {
|
|
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 build_menu(ctx: Arc<Context>, app: &Application) -> Menu {
|
|
let menu = Menu::new();
|
|
|
|
menu.append(Some("Settings"), Some("app.settings"));
|
|
menu.append(Some("About"), Some("app.about"));
|
|
menu.append(Some("Close"), Some("app.close"));
|
|
|
|
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 |_, _, _| {
|
|
let dialog = adw::AboutDialog::builder()
|
|
.developer_name("MeexReay")
|
|
.license(glib::markup_escape_text(include_str!("../../../LICENSE")))
|
|
.comments("better RAC client")
|
|
.website("https://github.com/MeexReay/bRAC")
|
|
.application_name("bRAC")
|
|
.application_icon("ru.themixray.bRAC")
|
|
.version(crate_version!())
|
|
.build();
|
|
dialog.present(app.active_window().as_ref());
|
|
}
|
|
))
|
|
.build(),
|
|
]);
|
|
|
|
menu
|
|
}
|
|
|
|
fn build_sidebar_button(
|
|
ctx: Arc<Context>,
|
|
split_view: &OverlaySplitView,
|
|
server: String,
|
|
servers_list: &GtkBox,
|
|
) -> GtkBox {
|
|
let hbox = GtkBox::new(Orientation::Horizontal, 5);
|
|
|
|
let button = Button::builder().label(&server).hexpand(true).build();
|
|
|
|
button.connect_clicked(clone!(
|
|
#[weak]
|
|
split_view,
|
|
#[weak]
|
|
ctx,
|
|
#[strong]
|
|
server,
|
|
move |_| {
|
|
let mut config = ctx.config.read().unwrap().clone();
|
|
config.host = server.clone();
|
|
ctx.set_config(&config);
|
|
try_save_config(get_config_path(), &config);
|
|
update_window_title(ctx.clone());
|
|
if split_view.is_collapsed() {
|
|
split_view.set_show_sidebar(false);
|
|
}
|
|
}
|
|
));
|
|
|
|
hbox.append(&button);
|
|
|
|
let delete_button = Button::from_icon_name("user-trash-symbolic");
|
|
|
|
delete_button.set_css_classes(&["destructive-action"]);
|
|
|
|
delete_button.connect_clicked(clone!(
|
|
#[weak]
|
|
ctx,
|
|
#[weak]
|
|
hbox,
|
|
#[weak]
|
|
servers_list,
|
|
#[strong]
|
|
server,
|
|
move |_| {
|
|
servers_list.remove(&hbox);
|
|
let mut config = ctx.config.read().unwrap().clone();
|
|
let index = config.servers.iter().position(|x| *x == server).unwrap();
|
|
config.servers.remove(index);
|
|
ctx.set_config(&config);
|
|
try_save_config(get_config_path(), &config);
|
|
}
|
|
));
|
|
|
|
hbox.append(&delete_button);
|
|
|
|
hbox
|
|
}
|
|
|
|
fn build_sidebar(ctx: Arc<Context>, app: &Application, split_view: &OverlaySplitView) -> GtkBox {
|
|
let sidebar = GtkBox::new(Orientation::Vertical, 15);
|
|
|
|
sidebar.set_margin_start(5);
|
|
sidebar.set_margin_end(5);
|
|
|
|
sidebar.append(
|
|
&Picture::builder()
|
|
.paintable(&Texture::for_pixbuf(
|
|
&load_pixbuf(include_bytes!("images/servers.png")).unwrap(),
|
|
))
|
|
.build(),
|
|
);
|
|
|
|
let servers_list = GtkBox::new(Orientation::Vertical, 5);
|
|
|
|
for server in ctx.config(|o| o.servers.clone()) {
|
|
servers_list.append(&build_sidebar_button(
|
|
ctx.clone(),
|
|
&split_view,
|
|
server,
|
|
&servers_list,
|
|
));
|
|
}
|
|
|
|
sidebar.append(&servers_list);
|
|
|
|
let add_server = Button::builder()
|
|
.label("Add Server")
|
|
// .start_icon_name("list-add-symbolic")
|
|
.css_classes(["suggested-action"])
|
|
.build();
|
|
|
|
add_server.connect_clicked(clone!(
|
|
#[weak]
|
|
app,
|
|
#[weak]
|
|
servers_list,
|
|
#[weak]
|
|
ctx,
|
|
#[weak]
|
|
split_view,
|
|
move |_| {
|
|
let dialog = Dialog::new();
|
|
|
|
let vbox = GtkBox::new(Orientation::Vertical, 10);
|
|
|
|
vbox.set_margin_bottom(20);
|
|
vbox.set_margin_top(20);
|
|
vbox.set_margin_end(20);
|
|
vbox.set_margin_start(20);
|
|
|
|
vbox.append(
|
|
&Label::builder()
|
|
.label("Add server")
|
|
.css_classes(["title-2"])
|
|
.build(),
|
|
);
|
|
|
|
let entry = Entry::builder()
|
|
.placeholder_text("Server host")
|
|
.hexpand(true)
|
|
.build();
|
|
|
|
vbox.append(&entry);
|
|
|
|
let hbox = GtkBox::new(Orientation::Horizontal, 5);
|
|
|
|
let confirm = Button::builder()
|
|
.label("Confirm")
|
|
.hexpand(true)
|
|
.css_classes(["suggested-action"])
|
|
.build();
|
|
|
|
confirm.connect_clicked(clone!(
|
|
#[weak]
|
|
dialog,
|
|
#[weak]
|
|
servers_list,
|
|
#[weak]
|
|
ctx,
|
|
#[weak]
|
|
split_view,
|
|
#[weak]
|
|
entry,
|
|
move |_| {
|
|
let server: String = entry.text().into();
|
|
|
|
let mut config = ctx.config.read().unwrap().clone();
|
|
config.servers.push(server.clone());
|
|
ctx.set_config(&config);
|
|
try_save_config(get_config_path(), &config);
|
|
|
|
servers_list.append(&build_sidebar_button(
|
|
ctx.clone(),
|
|
&split_view,
|
|
server,
|
|
&servers_list,
|
|
));
|
|
dialog.close();
|
|
}
|
|
));
|
|
|
|
hbox.append(&confirm);
|
|
|
|
let cancel = Button::builder().label("Cancel").hexpand(true).build();
|
|
|
|
cancel.connect_clicked(clone!(
|
|
#[weak]
|
|
dialog,
|
|
move |_| {
|
|
dialog.close();
|
|
}
|
|
));
|
|
|
|
hbox.append(&cancel);
|
|
|
|
vbox.append(&hbox);
|
|
|
|
dialog.set_child(Some(&vbox));
|
|
|
|
dialog.present(app.active_window().as_ref());
|
|
}
|
|
));
|
|
|
|
sidebar.append(&add_server);
|
|
|
|
sidebar
|
|
}
|
|
|
|
fn build_ui(ctx: Arc<Context>, 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, 0);
|
|
|
|
let (header, page, chat_box, chat_scrolled) = build_page(ctx.clone(), app);
|
|
|
|
let split_view = OverlaySplitView::builder()
|
|
.content(&page)
|
|
.enable_hide_gesture(true)
|
|
.enable_show_gesture(true)
|
|
.collapsed(true)
|
|
.build();
|
|
|
|
let sidebar = build_sidebar(ctx.clone(), &app, &split_view);
|
|
|
|
split_view.set_sidebar(Some(&sidebar));
|
|
|
|
main_box.append(&split_view);
|
|
|
|
let toggle_button = Button::from_icon_name("go-previous-symbolic");
|
|
|
|
toggle_button.connect_clicked(clone!(
|
|
#[weak]
|
|
split_view,
|
|
move |_| {
|
|
split_view.set_show_sidebar(!split_view.shows_sidebar());
|
|
}
|
|
));
|
|
|
|
header.pack_start(&toggle_button);
|
|
|
|
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)
|
|
.content(&main_box)
|
|
.build();
|
|
|
|
let breakpoint = Breakpoint::new(BreakpointCondition::new_length(
|
|
libadwaita::BreakpointConditionLengthType::MinWidth,
|
|
700.0,
|
|
libadwaita::LengthUnit::Px,
|
|
));
|
|
|
|
breakpoint.add_setter(&split_view, "collapsed", Some(&false.into()));
|
|
breakpoint.add_setter(&toggle_button, "visible", Some(&false.into()));
|
|
|
|
window.add_breakpoint(breakpoint);
|
|
|
|
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::<libnotify::Notification>::new())),
|
|
#[cfg(all(not(feature = "libnotify"), not(feature = "notify-rust")))]
|
|
notifications: Arc::new(RwLock::new(Vec::<String>::new())),
|
|
avatars: Arc::new(Mutex::new(HashMap::new())),
|
|
latest_sign: Arc::new(AtomicU64::new(0)),
|
|
}
|
|
}
|
|
|
|
fn setup(_: &Application, ctx: Arc<Context>, 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,
|
|
ctx.config(|o| o.proxy.clone()),
|
|
ctx.config(|o| o.max_avatar_size as usize),
|
|
) else {
|
|
println!("cant load avatar: {avatar_url} request error");
|
|
continue;
|
|
};
|
|
let Ok(pixbuf) = load_pixbuf(&avatar) else {
|
|
println!("cant load avatar: {avatar_url} pixbuf error");
|
|
continue;
|
|
};
|
|
let Some(pixbuf) =
|
|
pixbuf.scale_simple(32, 32, InterpType::Bilinear)
|
|
else {
|
|
println!("cant load avatar: {avatar_url} scale image error");
|
|
continue;
|
|
};
|
|
let texture = Texture::for_pixbuf(&pixbuf);
|
|
|
|
timeout_add_once(Duration::ZERO, {
|
|
move || {
|
|
GLOBAL.with(|global| {
|
|
if let Some(ui) = &*global.borrow() {
|
|
if let Some(pics) =
|
|
ui.avatars.lock().unwrap().remove(&avatar_id)
|
|
{
|
|
for pic in pics {
|
|
pic.set_custom_image(Some(&texture));
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
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<Context>, _: &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<Context>, 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<Context>, ui: &UiModel, title: &str, message: &str) {
|
|
use std::{
|
|
hash::{DefaultHasher, Hasher},
|
|
time::UNIX_EPOCH,
|
|
};
|
|
|
|
use gtk::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_avatar_id(url: &str) -> u64 {
|
|
let mut hasher = DefaultHasher::new();
|
|
hasher.write(url.as_bytes());
|
|
hasher.finish()
|
|
}
|
|
|
|
fn load_avatar(url: &str, proxy: Option<String>, response_limit: usize) -> Option<Vec<u8>> {
|
|
let client = if let Some(proxy) = proxy {
|
|
let proxy = if proxy.starts_with("socks5://") {
|
|
proxy
|
|
} else {
|
|
format!("socks5://{proxy}")
|
|
};
|
|
|
|
reqwest::blocking::Client::builder()
|
|
.proxy(reqwest::Proxy::all(&proxy).ok()?)
|
|
.build()
|
|
.ok()?
|
|
} else {
|
|
reqwest::blocking::Client::new()
|
|
};
|
|
|
|
client.get(url).send().ok().and_then(|mut resp| {
|
|
let mut data = Vec::new();
|
|
let mut length = 0;
|
|
|
|
loop {
|
|
if length >= response_limit {
|
|
break;
|
|
}
|
|
let mut buf = vec![0; (response_limit - length).min(1024)];
|
|
let now_len = resp.read(&mut buf).ok()?;
|
|
if now_len == 0 {
|
|
break;
|
|
}
|
|
buf.truncate(now_len);
|
|
length += now_len;
|
|
data.append(&mut buf);
|
|
}
|
|
|
|
Some(data)
|
|
})
|
|
}
|
|
|
|
// 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);
|
|
|
|
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<Context>) {
|
|
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<Context>) {
|
|
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<Context>) {
|
|
#[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();
|
|
}
|
|
}
|