mirror of
https://github.com/umutcamliyurt/Amnezichat.git
synced 2025-05-04 23:10:45 +01:00
Replace Egui with Web UI
This commit is contained in:
parent
64ac6eb981
commit
6cc3eb6a8b
@ -70,6 +70,8 @@ Amnezichat offers a highly secure and privacy-focused messaging experience by en
|
||||
|
||||
## Client usage:
|
||||
|
||||
**For Web UI connect to http://localhost:8000**
|
||||
|
||||
sudo apt update
|
||||
sudo apt install curl build-essential git tor
|
||||
sudo systemctl enable --now tor.service
|
||||
|
@ -70,6 +70,8 @@ Amnezichat, hiçbir kayıt tutulmamasını ve tüm mesaj verilerinin yalnızca s
|
||||
|
||||
## İstemci kullanımı:
|
||||
|
||||
**Web UI için http://localhost:8000 adresine bağlanın**
|
||||
|
||||
sudo apt update
|
||||
sudo apt install curl build-essential git tor
|
||||
sudo systemctl enable --now tor.service
|
||||
|
@ -21,7 +21,4 @@ rpassword = "7.3.1"
|
||||
sha3 = "0.10.8"
|
||||
x25519-dalek = "2.0.1"
|
||||
ed25519-dalek = "2.1.1"
|
||||
eframe = "0.24.1"
|
||||
image = "0.24"
|
||||
rfd = "0.12"
|
||||
winapi = { version = "0.3", features = ["winuser", "windef"] }
|
||||
rocket = { version = "0.5", features = ["json"] }
|
8
client/Rocket.toml
Normal file
8
client/Rocket.toml
Normal file
@ -0,0 +1,8 @@
|
||||
[default]
|
||||
address = "127.0.0.1"
|
||||
port = 8000
|
||||
|
||||
[default.limits]
|
||||
forms = "2 MiB"
|
||||
json = "2 MiB"
|
||||
file = "2 MiB"
|
@ -2,32 +2,35 @@ use crate::encrypt_data;
|
||||
use crate::receive_and_fetch_messages;
|
||||
use crate::send_encrypted_message;
|
||||
use crate::pad_message;
|
||||
use eframe::egui;
|
||||
use image::GenericImageView;
|
||||
use rfd::FileDialog;
|
||||
use std::fs;
|
||||
use rocket::{get, post, routes, serde::json::Json};
|
||||
use rocket::fs::NamedFile;
|
||||
use rocket::tokio;
|
||||
use rocket::fs::FileServer;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use regex::Regex;
|
||||
use base64;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MessagingApp {
|
||||
username: String,
|
||||
message_input: String,
|
||||
messages: Arc<Mutex<Vec<String>>>,
|
||||
shared_hybrid_secret: Arc<std::string::String>,
|
||||
shared_hybrid_secret: Arc<String>,
|
||||
shared_room_id: Arc<String>,
|
||||
shared_url: Arc<String>,
|
||||
image_data: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct MessageInput {
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl MessagingApp {
|
||||
pub fn new(
|
||||
username: String,
|
||||
shared_hybrid_secret: Arc<std::string::String>,
|
||||
shared_room_id: Arc<String>,
|
||||
shared_url: Arc<String>,
|
||||
shared_hybrid_secret: Arc<String>,
|
||||
shared_room_id: Arc<Mutex<String>>,
|
||||
shared_url: Arc<Mutex<String>>,
|
||||
) -> Self {
|
||||
let messages = Arc::new(Mutex::new(vec![]));
|
||||
let messages_clone = Arc::clone(&messages);
|
||||
@ -35,160 +38,125 @@ impl MessagingApp {
|
||||
let shared_room_id_clone = Arc::clone(&shared_room_id);
|
||||
let shared_url_clone = Arc::clone(&shared_url);
|
||||
|
||||
thread::spawn(move || loop {
|
||||
match receive_and_fetch_messages(
|
||||
&shared_room_id_clone,
|
||||
&shared_hybrid_secret_clone,
|
||||
&shared_url_clone,
|
||||
true,
|
||||
) {
|
||||
Ok(new_messages) => {
|
||||
let mut msgs = messages_clone.lock().unwrap();
|
||||
msgs.clear();
|
||||
msgs.extend(new_messages);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error fetching messages: {}", e);
|
||||
let room_id = Arc::new(shared_room_id_clone.lock().unwrap_or_else(|_| panic!("Failed to lock room_id")).clone());
|
||||
let url = Arc::new(shared_url_clone.lock().unwrap_or_else(|_| panic!("Failed to lock url")).clone());
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let room_id_str = shared_room_id_clone.lock().unwrap_or_else(|_| panic!("Failed to lock room_id")).clone();
|
||||
let url_str = shared_url_clone.lock().unwrap_or_else(|_| panic!("Failed to lock url")).clone();
|
||||
|
||||
match receive_and_fetch_messages(
|
||||
&room_id_str,
|
||||
&shared_hybrid_secret_clone,
|
||||
&url_str,
|
||||
true,
|
||||
) {
|
||||
Ok(new_messages) => {
|
||||
let mut msgs = messages_clone.lock().unwrap_or_else(|_| panic!("Failed to lock messages"));
|
||||
msgs.clear();
|
||||
msgs.extend(new_messages);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error fetching messages: {}", e);
|
||||
}
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(10)).await;
|
||||
}
|
||||
thread::sleep(Duration::from_secs(10));
|
||||
});
|
||||
|
||||
MessagingApp {
|
||||
username,
|
||||
message_input: String::new(),
|
||||
messages,
|
||||
shared_hybrid_secret,
|
||||
shared_room_id,
|
||||
shared_url,
|
||||
image_data: None,
|
||||
shared_room_id: room_id,
|
||||
shared_url: url,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_image_upload(&mut self) {
|
||||
if let Some(file_path) = FileDialog::new().add_filter("Image files", &["png", "jpg", "jpeg", "bmp", "gif"]).pick_file() {
|
||||
match fs::read(&file_path) {
|
||||
Ok(data) => {
|
||||
let encoded = base64::encode(data);
|
||||
self.image_data = Some(format!("[IMAGE_DATA]:{}[END DATA]", encoded));
|
||||
}
|
||||
Err(e) => eprintln!("Error reading image file: {}", e),
|
||||
#[get("/messages")]
|
||||
async fn get_messages(app: &rocket::State<MessagingApp>) -> Json<Vec<String>> {
|
||||
let result = fetch_and_update_messages(&app).await;
|
||||
|
||||
match result {
|
||||
Ok(msgs) => Json(msgs),
|
||||
Err(e) => {
|
||||
eprintln!("Error fetching messages: {}", e);
|
||||
|
||||
// Return current messages if fetching fails
|
||||
let msgs = app.messages.lock().unwrap_or_else(|_| panic!("Failed to lock messages"));
|
||||
Json(msgs.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_and_update_messages(app: &rocket::State<MessagingApp>) -> Result<Vec<String>, String> {
|
||||
let room_id_str = app.shared_room_id.clone();
|
||||
let url_str = app.shared_url.clone();
|
||||
|
||||
let new_messages = tokio::task::block_in_place(move || {
|
||||
receive_and_fetch_messages(
|
||||
&room_id_str,
|
||||
&app.shared_hybrid_secret,
|
||||
&url_str,
|
||||
true,
|
||||
)
|
||||
}).map_err(|e| format!("Error fetching messages: {}", e))?;
|
||||
|
||||
// Update the in-memory message storage
|
||||
let mut msgs = app.messages.lock().unwrap_or_else(|_| panic!("Failed to lock messages"));
|
||||
msgs.clear();
|
||||
msgs.extend(new_messages.clone());
|
||||
|
||||
Ok(new_messages)
|
||||
}
|
||||
|
||||
#[post("/send", data = "<input>")]
|
||||
async fn post_message(
|
||||
input: Json<MessageInput>,
|
||||
app: &rocket::State<MessagingApp>
|
||||
) -> Result<&'static str, rocket::http::Status> {
|
||||
// Create the formatted message once
|
||||
let formatted_message = format!("<strong>{}</strong>: {}", app.username, input.message);
|
||||
let padded_message = pad_message(&formatted_message, 2048);
|
||||
|
||||
let result = tokio::task::block_in_place(|| {
|
||||
// Encrypt the message
|
||||
let encrypted = encrypt_data(&padded_message, &app.shared_hybrid_secret)
|
||||
.map_err(|e| {
|
||||
eprintln!("Encryption error: {}", e);
|
||||
rocket::http::Status::InternalServerError
|
||||
})?;
|
||||
|
||||
// Send the encrypted message
|
||||
send_encrypted_message(&encrypted, &app.shared_room_id, &app.shared_url)
|
||||
.map_err(|e| {
|
||||
eprintln!("Error sending message: {}", e);
|
||||
rocket::http::Status::InternalServerError
|
||||
})
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
{
|
||||
let mut msgs = app.messages.lock().unwrap_or_else(|_| panic!("Failed to lock messages"));
|
||||
msgs.push(formatted_message);
|
||||
}
|
||||
Ok("Message sent")
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for MessagingApp {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
ui.vertical(|ui| {
|
||||
let chat_area_height = ui.available_height() - 80.0;
|
||||
|
||||
egui::Frame::none()
|
||||
.fill(egui::Color32::from_black_alpha(50))
|
||||
.rounding(10.0)
|
||||
.inner_margin(egui::style::Margin::same(10.0))
|
||||
.show(ui, |ui| {
|
||||
ui.set_height(chat_area_height);
|
||||
egui::ScrollArea::vertical()
|
||||
.auto_shrink([false, true])
|
||||
.show(ui, |ui| {
|
||||
let messages = self.messages.lock().unwrap();
|
||||
let re = Regex::new(r"</?strong>").unwrap();
|
||||
|
||||
for message in messages.iter() {
|
||||
if message.contains("[IMAGE_DATA]:") {
|
||||
if let Some(encoded) = message.split("[IMAGE_DATA]:").nth(1) {
|
||||
if let Some(end_idx) = encoded.find("[END DATA]") {
|
||||
let image_data = &encoded[..end_idx];
|
||||
match base64::decode(image_data) {
|
||||
Ok(decoded) => {
|
||||
if let Ok(image) = image::load_from_memory(&decoded) {
|
||||
let size = image.dimensions();
|
||||
let color_image = egui::ColorImage::from_rgba_unmultiplied(
|
||||
[size.0 as usize, size.1 as usize],
|
||||
&image.to_rgba8(),
|
||||
);
|
||||
let texture = ctx.load_texture(
|
||||
"received_image",
|
||||
color_image,
|
||||
egui::TextureOptions::LINEAR,
|
||||
);
|
||||
ui.image(&texture);
|
||||
} else {
|
||||
eprintln!("Failed to decode image format");
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!("Error decoding base64 image: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let cleaned_message = re.replace_all(message, "");
|
||||
ui.label(
|
||||
egui::RichText::new(cleaned_message.as_ref())
|
||||
.size(16.0)
|
||||
.color(egui::Color32::WHITE),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let input_box_width = ui.available_width() * 0.65;
|
||||
let button_width = ui.available_width() * 0.15;
|
||||
|
||||
let text_edit = egui::TextEdit::singleline(&mut self.message_input)
|
||||
.hint_text("Type a message...")
|
||||
.text_color(egui::Color32::WHITE)
|
||||
.frame(true);
|
||||
ui.add_sized([input_box_width, 40.0], text_edit);
|
||||
|
||||
if ui.add_sized([button_width, 40.0], egui::Button::new("Send")).clicked() {
|
||||
let mut message = format!("<strong>{}</strong>: {}", self.username, self.message_input);
|
||||
if let Some(image) = &self.image_data {
|
||||
message.push_str(image);
|
||||
self.image_data = None;
|
||||
}
|
||||
|
||||
// Pad the message to a fixed length (e.g., 2048 bytes)
|
||||
let padded_message = pad_message(&message, 2048);
|
||||
|
||||
if let Err(e) = send_encrypted_message(
|
||||
&encrypt_data(&padded_message, &self.shared_hybrid_secret).unwrap(),
|
||||
&self.shared_room_id,
|
||||
&self.shared_url,
|
||||
) {
|
||||
eprintln!("Error sending message: {}", e);
|
||||
} else {
|
||||
self.message_input.clear();
|
||||
}
|
||||
}
|
||||
|
||||
if ui.add_sized([button_width, 40.0], egui::Button::new("Upload Image")).clicked() {
|
||||
self.handle_image_upload();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
#[get("/")]
|
||||
async fn serve_webpage() -> Option<NamedFile> {
|
||||
NamedFile::open(PathBuf::from("static/index.html")).await.ok()
|
||||
}
|
||||
|
||||
pub fn run_gui(
|
||||
username: String,
|
||||
shared_hybrid_secret: Arc<std::string::String>,
|
||||
shared_room_id: Arc<String>,
|
||||
shared_url: Arc<String>,
|
||||
) -> Result<(), eframe::Error> {
|
||||
let app = MessagingApp::new(
|
||||
username,
|
||||
shared_hybrid_secret,
|
||||
shared_room_id,
|
||||
shared_url,
|
||||
);
|
||||
let native_options = eframe::NativeOptions {
|
||||
..Default::default()
|
||||
};
|
||||
eframe::run_native("Amnezichat", native_options, Box::new(|_| Box::new(app)))
|
||||
}
|
||||
pub fn create_rocket(app: MessagingApp) -> rocket::Rocket<rocket::Build> {
|
||||
rocket::build()
|
||||
.manage(app)
|
||||
.mount("/", routes![get_messages, post_message, serve_webpage])
|
||||
.mount("/static", FileServer::from("static"))
|
||||
}
|
@ -4,7 +4,8 @@ mod network_operations;
|
||||
mod key_exchange;
|
||||
mod authentication;
|
||||
mod encryption;
|
||||
use gui::run_gui;
|
||||
use gui::create_rocket;
|
||||
use gui::MessagingApp;
|
||||
use key_operations::key_operations_dilithium;
|
||||
use key_operations::key_operations_eddsa;
|
||||
use network_operations::create_client_with_proxy;
|
||||
@ -469,21 +470,29 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
|
||||
// Handle GUI or CLI messaging
|
||||
if interface_choice.to_lowercase() == "gui" {
|
||||
let shared_hybrid_secret_for_gui = shared_hybrid_secret;
|
||||
let shared_room_id_for_gui: Arc<String> = {
|
||||
let locked = shared_room_id.lock().unwrap();
|
||||
Arc::new(locked.clone())
|
||||
};
|
||||
let shared_url_for_gui: Arc<String> = {
|
||||
let locked = shared_url.lock().unwrap();
|
||||
Arc::new(locked.clone())
|
||||
};
|
||||
let _ = run_gui(
|
||||
username.clone(),
|
||||
shared_hybrid_secret_for_gui,
|
||||
shared_room_id_for_gui,
|
||||
shared_url_for_gui,
|
||||
);
|
||||
let rt = rocket::tokio::runtime::Runtime::new().unwrap();
|
||||
rt.block_on(async {
|
||||
// Wrap only for passing to run_gui
|
||||
let shared_hybrid_secret_for_gui = shared_hybrid_secret;
|
||||
|
||||
// Correctly clone Arc<Mutex<String>> instead of Arc<String>
|
||||
let shared_room_id_for_gui: Arc<Mutex<String>> = Arc::clone(&shared_room_id);
|
||||
let shared_url_for_gui: Arc<Mutex<String>> = Arc::clone(&shared_url);
|
||||
|
||||
// Pass the arguments
|
||||
let app = MessagingApp::new(
|
||||
username,
|
||||
shared_hybrid_secret_for_gui,
|
||||
shared_room_id_for_gui,
|
||||
shared_url_for_gui,
|
||||
);
|
||||
|
||||
// Await the async launch function
|
||||
if let Err(e) = create_rocket(app).launch().await {
|
||||
eprintln!("Rocket server failed: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
loop {
|
||||
let mut message = String::new();
|
||||
@ -741,26 +750,28 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
|
||||
|
||||
if interface_choice.to_lowercase() == "gui" {
|
||||
// Wrap only for passing to run_gui
|
||||
let shared_hybrid_secret_for_gui = shared_hybrid_secret;
|
||||
let rt = rocket::tokio::runtime::Runtime::new().unwrap();
|
||||
rt.block_on(async {
|
||||
// Wrap only for passing to run_gui
|
||||
let shared_hybrid_secret_for_gui = shared_hybrid_secret;
|
||||
|
||||
let shared_room_id_for_gui: Arc<String> = {
|
||||
let locked = shared_room_id.lock().unwrap();
|
||||
Arc::new(locked.clone())
|
||||
};
|
||||
// Correctly clone Arc<Mutex<String>> instead of Arc<String>
|
||||
let shared_room_id_for_gui: Arc<Mutex<String>> = Arc::clone(&shared_room_id);
|
||||
let shared_url_for_gui: Arc<Mutex<String>> = Arc::clone(&shared_url);
|
||||
|
||||
let shared_url_for_gui: Arc<String> = {
|
||||
let locked = shared_url.lock().unwrap();
|
||||
Arc::new(locked.clone())
|
||||
};
|
||||
// Pass the arguments
|
||||
let app = MessagingApp::new(
|
||||
username,
|
||||
shared_hybrid_secret_for_gui,
|
||||
shared_room_id_for_gui,
|
||||
shared_url_for_gui,
|
||||
);
|
||||
|
||||
// Pass the arguments
|
||||
let _ = run_gui(
|
||||
username.clone(),
|
||||
shared_hybrid_secret_for_gui,
|
||||
shared_room_id_for_gui,
|
||||
shared_url_for_gui,
|
||||
);
|
||||
// Await the async launch function
|
||||
if let Err(e) = create_rocket(app).launch().await {
|
||||
eprintln!("Rocket server failed: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
loop {
|
||||
|
@ -487,8 +487,13 @@ pub fn receive_and_fetch_messages(
|
||||
continue;
|
||||
}
|
||||
|
||||
// If gui is false, skip messages containing `[IMAGE_DATA]:`
|
||||
if !gui && unpadded_message.contains("[IMAGE_DATA]:") {
|
||||
// If gui is false, skip messages containing `<media>`
|
||||
if !gui && unpadded_message.contains("<media>") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If gui is false, skip messages containing `<pfp>`
|
||||
if !gui && unpadded_message.contains("<pfp>") {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
147
client/static/index.html
Normal file
147
client/static/index.html
Normal file
@ -0,0 +1,147 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Amnezichat</title>
|
||||
<link rel="stylesheet" href="/static/styles.css">
|
||||
<script src="/static/purify.min.js"></script>
|
||||
<script>
|
||||
let profilePicBase64 = "";
|
||||
|
||||
// Handle profile picture upload
|
||||
function handleProfilePicChange(event) {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = function () {
|
||||
profilePicBase64 = reader.result;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle media upload (image/video)
|
||||
function handleMediaChange(event) {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = async function () {
|
||||
const mediaBase64 = reader.result;
|
||||
|
||||
let mediaMessage = { message: `<media>${mediaBase64}</media>` };
|
||||
|
||||
// Include profile picture in media message if available
|
||||
if (profilePicBase64) {
|
||||
mediaMessage.message = `<pfp>${profilePicBase64}</pfp>` + mediaMessage.message;
|
||||
}
|
||||
|
||||
// Send media as a separate message
|
||||
await fetch('/send', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(mediaMessage)
|
||||
});
|
||||
|
||||
fetchMessages();
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch messages from the server
|
||||
async function fetchMessages() {
|
||||
const response = await fetch('/messages');
|
||||
const messages = await response.json();
|
||||
|
||||
const messageHTML = messages.map(msg => {
|
||||
const base64ImagePattern = /^data:image\/(png|jpeg|jpg|gif);base64,/;
|
||||
const base64VideoPattern = /^data:video\/(mp4|webm|ogg);base64,/;
|
||||
const pfpMatch = msg.match(/<pfp>(.*?)<\/pfp>/);
|
||||
const mediaMatch = msg.match(/<media>(.*?)<\/media>/);
|
||||
let messageText = msg.replace(/<pfp>.*?<\/pfp>/, '').replace(/<media>.*?<\/media>/, '');
|
||||
let profilePic = "";
|
||||
let mediaContent = "";
|
||||
|
||||
// Display profile picture if available
|
||||
if (pfpMatch && base64ImagePattern.test(pfpMatch[1])) {
|
||||
profilePic = `<img src="${pfpMatch[1]}" alt="Profile Picture" style="width: 50px; height: 50px; border-radius: 50%; margin-right: 10px;">`;
|
||||
}
|
||||
|
||||
// Display media as a separate full-size element
|
||||
if (mediaMatch && (base64ImagePattern.test(mediaMatch[1]) || base64VideoPattern.test(mediaMatch[1]))) {
|
||||
if (base64ImagePattern.test(mediaMatch[1])) {
|
||||
mediaContent = `<div>${profilePic}<img src="${mediaMatch[1]}" alt="Media" style="width: 100%; max-width: 600px; margin-top: 10px;"></div>`;
|
||||
} else if (base64VideoPattern.test(mediaMatch[1])) {
|
||||
mediaContent = `<div>${profilePic}<video controls style="width: 100%; max-width: 600px; margin-top: 10px;"><source src="${mediaMatch[1]}" type="video/mp4">Your browser does not support the video tag.</video></div>`;
|
||||
}
|
||||
return mediaContent; // Media is a separate message
|
||||
}
|
||||
|
||||
return `<div style="display: flex; align-items: center; margin-bottom: 10px;">${profilePic}<p>${DOMPurify.sanitize(messageText)}</p></div>`;
|
||||
}).join('');
|
||||
|
||||
document.getElementById('messages').innerHTML = messageHTML;
|
||||
document.getElementById('messages').scrollTop = document.getElementById('messages').scrollHeight;
|
||||
}
|
||||
|
||||
// Send text message to the server
|
||||
async function sendMessage() {
|
||||
const message = document.getElementById('messageInput').value;
|
||||
if (message.trim() !== "") {
|
||||
let messageData = { message: DOMPurify.sanitize(message) };
|
||||
|
||||
// Include profile picture in text message if available
|
||||
if (profilePicBase64) {
|
||||
messageData.message = `<pfp>${profilePicBase64}</pfp>` + messageData.message;
|
||||
}
|
||||
|
||||
await fetch('/send', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(messageData)
|
||||
});
|
||||
|
||||
document.getElementById('messageInput').value = '';
|
||||
fetchMessages();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSettings() {
|
||||
const modal = document.getElementById('settings');
|
||||
modal.style.display = modal.style.display === 'flex' ? 'none' : 'flex';
|
||||
}
|
||||
|
||||
function closeSettings() {
|
||||
document.getElementById('settings').style.display = 'none';
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body onload="fetchMessages()">
|
||||
<div class="container">
|
||||
<div id="messages"></div>
|
||||
<div class="input-container">
|
||||
<input type="text" id="messageInput" placeholder="Type a message..." autocomplete="off">
|
||||
<button id="settingsButton" onclick="toggleSettings()">Settings</button>
|
||||
<button onclick="sendMessage()">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="settings">
|
||||
<div class="settings-content">
|
||||
<h2>Settings</h2>
|
||||
<!-- Hidden file input for selecting profile picture -->
|
||||
<input type="file" accept="image/*" id="profilePicInput" style="display:none;" onchange="handleProfilePicChange(event)">
|
||||
<!-- Button that opens the file input for profile picture -->
|
||||
<button onclick="document.getElementById('profilePicInput').click()">Choose Profile Picture</button>
|
||||
|
||||
<!-- Hidden file input for selecting media (image or video) -->
|
||||
<input type="file" accept="image/*,video/*" id="mediaInput" style="display:none;" onchange="handleMediaChange(event)">
|
||||
<!-- Button that opens the file input for media -->
|
||||
<button onclick="document.getElementById('mediaInput').click()">Send Media</button>
|
||||
|
||||
<button onclick="closeSettings()">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
3
client/static/purify.min.js
vendored
Normal file
3
client/static/purify.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
140
client/static/styles.css
Normal file
140
client/static/styles.css
Normal file
@ -0,0 +1,140 @@
|
||||
/* Reset the body to cover the full page */
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
font-family: 'Helvetica Neue', Arial, sans-serif; /* Clean sans-serif font */
|
||||
background-color: #1e1f22; /* Dark background for the whole page */
|
||||
color: #e1e1e1; /* Light text for contrast */
|
||||
}
|
||||
|
||||
/* Center the content and give it full height */
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
box-sizing: border-box; /* Ensure padding is included in height calculations */
|
||||
}
|
||||
|
||||
/* Styling for the messages area */
|
||||
#messages {
|
||||
flex-grow: 1;
|
||||
padding: 15px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #333b41; /* Darker border */
|
||||
background-color: #2c2f36; /* Dark background for message container */
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px; /* Space between messages */
|
||||
margin-bottom: 20px; /* Space between messages and input */
|
||||
}
|
||||
|
||||
/* Styling each message */
|
||||
#messages p {
|
||||
padding: 10px;
|
||||
background-color: #444c56; /* Slightly lighter gray message bubbles */
|
||||
border-radius: 20px;
|
||||
max-width: 75%; /* Keep messages from stretching too wide */
|
||||
}
|
||||
|
||||
/* Styling input and button area */
|
||||
.input-container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
justify-content: flex-end; /* Align input and buttons to the bottom */
|
||||
width: 100%; /* Ensure it takes full width */
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
flex-grow: 1; /* Make the input take up remaining space */
|
||||
padding: 10px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid #444c56; /* Dark border */
|
||||
background-color: #2c2f36; /* Dark background */
|
||||
color: #e1e1e1; /* Light text for input */
|
||||
}
|
||||
|
||||
input[type="text"]:focus {
|
||||
outline: none;
|
||||
border-color: #4c8bf5; /* Light blue when focused */
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 20px; /* Rounded buttons */
|
||||
background-color: #4c8bf5; /* Signal's blue color */
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #3978d1; /* Slightly darker blue on hover */
|
||||
}
|
||||
|
||||
button:active {
|
||||
background-color: #2962a1; /* Even darker blue when pressed */
|
||||
}
|
||||
|
||||
/* Styling for the settings button inside the input container */
|
||||
#settingsButton {
|
||||
padding: 10px;
|
||||
border: none;
|
||||
border-radius: 20px; /* Rounded button */
|
||||
background-color: #4c8bf5; /* Signal's blue */
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#settingsButton:hover {
|
||||
background-color: #3978d1;
|
||||
}
|
||||
|
||||
#settingsButton:active {
|
||||
background-color: #2962a1;
|
||||
}
|
||||
|
||||
/* Modal for settings (hidden by default) */
|
||||
#settings {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.7); /* Dark background for modal */
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
background-color: #2c2f36; /* Dark background for modal */
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.settings-content h2 {
|
||||
margin-top: 0;
|
||||
color: #e1e1e1; /* Light text for headings */
|
||||
}
|
||||
|
||||
.settings-content button {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
background-color: #4c8bf5; /* Blue background for buttons */
|
||||
}
|
||||
|
||||
/* Styling for profile picture preview */
|
||||
#profilePicPreview {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%; /* Circular image */
|
||||
margin-top: 10px;
|
||||
object-fit: cover;
|
||||
border: 2px solid #444c56; /* Dark border around the profile picture */
|
||||
}
|
BIN
screenshot.png
BIN
screenshot.png
Binary file not shown.
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 308 KiB |
@ -1,3 +1,8 @@
|
||||
[default]
|
||||
address = "0.0.0.0"
|
||||
port = 8080
|
||||
|
||||
[default.limits]
|
||||
forms = "2 MiB"
|
||||
json = "2 MiB"
|
||||
file = "2 MiB"
|
||||
|
@ -51,8 +51,8 @@
|
||||
<a href="#" class="close-button">×</a>
|
||||
<h2>Installation Instructions</h2>
|
||||
|
||||
<!-- Option 1: Build it yourself -->
|
||||
<h3>Option 1: Build it Yourself</h3>
|
||||
<!-- Build it yourself -->
|
||||
<h3>Build it Yourself</h3>
|
||||
<pre>
|
||||
sudo apt update
|
||||
sudo apt install curl build-essential git tor
|
||||
@ -63,10 +63,6 @@ cd Amnezichat/client/
|
||||
cargo build --release
|
||||
cargo run --release
|
||||
</pre>
|
||||
<!-- Option 2: Get it from GitHub Releases -->
|
||||
<h3>Option 2: Get it from GitHub Releases</h3>
|
||||
<p>If you prefer not to build it yourself, you can download precompiled binaries directly from the
|
||||
<a href="https://github.com/umutcamliyurt/Amnezichat/releases/" target="_blank" style="color: white;">GitHub Releases page</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
@ -5,15 +5,15 @@ body {
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
background: linear-gradient(135deg, #2c3e50, #4ca1af);
|
||||
background: linear-gradient(135deg, #1e3c72, #2a5298);
|
||||
color: #ecf0f1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 30px;
|
||||
background: rgba(44, 62, 80, 0.95);
|
||||
border: 1px solid #34495e;
|
||||
background: rgba(15, 30, 60, 0.95);
|
||||
border: 1px solid #0f3b5f;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.6);
|
||||
max-width: 600px;
|
||||
@ -37,7 +37,7 @@ a.download-button {
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
color: #ffffff;
|
||||
background: linear-gradient(45deg, #1abc9c, #16a085);
|
||||
background: linear-gradient(45deg, #2980b9, #1e90ff);
|
||||
padding: 12px 25px;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
@ -46,7 +46,7 @@ a.download-button {
|
||||
}
|
||||
|
||||
a.download-button:hover {
|
||||
background: linear-gradient(45deg, #16a085, #1abc9c);
|
||||
background: linear-gradient(45deg, #1e90ff, #2980b9);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
@ -56,14 +56,14 @@ a.download-button:hover {
|
||||
|
||||
.main-links a {
|
||||
text-decoration: none;
|
||||
color: #1abc9c;
|
||||
color: #2980b9;
|
||||
margin: 0 8px;
|
||||
font-size: 1rem;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.main-links a:hover {
|
||||
color: #16a085;
|
||||
color: #1e90ff;
|
||||
}
|
||||
|
||||
details {
|
||||
@ -102,9 +102,9 @@ details p, details ul {
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: #2c3e50;
|
||||
background: #0f1e3c;
|
||||
padding: 20px;
|
||||
border: 1px solid #34495e;
|
||||
border: 1px solid #0a2b4f;
|
||||
border-radius: 12px;
|
||||
width: 90%;
|
||||
max-width: 600px;
|
||||
@ -112,7 +112,7 @@ details p, details ul {
|
||||
}
|
||||
|
||||
.modal-content pre {
|
||||
background: #34495e;
|
||||
background: #0a2b4f;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
color: #ecf0f1;
|
||||
|
Loading…
x
Reference in New Issue
Block a user