UI Optimizations, Basic Multi Panel system

This commit is contained in:
Alex Emmet 2025-10-09 19:50:46 +00:00
commit beea361971
14 changed files with 608 additions and 618 deletions

View file

@ -23,159 +23,155 @@ pub struct AuthUser {
pub sub_end: i32, 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 { pub async fn unregister_user(user_id: Uuid, reset_token: &str) -> Option<bool> {
fn client() -> Client { let url = format!("https:/auth.tensamin.methanium.net/api/delete/{}/", user_id);
Client::builder() let client = client();
.connect_timeout(Duration::from_secs(100))
.timeout(Duration::from_secs(150)) let mut payload = JsonValue::new_object();
.build() 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<Uuid> {
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<AuthUser> {
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() .unwrap()
} .to_string()
.parse::<i64>()
.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::<i32>()
.unwrap_or(-1),
sub_end: cv
.get_data(DataTypes::sub_end)
.unwrap()
.to_string()
.parse::<i32>()
.unwrap_or(-1),
})
}
pub async fn unregister_user(user_id: Uuid, reset_token: &str) -> Option<bool> { pub async fn get_register() -> Option<Uuid> {
let url = format!("https:/auth.tensamin.methanium.net/api/delete/{}/", user_id); let url = "https://auth.tensamin.methanium.net/api/register/init/".to_string();
let client = Self::client(); let client = client();
let res = client.get(&url).send().await.ok()?;
let json = res.text().await.ok()?;
let mut payload = JsonValue::new_object(); let mut cv = CommunicationValue::from_json(&json);
payload["reset_token"] = reset_token.into(); Uuid::parse_str(&*cv.get_data(DataTypes::user_id).unwrap().to_string()).ok()
}
let res = client pub async fn complete_register(user_profile: &UserProfile, iota_id: &str) -> bool {
.post(&url) let url = "https://auth.tensamin.methanium.net/api/register/complete/";
.header(CONTENT_TYPE, "application/json") let client = client();
.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<Uuid> { let mut payload = JsonValue::new_object();
let url = format!( payload["uuid"] = user_profile.user_id.to_string().into();
"https://auth.tensamin.methanium.net/api/get/uuid/{}/", payload["public_key"] = user_profile.public_key.clone().into();
username payload["private_key_hash"] = user_profile.private_key_hash.clone().into();
); payload["username"] = user_profile.username.clone().into();
let client = Self::client(); payload["iota_id"] = iota_id.into();
let res = client.get(&url).send().await.ok()?; payload["reset_token"] = user_profile.reset_token.clone().into();
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<AuthUser> { let res = client
let url = format!("https://auth.tensamin.methanium.net/api/get/{}/", user_id); .post(url)
let client = Self::client(); .header(CONTENT_TYPE, "application/json")
let res = client.get(&url).send().await.ok()?; .body(payload.to_string())
let json = res.text().await.ok()?; .send()
.await
.unwrap();
let mut cv = CommunicationValue::from_json(&json); println!("Status: {}", res.status());
if cv.comm_type != CommunicationType::success { let body = res.text().await.unwrap();
return None; println!("Response body:\n{}", body);
}
Some(AuthUser { CommunicationValue::from_json(&body).is_type(CommunicationType::success)
created_at: cv }
.get_data(DataTypes::created_at)
.unwrap()
.to_string()
.parse::<i64>()
.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::<i32>()
.unwrap_or(-1),
sub_end: cv
.get_data(DataTypes::sub_end)
.unwrap()
.to_string()
.parse::<i32>()
.unwrap_or(-1),
})
}
pub async fn get_register() -> Option<Uuid> { pub async fn migrate_user(user_profile: &mut UserProfile) -> bool {
let url = "https://auth.tensamin.methanium.net/api/register/init/".to_string(); let url = format!(
let client = Self::client(); "https://auth.tensamin.methanium.net/api/change/iota-id/{}",
let res = client.get(&url).send().await.ok()?; user_profile.user_id
let json = res.text().await.ok()?; );
let client = client();
let mut cv = CommunicationValue::from_json(&json); let mut payload = JsonValue::new_object();
Uuid::parse_str(&*cv.get_data(DataTypes::user_id).unwrap().to_string()).ok() 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 res = client
let url = "https://auth.tensamin.methanium.net/api/register/complete/"; .post(url)
let client = Self::client(); .header(CONTENT_TYPE, "application/json")
.body(payload.dump())
.send()
.await;
let mut payload = JsonValue::new_object(); match res {
payload["uuid"] = user_profile.user_id.to_string().into(); Ok(resp) => handle_response(resp).await,
payload["public_key"] = user_profile.public_key.clone().into(); Err(_) => false,
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,
}
} }
}
async fn handle_response(resp: Response) -> bool { async fn handle_response(resp: Response) -> bool {
match resp.text().await { match resp.text().await {
Ok(text) => { Ok(text) => {
let cv = CommunicationValue::from_json(&text); let cv = CommunicationValue::from_json(&text.to_string());
cv.comm_type == CommunicationType::success cv.comm_type == CommunicationType::success
}
Err(_) => false,
} }
Err(_) => false,
} }
} }

View file

@ -24,8 +24,8 @@ pub enum DataTypes {
accepted, accepted,
accepted_profiles, accepted_profiles,
denied_profiles, denied_profiles,
message_content, content,
message_chunk, messages,
send_time, send_time,
get_time, get_time,
get_variant, get_variant,
@ -50,8 +50,8 @@ pub enum DataTypes {
ping_clients, ping_clients,
matches, matches,
omikron, omikron,
loaded_messages, offset,
message_amount, amount,
position, position,
name, name,
path, path,
@ -97,8 +97,8 @@ impl DataTypes {
"accepted" => DataTypes::accepted, "accepted" => DataTypes::accepted,
"acceptedprofiles" => DataTypes::accepted_profiles, "acceptedprofiles" => DataTypes::accepted_profiles,
"deniedprofiles" => DataTypes::denied_profiles, "deniedprofiles" => DataTypes::denied_profiles,
"messagecontent" => DataTypes::message_content, "content" => DataTypes::content,
"messagechunk" => DataTypes::message_chunk, "messages" => DataTypes::messages,
"sendtime" => DataTypes::send_time, "sendtime" => DataTypes::send_time,
"gettime" => DataTypes::get_time, "gettime" => DataTypes::get_time,
"getvariant" => DataTypes::get_variant, "getvariant" => DataTypes::get_variant,
@ -123,8 +123,8 @@ impl DataTypes {
"pingclients" => DataTypes::ping_clients, "pingclients" => DataTypes::ping_clients,
"matches" => DataTypes::matches, "matches" => DataTypes::matches,
"omikron" => DataTypes::omikron, "omikron" => DataTypes::omikron,
"loadedmessages" => DataTypes::loaded_messages, "offset" => DataTypes::offset,
"messageamount" => DataTypes::message_amount, "amount" => DataTypes::amount,
"position" => DataTypes::position, "position" => DataTypes::position,
"name" => DataTypes::name, "name" => DataTypes::name,
"path" => DataTypes::path, "path" => DataTypes::path,
@ -158,10 +158,11 @@ pub enum CommunicationType {
error, error,
success, success,
message, message,
message_send,
message_live, message_live,
message_other_iota, message_other_iota,
message_chunk, message_chunk,
message_get, messages_get,
change_confirm, change_confirm,
confirm_receive, confirm_receive,
confirm_read, confirm_read,
@ -210,7 +211,8 @@ impl CommunicationType {
"messagelive" => CommunicationType::message_live, "messagelive" => CommunicationType::message_live,
"messageotheriota" => CommunicationType::message_other_iota, "messageotheriota" => CommunicationType::message_other_iota,
"messagechunk" => CommunicationType::message_chunk, "messagechunk" => CommunicationType::message_chunk,
"messageget" => CommunicationType::message_get, "messagesget" => CommunicationType::messages_get,
"message_send" => CommunicationType::message_send,
"changeconfirm" => CommunicationType::change_confirm, "changeconfirm" => CommunicationType::change_confirm,
"confirmreceive" => CommunicationType::confirm_receive, "confirmreceive" => CommunicationType::confirm_receive,
"confirmread" => CommunicationType::confirm_read, "confirmread" => CommunicationType::confirm_read,
@ -348,11 +350,10 @@ impl CommunicationValue {
let mut data = HashMap::new(); let mut data = HashMap::new();
if parsed["data"].is_object() { if parsed["data"].is_object() {
for (k, v) in parsed["data"].entries() { 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 { Self {
id: uuid, id: uuid,
comm_type, comm_type,
@ -389,13 +390,8 @@ impl CommunicationValue {
.with_receiver(receiver.unwrap()) .with_receiver(receiver.unwrap())
.add_data(DataTypes::send_time, JsonValue::String(now_ms.to_string())) .add_data(DataTypes::send_time, JsonValue::String(now_ms.to_string()))
.add_data( .add_data(
DataTypes::message_content, DataTypes::content,
JsonValue::String( JsonValue::String(original.get_data(DataTypes::content).unwrap().to_string()),
original
.get_data(DataTypes::message_content)
.unwrap()
.to_string(),
),
); );
// include sender_id if the original had one // include sender_id if the original had one

70
src/gui/app_state.rs Normal file
View file

@ -0,0 +1,70 @@
use std::collections::VecDeque;
#[derive(Clone)]
pub struct AppState {
pub logs: VecDeque<String>,
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);
}
}
}

View file

@ -1,10 +1,16 @@
use crate::APP_STATE; use crate::APP_STATE;
use crate::gui::ratatui_interface::TERMINAL;
use crate::gui::widgets::betterblock::draw_block_joins; use crate::gui::widgets::betterblock::draw_block_joins;
use crate::langu::language_manager::from_key; use crate::langu::language_manager::from_key;
use crossterm::{ use crossterm::{
ExecutableCommand, ExecutableCommand, execute,
terminal::{LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, terminal::{
Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode,
enable_raw_mode,
},
}; };
use futures_util::lock::Mutex;
use ratatui::widgets::{ use ratatui::widgets::{
BorderType, BorderType,
canvas::{Canvas, Line}, canvas::{Canvas, Line},
@ -13,83 +19,20 @@ use ratatui::{
Terminal, Terminal,
backend::CrosstermBackend, backend::CrosstermBackend,
layout::{Constraint, Direction, Layout}, layout::{Constraint, Direction, Layout},
style::Color, style::{Color, Style},
symbols, symbols,
widgets::{Block, Borders, List, ListItem, Paragraph}, 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}; use sysinfo::{RefreshKind, System};
const MAX_POINTS: usize = 1000;
const MAX_LOGS: usize = 100;
#[derive(Clone)]
pub struct AppState {
logs: VecDeque<String>,
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<String>) { pub fn log_message_trans(key: impl Into<String>) {
APP_STATE.lock().unwrap().push_log(from_key(&key.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<f64> {
// 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::<f64>() {
return Some(f);
}
}
}
}
}
None
}
pub fn setup() { pub fn setup() {
enable_raw_mode().unwrap();
// Start a background thread to sample metrics // Start a background thread to sample metrics
thread::spawn(move || { thread::spawn(move || {
let mut sys = System::new_with_specifics(RefreshKind::new()); let mut sys = System::new_with_specifics(RefreshKind::new());
@ -222,256 +136,248 @@ pub fn setup() {
} }
counter += 1.0; 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 || { pub fn render() {
let stdout = stdout(); tokio::spawn(async move {
let mut terminal = Terminal::new(CrosstermBackend::new(stdout)).unwrap(); TERMINAL
.lock()
enable_raw_mode().unwrap(); .await
.draw(|f| {
loop { let sys = System::new_all();
terminal
.draw(|f| { let size = f.size();
let sys = System::new_all(); let chunks = Layout::default()
.direction(Direction::Horizontal)
let size = f.size(); .constraints([Constraint::Percentage(40), Constraint::Percentage(60)])
let chunks = Layout::default() .split(size);
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(40), Constraint::Percentage(60)]) let left = chunks[0];
.split(size); let right = chunks[1];
let left = chunks[0]; {
let right = chunks[1]; let st = APP_STATE.lock().unwrap();
let items: Vec<ListItem> = st
{ .logs
let st = APP_STATE.lock().unwrap(); .iter()
let items: Vec<ListItem> = st .rev()
.logs .map(|s| ListItem::new(s.clone()))
.iter() .collect();
.rev() let list = List::new(items)
.map(|s| ListItem::new(s.clone())) .block(Block::default().title("Logs").borders(Borders::ALL));
.collect(); f.render_widget(list, left);
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)])
let right_chunks = Layout::default() .split(right);
.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()
let st = APP_STATE.lock().unwrap(); .title("System Info")
let header = Paragraph::new(st.sys_info.clone()).block( .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() Block::default()
.title("System Info") .title(format!(
.borders(Borders::TOP.union(Borders::LEFT).union(Borders::RIGHT)), "RAM {:.1}% ({:.1}GiB/{:.1}GiB)",
); ram, ram_used, ram_total
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),
)) ))
.x_bounds([min_x, max_x]) .borders(Borders::ALL),
.y_bounds([0.0, max_sum]) )
.paint(|ctx| { .x_bounds([min_x, max_x])
for (x, dval) in &down_ds { .y_bounds([0.0, 100.0])
ctx.draw(&Line { .paint(|ctx| {
x1: *x, for (x, y) in &ram_ds {
y1: 0.0, ctx.draw(&Line {
x2: *x, x1: *x,
y2: *dval, y1: 0.0,
color: Color::Red, x2: *x,
}); y2: *y,
} color: Color::Magenta,
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, f.render_widget(canvas, left_column[1]);
right_column[1], draw_block_joins(
Borders::TOP.union(Borders::RIGHT).union(Borders::BOTTOM), f,
Borders::TOP, 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();
} }

View file

@ -1,4 +1,6 @@
pub mod app_state;
pub mod log_panel; pub mod log_panel;
pub mod nav_bar;
pub mod ratatui_interface; pub mod ratatui_interface;
pub mod widgets { pub mod widgets {
pub mod betterblock; pub mod betterblock;

34
src/gui/nav_bar.rs Normal file
View file

@ -0,0 +1,34 @@
use std::str;
use crate::gui::log_panel;
pub struct Screen {
pub title: String,
pub render: Box<dyn Fn() + Send + Sync + 'static>,
}
impl Screen {
pub fn new(title: &str, render: Box<dyn Fn() + Send + Sync + 'static>) -> Self {
Screen {
title: String::from(title),
render,
}
}
pub fn renderf(&self) {
(self.render)();
}
}
pub struct NavBar {
pub current_screen: Screen,
pub screens: Vec<Screen>,
}
impl NavBar {
pub fn new() -> Self {
NavBar {
current_screen: Screen {
title: String::from("Main"),
render: Box::new(|| log_panel::render()),
},
screens: Vec::new(),
}
}
}

View file

@ -1,58 +1,45 @@
use std::{
io::Stdout,
sync::{Arc, LazyLock},
time::Duration,
};
use crate::gui::nav_bar::NavBar;
use color_eyre::Result; use color_eyre::Result;
use ratatui::Frame; use crossterm::{
use ratatui::layout::{Constraint, Direction, Layout}; execute,
use ratatui::widgets::{Block, Paragraph}; terminal::{EnterAlternateScreen, enable_raw_mode},
use sys_info; };
use futures_util::lock::Mutex;
use ratatui::{Terminal, prelude::CrosstermBackend};
use std::io::stdout;
use tokio::task; use tokio::task;
pub fn launch(connection_status: bool) -> Result<()> { pub static TERMINAL: LazyLock<Arc<Mutex<Terminal<CrosstermBackend<Stdout>>>>> =
LazyLock::new(|| {
Arc::new(Mutex::new(
Terminal::new(CrosstermBackend::new(stdout())).unwrap(),
))
});
pub static NAV_BAR: LazyLock<Arc<Mutex<NavBar>>> =
LazyLock::new(|| Arc::new(Mutex::new(NavBar::new())));
pub fn launch() -> Result<()> {
color_eyre::install()?; color_eyre::install()?;
task::spawn(run(connection_status)); task::spawn(run());
ratatui::restore(); ratatui::restore();
Ok(()) 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 { 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);
}

View file

@ -0,0 +1 @@

View file

@ -0,0 +1 @@

View file

@ -15,8 +15,9 @@ mod users;
mod util; mod util;
use crate::data::communication::{CommunicationType, CommunicationValue, DataTypes}; use crate::data::communication::{CommunicationType, CommunicationValue, DataTypes};
use crate::gui::log_panel; use crate::gui::app_state::AppState;
use crate::gui::log_panel::{AppState, log_message, log_message_trans}; use crate::gui::log_panel::{log_message, log_message_trans};
use crate::gui::{log_panel, ratatui_interface};
use crate::langu::language_creator; use crate::langu::language_creator;
use crate::omikron::omikron_connection::OmikronConnection; use crate::omikron::omikron_connection::OmikronConnection;
use crate::users::user_manager::UserManager; use crate::users::user_manager::UserManager;
@ -34,8 +35,8 @@ async fn main() {
//} //}
// UI // UI
log_panel::setup(); log_panel::setup();
ratatui_interface::launch();
// LANGUAGE PACK // LANGUAGE PACK
language_creator::create_languages(); language_creator::create_languages();

View file

@ -1,17 +1,13 @@
use crate::APP_STATE;
use crate::data::communication::{CommunicationType, CommunicationValue, DataTypes}; use crate::data::communication::{CommunicationType, CommunicationValue, DataTypes};
use crate::gui::log_panel::log_message; use crate::gui::log_panel::log_message;
use crate::gui::log_panel::log_message_trans; use crate::gui::log_panel::log_message_trans;
use crate::omikron::ping_pong_task::*;
use crate::users::contact::Contact; use crate::users::contact::Contact;
use crate::users::user_community_util::UserCommunityUtil; use crate::users::user_community_util::UserCommunityUtil;
use crate::util::chat_files::ChatFiles; use crate::util::chat_files::ChatFiles;
use crate::util::chats_util::{get_user, get_users, mod_user}; use crate::util::chats_util::{get_user, get_users, mod_user};
use futures_util::{SinkExt, StreamExt}; use futures_util::{SinkExt, StreamExt};
use json::JsonValue; use json::JsonValue;
use json::number::Number;
use std::collections::HashMap; use std::collections::HashMap;
use std::ops::Deref;
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
@ -121,7 +117,6 @@ impl OmikronConnection {
// ************************************************ // // ************************************************ //
// Direct messages // // Direct messages //
// ************************************************ // // ************************************************ //
log_message_trans(format!("{:?}", &cv.comm_type)); log_message_trans(format!("{:?}", &cv.comm_type));
if cv.is_type(CommunicationType::message_other_iota) { if cv.is_type(CommunicationType::message_other_iota) {
let sender_id = &cv.get_sender(); let sender_id = &cv.get_sender();
@ -135,10 +130,7 @@ impl OmikronConnection {
false, false,
*receiver_id, *receiver_id,
*sender_id, *sender_id,
cv.get_data(DataTypes::message_content) cv.get_data(DataTypes::content).unwrap().as_str().unwrap(),
.unwrap()
.as_str()
.unwrap(),
); );
let response = CommunicationValue::new(CommunicationType::message_live) let response = CommunicationValue::new(CommunicationType::message_live)
.with_id(cv.get_id()) .with_id(cv.get_id())
@ -149,7 +141,7 @@ impl OmikronConnection {
) )
.add_data( .add_data(
DataTypes::message, DataTypes::message,
cv.get_data(DataTypes::message_content).unwrap().clone(), cv.get_data(DataTypes::content).unwrap().clone(),
) )
.add_data( .add_data(
DataTypes::sender_id, DataTypes::sender_id,
@ -163,7 +155,7 @@ impl OmikronConnection {
continue; continue;
} }
if cv.is_type(CommunicationType::message) { if cv.is_type(CommunicationType::message_send) {
/* DATA CONTAINER: /* DATA CONTAINER:
"sent_by_self": true, "sent_by_self": true,
"timestamp": unixTimestamp, "timestamp": unixTimestamp,
@ -189,8 +181,14 @@ impl OmikronConnection {
true, true,
my_id, my_id,
other_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); let ack = CommunicationValue::ack_message(cv.get_id(), my_id);
Self::send_message_static(&writer.clone(), ack.to_json().to_string()) Self::send_message_static(&writer.clone(), ack.to_json().to_string())
.await; .await;
@ -203,30 +201,32 @@ impl OmikronConnection {
continue; continue;
} }
if cv.is_type(CommunicationType::message_get) { if cv.is_type(CommunicationType::messages_get) {
let my_id = cv.get_sender(); let my_id = cv.get_sender();
let partner_id = Uuid::from_str( let partner_id = Uuid::from_str(
&*cv.get_data(DataTypes::user_id).unwrap().to_string(), &*cv.get_data(DataTypes::user_id).unwrap().to_string(),
) )
.unwrap(); .unwrap();
let offset = cv let offset = cv
.get_data(DataTypes::loaded_messages) .get_data(DataTypes::offset)
.unwrap_or(&JsonValue::Null) .unwrap_or(&JsonValue::Null)
.to_string() .to_string()
.parse::<i64>() .parse::<i64>()
.unwrap_or(0); .unwrap_or(0);
let amount = cv let amount = cv
.get_data(DataTypes::message_amount) .get_data(DataTypes::amount)
.unwrap_or(&JsonValue::Null) .unwrap_or(&JsonValue::Null)
.to_string() .to_string()
.parse::<i64>() .parse::<i64>()
.unwrap_or(0); .unwrap_or(0);
log_message(format!("A:{} O:{}, P:{}", amount, offset, partner_id));
let messages = let messages =
ChatFiles::get_messages(my_id, partner_id, offset, amount); 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_id(cv.get_id())
.with_receiver(my_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()) Self::send_message_static(&writer.clone(), resp.to_json().to_string())
.await; .await;

View file

@ -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::UserProfile;
use crate::users::user_profile_full::UserProfileFull; use crate::users::user_profile_full::UserProfileFull;
use crate::util::config_util::CONFIG; use crate::util::config_util::CONFIG;
@ -29,7 +29,7 @@ static UNIQUE: Lazy<Mutex<bool>> = Lazy::new(|| Mutex::new(false));
impl UserManager { impl UserManager {
pub async fn create_user(username: &str) -> Option<UserProfileFull> { pub async fn create_user(username: &str) -> Option<UserProfileFull> {
let user_id = AuthConnector::get_register().await.unwrap(); let user_id = auth_connector::get_register().await.unwrap();
let mut buf = [0u8; 56]; let mut buf = [0u8; 56];
let mut rng = OsRng; let mut rng = OsRng;
rng.fill_bytes(&mut buf); rng.fill_bytes(&mut buf);
@ -63,7 +63,7 @@ impl UserManager {
private_key: general_purpose::STANDARD.encode(&private_key.as_bytes()), 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; .await;
save_file( save_file(
"", "",

View file

@ -1,4 +1,4 @@
use crate::auth::auth_connector::AuthConnector; use crate::auth::auth_connector;
use crate::gui::log_panel::log_message; use crate::gui::log_panel::log_message;
use crate::users::user_manager::UserManager; use crate::users::user_manager::UserManager;
use base64::{Engine as _, engine::general_purpose}; use base64::{Engine as _, engine::general_purpose};
@ -79,7 +79,7 @@ impl UserProfile {
|| j.has_key("move") || j.has_key("move")
|| j.has_key("moving") || 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)); log_message(format!("[INFO] Migration triggered for {}", up.username));
UserManager::set_unique(true); UserManager::set_unique(true);
} }

View file

@ -1,10 +1,15 @@
use crate::util::file_util::{get_children, load_file, save_file};
use json::{self, JsonValue, array, object}; use json::{self, JsonValue, array, object};
use sha2::digest::typenum::Add1; use sha2::digest::typenum::Add1;
use std::fs::{self, File}; use std::fs::{self, File};
use std::io::{Read, Write}; use std::io::{Read, Write};
use std::path::Path; use std::path::Path;
use sys_info::loadavg;
use uuid::Uuid; use uuid::Uuid;
use crate::gui::log_panel::log_message;
use crate::langu::language_manager::format;
pub enum MessageState { pub enum MessageState {
Read, Read,
Received, Received,
@ -26,23 +31,6 @@ impl MessageState {
pub struct ChatFiles; pub struct ChatFiles;
impl 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( pub fn add_message(
send_time: i64, send_time: i64,
storage_owner_is_sender: bool, storage_owner_is_sender: bool,
@ -52,25 +40,38 @@ impl ChatFiles {
) { ) {
let user_dir = format!("users/{}/chats/{}", storage_owner, external_user); 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 chunk_index = 0;
let mut message_chunk = array![]; let mut message_chunk = array![];
// find latest chunk not full (max 800 msgs) // find latest chunk not full (max 800 msgs)
loop { loop {
let file_name = format!("msgs_{}.json", chunk_index); 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 !file_content.is_empty() {
if let Ok(current_chunk) = json::parse(&file_content) { 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; message_chunk = current_chunk;
break; break;
} }
} else {
log_message(format!("Failed to parse existing JSON file: {}", file_name));
} }
} else { } else {
// New file, use empty array
break; break;
} }
chunk_index += 1; chunk_index += 1;
if chunk_index > 1000 {
log_message(format!("Too many message chunks. Aborting add."));
return;
}
} }
let json_obj = object! { let json_obj = object! {
@ -79,15 +80,16 @@ impl ChatFiles {
"sender_is_me" => storage_owner_is_sender, "sender_is_me" => storage_owner_is_sender,
"message_state" => MessageState::Sending.as_str() "message_state" => MessageState::Sending.as_str()
}; };
message_chunk.push(json_obj).unwrap();
Self::save_file( if let Err(e) = message_chunk.push(json_obj) {
&user_dir, log_message(format!("Failed to push new message into JSON array: {}", e));
&format!("msgs_{}.json", chunk_index), return;
&message_chunk.dump(), }
);
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( pub fn change_message_state(
storage_owner: Uuid, storage_owner: Uuid,
external_user: Uuid, external_user: Uuid,
@ -108,7 +110,7 @@ impl ChatFiles {
let fname_str = fname.to_string_lossy(); let fname_str = fname.to_string_lossy();
if fname_str.starts_with("msgs_") && fname_str.ends_with(".json") { 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() { if file_content.is_empty() {
continue; continue;
} }
@ -124,7 +126,7 @@ impl ChatFiles {
} }
if modified { if modified {
Self::save_file(&user_dir, &fname_str, &chunk.dump()); save_file(&user_dir, &fname_str, &chunk.dump());
break; break;
} }
} }
@ -139,29 +141,20 @@ impl ChatFiles {
loaded_messages: i64, loaded_messages: i64,
amount: i64, amount: i64,
) -> JsonValue { ) -> JsonValue {
let user_dir = format!("users/{}/chats/{}", storage_owner, external_user);
let path = Path::new(&user_dir);
let mut messages = array![]; let mut messages = array![];
if !path.exists() {
return messages;
}
let mut latest_chunk_index: i32 = -1; let mut latest_chunk_index: i32 = -1;
if let Ok(entries) = fs::read_dir(path) { let files = get_children(&format!("users/{}/chats/{}", storage_owner, external_user));
for entry in entries.flatten() {
let fname = entry.file_name(); for entry in files {
let fname_str = fname.to_string_lossy(); if let Some(num) = {
if fname_str.starts_with("msgs_") && fname_str.ends_with(".json") { entry
if let Some(num) = fname_str .strip_prefix("msgs_")
.strip_prefix("msgs_") .and_then(|s| s.strip_suffix(".json"))
.and_then(|s| s.strip_suffix(".json")) } {
{ if let Ok(index) = num.parse::<i32>() {
if let Ok(index) = num.parse::<i32>() { if index > latest_chunk_index {
if index > latest_chunk_index { latest_chunk_index = index;
latest_chunk_index = index;
}
}
} }
} }
} }
@ -179,7 +172,10 @@ impl ChatFiles {
break; break;
} }
let file_name = format!("msgs_{}.json", chunk_index); 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() { if file_content.is_empty() {
continue; continue;
} }