From beea361971789c1e9af2955ba28152130fbc9523 Mon Sep 17 00:00:00 2001 From: Alex Emmet <111742636+Alex-Emmet@users.noreply.github.com> Date: Thu, 9 Oct 2025 19:50:46 +0000 Subject: [PATCH] UI Optimizations, Basic Multi Panel system --- src/auth/auth_connector.rs | 272 +++++++------- src/data/communication.rs | 36 +- src/gui/app_state.rs | 70 ++++ src/gui/log_panel.rs | 600 +++++++++++++----------------- src/gui/mod.rs | 2 + src/gui/nav_bar.rs | 34 ++ src/gui/ratatui_interface.rs | 81 ++-- src/gui/settings_panel.rs | 1 + src/gui/user_panel.rs | 1 + src/main.rs | 7 +- src/omikron/omikron_connection.rs | 34 +- src/users/user_manager.rs | 6 +- src/users/user_profile.rs | 4 +- src/util/chat_files.rs | 94 +++-- 14 files changed, 616 insertions(+), 626 deletions(-) create mode 100644 src/gui/app_state.rs create mode 100644 src/gui/nav_bar.rs diff --git a/src/auth/auth_connector.rs b/src/auth/auth_connector.rs index 68ad0a6..edd9418 100644 --- a/src/auth/auth_connector.rs +++ b/src/auth/auth_connector.rs @@ -23,159 +23,155 @@ pub struct AuthUser { pub sub_end: i32, } -pub struct AuthConnector; +fn client() -> Client { + Client::builder() + .connect_timeout(Duration::from_secs(100)) + .timeout(Duration::from_secs(150)) + .build() + .unwrap() +} -impl AuthConnector { - fn client() -> Client { - Client::builder() - .connect_timeout(Duration::from_secs(100)) - .timeout(Duration::from_secs(150)) - .build() +pub async fn unregister_user(user_id: Uuid, reset_token: &str) -> Option { + let url = format!("https:/auth.tensamin.methanium.net/api/delete/{}/", user_id); + let client = client(); + + let mut payload = JsonValue::new_object(); + payload["reset_token"] = reset_token.into(); + + let res = client + .post(&url) + .header(CONTENT_TYPE, "application/json") + .body(payload.dump()) + .send() + .await + .ok()?; + let json = res.text().await.ok()?; + let cv = CommunicationValue::from_json(&json); + Option::from(cv.is_type(CommunicationType::success)) +} + +pub async fn get_uuid(username: &str) -> Option { + let url = format!( + "https://auth.tensamin.methanium.net/api/get/uuid/{}/", + username + ); + let client = client(); + let res = client.get(&url).send().await.ok()?; + let json = res.text().await.ok()?; + let mut cv = CommunicationValue::from_json(&json); + if !cv.is_type(CommunicationType::success) { + return None; + } + Uuid::parse_str(&*cv.get_data(DataTypes::user_id).unwrap().to_string()).ok() +} + +pub async fn get_user(user_id: Uuid) -> Option { + let url = format!("https://auth.tensamin.methanium.net/api/get/{}/", user_id); + let client = client(); + let res = client.get(&url).send().await.ok()?; + let json = res.text().await.ok()?; + + let mut cv = CommunicationValue::from_json(&json); + if cv.comm_type != CommunicationType::success { + return None; + } + + Some(AuthUser { + created_at: cv + .get_data(DataTypes::created_at) .unwrap() - } + .to_string() + .parse::() + .unwrap_or(-1), + username: cv.get_data(DataTypes::username).unwrap().to_string(), + display: cv.get_data(DataTypes::display).unwrap().to_string(), + avatar: cv.get_data(DataTypes::avatar).unwrap().to_string(), + about: cv.get_data(DataTypes::about).unwrap().to_string(), + status: cv.get_data(DataTypes::status).unwrap().to_string(), + public_key: cv.get_data(DataTypes::public_key).unwrap().to_string(), + sub_level: cv + .get_data(DataTypes::sub_level) + .unwrap() + .to_string() + .parse::() + .unwrap_or(-1), + sub_end: cv + .get_data(DataTypes::sub_end) + .unwrap() + .to_string() + .parse::() + .unwrap_or(-1), + }) +} - pub async fn unregister_user(user_id: Uuid, reset_token: &str) -> Option { - let url = format!("https:/auth.tensamin.methanium.net/api/delete/{}/", user_id); - let client = Self::client(); +pub async fn get_register() -> Option { + let url = "https://auth.tensamin.methanium.net/api/register/init/".to_string(); + let client = client(); + let res = client.get(&url).send().await.ok()?; + let json = res.text().await.ok()?; - let mut payload = JsonValue::new_object(); - payload["reset_token"] = reset_token.into(); + let mut cv = CommunicationValue::from_json(&json); + Uuid::parse_str(&*cv.get_data(DataTypes::user_id).unwrap().to_string()).ok() +} - let res = client - .post(&url) - .header(CONTENT_TYPE, "application/json") - .body(payload.dump()) - .send() - .await - .ok()?; - let json = res.text().await.ok()?; - let cv = CommunicationValue::from_json(&json); - Option::from(cv.is_type(CommunicationType::success)) - } +pub async fn complete_register(user_profile: &UserProfile, iota_id: &str) -> bool { + let url = "https://auth.tensamin.methanium.net/api/register/complete/"; + let client = client(); - pub async fn get_uuid(username: &str) -> Option { - let url = format!( - "https://auth.tensamin.methanium.net/api/get/uuid/{}/", - username - ); - let client = Self::client(); - let res = client.get(&url).send().await.ok()?; - let json = res.text().await.ok()?; - let mut cv = CommunicationValue::from_json(&json); - if !cv.is_type(CommunicationType::success) { - return None; - } - Uuid::parse_str(&*cv.get_data(DataTypes::user_id).unwrap().to_string()).ok() - } + let mut payload = JsonValue::new_object(); + payload["uuid"] = user_profile.user_id.to_string().into(); + payload["public_key"] = user_profile.public_key.clone().into(); + payload["private_key_hash"] = user_profile.private_key_hash.clone().into(); + payload["username"] = user_profile.username.clone().into(); + payload["iota_id"] = iota_id.into(); + payload["reset_token"] = user_profile.reset_token.clone().into(); - pub async fn get_user(user_id: Uuid) -> Option { - let url = format!("https://auth.tensamin.methanium.net/api/get/{}/", user_id); - let client = Self::client(); - let res = client.get(&url).send().await.ok()?; - let json = res.text().await.ok()?; + let res = client + .post(url) + .header(CONTENT_TYPE, "application/json") + .body(payload.to_string()) + .send() + .await + .unwrap(); - let mut cv = CommunicationValue::from_json(&json); - if cv.comm_type != CommunicationType::success { - return None; - } + println!("Status: {}", res.status()); + let body = res.text().await.unwrap(); + println!("Response body:\n{}", body); - Some(AuthUser { - created_at: cv - .get_data(DataTypes::created_at) - .unwrap() - .to_string() - .parse::() - .unwrap_or(-1), - username: cv.get_data(DataTypes::username).unwrap().to_string(), - display: cv.get_data(DataTypes::display).unwrap().to_string(), - avatar: cv.get_data(DataTypes::avatar).unwrap().to_string(), - about: cv.get_data(DataTypes::about).unwrap().to_string(), - status: cv.get_data(DataTypes::status).unwrap().to_string(), - public_key: cv.get_data(DataTypes::public_key).unwrap().to_string(), - sub_level: cv - .get_data(DataTypes::sub_level) - .unwrap() - .to_string() - .parse::() - .unwrap_or(-1), - sub_end: cv - .get_data(DataTypes::sub_end) - .unwrap() - .to_string() - .parse::() - .unwrap_or(-1), - }) - } + CommunicationValue::from_json(&body).is_type(CommunicationType::success) +} - pub async fn get_register() -> Option { - let url = "https://auth.tensamin.methanium.net/api/register/init/".to_string(); - let client = Self::client(); - let res = client.get(&url).send().await.ok()?; - let json = res.text().await.ok()?; +pub async fn migrate_user(user_profile: &mut UserProfile) -> bool { + let url = format!( + "https://auth.tensamin.methanium.net/api/change/iota-id/{}", + user_profile.user_id + ); + let client = client(); - let mut cv = CommunicationValue::from_json(&json); - Uuid::parse_str(&*cv.get_data(DataTypes::user_id).unwrap().to_string()).ok() - } + let mut payload = JsonValue::new_object(); + payload["iota_id"] = JsonValue::String(CONFIG.lock().unwrap().get_iota_id().to_string()); + payload["reset_token"] = user_profile.reset_token.clone().into(); + payload["new_token"] = user_profile.randomize_reset_token().into(); - pub async fn complete_register(user_profile: &UserProfile, iota_id: &str) -> bool { - let url = "https://auth.tensamin.methanium.net/api/register/complete/"; - let client = Self::client(); + let res = client + .post(url) + .header(CONTENT_TYPE, "application/json") + .body(payload.dump()) + .send() + .await; - let mut payload = JsonValue::new_object(); - payload["uuid"] = user_profile.user_id.to_string().into(); - payload["public_key"] = user_profile.public_key.clone().into(); - payload["private_key_hash"] = user_profile.private_key_hash.clone().into(); - payload["username"] = user_profile.username.clone().into(); - payload["iota_id"] = iota_id.into(); - payload["reset_token"] = user_profile.reset_token.clone().into(); - - let res = client - .post(url) - .header(CONTENT_TYPE, "application/json") - .body(payload.to_string()) - .send() - .await - .unwrap(); - - println!("Status: {}", res.status()); - let body = res.text().await.unwrap(); - println!("Response body:\n{}", body); - - CommunicationValue::from_json(&body).is_type(CommunicationType::success) - } - - pub async fn migrate_user(user_profile: &mut UserProfile) -> bool { - let url = format!( - "https://auth.tensamin.methanium.net/api/change/iota-id/{}", - user_profile.user_id - ); - let client = Self::client(); - - let mut payload = JsonValue::new_object(); - payload["iota_id"] = JsonValue::String(CONFIG.lock().unwrap().get_iota_id().to_string()); - payload["reset_token"] = user_profile.reset_token.clone().into(); - payload["new_token"] = user_profile.randomize_reset_token().into(); - - let res = client - .post(url) - .header(CONTENT_TYPE, "application/json") - .body(payload.dump()) - .send() - .await; - - match res { - Ok(resp) => Self::handle_response(resp).await, - Err(_) => false, - } + match res { + Ok(resp) => handle_response(resp).await, + Err(_) => false, } +} - async fn handle_response(resp: Response) -> bool { - match resp.text().await { - Ok(text) => { - let cv = CommunicationValue::from_json(&text); - cv.comm_type == CommunicationType::success - } - Err(_) => false, +async fn handle_response(resp: Response) -> bool { + match resp.text().await { + Ok(text) => { + let cv = CommunicationValue::from_json(&text.to_string()); + cv.comm_type == CommunicationType::success } + Err(_) => false, } } diff --git a/src/data/communication.rs b/src/data/communication.rs index a0e411e..80913f6 100644 --- a/src/data/communication.rs +++ b/src/data/communication.rs @@ -24,8 +24,8 @@ pub enum DataTypes { accepted, accepted_profiles, denied_profiles, - message_content, - message_chunk, + content, + messages, send_time, get_time, get_variant, @@ -50,8 +50,8 @@ pub enum DataTypes { ping_clients, matches, omikron, - loaded_messages, - message_amount, + offset, + amount, position, name, path, @@ -97,8 +97,8 @@ impl DataTypes { "accepted" => DataTypes::accepted, "acceptedprofiles" => DataTypes::accepted_profiles, "deniedprofiles" => DataTypes::denied_profiles, - "messagecontent" => DataTypes::message_content, - "messagechunk" => DataTypes::message_chunk, + "content" => DataTypes::content, + "messages" => DataTypes::messages, "sendtime" => DataTypes::send_time, "gettime" => DataTypes::get_time, "getvariant" => DataTypes::get_variant, @@ -123,8 +123,8 @@ impl DataTypes { "pingclients" => DataTypes::ping_clients, "matches" => DataTypes::matches, "omikron" => DataTypes::omikron, - "loadedmessages" => DataTypes::loaded_messages, - "messageamount" => DataTypes::message_amount, + "offset" => DataTypes::offset, + "amount" => DataTypes::amount, "position" => DataTypes::position, "name" => DataTypes::name, "path" => DataTypes::path, @@ -158,10 +158,11 @@ pub enum CommunicationType { error, success, message, + message_send, message_live, message_other_iota, message_chunk, - message_get, + messages_get, change_confirm, confirm_receive, confirm_read, @@ -210,7 +211,8 @@ impl CommunicationType { "messagelive" => CommunicationType::message_live, "messageotheriota" => CommunicationType::message_other_iota, "messagechunk" => CommunicationType::message_chunk, - "messageget" => CommunicationType::message_get, + "messagesget" => CommunicationType::messages_get, + "message_send" => CommunicationType::message_send, "changeconfirm" => CommunicationType::change_confirm, "confirmreceive" => CommunicationType::confirm_receive, "confirmread" => CommunicationType::confirm_read, @@ -348,11 +350,10 @@ impl CommunicationValue { let mut data = HashMap::new(); if parsed["data"].is_object() { for (k, v) in parsed["data"].entries() { - if let Some(val) = v.as_str() { - data.insert(DataTypes::parse(k.to_string()), v.clone()); - } + data.insert(DataTypes::parse(k.to_string()), v.clone()); } } + Self { id: uuid, comm_type, @@ -389,13 +390,8 @@ impl CommunicationValue { .with_receiver(receiver.unwrap()) .add_data(DataTypes::send_time, JsonValue::String(now_ms.to_string())) .add_data( - DataTypes::message_content, - JsonValue::String( - original - .get_data(DataTypes::message_content) - .unwrap() - .to_string(), - ), + DataTypes::content, + JsonValue::String(original.get_data(DataTypes::content).unwrap().to_string()), ); // include sender_id if the original had one diff --git a/src/gui/app_state.rs b/src/gui/app_state.rs new file mode 100644 index 0000000..8f4e380 --- /dev/null +++ b/src/gui/app_state.rs @@ -0,0 +1,70 @@ +use std::collections::VecDeque; + +#[derive(Clone)] +pub struct AppState { + pub logs: VecDeque, + pub cpu: Vec<(f64, f64)>, + pub ram: Vec<(f64, f64)>, + pub ping: Vec<(f64, f64)>, + pub net_up: Vec<(f64, f64)>, + pub net_down: Vec<(f64, f64)>, + pub sys_info: String, +} +const MAX_POINTS: usize = 1000; +const MAX_LOGS: usize = 100; + +impl AppState { + pub fn new() -> Self { + Self { + logs: VecDeque::new(), + cpu: Vec::new(), + ram: Vec::new(), + ping: Vec::new(), + net_up: Vec::new(), + net_down: Vec::new(), + sys_info: String::from("Loading..."), + } + } + + pub fn push_log(&mut self, msg: String) { + if self.logs.len() >= MAX_LOGS { + self.logs.pop_front(); + } + self.logs.push_back(msg); + } + + pub fn push_cpu(&mut self, pt: (f64, f64)) { + self.cpu.push(pt); + if self.cpu.len() > MAX_POINTS { + self.cpu.remove(0); + } + } + + pub fn push_ram(&mut self, pt: (f64, f64)) { + self.ram.push(pt); + if self.ram.len() > MAX_POINTS { + self.ram.remove(0); + } + } + + pub fn push_ping_val(&mut self, pt: f64) { + self.ping.push((self.ping.len() as f64, pt)); + if self.ping.len() > MAX_POINTS { + self.ping.remove(0); + } + } + + pub fn push_net_up(&mut self, pt: (f64, f64)) { + self.net_up.push(pt); + if self.net_up.len() > MAX_POINTS { + self.net_up.remove(0); + } + } + + pub fn push_net_down(&mut self, pt: (f64, f64)) { + self.net_down.push(pt); + if self.net_down.len() > MAX_POINTS { + self.net_down.remove(0); + } + } +} diff --git a/src/gui/log_panel.rs b/src/gui/log_panel.rs index 5618198..5bf2493 100644 --- a/src/gui/log_panel.rs +++ b/src/gui/log_panel.rs @@ -1,10 +1,16 @@ use crate::APP_STATE; +use crate::gui::ratatui_interface::TERMINAL; use crate::gui::widgets::betterblock::draw_block_joins; use crate::langu::language_manager::from_key; use crossterm::{ - ExecutableCommand, - terminal::{LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, + ExecutableCommand, execute, + terminal::{ + Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, + enable_raw_mode, + }, }; + +use futures_util::lock::Mutex; use ratatui::widgets::{ BorderType, canvas::{Canvas, Line}, @@ -13,83 +19,20 @@ use ratatui::{ Terminal, backend::CrosstermBackend, layout::{Constraint, Direction, Layout}, - style::Color, + style::{Color, Style}, symbols, widgets::{Block, Borders, List, ListItem, Paragraph}, }; -use std::{collections::VecDeque, io::stdout, process::Command, thread, time::Duration}; +use std::{ + collections::VecDeque, + io::{Stdout, Write, stdout}, + process::Command, + sync::{Arc, LazyLock}, + thread, + time::Duration, +}; use sysinfo::{RefreshKind, System}; -const MAX_POINTS: usize = 1000; -const MAX_LOGS: usize = 100; - -#[derive(Clone)] -pub struct AppState { - logs: VecDeque, - cpu: Vec<(f64, f64)>, - ram: Vec<(f64, f64)>, - ping: Vec<(f64, f64)>, - net_up: Vec<(f64, f64)>, - net_down: Vec<(f64, f64)>, - sys_info: String, -} - -impl AppState { - pub fn new() -> Self { - Self { - logs: VecDeque::new(), - cpu: Vec::new(), - ram: Vec::new(), - ping: Vec::new(), - net_up: Vec::new(), - net_down: Vec::new(), - sys_info: String::from("Loading..."), - } - } - - pub fn push_log(&mut self, msg: String) { - if self.logs.len() >= MAX_LOGS { - self.logs.pop_front(); - } - self.logs.push_back(msg); - } - - pub fn push_cpu(&mut self, pt: (f64, f64)) { - self.cpu.push(pt); - if self.cpu.len() > MAX_POINTS { - self.cpu.remove(0); - } - } - - pub fn push_ram(&mut self, pt: (f64, f64)) { - self.ram.push(pt); - if self.ram.len() > MAX_POINTS { - self.ram.remove(0); - } - } - - pub fn push_ping_val(&mut self, pt: f64) { - self.ping.push((self.ping.len() as f64, pt)); - if self.ping.len() > MAX_POINTS { - self.ping.remove(0); - } - } - - pub fn push_net_up(&mut self, pt: (f64, f64)) { - self.net_up.push(pt); - if self.net_up.len() > MAX_POINTS { - self.net_up.remove(0); - } - } - - pub fn push_net_down(&mut self, pt: (f64, f64)) { - self.net_down.push(pt); - if self.net_down.len() > MAX_POINTS { - self.net_down.remove(0); - } - } -} - pub fn log_message_trans(key: impl Into) { APP_STATE.lock().unwrap().push_log(from_key(&key.into())); } @@ -146,36 +89,7 @@ fn downsample_to_fit_width(data: &[(f64, f64)], width: u16) -> Vec<(f64, f64)> { } } -fn measure_ping_ms(host: &str) -> Option { - // adapt for Windows - let output = Command::new("ping") - .arg("-c") - .arg("1") - .arg("-W") - .arg("1") - .arg(host) - .output() - .ok()?; - let out = String::from_utf8_lossy(&output.stdout); - for line in out.lines() { - if line.contains("time=") { - if let Some(idx) = line.find("time=") { - let substr = &line[idx + 5..]; - if let Some(end) = substr.find(" ms") { - let num = &substr[..end]; - if let Ok(f) = num.parse::() { - return Some(f); - } - } - } - } - } - None -} - pub fn setup() { - enable_raw_mode().unwrap(); - // Start a background thread to sample metrics thread::spawn(move || { let mut sys = System::new_with_specifics(RefreshKind::new()); @@ -222,256 +136,248 @@ pub fn setup() { } counter += 1.0; - thread::sleep(Duration::from_millis(250)); + thread::sleep(Duration::from_millis(1000)); } }); - - // UI rendering loop in a separate thread - thread::spawn(move || { - let stdout = stdout(); - let mut terminal = Terminal::new(CrosstermBackend::new(stdout)).unwrap(); - - enable_raw_mode().unwrap(); - - loop { - terminal - .draw(|f| { - let sys = System::new_all(); - - let size = f.size(); - let chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(40), Constraint::Percentage(60)]) - .split(size); - - let left = chunks[0]; - let right = chunks[1]; - - { - let st = APP_STATE.lock().unwrap(); - let items: Vec = st - .logs - .iter() - .rev() - .map(|s| ListItem::new(s.clone())) - .collect(); - let list = List::new(items) - .block(Block::default().title("Logs").borders(Borders::ALL)); - f.render_widget(list, left); - } - - let right_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Length(3), Constraint::Min(0)]) - .split(right); - - { - let st = APP_STATE.lock().unwrap(); - let header = Paragraph::new(st.sys_info.clone()).block( +} + +pub fn render() { + tokio::spawn(async move { + TERMINAL + .lock() + .await + .draw(|f| { + let sys = System::new_all(); + + let size = f.size(); + let chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(40), Constraint::Percentage(60)]) + .split(size); + + let left = chunks[0]; + let right = chunks[1]; + + { + let st = APP_STATE.lock().unwrap(); + let items: Vec = st + .logs + .iter() + .rev() + .map(|s| ListItem::new(s.clone())) + .collect(); + let list = List::new(items) + .block(Block::default().title("Logs").borders(Borders::ALL)); + f.render_widget(list, left); + } + + let right_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(3), Constraint::Min(0)]) + .split(right); + + { + let st = APP_STATE.lock().unwrap(); + let header = Paragraph::new(st.sys_info.clone()).block( + Block::default() + .title("System Info") + .borders(Borders::TOP.union(Borders::LEFT).union(Borders::RIGHT)), + ); + f.render_widget(header, right_chunks[0]); + } + + let grid_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(49), Constraint::Percentage(51)]) + .split(right_chunks[1]); + + let left_column = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Percentage(49), Constraint::Percentage(51)]) + .split(grid_chunks[0]); + + let right_column = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Percentage(49), Constraint::Percentage(51)]) + .split(grid_chunks[1]); + + let st = APP_STATE.lock().unwrap(); + let w = grid_chunks[0].width.saturating_sub(2); + + let ping_ds = downsample_to_fit_width(&smooth_data(&st.ping, 3), w + 4); + let cpu_ds = downsample_to_fit_width(&smooth_data(&st.cpu, 3), w); + let ram_ds = downsample_to_fit_width(&smooth_data(&st.ram, 3), w); + let down_ds = downsample_to_fit_width(&st.net_down, w + 4); + let up_ds = downsample_to_fit_width(&st.net_up, w + 4); + + // ---- Render CPU ---- + { + let min_x = cpu_ds.first().map(|(x, _)| *x).unwrap_or(0.0); + let max_x = cpu_ds.last().map(|(x, _)| *x).unwrap_or(100.0); + let block = Block::default() + .title(format!( + "CPU {}%", + &st.cpu.last().unwrap_or(&(0.0 as f64, 0.0 as f64)).1 + )) + .borders(Borders::TOP.union(Borders::RIGHT).union(Borders::LEFT)); + let canvas = Canvas::default() + .block(block) + .x_bounds([min_x, max_x]) + .y_bounds([0.0, 100.0]) + .paint(|ctx| { + for (x, y) in &cpu_ds { + ctx.draw(&Line { + x1: *x, + y1: 0.0, + x2: *x, + y2: *y, + color: Color::Cyan, + }); + } + }); + f.render_widget(canvas, left_column[0]); + draw_block_joins( + f, + left_column[0], + Borders::TOP.union(Borders::LEFT), + Borders::TOP, + ); + draw_block_joins( + f, + left_column[0], + Borders::TOP.union(Borders::RIGHT), + Borders::RIGHT, + ); + } + + // ---- Render RAM ---- + { + let min_x = ram_ds.first().map(|(x, _)| *x).unwrap_or(0.0); + let max_x = ram_ds.last().map(|(x, _)| *x).unwrap_or(100.0); + + let ram_used = sys.used_memory() as f64 / 1024.0 / 1024.0 / 1024.0; + let ram_total = sys.total_memory() as f64 / 1024.0 / 1024.0 / 1024.0; + let ram = (sys.used_memory() as f64 / sys.total_memory() as f64) * 100.0; + + let canvas = Canvas::default() + .block( Block::default() - .title("System Info") - .borders(Borders::TOP.union(Borders::LEFT).union(Borders::RIGHT)), - ); - f.render_widget(header, right_chunks[0]); - } - - let grid_chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(49), Constraint::Percentage(51)]) - .split(right_chunks[1]); - - let left_column = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Percentage(49), Constraint::Percentage(51)]) - .split(grid_chunks[0]); - - let right_column = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Percentage(49), Constraint::Percentage(51)]) - .split(grid_chunks[1]); - - let st = APP_STATE.lock().unwrap(); - let w = grid_chunks[0].width.saturating_sub(2); - - let ping_ds = downsample_to_fit_width(&smooth_data(&st.ping, 3), w + 1); - let cpu_ds = downsample_to_fit_width(&smooth_data(&st.cpu, 3), w); - let ram_ds = downsample_to_fit_width(&smooth_data(&st.ram, 3), w); - let down_ds = downsample_to_fit_width(&st.net_down, w + 1); - let up_ds = downsample_to_fit_width(&st.net_up, w + 1); - - // ---- Render CPU ---- - { - let min_x = cpu_ds.first().map(|(x, _)| *x).unwrap_or(0.0); - let max_x = cpu_ds.last().map(|(x, _)| *x).unwrap_or(100.0); - let block = Block::default() - .title(format!( - "CPU {}%", - &st.cpu.last().unwrap_or(&(0.0 as f64, 0.0 as f64)).1 - )) - .borders(Borders::TOP.union(Borders::RIGHT).union(Borders::LEFT)); - let canvas = Canvas::default() - .block(block) - .x_bounds([min_x, max_x]) - .y_bounds([0.0, 100.0]) - .paint(|ctx| { - for (x, y) in &cpu_ds { - ctx.draw(&Line { - x1: *x, - y1: 0.0, - x2: *x, - y2: *y, - color: Color::Cyan, - }); - } - }); - f.render_widget(canvas, left_column[0]); - draw_block_joins( - f, - left_column[0], - Borders::TOP.union(Borders::LEFT), - Borders::TOP, - ); - draw_block_joins( - f, - left_column[0], - Borders::TOP.union(Borders::RIGHT), - Borders::RIGHT, - ); - } - - // ---- Render RAM ---- - { - let min_x = ram_ds.first().map(|(x, _)| *x).unwrap_or(0.0); - let max_x = ram_ds.last().map(|(x, _)| *x).unwrap_or(100.0); - - let ram_used = sys.used_memory() as f64 / 1024.0 / 1024.0 / 1024.0; - let ram_total = sys.total_memory() as f64 / 1024.0 / 1024.0 / 1024.0; - let ram = (sys.used_memory() as f64 / sys.total_memory() as f64) * 100.0; - - let canvas = Canvas::default() - .block( - Block::default() - .title(format!( - "RAM {:.1}% ({:.1}GiB/{:.1}GiB)", - ram, ram_used, ram_total - )) - .borders(Borders::ALL), - ) - .x_bounds([min_x, max_x]) - .y_bounds([0.0, 100.0]) - .paint(|ctx| { - for (x, y) in &ram_ds { - ctx.draw(&Line { - x1: *x, - y1: 0.0, - x2: *x, - y2: *y, - color: Color::Magenta, - }); - } - }); - - f.render_widget(canvas, left_column[1]); - draw_block_joins( - f, - left_column[1], - Borders::ALL, - Borders::TOP.union(Borders::RIGHT), - ); - } - - // ---- Render Ping ---- - { - let min_x = ping_ds.first().map(|(x, _)| *x).unwrap_or(0.0); - let max_x = ping_ds.last().map(|(x, _)| *x).unwrap_or(100.0); - let max_y = ping_ds.iter().map(|(_, y)| *y).fold(1.0, f64::max); - - let canvas = Canvas::default() - .block( - Block::default() - .title(format!("Ping {:.1}(ms)", max_y)) - .borders(Borders::TOP.union(Borders::RIGHT)), - ) - .x_bounds([min_x, max_x]) - .y_bounds([0.0, max_y]) - .paint(|ctx| { - for (x, y) in &ping_ds { - ctx.draw(&Line { - x1: *x, - y1: 0.0, - x2: *x, - y2: *y, - color: Color::Yellow, - }); - } - }); - f.render_widget(canvas, right_column[0]); - draw_block_joins( - f, - right_column[0], - Borders::RIGHT.union(Borders::TOP), - Borders::TOP, - ); - } - - // ---- Render Network ---- - { - let min_x = up_ds.first().map(|(x, _)| *x).unwrap_or(0.0); - let max_x = up_ds.last().map(|(x, _)| *x).unwrap_or(100.0); - let mut max_sum: f64 = 1.0; - for ((_, up), (_, down)) in up_ds.iter().zip(down_ds.iter()) { - max_sum = max_sum.max(up + down); - } - - let canvas = - Canvas::default() - .block(Block::default().title("Network Up/Down").borders( - Borders::TOP.union(Borders::RIGHT).union(Borders::BOTTOM), + .title(format!( + "RAM {:.1}% ({:.1}GiB/{:.1}GiB)", + ram, ram_used, ram_total )) - .x_bounds([min_x, max_x]) - .y_bounds([0.0, max_sum]) - .paint(|ctx| { - for (x, dval) in &down_ds { - ctx.draw(&Line { - x1: *x, - y1: 0.0, - x2: *x, - y2: *dval, - color: Color::Red, - }); - } - for (x, uval) in &up_ds { - let dval = down_ds - .iter() - .find(|(xx, _)| (*xx - *x).abs() < f64::EPSILON) - .map(|(_, y)| *y) - .unwrap_or(0.0); - - ctx.draw(&Line { - x1: *x, - y1: dval, - x2: *x, - y2: dval + *uval, - color: Color::Green, - }); - } + .borders(Borders::ALL), + ) + .x_bounds([min_x, max_x]) + .y_bounds([0.0, 100.0]) + .paint(|ctx| { + for (x, y) in &ram_ds { + ctx.draw(&Line { + x1: *x, + y1: 0.0, + x2: *x, + y2: *y, + color: Color::Magenta, }); - - f.render_widget(canvas, right_column[1]); - draw_block_joins( - f, - right_column[1], - Borders::TOP.union(Borders::RIGHT).union(Borders::BOTTOM), - Borders::TOP, - ); + } + }); + + f.render_widget(canvas, left_column[1]); + draw_block_joins( + f, + left_column[1], + Borders::ALL, + Borders::TOP.union(Borders::RIGHT), + ); + } + + // ---- Render Ping ---- + { + let min_x = ping_ds.first().map(|(x, _)| *x).unwrap_or(0.0); + let max_x = ping_ds.last().map(|(x, _)| *x).unwrap_or(100.0); + let max_y = ping_ds.iter().map(|(_, y)| *y).fold(1.0, f64::max); + + let canvas = Canvas::default() + .block( + Block::default() + .title(format!("Ping {:.1}(ms)", max_y)) + .borders(Borders::TOP.union(Borders::RIGHT)), + ) + .x_bounds([min_x, max_x]) + .y_bounds([0.0, max_y]) + .paint(|ctx| { + for (x, y) in &ping_ds { + ctx.draw(&Line { + x1: *x, + y1: 0.0, + x2: *x, + y2: *y, + color: Color::Yellow, + }); + } + }); + f.render_widget(canvas, right_column[0]); + draw_block_joins( + f, + right_column[0], + Borders::RIGHT.union(Borders::TOP), + Borders::TOP, + ); + } + + // ---- Render Network ---- + { + let min_x = up_ds.first().map(|(x, _)| *x).unwrap_or(0.0); + let max_x = up_ds.last().map(|(x, _)| *x).unwrap_or(100.0); + let mut max_sum: f64 = 1.0; + for ((_, up), (_, down)) in up_ds.iter().zip(down_ds.iter()) { + max_sum = max_sum.max(up + down); } - }) - .unwrap(); - thread::sleep(Duration::from_millis(100)); - } + let canvas = Canvas::default() + .block( + Block::default() + .title("Network Up/Down") + .borders(Borders::TOP.union(Borders::RIGHT).union(Borders::BOTTOM)), + ) + .x_bounds([min_x, max_x]) + .y_bounds([0.0, max_sum]) + .paint(|ctx| { + for (x, dval) in &down_ds { + ctx.draw(&Line { + x1: *x, + y1: 0.0, + x2: *x, + y2: *dval, + color: Color::Red, + }); + } + for (x, uval) in &up_ds { + let dval = down_ds + .iter() + .find(|(xx, _)| (*xx - *x).abs() < f64::EPSILON) + .map(|(_, y)| *y) + .unwrap_or(0.0); + + ctx.draw(&Line { + x1: *x, + y1: dval, + x2: *x, + y2: dval + *uval, + color: Color::Green, + }); + } + }); + + f.render_widget(canvas, right_column[1]); + draw_block_joins( + f, + right_column[1], + Borders::TOP.union(Borders::RIGHT).union(Borders::BOTTOM), + Borders::TOP, + ); + } + }) + .unwrap(); }); - // Clean up terminal before exit - disable_raw_mode().unwrap(); - stdout().execute(LeaveAlternateScreen).unwrap(); } diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 0f4d72d..1d6a574 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -1,4 +1,6 @@ +pub mod app_state; pub mod log_panel; +pub mod nav_bar; pub mod ratatui_interface; pub mod widgets { pub mod betterblock; diff --git a/src/gui/nav_bar.rs b/src/gui/nav_bar.rs new file mode 100644 index 0000000..0919daf --- /dev/null +++ b/src/gui/nav_bar.rs @@ -0,0 +1,34 @@ +use std::str; + +use crate::gui::log_panel; + +pub struct Screen { + pub title: String, + pub render: Box, +} +impl Screen { + pub fn new(title: &str, render: Box) -> Self { + Screen { + title: String::from(title), + render, + } + } + pub fn renderf(&self) { + (self.render)(); + } +} +pub struct NavBar { + pub current_screen: Screen, + pub screens: Vec, +} +impl NavBar { + pub fn new() -> Self { + NavBar { + current_screen: Screen { + title: String::from("Main"), + render: Box::new(|| log_panel::render()), + }, + screens: Vec::new(), + } + } +} diff --git a/src/gui/ratatui_interface.rs b/src/gui/ratatui_interface.rs index 36abbf3..2edce44 100644 --- a/src/gui/ratatui_interface.rs +++ b/src/gui/ratatui_interface.rs @@ -1,58 +1,45 @@ +use std::{ + io::Stdout, + sync::{Arc, LazyLock}, + time::Duration, +}; + +use crate::gui::nav_bar::NavBar; use color_eyre::Result; -use ratatui::Frame; -use ratatui::layout::{Constraint, Direction, Layout}; -use ratatui::widgets::{Block, Paragraph}; -use sys_info; +use crossterm::{ + execute, + terminal::{EnterAlternateScreen, enable_raw_mode}, +}; +use futures_util::lock::Mutex; +use ratatui::{Terminal, prelude::CrosstermBackend}; +use std::io::stdout; use tokio::task; -pub fn launch(connection_status: bool) -> Result<()> { +pub static TERMINAL: LazyLock>>>> = + LazyLock::new(|| { + Arc::new(Mutex::new( + Terminal::new(CrosstermBackend::new(stdout())).unwrap(), + )) + }); +pub static NAV_BAR: LazyLock>> = + LazyLock::new(|| Arc::new(Mutex::new(NavBar::new()))); + +pub fn launch() -> Result<()> { color_eyre::install()?; - task::spawn(run(connection_status)); + task::spawn(run()); ratatui::restore(); Ok(()) } -async fn run(connection_status: bool) -> Result<()> { +fn init_terminal() { + let mut stdout = stdout(); + execute!(stdout, EnterAlternateScreen).unwrap(); + enable_raw_mode().unwrap(); +} +async fn run() -> Result<()> { + init_terminal(); loop { - ratatui::init().draw(|frame| render(frame, connection_status))?; + NAV_BAR.lock().await.current_screen.renderf(); + tokio::time::sleep(Duration::from_millis(1000)).await; } } - -fn render(frame: &mut Frame, connection_status: bool) { - let main_layout = Layout::default() - .direction(Direction::Horizontal) - .margin(1) - .constraints([Constraint::Percentage((100))].as_ref()); - let main_block = Block::bordered().title("Tensamin - Iota"); - let main_chunks = main_layout.split(frame.area()); - - let [left, right] = Layout::horizontal([Constraint::Fill(1); 2]).areas(main_chunks[0]); - let [top_right, bottom_right] = Layout::vertical([Constraint::Fill(1); 2]).areas(right); - - let block_logs = Block::bordered().title("Logs"); - let block_systeminfo = Block::bordered().title("System Info"); - let block_ping = Block::bordered().title("Ping"); - - let mut operating_system: String; - match sys_info::os_type() { - Ok(os) => operating_system = os, - Err(error) => operating_system = error.to_string(), - } - - let mut text_content: String = String::from(""); - text_content.push_str("Operating System: "); - text_content.push_str(operating_system.as_str()); - text_content.push_str("\nConnection: "); - text_content.push_str(if connection_status { - "Connected" - } else { - "Disconnected" - }); - - let paragraph_systeminfo = Paragraph::new(text_content).block(block_systeminfo); - - frame.render_widget(Block::bordered().title("Tensamin - Iota"), frame.area()); - frame.render_widget(Block::bordered().title("Logs"), left); - frame.render_widget(paragraph_systeminfo, top_right); - frame.render_widget(Block::bordered().title("Ping"), bottom_right); -} diff --git a/src/gui/settings_panel.rs b/src/gui/settings_panel.rs index e69de29..8b13789 100644 --- a/src/gui/settings_panel.rs +++ b/src/gui/settings_panel.rs @@ -0,0 +1 @@ + diff --git a/src/gui/user_panel.rs b/src/gui/user_panel.rs index e69de29..8b13789 100644 --- a/src/gui/user_panel.rs +++ b/src/gui/user_panel.rs @@ -0,0 +1 @@ + diff --git a/src/main.rs b/src/main.rs index e98d59e..f03f1d4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,8 +15,9 @@ mod users; mod util; use crate::data::communication::{CommunicationType, CommunicationValue, DataTypes}; -use crate::gui::log_panel; -use crate::gui::log_panel::{AppState, log_message, log_message_trans}; +use crate::gui::app_state::AppState; +use crate::gui::log_panel::{log_message, log_message_trans}; +use crate::gui::{log_panel, ratatui_interface}; use crate::langu::language_creator; use crate::omikron::omikron_connection::OmikronConnection; use crate::users::user_manager::UserManager; @@ -34,8 +35,8 @@ async fn main() { //} // UI - log_panel::setup(); + ratatui_interface::launch(); // LANGUAGE PACK language_creator::create_languages(); diff --git a/src/omikron/omikron_connection.rs b/src/omikron/omikron_connection.rs index 2d4f4da..70b6240 100644 --- a/src/omikron/omikron_connection.rs +++ b/src/omikron/omikron_connection.rs @@ -1,17 +1,13 @@ -use crate::APP_STATE; use crate::data::communication::{CommunicationType, CommunicationValue, DataTypes}; use crate::gui::log_panel::log_message; use crate::gui::log_panel::log_message_trans; -use crate::omikron::ping_pong_task::*; use crate::users::contact::Contact; use crate::users::user_community_util::UserCommunityUtil; use crate::util::chat_files::ChatFiles; use crate::util::chats_util::{get_user, get_users, mod_user}; use futures_util::{SinkExt, StreamExt}; use json::JsonValue; -use json::number::Number; use std::collections::HashMap; -use std::ops::Deref; use std::str::FromStr; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; @@ -121,7 +117,6 @@ impl OmikronConnection { // ************************************************ // // Direct messages // // ************************************************ // - log_message_trans(format!("{:?}", &cv.comm_type)); if cv.is_type(CommunicationType::message_other_iota) { let sender_id = &cv.get_sender(); @@ -135,10 +130,7 @@ impl OmikronConnection { false, *receiver_id, *sender_id, - cv.get_data(DataTypes::message_content) - .unwrap() - .as_str() - .unwrap(), + cv.get_data(DataTypes::content).unwrap().as_str().unwrap(), ); let response = CommunicationValue::new(CommunicationType::message_live) .with_id(cv.get_id()) @@ -149,7 +141,7 @@ impl OmikronConnection { ) .add_data( DataTypes::message, - cv.get_data(DataTypes::message_content).unwrap().clone(), + cv.get_data(DataTypes::content).unwrap().clone(), ) .add_data( DataTypes::sender_id, @@ -163,7 +155,7 @@ impl OmikronConnection { continue; } - if cv.is_type(CommunicationType::message) { + if cv.is_type(CommunicationType::message_send) { /* DATA CONTAINER: "sent_by_self": true, "timestamp": unixTimestamp, @@ -189,8 +181,14 @@ impl OmikronConnection { true, my_id, other_id, - &*cv.get_data(DataTypes::message_content).unwrap().to_string(), + &*cv.get_data(DataTypes::content).unwrap().to_string(), ); + log_message(format!( + "Received a message from {} reading {} to {}", + my_id, + &*cv.get_data(DataTypes::content).unwrap().to_string(), + other_id + )); let ack = CommunicationValue::ack_message(cv.get_id(), my_id); Self::send_message_static(&writer.clone(), ack.to_json().to_string()) .await; @@ -203,30 +201,32 @@ impl OmikronConnection { continue; } - if cv.is_type(CommunicationType::message_get) { + if cv.is_type(CommunicationType::messages_get) { let my_id = cv.get_sender(); let partner_id = Uuid::from_str( &*cv.get_data(DataTypes::user_id).unwrap().to_string(), ) .unwrap(); let offset = cv - .get_data(DataTypes::loaded_messages) + .get_data(DataTypes::offset) .unwrap_or(&JsonValue::Null) .to_string() .parse::() .unwrap_or(0); let amount = cv - .get_data(DataTypes::message_amount) + .get_data(DataTypes::amount) .unwrap_or(&JsonValue::Null) .to_string() .parse::() .unwrap_or(0); + log_message(format!("A:{} O:{}, P:{}", amount, offset, partner_id)); let messages = ChatFiles::get_messages(my_id, partner_id, offset, amount); - let resp = CommunicationValue::new(CommunicationType::message_chunk) + log_message(messages.to_string()); + let resp = CommunicationValue::new(CommunicationType::messages_get) .with_id(cv.get_id()) .with_receiver(my_id) - .add_data(DataTypes::message_chunk, messages); + .add_data(DataTypes::messages, messages); Self::send_message_static(&writer.clone(), resp.to_json().to_string()) .await; diff --git a/src/users/user_manager.rs b/src/users/user_manager.rs index 4d74b91..ea45b4c 100644 --- a/src/users/user_manager.rs +++ b/src/users/user_manager.rs @@ -1,4 +1,4 @@ -use crate::auth::auth_connector::AuthConnector; +use crate::auth::auth_connector; use crate::users::user_profile::UserProfile; use crate::users::user_profile_full::UserProfileFull; use crate::util::config_util::CONFIG; @@ -29,7 +29,7 @@ static UNIQUE: Lazy> = Lazy::new(|| Mutex::new(false)); impl UserManager { pub async fn create_user(username: &str) -> Option { - let user_id = AuthConnector::get_register().await.unwrap(); + let user_id = auth_connector::get_register().await.unwrap(); let mut buf = [0u8; 56]; let mut rng = OsRng; rng.fill_bytes(&mut buf); @@ -63,7 +63,7 @@ impl UserManager { private_key: general_purpose::STANDARD.encode(&private_key.as_bytes()), }; - AuthConnector::complete_register(&up, &CONFIG.lock().unwrap().get_iota_id().to_string()) + auth_connector::complete_register(&up, &CONFIG.lock().unwrap().get_iota_id().to_string()) .await; save_file( "", diff --git a/src/users/user_profile.rs b/src/users/user_profile.rs index 2e2dd86..f290fdc 100644 --- a/src/users/user_profile.rs +++ b/src/users/user_profile.rs @@ -1,4 +1,4 @@ -use crate::auth::auth_connector::AuthConnector; +use crate::auth::auth_connector; use crate::gui::log_panel::log_message; use crate::users::user_manager::UserManager; use base64::{Engine as _, engine::general_purpose}; @@ -79,7 +79,7 @@ impl UserProfile { || j.has_key("move") || j.has_key("moving") { - if AuthConnector::migrate_user(&mut up).await { + if auth_connector::migrate_user(&mut up).await { log_message(format!("[INFO] Migration triggered for {}", up.username)); UserManager::set_unique(true); } diff --git a/src/util/chat_files.rs b/src/util/chat_files.rs index c8b965d..4adaa60 100644 --- a/src/util/chat_files.rs +++ b/src/util/chat_files.rs @@ -1,10 +1,15 @@ +use crate::util::file_util::{get_children, load_file, save_file}; use json::{self, JsonValue, array, object}; use sha2::digest::typenum::Add1; use std::fs::{self, File}; use std::io::{Read, Write}; use std::path::Path; +use sys_info::loadavg; use uuid::Uuid; +use crate::gui::log_panel::log_message; +use crate::langu::language_manager::format; + pub enum MessageState { Read, Received, @@ -26,23 +31,6 @@ impl MessageState { pub struct ChatFiles; impl ChatFiles { - fn load_file(dir: &str, file_name: &str) -> String { - let path = Path::new(dir).join(file_name); - if let Ok(mut f) = File::open(&path) { - let mut content = String::new(); - let _ = f.read_to_string(&mut content); - content - } else { - String::new() - } - } - - fn save_file(dir: &str, file_name: &str, content: &str) { - fs::create_dir_all(dir); - let path = Path::new(dir).join(file_name); - let mut file = File::create(path).unwrap(); - file.write_all(content.as_bytes()); - } pub fn add_message( send_time: i64, storage_owner_is_sender: bool, @@ -52,25 +40,38 @@ impl ChatFiles { ) { let user_dir = format!("users/{}/chats/{}", storage_owner, external_user); + if let Err(e) = fs::create_dir_all(&user_dir) { + log_message(format!("Failed to create chat directory: {}", e)); + return; + } + let mut chunk_index = 0; let mut message_chunk = array![]; // find latest chunk not full (max 800 msgs) loop { let file_name = format!("msgs_{}.json", chunk_index); - let file_content = Self::load_file(&user_dir, &file_name); + let file_content = load_file(&user_dir, &file_name); if !file_content.is_empty() { if let Ok(current_chunk) = json::parse(&file_content) { - if current_chunk.len() < 800 { + if current_chunk.is_array() && current_chunk.len() < 800 { message_chunk = current_chunk; break; } + } else { + log_message(format!("Failed to parse existing JSON file: {}", file_name)); } } else { + // New file, use empty array break; } + chunk_index += 1; + if chunk_index > 1000 { + log_message(format!("Too many message chunks. Aborting add.")); + return; + } } let json_obj = object! { @@ -79,15 +80,16 @@ impl ChatFiles { "sender_is_me" => storage_owner_is_sender, "message_state" => MessageState::Sending.as_str() }; - message_chunk.push(json_obj).unwrap(); - Self::save_file( - &user_dir, - &format!("msgs_{}.json", chunk_index), - &message_chunk.dump(), - ); + if let Err(e) = message_chunk.push(json_obj) { + log_message(format!("Failed to push new message into JSON array: {}", e)); + return; + } + + let file_name = format!("msgs_{}.json", chunk_index); + log_message(format!("Saving message to {}/{}", user_dir, file_name)); + save_file(&user_dir, &file_name, &message_chunk.dump()); } - pub fn change_message_state( storage_owner: Uuid, external_user: Uuid, @@ -108,7 +110,7 @@ impl ChatFiles { let fname_str = fname.to_string_lossy(); if fname_str.starts_with("msgs_") && fname_str.ends_with(".json") { - let file_content = Self::load_file(&user_dir, &fname_str); + let file_content = load_file(&user_dir, &fname_str); if file_content.is_empty() { continue; } @@ -124,7 +126,7 @@ impl ChatFiles { } if modified { - Self::save_file(&user_dir, &fname_str, &chunk.dump()); + save_file(&user_dir, &fname_str, &chunk.dump()); break; } } @@ -139,29 +141,20 @@ impl ChatFiles { loaded_messages: i64, amount: i64, ) -> JsonValue { - let user_dir = format!("users/{}/chats/{}", storage_owner, external_user); - let path = Path::new(&user_dir); let mut messages = array![]; - if !path.exists() { - return messages; - } - let mut latest_chunk_index: i32 = -1; - if let Ok(entries) = fs::read_dir(path) { - for entry in entries.flatten() { - let fname = entry.file_name(); - let fname_str = fname.to_string_lossy(); - if fname_str.starts_with("msgs_") && fname_str.ends_with(".json") { - if let Some(num) = fname_str - .strip_prefix("msgs_") - .and_then(|s| s.strip_suffix(".json")) - { - if let Ok(index) = num.parse::() { - if index > latest_chunk_index { - latest_chunk_index = index; - } - } + let files = get_children(&format!("users/{}/chats/{}", storage_owner, external_user)); + + for entry in files { + if let Some(num) = { + entry + .strip_prefix("msgs_") + .and_then(|s| s.strip_suffix(".json")) + } { + if let Ok(index) = num.parse::() { + if index > latest_chunk_index { + latest_chunk_index = index; } } } @@ -179,7 +172,10 @@ impl ChatFiles { break; } let file_name = format!("msgs_{}.json", chunk_index); - let file_content = Self::load_file(&user_dir, &file_name); + let file_content = load_file( + &format!("users/{}/chats/{}", storage_owner, external_user), + &file_name, + ); if file_content.is_empty() { continue; }