work version
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/target
|
||||
Generated
+2597
File diff suppressed because it is too large
Load Diff
+18
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "msgs"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.7", features = ["ws", "json"] }
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
tower-http = { version = "0.5", features = ["fs"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
sqlx = { version = "0.7", features = ["runtime-tokio-native-tls", "sqlite", "macros", "chrono"] }
|
||||
futures-util = "0.3"
|
||||
bcrypt = "0.15"
|
||||
rand = "0.8"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
+651
@@ -0,0 +1,651 @@
|
||||
use axum::{
|
||||
extract::{
|
||||
ws::{Message, WebSocket, WebSocketUpgrade},
|
||||
State, Json,
|
||||
},
|
||||
response::Response,
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{sqlite::SqlitePool, Row};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
net::SocketAddr,
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::sync::{mpsc, RwLock};
|
||||
use tower_http::services::ServeDir;
|
||||
use futures_util::{StreamExt, SinkExt};
|
||||
use bcrypt::{hash, verify, DEFAULT_COST};
|
||||
use rand::distributions::Alphanumeric;
|
||||
use rand::{thread_rng, Rng};
|
||||
use chrono::{Utc, Duration};
|
||||
|
||||
// Types for shared state
|
||||
type UserMap = Arc<RwLock<HashMap<String, mpsc::UnboundedSender<Message>>>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
users: UserMap,
|
||||
db_pool: SqlitePool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
struct ChatMessage {
|
||||
id: String,
|
||||
from_user: String,
|
||||
to_user: String,
|
||||
content: String,
|
||||
timestamp: i64,
|
||||
message_type: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct UserList {
|
||||
users: Vec<String>,
|
||||
}
|
||||
|
||||
// Структуры для аутентификации
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RegisterRequest {
|
||||
phone: String,
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct LoginRequest {
|
||||
phone: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct AuthResponse {
|
||||
success: bool,
|
||||
message: String,
|
||||
username: Option<String>,
|
||||
token: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct UserProfile {
|
||||
username: String,
|
||||
phone: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct ValidateTokenResponse {
|
||||
success: bool,
|
||||
username: Option<String>,
|
||||
phone: Option<String>,
|
||||
message: Option<String>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Initialize logging
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
// Initialize database
|
||||
let db_pool = init_db().await?;
|
||||
|
||||
// Create shared state
|
||||
let state = Arc::new(AppState {
|
||||
users: Arc::new(RwLock::new(HashMap::new())),
|
||||
db_pool,
|
||||
});
|
||||
|
||||
// Build the application
|
||||
let app = Router::new()
|
||||
.route("/ws", get(websocket_handler))
|
||||
.route("/api/register", post(register_handler))
|
||||
.route("/api/login", post(login_handler))
|
||||
.route("/api/validate-token", post(validate_token_handler))
|
||||
.route("/api/profile", get(profile_handler))
|
||||
.nest_service("/", ServeDir::new("web"))
|
||||
.with_state(state);
|
||||
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], 3333));
|
||||
println!("Server running on http://{}", addr);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||
axum::serve(listener, app).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn init_db() -> Result<SqlitePool, Box<dyn std::error::Error>> {
|
||||
let pool = SqlitePool::connect("sqlite:chat.db").await?;
|
||||
|
||||
// Create users table
|
||||
sqlx::query(
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
phone TEXT UNIQUE NOT NULL,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
|
||||
// Create messages table
|
||||
sqlx::query(
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id TEXT PRIMARY KEY,
|
||||
from_user TEXT NOT NULL,
|
||||
to_user TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
timestamp INTEGER NOT NULL,
|
||||
message_type TEXT NOT NULL
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
|
||||
// Create sessions table
|
||||
sqlx::query(
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
token TEXT PRIMARY KEY,
|
||||
username TEXT NOT NULL,
|
||||
phone TEXT NOT NULL,
|
||||
expires_at DATETIME NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
|
||||
Ok(pool)
|
||||
}
|
||||
|
||||
// Обработчики HTTP API
|
||||
async fn register_handler(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(payload): Json<RegisterRequest>,
|
||||
) -> Json<AuthResponse> {
|
||||
// Validate input
|
||||
if payload.phone.len() < 5 {
|
||||
return Json(AuthResponse {
|
||||
success: false,
|
||||
message: "Phone number is too short".to_string(),
|
||||
username: None,
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
|
||||
if payload.username.len() < 3 {
|
||||
return Json(AuthResponse {
|
||||
success: false,
|
||||
message: "Username must be at least 3 characters".to_string(),
|
||||
username: None,
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
|
||||
if payload.password.len() < 6 {
|
||||
return Json(AuthResponse {
|
||||
success: false,
|
||||
message: "Password must be at least 6 characters".to_string(),
|
||||
username: None,
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
|
||||
// Hash password
|
||||
let password_hash = match hash(&payload.password, DEFAULT_COST) {
|
||||
Ok(hash) => hash,
|
||||
Err(_) => {
|
||||
return Json(AuthResponse {
|
||||
success: false,
|
||||
message: "Error hashing password".to_string(),
|
||||
username: None,
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Insert user into database
|
||||
let result = sqlx::query(
|
||||
"INSERT INTO users (phone, username, password_hash) VALUES (?, ?, ?)"
|
||||
)
|
||||
.bind(&payload.phone)
|
||||
.bind(&payload.username)
|
||||
.bind(&password_hash)
|
||||
.execute(&state.db_pool)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
// Generate session token
|
||||
let token = generate_token();
|
||||
let expires_at = Utc::now() + Duration::days(30);
|
||||
|
||||
// Store session
|
||||
let _ = sqlx::query(
|
||||
"INSERT INTO sessions (token, username, phone, expires_at) VALUES (?, ?, ?, ?)"
|
||||
)
|
||||
.bind(&token)
|
||||
.bind(&payload.username)
|
||||
.bind(&payload.phone)
|
||||
.bind(expires_at.naive_utc())
|
||||
.execute(&state.db_pool)
|
||||
.await;
|
||||
|
||||
Json(AuthResponse {
|
||||
success: true,
|
||||
message: "Registration successful".to_string(),
|
||||
username: Some(payload.username),
|
||||
token: Some(token),
|
||||
})
|
||||
}
|
||||
Err(e) => {
|
||||
let error_message = if e.to_string().contains("UNIQUE constraint failed: users.phone") {
|
||||
"Phone number already registered".to_string()
|
||||
} else if e.to_string().contains("UNIQUE constraint failed: users.username") {
|
||||
"Username already taken".to_string()
|
||||
} else {
|
||||
"Registration failed".to_string()
|
||||
};
|
||||
|
||||
Json(AuthResponse {
|
||||
success: false,
|
||||
message: error_message,
|
||||
username: None,
|
||||
token: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn login_handler(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(payload): Json<LoginRequest>,
|
||||
) -> Json<AuthResponse> {
|
||||
// Get user from database
|
||||
let user_result = sqlx::query(
|
||||
"SELECT username, password_hash FROM users WHERE phone = ?"
|
||||
)
|
||||
.bind(&payload.phone)
|
||||
.fetch_optional(&state.db_pool)
|
||||
.await;
|
||||
|
||||
match user_result {
|
||||
Ok(Some(row)) => {
|
||||
let username: String = row.get("username");
|
||||
let password_hash: String = row.get("password_hash");
|
||||
|
||||
// Verify password
|
||||
match verify(&payload.password, &password_hash) {
|
||||
Ok(true) => {
|
||||
// Generate session token
|
||||
let token = generate_token();
|
||||
let expires_at = Utc::now() + Duration::days(30);
|
||||
|
||||
// Store session
|
||||
let _ = sqlx::query(
|
||||
"INSERT INTO sessions (token, username, phone, expires_at) VALUES (?, ?, ?, ?)"
|
||||
)
|
||||
.bind(&token)
|
||||
.bind(&username)
|
||||
.bind(&payload.phone)
|
||||
.bind(expires_at.naive_utc())
|
||||
.execute(&state.db_pool)
|
||||
.await;
|
||||
|
||||
Json(AuthResponse {
|
||||
success: true,
|
||||
message: "Login successful".to_string(),
|
||||
username: Some(username),
|
||||
token: Some(token),
|
||||
})
|
||||
}
|
||||
Ok(false) => Json(AuthResponse {
|
||||
success: false,
|
||||
message: "Invalid password".to_string(),
|
||||
username: None,
|
||||
token: None,
|
||||
}),
|
||||
Err(_) => Json(AuthResponse {
|
||||
success: false,
|
||||
message: "Error verifying password".to_string(),
|
||||
username: None,
|
||||
token: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
Ok(None) => Json(AuthResponse {
|
||||
success: false,
|
||||
message: "Phone number not found".to_string(),
|
||||
username: None,
|
||||
token: None,
|
||||
}),
|
||||
Err(_) => Json(AuthResponse {
|
||||
success: false,
|
||||
message: "Login failed".to_string(),
|
||||
username: None,
|
||||
token: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ValidateTokenRequest {
|
||||
token: String,
|
||||
}
|
||||
|
||||
async fn validate_token_handler(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(payload): Json<ValidateTokenRequest>,
|
||||
) -> Json<ValidateTokenResponse> {
|
||||
match verify_token(&state.db_pool, &payload.token).await {
|
||||
Ok(Some(username)) => {
|
||||
// Get user details
|
||||
let user_result = sqlx::query(
|
||||
"SELECT phone FROM users WHERE username = ?"
|
||||
)
|
||||
.bind(&username)
|
||||
.fetch_optional(&state.db_pool)
|
||||
.await;
|
||||
|
||||
match user_result {
|
||||
Ok(Some(row)) => {
|
||||
let phone: String = row.get("phone");
|
||||
Json(ValidateTokenResponse {
|
||||
success: true,
|
||||
username: Some(username),
|
||||
phone: Some(phone),
|
||||
message: None,
|
||||
})
|
||||
}
|
||||
Ok(None) => Json(ValidateTokenResponse {
|
||||
success: false,
|
||||
username: None,
|
||||
phone: None,
|
||||
message: Some("User not found".to_string()),
|
||||
}),
|
||||
Err(_) => Json(ValidateTokenResponse {
|
||||
success: false,
|
||||
username: None,
|
||||
phone: None,
|
||||
message: Some("Database error".to_string()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
Ok(None) => Json(ValidateTokenResponse {
|
||||
success: false,
|
||||
username: None,
|
||||
phone: None,
|
||||
message: Some("Invalid or expired token".to_string()),
|
||||
}),
|
||||
Err(_) => Json(ValidateTokenResponse {
|
||||
success: false,
|
||||
username: None,
|
||||
phone: None,
|
||||
message: Some("Token verification error".to_string()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
async fn profile_handler() -> Json<UserProfile> {
|
||||
// This would normally verify the token and return user profile
|
||||
// For now, return a placeholder
|
||||
Json(UserProfile {
|
||||
username: "user".to_string(),
|
||||
phone: "+1234567890".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
// WebSocket handlers
|
||||
async fn websocket_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Response {
|
||||
ws.on_upgrade(|socket| handle_websocket(socket, state))
|
||||
}
|
||||
|
||||
async fn handle_websocket(socket: WebSocket, state: Arc<AppState>) {
|
||||
// Split the socket into sender and receiver
|
||||
let (mut sender, mut receiver) = socket.split();
|
||||
|
||||
// Create channel for sending messages to this client
|
||||
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||
|
||||
// Handle authentication - receive first message with token
|
||||
let username = match receiver.next().await {
|
||||
Some(Ok(Message::Text(auth_msg))) => {
|
||||
match serde_json::from_str::<serde_json::Value>(&auth_msg) {
|
||||
Ok(auth) => {
|
||||
if let (Some(token), Some(auth_type)) = (auth["token"].as_str(), auth["type"].as_str()) {
|
||||
if auth_type == "auth" {
|
||||
// Verify token and get username
|
||||
match verify_token(&state.db_pool, token).await {
|
||||
Ok(Some(user_username)) => {
|
||||
// Check if username is already connected
|
||||
if state.users.read().await.contains_key(&user_username) {
|
||||
let _ = sender.send(Message::Text(
|
||||
serde_json::json!({
|
||||
"type": "error",
|
||||
"content": "User already connected"
|
||||
}).to_string(),
|
||||
)).await;
|
||||
return;
|
||||
}
|
||||
|
||||
// Register user
|
||||
state.users.write().await.insert(user_username.clone(), tx);
|
||||
|
||||
// Send success message
|
||||
let _ = sender.send(Message::Text(
|
||||
serde_json::json!({
|
||||
"type": "system",
|
||||
"content": "Connected successfully"
|
||||
}).to_string(),
|
||||
)).await;
|
||||
|
||||
// Send user list
|
||||
broadcast_user_list(&state.users).await;
|
||||
|
||||
// Send message history
|
||||
if let Ok(messages) = get_message_history(&state.db_pool, &user_username).await {
|
||||
for msg in messages {
|
||||
let message_json = serde_json::to_string(&msg).unwrap();
|
||||
let _ = sender.send(Message::Text(message_json)).await;
|
||||
}
|
||||
}
|
||||
|
||||
user_username
|
||||
}
|
||||
Ok(None) => {
|
||||
let _ = sender.send(Message::Text(
|
||||
serde_json::json!({
|
||||
"type": "error",
|
||||
"content": "Invalid or expired token"
|
||||
}).to_string(),
|
||||
)).await;
|
||||
return;
|
||||
}
|
||||
Err(_) => {
|
||||
let _ = sender.send(Message::Text(
|
||||
serde_json::json!({
|
||||
"type": "error",
|
||||
"content": "Authentication error"
|
||||
}).to_string(),
|
||||
)).await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
Err(_) => return,
|
||||
}
|
||||
}
|
||||
_ => return,
|
||||
};
|
||||
|
||||
println!("User {} connected", username);
|
||||
|
||||
// Clone state for cleanup
|
||||
let state_clone = state.clone();
|
||||
let username_clone = username.clone();
|
||||
|
||||
// Spawn a task to handle sending messages to the client
|
||||
let mut send_sender = sender;
|
||||
let send_task = tokio::spawn(async move {
|
||||
while let Some(message) = rx.recv().await {
|
||||
if send_sender.send(message).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle receiving messages from the client in the main task
|
||||
let recv_state = state.clone();
|
||||
|
||||
let recv_task = tokio::spawn(async move {
|
||||
while let Some(result) = receiver.next().await {
|
||||
match result {
|
||||
Ok(Message::Text(text)) => {
|
||||
println!("Received message from {}: {}", username, text);
|
||||
|
||||
if let Ok(mut chat_msg) = serde_json::from_str::<ChatMessage>(&text) {
|
||||
// Set the from_user to the authenticated username
|
||||
chat_msg.from_user = username.clone();
|
||||
|
||||
println!("Processing message from {} to {}", chat_msg.from_user, chat_msg.to_user);
|
||||
|
||||
// Save message to database
|
||||
if let Err(e) = save_message(&recv_state.db_pool, &chat_msg).await {
|
||||
eprintln!("Failed to save message: {}", e);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Send to recipient if online
|
||||
let users = recv_state.users.read().await;
|
||||
if let Some(recipient_tx) = users.get(&chat_msg.to_user) {
|
||||
let message_json = serde_json::to_string(&chat_msg).unwrap();
|
||||
println!("Sending message to {}: {}", chat_msg.to_user, message_json);
|
||||
let _ = recipient_tx.send(Message::Text(message_json));
|
||||
} else {
|
||||
println!("Recipient {} not found online", chat_msg.to_user);
|
||||
}
|
||||
|
||||
// Also send the message back to the sender for confirmation
|
||||
if let Some(sender_tx) = users.get(&username) {
|
||||
let message_json = serde_json::to_string(&chat_msg).unwrap();
|
||||
let _ = sender_tx.send(Message::Text(message_json));
|
||||
}
|
||||
} else {
|
||||
println!("Failed to parse message as ChatMessage: {}", text);
|
||||
}
|
||||
}
|
||||
Ok(Message::Close(_)) => {
|
||||
println!("WebSocket closed by client");
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("WebSocket error: {}", e);
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for both tasks to finish
|
||||
tokio::select! {
|
||||
_ = send_task => {},
|
||||
_ = recv_task => {},
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
state_clone.users.write().await.remove(&username_clone);
|
||||
broadcast_user_list(&state_clone.users).await;
|
||||
println!("User {} disconnected", username_clone);
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
fn generate_token() -> String {
|
||||
thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(32)
|
||||
.map(char::from)
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn verify_token(pool: &SqlitePool, token: &str) -> Result<Option<String>, sqlx::Error> {
|
||||
let result = sqlx::query(
|
||||
"SELECT username FROM sessions WHERE token = ? AND expires_at > datetime('now')"
|
||||
)
|
||||
.bind(token)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
|
||||
Ok(result.map(|row| row.get("username")))
|
||||
}
|
||||
|
||||
async fn broadcast_user_list(users: &UserMap) {
|
||||
let user_list: Vec<String> = users.read().await.keys().cloned().collect();
|
||||
let user_list_msg = serde_json::json!({
|
||||
"type": "user_list",
|
||||
"users": user_list
|
||||
});
|
||||
|
||||
for tx in users.read().await.values() {
|
||||
let _ = tx.send(Message::Text(user_list_msg.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
async fn save_message(pool: &SqlitePool, msg: &ChatMessage) -> Result<(), sqlx::Error> {
|
||||
sqlx::query(
|
||||
"INSERT OR REPLACE INTO messages (id, from_user, to_user, content, timestamp, message_type) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
)
|
||||
.bind(&msg.id)
|
||||
.bind(&msg.from_user)
|
||||
.bind(&msg.to_user)
|
||||
.bind(&msg.content)
|
||||
.bind(msg.timestamp)
|
||||
.bind(&msg.message_type)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_message_history(pool: &SqlitePool, username: &str) -> Result<Vec<ChatMessage>, sqlx::Error> {
|
||||
let rows = sqlx::query(
|
||||
"SELECT id, from_user, to_user, content, timestamp, message_type FROM messages WHERE from_user = ? OR to_user = ? ORDER BY timestamp"
|
||||
)
|
||||
.bind(username)
|
||||
.bind(username)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
let mut messages = Vec::new();
|
||||
for row in rows {
|
||||
let message = ChatMessage {
|
||||
id: row.get("id"),
|
||||
from_user: row.get("from_user"),
|
||||
to_user: row.get("to_user"),
|
||||
content: row.get("content"),
|
||||
timestamp: row.get("timestamp"),
|
||||
message_type: row.get("message_type"),
|
||||
};
|
||||
messages.push(message);
|
||||
}
|
||||
|
||||
Ok(messages)
|
||||
}
|
||||
+868
@@ -0,0 +1,868 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Secure Chat App</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 90%;
|
||||
max-width: 1200px;
|
||||
height: 90vh;
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.auth-container {
|
||||
width: 400px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
padding: 40px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.auth-tabs {
|
||||
display: flex;
|
||||
margin-bottom: 30px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.auth-tab {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
border-bottom: 3px solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.auth-tab.active {
|
||||
border-bottom-color: #3498db;
|
||||
color: #3498db;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.auth-form {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.auth-form.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 15px 20px;
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 10px;
|
||||
font-size: 16px;
|
||||
outline: none;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #3498db;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
background: linear-gradient(135deg, #3498db, #2980b9);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(52, 152, 219, 0.4);
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
/* Chat styles remain the same as before */
|
||||
.chat-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.users-panel {
|
||||
width: 300px;
|
||||
background: #2c3e50;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.users-panel h2 {
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
font-weight: 300;
|
||||
border-bottom: 1px solid #34495e;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.user-list {
|
||||
list-style: none;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.user-item {
|
||||
padding: 15px;
|
||||
margin: 8px 0;
|
||||
background: #34495e;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border-left: 4px solid transparent;
|
||||
}
|
||||
|
||||
.user-item:hover {
|
||||
background: #3d566e;
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.user-item.active {
|
||||
background: #3498db;
|
||||
border-left-color: #2980b9;
|
||||
}
|
||||
|
||||
.chat-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
padding: 8px 16px;
|
||||
background: #e74c3c;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.logout-btn:hover {
|
||||
background: #c0392b;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.message {
|
||||
max-width: 70%;
|
||||
margin: 15px 0;
|
||||
padding: 15px 20px;
|
||||
border-radius: 20px;
|
||||
position: relative;
|
||||
word-wrap: break-word;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.incoming {
|
||||
background: white;
|
||||
border: 1px solid #e9ecef;
|
||||
align-self: flex-start;
|
||||
border-bottom-left-radius: 5px;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.outgoing {
|
||||
background: linear-gradient(135deg, #3498db, #2980b9);
|
||||
color: white;
|
||||
align-self: flex-end;
|
||||
margin-left: auto;
|
||||
border-bottom-right-radius: 5px;
|
||||
box-shadow: 0 2px 5px rgba(52, 152, 219, 0.3);
|
||||
}
|
||||
|
||||
.message-sender {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 5px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
margin: 8px 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 11px;
|
||||
text-align: right;
|
||||
opacity: 0.7;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.chat-input-area {
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-top: 1px solid #e9ecef;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.message-input {
|
||||
flex: 1;
|
||||
padding: 15px 20px;
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 25px;
|
||||
outline: none;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.message-input:focus {
|
||||
border-color: #3498db;
|
||||
}
|
||||
|
||||
.send-button {
|
||||
padding: 15px 25px;
|
||||
background: linear-gradient(135deg, #3498db, #2980b9);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 25px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.send-button:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(52, 152, 219, 0.4);
|
||||
}
|
||||
|
||||
.send-button:disabled {
|
||||
background: #bdc3c7;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.system-message {
|
||||
text-align: center;
|
||||
color: #7f8c8d;
|
||||
font-style: italic;
|
||||
margin: 15px 0;
|
||||
font-size: 13px;
|
||||
padding: 10px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Authentication Container -->
|
||||
<div id="authContainer" class="auth-container">
|
||||
<div class="auth-tabs">
|
||||
<div class="auth-tab active" onclick="showTab('login')">Login</div>
|
||||
<div class="auth-tab" onclick="showTab('register')">Register</div>
|
||||
</div>
|
||||
|
||||
<div id="alert" class="alert" style="display: none;"></div>
|
||||
|
||||
<!-- Login Form -->
|
||||
<div id="loginForm" class="auth-form active">
|
||||
<div class="form-group">
|
||||
<label for="loginPhone">Phone Number</label>
|
||||
<input type="text" id="loginPhone" class="form-control" placeholder="+1234567890">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="loginPassword">Password</label>
|
||||
<input type="password" id="loginPassword" class="form-control" placeholder="Enter your password">
|
||||
</div>
|
||||
<button class="btn" onclick="login()">Login</button>
|
||||
</div>
|
||||
|
||||
<!-- Register Form -->
|
||||
<div id="registerForm" class="auth-form">
|
||||
<div class="form-group">
|
||||
<label for="registerPhone">Phone Number</label>
|
||||
<input type="text" id="registerPhone" class="form-control" placeholder="+1234567890">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="registerUsername">Username</label>
|
||||
<input type="text" id="registerUsername" class="form-control" placeholder="Choose a username">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="registerPassword">Password</label>
|
||||
<input type="password" id="registerPassword" class="form-control" placeholder="Choose a password (min 6 characters)">
|
||||
</div>
|
||||
<button class="btn" onclick="register()">Register</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chat Container -->
|
||||
<div class="container" style="display: none;" id="chatContainer">
|
||||
<div class="users-panel">
|
||||
<h2>Online Users</h2>
|
||||
<ul class="user-list" id="userList"></ul>
|
||||
</div>
|
||||
|
||||
<div class="chat-panel">
|
||||
<div class="chat-header">
|
||||
<span id="chatHeader">Select a user to start chatting</span>
|
||||
<button class="logout-btn" onclick="logout()">Logout</button>
|
||||
</div>
|
||||
|
||||
<div class="chat-messages" id="chatMessages"></div>
|
||||
|
||||
<div class="chat-input-area">
|
||||
<input type="text" id="messageInput" class="message-input" placeholder="Type a message..." disabled>
|
||||
<button onclick="sendMessage()" class="send-button" id="sendButton" disabled>Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let ws = null;
|
||||
let currentUser = '';
|
||||
let currentToken = '';
|
||||
let currentRecipient = '';
|
||||
let userList = [];
|
||||
let messageHistory = {};
|
||||
|
||||
// Check for saved session on page load
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
const savedToken = localStorage.getItem('chatToken');
|
||||
const savedUsername = localStorage.getItem('chatUsername');
|
||||
|
||||
if (savedToken && savedUsername) {
|
||||
validateToken(savedToken, savedUsername);
|
||||
}
|
||||
});
|
||||
|
||||
function showTab(tabName) {
|
||||
// Update tabs
|
||||
document.querySelectorAll('.auth-tab').forEach(tab => {
|
||||
tab.classList.remove('active');
|
||||
});
|
||||
document.querySelectorAll('.auth-form').forEach(form => {
|
||||
form.classList.remove('active');
|
||||
});
|
||||
|
||||
// Show selected tab
|
||||
if (tabName === 'login') {
|
||||
document.querySelector('.auth-tab:nth-child(1)').classList.add('active');
|
||||
document.getElementById('loginForm').classList.add('active');
|
||||
} else {
|
||||
document.querySelector('.auth-tab:nth-child(2)').classList.add('active');
|
||||
document.getElementById('registerForm').classList.add('active');
|
||||
}
|
||||
|
||||
// Clear alerts
|
||||
hideAlert();
|
||||
}
|
||||
|
||||
function showAlert(message, type) {
|
||||
const alert = document.getElementById('alert');
|
||||
alert.textContent = message;
|
||||
alert.className = `alert alert-${type}`;
|
||||
alert.style.display = 'block';
|
||||
}
|
||||
|
||||
function hideAlert() {
|
||||
document.getElementById('alert').style.display = 'none';
|
||||
}
|
||||
|
||||
async function validateToken(token, username) {
|
||||
try {
|
||||
const response = await fetch('/api/validate-token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ token }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
currentUser = data.username;
|
||||
currentToken = token;
|
||||
|
||||
// Save to localStorage
|
||||
localStorage.setItem('chatToken', token);
|
||||
localStorage.setItem('chatUsername', data.username);
|
||||
|
||||
// Hide auth, show chat
|
||||
document.getElementById('authContainer').style.display = 'none';
|
||||
document.getElementById('chatContainer').style.display = 'flex';
|
||||
document.getElementById('chatHeader').textContent = `Welcome, ${currentUser}`;
|
||||
|
||||
// Connect to WebSocket
|
||||
connectWebSocket();
|
||||
} else {
|
||||
// Clear invalid token
|
||||
localStorage.removeItem('chatToken');
|
||||
localStorage.removeItem('chatUsername');
|
||||
showAlert('Session expired. Please login again.', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Token validation failed:', error);
|
||||
localStorage.removeItem('chatToken');
|
||||
localStorage.removeItem('chatUsername');
|
||||
showAlert('Session validation failed. Please login again.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function register() {
|
||||
const phone = document.getElementById('registerPhone').value.trim();
|
||||
const username = document.getElementById('registerUsername').value.trim();
|
||||
const password = document.getElementById('registerPassword').value;
|
||||
|
||||
if (!phone || !username || !password) {
|
||||
showAlert('Please fill all fields', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
showAlert('Password must be at least 6 characters', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/register', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ phone, username, password }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// Save token and username to localStorage
|
||||
localStorage.setItem('chatToken', data.token);
|
||||
localStorage.setItem('chatUsername', data.username);
|
||||
|
||||
currentUser = data.username;
|
||||
currentToken = data.token;
|
||||
|
||||
// Hide auth, show chat
|
||||
document.getElementById('authContainer').style.display = 'none';
|
||||
document.getElementById('chatContainer').style.display = 'flex';
|
||||
document.getElementById('chatHeader').textContent = `Welcome, ${currentUser}`;
|
||||
|
||||
// Connect to WebSocket
|
||||
connectWebSocket();
|
||||
|
||||
showAlert('Registration successful!', 'success');
|
||||
} else {
|
||||
showAlert(data.message, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showAlert('Registration failed. Please try again.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function login() {
|
||||
const phone = document.getElementById('loginPhone').value.trim();
|
||||
const password = document.getElementById('loginPassword').value;
|
||||
|
||||
if (!phone || !password) {
|
||||
showAlert('Please fill all fields', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ phone, password }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// Save token and username to localStorage
|
||||
localStorage.setItem('chatToken', data.token);
|
||||
localStorage.setItem('chatUsername', data.username);
|
||||
|
||||
currentUser = data.username;
|
||||
currentToken = data.token;
|
||||
|
||||
// Hide auth, show chat
|
||||
document.getElementById('authContainer').style.display = 'none';
|
||||
document.getElementById('chatContainer').style.display = 'flex';
|
||||
document.getElementById('chatHeader').textContent = `Welcome, ${currentUser}`;
|
||||
|
||||
// Connect to WebSocket
|
||||
connectWebSocket();
|
||||
} else {
|
||||
showAlert(data.message, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showAlert('Login failed. Please try again.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
// Clear localStorage
|
||||
localStorage.removeItem('chatToken');
|
||||
localStorage.removeItem('chatUsername');
|
||||
|
||||
// Close WebSocket connection
|
||||
if (ws) {
|
||||
ws.close();
|
||||
}
|
||||
|
||||
// Reset state
|
||||
currentUser = '';
|
||||
currentToken = '';
|
||||
currentRecipient = '';
|
||||
userList = [];
|
||||
messageHistory = {};
|
||||
|
||||
// Show auth, hide chat
|
||||
document.getElementById('authContainer').style.display = 'block';
|
||||
document.getElementById('chatContainer').style.display = 'none';
|
||||
|
||||
// Clear forms
|
||||
document.getElementById('loginPhone').value = '';
|
||||
document.getElementById('loginPassword').value = '';
|
||||
document.getElementById('registerPhone').value = '';
|
||||
document.getElementById('registerUsername').value = '';
|
||||
document.getElementById('registerPassword').value = '';
|
||||
|
||||
showTab('login');
|
||||
}
|
||||
|
||||
function connectWebSocket() {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${window.location.host}/ws`;
|
||||
|
||||
ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('WebSocket connected');
|
||||
// Send authentication
|
||||
ws.send(JSON.stringify({
|
||||
type: 'auth',
|
||||
token: currentToken
|
||||
}));
|
||||
|
||||
addSystemMessage('Connected to chat server');
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
console.log('Received message:', event.data);
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
if (data.type === 'user_list') {
|
||||
updateUserList(data.users);
|
||||
} else if (data.type === 'error') {
|
||||
showAlert(data.content, 'error');
|
||||
// If authentication error, logout
|
||||
if (data.content.includes('Invalid') || data.content.includes('expired') || data.content.includes('auth')) {
|
||||
logout();
|
||||
}
|
||||
} else if (data.type === 'system') {
|
||||
addSystemMessage(data.content);
|
||||
} else {
|
||||
// Regular chat message
|
||||
handleChatMessage(data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing message:', e, 'Raw data:', event.data);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('WebSocket disconnected');
|
||||
addSystemMessage('Disconnected from server');
|
||||
document.getElementById('sendButton').disabled = true;
|
||||
document.getElementById('messageInput').disabled = true;
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
addSystemMessage('Connection error');
|
||||
};
|
||||
}
|
||||
|
||||
function updateUserList(users) {
|
||||
userList = users.filter(user => user !== currentUser);
|
||||
const userListElement = document.getElementById('userList');
|
||||
userListElement.innerHTML = '';
|
||||
|
||||
userList.forEach(user => {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'user-item';
|
||||
li.textContent = user;
|
||||
li.onclick = () => selectUser(user);
|
||||
userListElement.appendChild(li);
|
||||
});
|
||||
|
||||
// Auto-select first user if none selected
|
||||
if (!currentRecipient && userList.length > 0) {
|
||||
selectUser(userList[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function selectUser(user) {
|
||||
currentRecipient = user;
|
||||
|
||||
// Update active state in user list
|
||||
document.querySelectorAll('.user-item').forEach(item => {
|
||||
item.classList.remove('active');
|
||||
if (item.textContent === user) {
|
||||
item.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
// Update chat header
|
||||
document.getElementById('chatHeader').textContent = `Chat with ${user}`;
|
||||
|
||||
// Enable input
|
||||
document.getElementById('sendButton').disabled = false;
|
||||
document.getElementById('messageInput').disabled = false;
|
||||
document.getElementById('messageInput').focus();
|
||||
|
||||
// Display chat history with this user
|
||||
displayChatHistory(user);
|
||||
}
|
||||
|
||||
function displayChatHistory(user) {
|
||||
const chatMessages = document.getElementById('chatMessages');
|
||||
chatMessages.innerHTML = '';
|
||||
|
||||
if (messageHistory[user]) {
|
||||
messageHistory[user].forEach(msg => {
|
||||
displayMessage(msg, false);
|
||||
});
|
||||
}
|
||||
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
}
|
||||
|
||||
function handleChatMessage(msg) {
|
||||
console.log('Handling chat message:', msg);
|
||||
const otherUser = msg.from_user === currentUser ? msg.to_user : msg.from_user;
|
||||
|
||||
if (!messageHistory[otherUser]) {
|
||||
messageHistory[otherUser] = [];
|
||||
}
|
||||
|
||||
// Check if message already exists to avoid duplicates
|
||||
const messageExists = messageHistory[otherUser].some(
|
||||
existingMsg => existingMsg.id === msg.id
|
||||
);
|
||||
|
||||
if (!messageExists) {
|
||||
messageHistory[otherUser].push(msg);
|
||||
}
|
||||
|
||||
if (otherUser === currentRecipient) {
|
||||
displayMessage(msg, true);
|
||||
}
|
||||
}
|
||||
|
||||
function displayMessage(msg, scrollToBottom) {
|
||||
const chatMessages = document.getElementById('chatMessages');
|
||||
const messageDiv = document.createElement('div');
|
||||
|
||||
if (msg.message_type === 'system') {
|
||||
messageDiv.className = 'system-message';
|
||||
messageDiv.textContent = msg.content;
|
||||
} else {
|
||||
const isOutgoing = msg.from_user === currentUser;
|
||||
messageDiv.className = `message ${isOutgoing ? 'outgoing' : 'incoming'}`;
|
||||
|
||||
if (!isOutgoing) {
|
||||
const senderDiv = document.createElement('div');
|
||||
senderDiv.className = 'message-sender';
|
||||
senderDiv.textContent = msg.from_user;
|
||||
messageDiv.appendChild(senderDiv);
|
||||
}
|
||||
|
||||
const contentDiv = document.createElement('div');
|
||||
contentDiv.className = 'message-content';
|
||||
contentDiv.textContent = msg.content;
|
||||
messageDiv.appendChild(contentDiv);
|
||||
|
||||
const timeDiv = document.createElement('div');
|
||||
timeDiv.className = 'message-time';
|
||||
const date = new Date(msg.timestamp);
|
||||
timeDiv.textContent = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
messageDiv.appendChild(timeDiv);
|
||||
}
|
||||
|
||||
chatMessages.appendChild(messageDiv);
|
||||
|
||||
if (scrollToBottom) {
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
function addSystemMessage(content) {
|
||||
const chatMessages = document.getElementById('chatMessages');
|
||||
const systemDiv = document.createElement('div');
|
||||
systemDiv.className = 'system-message';
|
||||
systemDiv.textContent = content;
|
||||
chatMessages.appendChild(systemDiv);
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
}
|
||||
|
||||
function sendMessage() {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||
showAlert('Please connect to the server first', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentRecipient) {
|
||||
showAlert('Please select a user to chat with', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const input = document.getElementById('messageInput');
|
||||
const content = input.value.trim();
|
||||
|
||||
if (!content) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = {
|
||||
id: generateId(),
|
||||
from_user: currentUser, // This will be overwritten by server, but needed for structure
|
||||
to_user: currentRecipient,
|
||||
content: content,
|
||||
timestamp: Date.now(),
|
||||
message_type: 'text'
|
||||
};
|
||||
|
||||
console.log('Sending message:', message);
|
||||
ws.send(JSON.stringify(message));
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
function generateId() {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substr(2);
|
||||
}
|
||||
|
||||
// Event listeners for Enter key
|
||||
document.getElementById('messageInput').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('loginPassword').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
login();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('registerPassword').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
register();
|
||||
}
|
||||
});
|
||||
|
||||
// Focus phone input on load
|
||||
document.getElementById('loginPhone').focus();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user