use warp::Filter;
use std::sync::{Arc, Mutex};
use futures::{SinkExt, StreamExt};
use warp::ws::{Message, WebSocket};
type Clients = Arc<Mutex<Vec<tokio::sync::mpsc::UnboundedSender<Result<Message, warp::Error>>>>>;
#[tokio::main]
async fn main() {
let clients: Clients = Arc::new(Mutex::new(Vec::new()));
let index = warp::path::end().map(|| warp::reply::html(INDEX_HTML));
let chat = warp::path("chat")
.and(warp::ws())
.and(with_clients(clients.clone()))
.map(|ws: warp::ws::Ws, clients| {
ws.on_upgrade(move |socket| client_connected(socket, clients))
});
let routes = index.or(chat);
println!("✅ Web chat server running at: http://127.0.0.1:3030");
println!("🔗 Tunnel it with: ngrok http 3030");
warp::serve(routes).run(([0, 0, 0, 0], 3030)).await;
}
fn with_clients(clients: Clients) -> impl Filter<Extract = (Clients,), Error = std::convert::Infallible> + Clone {
warp::any().map(move || clients.clone())
}
async fn client_connected(ws: WebSocket, clients: Clients) {
let (mut user_ws_tx, mut user_ws_rx) = ws.split();
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
clients.lock().unwrap().push(tx);
tokio::task::spawn(async move {
while let Some(result) = rx.recv().await {
if let Ok(msg) = result {
if let Err(e) = user_ws_tx.send(msg).await {
eprintln!("Send error: {}", e);
break;
}
}
}
});
while let Some(result) = user_ws_rx.next().await {
if let Ok(msg) = result {
if let Ok(text) = msg.to_str() {
let clients_guard = clients.lock().unwrap();
for client in clients_guard.iter() {
let _ = client.send(Ok(Message::text(text.to_string())));
}
}
}
}
}
static INDEX_HTML: &str = r#"<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hop Web Chat</title>
<style>
body {
font-family: Arial, sans-serif;
background: #1e1e2f;
color: #fff;
margin: 0;
display: flex;
flex-direction: column;
height: 100vh;
}
header {
background: #27293d;
padding: 15px;
text-align: center;
font-size: 1.4em;
font-weight: bold;
border-bottom: 2px solid #3f3f5f;
}
#chat-container {
flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: 10px;
}
#messages {
flex: 1;
overflow-y: auto;
border: 1px solid #3f3f5f;
background: #2a2a40;
padding: 10px;
border-radius: 6px;
}
.message {
margin-bottom: 8px;
padding: 6px 10px;
background: #3b3b5c;
border-radius: 6px;
max-width: 75%;
word-wrap: break-word;
}
.message.self {
background: #5b5bf0;
align-self: flex-end;
}
#input-area {
display: flex;
margin-top: 10px;
}
#input {
flex: 1;
padding: 10px;
border-radius: 6px;
border: none;
outline: none;
font-size: 1em;
}
button {
padding: 10px 15px;
margin-left: 10px;
background: #5b5bf0;
color: white;
border: none;
border-radius: 6px;
font-weight: bold;
cursor: pointer;
}
button:hover {
background: #7676ff;
}
#username-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.8);
display: flex;
justify-content: center;
align-items: center;
}
#username-box {
background: #2a2a40;
padding: 20px;
border-radius: 8px;
text-align: center;
}
#username-input {
padding: 8px;
width: 200px;
border-radius: 6px;
border: none;
margin-top: 10px;
outline: none;
}
</style>
</head>
<body>
<header>💬 Hop Web Chat</header>
<div id="chat-container" style="display:none;">
<div id="messages"></div>
<div id="input-area">
<input id="input" type="text" placeholder="Type your message..." autocomplete="off" />
<button onclick="sendMessage()">Send</button>
</div>
</div>
<div id="username-modal">
<div id="username-box">
<h2>Enter your nickname</h2>
<input id="username-input" type="text" placeholder="e.g. LexLuger" />
<br/><br/>
<button onclick="setUsername()">Join Chat</button>
</div>
</div>
<script>
let username = "";
const protocol = location.protocol === "https:" ? "wss" : "ws";
const ws = new WebSocket(`${protocol}://${location.host}/chat`);
const messages = document.getElementById("messages");
const input = document.getElementById("input");
function setUsername() {
const nameInput = document.getElementById("username-input");
if (nameInput.value.trim() === "") return alert("Please enter a name");
username = nameInput.value.trim();
document.getElementById("username-modal").style.display = "none";
document.getElementById("chat-container").style.display = "flex";
ws.send(`🟢 ${username} joined the chat`);
}
ws.onmessage = (event) => {
const div = document.createElement("div");
div.classList.add("message");
div.textContent = event.data;
messages.appendChild(div);
messages.scrollTop = messages.scrollHeight;
};
function sendMessage() {
const text = input.value.trim();
if (text !== "" && username !== "") {
const msg = `${username}: ${text}`;
ws.send(msg);
const div = document.createElement("div");
div.classList.add("message", "self");
div.textContent = msg;
messages.appendChild(div);
messages.scrollTop = messages.scrollHeight;
input.value = "";
}
}
input.addEventListener("keypress", function(e) {
if (e.key === "Enter") {
sendMessage();
e.preventDefault();
}
});
</script>
</body>
</html>
"#;