Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Private Messenger</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
<style> | |
/* Custom scrollbar */ | |
.custom-scrollbar::-webkit-scrollbar { | |
width: 6px; | |
} | |
.custom-scrollbar::-webkit-scrollbar-track { | |
background: #f1f1f1; | |
} | |
.custom-scrollbar::-webkit-scrollbar-thumb { | |
background: #888; | |
border-radius: 3px; | |
} | |
.custom-scrollbar::-webkit-scrollbar-thumb:hover { | |
background: #555; | |
} | |
/* Animation for new messages */ | |
@keyframes slideIn { | |
from { | |
transform: translateY(10px); | |
opacity: 0; | |
} | |
to { | |
transform: translateY(0); | |
opacity: 1; | |
} | |
} | |
.animate-slide-in { | |
animation: slideIn 0.2s ease-out; | |
} | |
/* Pulse animation for typing indicator */ | |
@keyframes pulse { | |
0%, 100% { | |
opacity: 1; | |
} | |
50% { | |
opacity: 0.5; | |
} | |
} | |
.animate-pulse { | |
animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite; | |
} | |
</style> | |
</head> | |
<body class="bg-gray-100 h-screen flex flex-col"> | |
<!-- App Header --> | |
<header class="bg-indigo-600 text-white shadow-lg"> | |
<div class="container mx-auto px-4 py-3 flex justify-between items-center"> | |
<div class="flex items-center space-x-3"> | |
<img src="logo.png" alt="Messenger Logo" class="h-8 w-8"> | |
<h1 class="text-xl font-bold">Private Messenger</h1> | |
</div> | |
<div id="user-info" class="hidden items-center space-x-2"> | |
<span id="username-display" class="font-medium"></span> | |
<div class="w-8 h-8 rounded-full bg-indigo-400 flex items-center justify-center"> | |
<i class="fas fa-user text-white"></i> | |
</div> | |
</div> | |
</div> | |
</header> | |
<!-- Main Content --> | |
<main class="flex-1 container mx-auto px-4 py-6 flex flex-col md:flex-row gap-6"> | |
<!-- Join Form (shown by default) --> | |
<div id="join-container" class="w-full md:w-1/3 lg:w-1/4 bg-white rounded-lg shadow-md p-6 self-center md:self-auto"> | |
<h2 class="text-xl font-bold text-gray-800 mb-4">Join the Chat</h2> | |
<div class="mb-4"> | |
<label for="username" class="block text-sm font-medium text-gray-700 mb-1">Username</label> | |
<input type="text" id="username" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500" placeholder="Enter your name"> | |
</div> | |
<button id="join-btn" class="w-full bg-indigo-600 text-white py-2 px-4 rounded-md hover:bg-indigo-700 transition duration-200 flex items-center justify-center"> | |
<i class="fas fa-sign-in-alt mr-2"></i> Join Chat | |
</button> | |
<div class="mt-4 text-center text-sm text-gray-500"> | |
<p>Your messages are end-to-end encrypted</p> | |
</div> | |
</div> | |
<!-- Chat Container (hidden by default) --> | |
<div id="chat-container" class="hidden w-full md:w-2/3 lg:w-3/4 flex flex-col bg-white rounded-lg shadow-md overflow-hidden"> | |
<!-- Chat Header --> | |
<div class="bg-indigo-50 px-4 py-3 border-b border-gray-200 flex justify-between items-center"> | |
<h3 class="font-semibold text-indigo-800">Global Chat Room</h3> | |
<div id="typing-indicator" class="hidden text-sm text-gray-500 italic"> | |
<span id="typing-users"></span> is typing... | |
</div> | |
<div class="flex items-center space-x-2"> | |
<span id="online-count" class="text-sm text-gray-600">0 online</span> | |
<div class="w-2 h-2 rounded-full bg-green-500"></div> | |
</div> | |
</div> | |
<!-- Messages Container --> | |
<div id="messages" class="flex-1 p-4 overflow-y-auto custom-scrollbar space-y-3"> | |
<div class="text-center text-gray-500 text-sm py-4"> | |
Welcome to the chat! Say hello to everyone. | |
</div> | |
</div> | |
<!-- Message Input --> | |
<div class="border-t border-gray-200 p-4 bg-gray-50"> | |
<div class="flex space-x-2"> | |
<input type="text" id="message-input" placeholder="Type your message..." class="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"> | |
<button id="send-btn" class="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700 transition duration-200"> | |
<i class="fas fa-paper-plane"></i> | |
</button> | |
</div> | |
<div class="mt-2 text-xs text-gray-500"> | |
Press Enter to send, Shift+Enter for new line | |
</div> | |
</div> | |
</div> | |
</main> | |
<!-- Footer --> | |
<footer class="bg-gray-800 text-white py-4"> | |
<div class="container mx-auto px-4 text-center text-sm"> | |
<p>Private Messenger © 2023 | All messages are encrypted</p> | |
</div> | |
</footer> | |
<script> | |
// DOM Elements | |
const joinContainer = document.getElementById('join-container'); | |
const chatContainer = document.getElementById('chat-container'); | |
const joinBtn = document.getElementById('join-btn'); | |
const usernameInput = document.getElementById('username'); | |
const usernameDisplay = document.getElementById('username-display'); | |
const userInfo = document.getElementById('user-info'); | |
const messagesContainer = document.getElementById('messages'); | |
const messageInput = document.getElementById('message-input'); | |
const sendBtn = document.getElementById('send-btn'); | |
const typingIndicator = document.getElementById('typing-indicator'); | |
const typingUsers = document.getElementById('typing-users'); | |
const onlineCount = document.getElementById('online-count'); | |
// App State | |
let username = ''; | |
let usersTyping = new Set(); | |
let onlineUsers = 0; | |
let lastTypingTime = 0; | |
let typingTimeout; | |
// Simulated WebSocket connection (in a real app, replace with actual WebSocket) | |
const socket = { | |
listeners: {}, | |
connect() { | |
console.log('Connected to chat server'); | |
}, | |
send(data) { | |
// In a real app, this would send data to the server | |
console.log('Sending:', data); | |
// Simulate receiving messages after a short delay | |
if (data.type === 'message') { | |
setTimeout(() => { | |
this.onMessage({ | |
type: 'message', | |
username: username, | |
text: data.text, | |
timestamp: new Date().getTime() | |
}); | |
}, 100); | |
} else if (data.type === 'typing') { | |
setTimeout(() => { | |
this.onTyping({ | |
type: 'typing', | |
username: username, | |
isTyping: data.isTyping | |
}); | |
}, 100); | |
} | |
}, | |
on(event, callback) { | |
this.listeners[event] = callback; | |
}, | |
onMessage(message) { | |
if (this.listeners['message']) { | |
this.listeners['message'](message); | |
} | |
}, | |
onTyping(data) { | |
if (this.listeners['typing']) { | |
this.listeners['typing'](data); | |
} | |
}, | |
onUserCount(count) { | |
if (this.listeners['userCount']) { | |
this.listeners['userCount'](count); | |
} | |
} | |
}; | |
// Join chat | |
joinBtn.addEventListener('click', () => { | |
username = usernameInput.value.trim(); | |
if (username) { | |
joinChat(); | |
} else { | |
alert('Please enter a username'); | |
} | |
}); | |
// Also allow joining with Enter key | |
usernameInput.addEventListener('keypress', (e) => { | |
if (e.key === 'Enter') { | |
username = usernameInput.value.trim(); | |
if (username) { | |
joinChat(); | |
} | |
} | |
}); | |
function joinChat() { | |
// Connect to the chat | |
socket.connect(); | |
// Update UI | |
joinContainer.classList.add('hidden'); | |
chatContainer.classList.remove('hidden'); | |
userInfo.classList.remove('hidden'); | |
usernameDisplay.textContent = username; | |
// Simulate other users joining | |
setTimeout(() => { | |
socket.onUserCount(Math.floor(Math.random() * 10) + 3); | |
}, 1000); | |
// Set up message listeners | |
socket.on('message', handleNewMessage); | |
socket.on('typing', handleTyping); | |
socket.on('userCount', handleUserCount); | |
// Focus the message input | |
messageInput.focus(); | |
} | |
// Send message | |
sendBtn.addEventListener('click', sendMessage); | |
messageInput.addEventListener('keypress', (e) => { | |
if (e.key === 'Enter' && !e.shiftKey) { | |
e.preventDefault(); | |
sendMessage(); | |
} | |
}); | |
// Typing indicator | |
messageInput.addEventListener('input', () => { | |
const now = new Date().getTime(); | |
if (now - lastTypingTime > 2000) { | |
socket.send({ | |
type: 'typing', | |
isTyping: true | |
}); | |
lastTypingTime = now; | |
} | |
// Reset typing indicator after 3 seconds of inactivity | |
clearTimeout(typingTimeout); | |
typingTimeout = setTimeout(() => { | |
socket.send({ | |
type: 'typing', | |
isTyping: false | |
}); | |
}, 3000); | |
}); | |
function sendMessage() { | |
const text = messageInput.value.trim(); | |
if (text) { | |
// Send message to server | |
socket.send({ | |
type: 'message', | |
text: text | |
}); | |
// Clear input | |
messageInput.value = ''; | |
// Notify server that user stopped typing | |
socket.send({ | |
type: 'typing', | |
isTyping: false | |
}); | |
} | |
} | |
function handleNewMessage(message) { | |
const isCurrentUser = message.username === username; | |
const messageElement = document.createElement('div'); | |
messageElement.classList.add('flex', 'animate-slide-in'); | |
if (isCurrentUser) { | |
messageElement.classList.add('justify-end'); | |
messageElement.innerHTML = ` | |
<div class="max-w-xs md:max-w-md lg:max-w-lg bg-indigo-100 rounded-lg p-3"> | |
<div class="flex justify-between items-baseline mb-1"> | |
<span class="text-xs font-semibold text-indigo-800">You</span> | |
<span class="text-xs text-gray-500 ml-2">${formatTime(message.timestamp)}</span> | |
</div> | |
<p class="text-gray-800">${escapeHtml(message.text)}</p> | |
</div> | |
`; | |
} else { | |
messageElement.classList.add('justify-start'); | |
messageElement.innerHTML = ` | |
<div class="max-w-xs md:max-w-md lg:max-w-lg bg-gray-100 rounded-lg p-3"> | |
<div class="flex justify-between items-baseline mb-1"> | |
<span class="text-xs font-semibold text-gray-800">${escapeHtml(message.username)}</span> | |
<span class="text-xs text-gray-500 ml-2">${formatTime(message.timestamp)}</span> | |
</div> | |
<p class="text-gray-800">${escapeHtml(message.text)}</p> | |
</div> | |
`; | |
} | |
messagesContainer.appendChild(messageElement); | |
messagesContainer.scrollTop = messagesContainer.scrollHeight; | |
} | |
function handleTyping(data) { | |
if (data.username === username) return; | |
if (data.isTyping) { | |
usersTyping.add(data.username); | |
} else { | |
usersTyping.delete(data.username); | |
} | |
updateTypingIndicator(); | |
} | |
function updateTypingIndicator() { | |
if (usersTyping.size > 0) { | |
const users = Array.from(usersTyping); | |
let text; | |
if (users.length === 1) { | |
text = users[0]; | |
} else if (users.length === 2) { | |
text = `${users[0]} and ${users[1]}`; | |
} else { | |
text = `${users[0]}, ${users[1]}, and ${users.length - 2} others`; | |
} | |
typingUsers.textContent = text; | |
typingIndicator.classList.remove('hidden'); | |
} else { | |
typingIndicator.classList.add('hidden'); | |
} | |
} | |
function handleUserCount(count) { | |
onlineUsers = count; | |
onlineCount.textContent = `${count} online`; | |
} | |
// Helper functions | |
function formatTime(timestamp) { | |
const date = new Date(timestamp); | |
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); | |
} | |
function escapeHtml(text) { | |
const div = document.createElement('div'); | |
div.textContent = text; | |
return div.innerHTML; | |
} | |
// Simulate receiving messages from other users | |
setInterval(() => { | |
if (username && Math.random() > 0.9) { | |
const sampleMessages = [ | |
"Hello there!", | |
"How are you doing?", | |
"Anyone here?", | |
"This chat is awesome!", | |
"What's new?", | |
"Just testing the chat", | |
"Have a great day!", | |
"Typing... just kidding :)" | |
]; | |
const randomUser = ["Alice", "Bob", "Charlie", "Dana", "Eve"][Math.floor(Math.random() * 5)]; | |
socket.onMessage({ | |
type: 'message', | |
username: randomUser, | |
text: sampleMessages[Math.floor(Math.random() * sampleMessages.length)], | |
timestamp: new Date().getTime() | |
}); | |
} | |
}, 10000); | |
</script> | |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Junaed59/private-messenger" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |