fixed some stuff mainly chat interface and made some stuff work
This commit is contained in:
28
dockerfile
Normal file
28
dockerfile
Normal file
@@ -0,0 +1,28 @@
|
||||
# Use an official lightweight Python image
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
# Create a working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies (if needed)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Python dependencies
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy the application code
|
||||
COPY . .
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3248
|
||||
|
||||
# Run with Gunicorn WSGI server
|
||||
# Assumes your Flask app is named "app" inside "app.py"
|
||||
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:3248", "app:app"]
|
||||
BIN
src/static/favicon.png
Normal file
BIN
src/static/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
@@ -0,0 +1,925 @@
|
||||
// Content Security Policy enforcement
|
||||
if (!window.crypto || !window.crypto.subtle) {
|
||||
alert('This browser does not support required cryptographic features. Please use a modern browser.');
|
||||
throw new Error('WebCrypto API not available');
|
||||
}
|
||||
|
||||
// Global variables
|
||||
let socket = null;
|
||||
let currentRoom = null;
|
||||
let currentUserId = null;
|
||||
let currentDisplayName = null;
|
||||
let keyPair = null;
|
||||
let sessionKey = null;
|
||||
let roomUsers = {};
|
||||
let keysReady = false;
|
||||
let lastMessageTime = 0;
|
||||
let messageQueue = [];
|
||||
let isConnected = false;
|
||||
let reconnectAttempts = 0;
|
||||
const MAX_RECONNECT_ATTEMPTS = 5;
|
||||
const MESSAGE_RATE_LIMIT = 1000;
|
||||
const MAX_MESSAGE_LENGTH = 4000;
|
||||
const MAX_ROOM_ID_LENGTH = 32;
|
||||
const MAX_MESSAGES = 256;
|
||||
|
||||
// Security utilities
|
||||
const SecurityUtils = {
|
||||
sanitizeInput: function(input) {
|
||||
if (typeof input !== 'string') return '';
|
||||
return input.trim().substring(0, MAX_MESSAGE_LENGTH);
|
||||
},
|
||||
isValidRoomId: function(roomId) {
|
||||
if (!roomId || typeof roomId !== 'string') return false;
|
||||
if (roomId.length > MAX_ROOM_ID_LENGTH) return false;
|
||||
return /^[a-zA-Z0-9\-_]+$/.test(roomId);
|
||||
},
|
||||
canSendMessage: function() {
|
||||
const now = Date.now();
|
||||
if (now - lastMessageTime < MESSAGE_RATE_LIMIT) {
|
||||
return false;
|
||||
}
|
||||
lastMessageTime = now;
|
||||
return true;
|
||||
},
|
||||
generateSecureId: function(length = 8) {
|
||||
const array = new Uint8Array(length);
|
||||
crypto.getRandomValues(array);
|
||||
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('').toUpperCase().substring(0, length);
|
||||
},
|
||||
isValidBase64: function(str) {
|
||||
try {
|
||||
return btoa(atob(str)) === str;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class SecureChatError extends Error {
|
||||
constructor(message, code = 'UNKNOWN') {
|
||||
super(message);
|
||||
this.name = 'SecureChatError';
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the app
|
||||
async function initializeApp() {
|
||||
try {
|
||||
console.log("Initializing bytechat");
|
||||
|
||||
// Initialize socket connection
|
||||
socket = io({
|
||||
transports: ['websocket', 'polling'],
|
||||
reconnection: true,
|
||||
reconnectionAttempts: MAX_RECONNECT_ATTEMPTS,
|
||||
reconnectionDelay: 1000,
|
||||
reconnectionDelayMax: 5000,
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
await generateKeyPair();
|
||||
setupSocketListeners();
|
||||
setupSecurityFeatures();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize app:', error);
|
||||
updateStatus('Failed to initialize application', true);
|
||||
}
|
||||
}
|
||||
|
||||
function setupSecurityFeatures() {
|
||||
document.addEventListener('visibilitychange', function() {
|
||||
if (document.hidden) {
|
||||
const messageInput = document.getElementById('messageInput');
|
||||
if (messageInput) {
|
||||
messageInput.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('beforeunload', function() {
|
||||
clearSensitiveData();
|
||||
});
|
||||
|
||||
const securityIndicator = document.getElementById('securityIndicator');
|
||||
securityIndicator.style.display = 'block';
|
||||
}
|
||||
|
||||
function clearSensitiveData() {
|
||||
sessionKey = null;
|
||||
keyPair = null;
|
||||
roomUsers = {};
|
||||
|
||||
const passwordInput = document.getElementById('roomPasswordInput');
|
||||
if (passwordInput) {
|
||||
passwordInput.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
async function generateKeyPair() {
|
||||
try {
|
||||
updateStatus('Generating encryption keys...', false);
|
||||
|
||||
keyPair = await window.crypto.subtle.generateKey(
|
||||
{
|
||||
name: "RSA-OAEP",
|
||||
modulusLength: 2048, // Reduced from 4096 for better performance
|
||||
publicExponent: new Uint8Array([1, 0, 1]),
|
||||
hash: "SHA-256"
|
||||
},
|
||||
false,
|
||||
["encrypt", "decrypt"]
|
||||
);
|
||||
|
||||
keysReady = true;
|
||||
updateStatus("Ready to join a room", false);
|
||||
console.log("Key pair generated successfully");
|
||||
|
||||
} catch (error) {
|
||||
console.error("Failed to generate key pair:", error);
|
||||
updateStatus("Error: Failed to generate encryption keys", true);
|
||||
throw new SecureChatError("Key generation failed", "KEY_GEN_ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
async function generateSessionKey() {
|
||||
try {
|
||||
sessionKey = await window.crypto.subtle.generateKey(
|
||||
{ name: "AES-GCM", length: 256 },
|
||||
true,
|
||||
["encrypt", "decrypt"]
|
||||
);
|
||||
console.log("Session key generated");
|
||||
return sessionKey;
|
||||
} catch (error) {
|
||||
console.error("Failed to generate session key:", error);
|
||||
throw new SecureChatError("Session key generation failed", "SESSION_KEY_ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
async function exportPublicKey(key) {
|
||||
try {
|
||||
const exported = await window.crypto.subtle.exportKey("spki", key);
|
||||
const base64Key = btoa(String.fromCharCode(...new Uint8Array(exported)));
|
||||
|
||||
if (!SecurityUtils.isValidBase64(base64Key)) {
|
||||
throw new Error('Invalid key export');
|
||||
}
|
||||
|
||||
return base64Key;
|
||||
} catch (error) {
|
||||
console.error("Failed to export public key:", error);
|
||||
throw new SecureChatError("Public key export failed", "KEY_EXPORT_ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
async function importPublicKey(keyString) {
|
||||
try {
|
||||
if (!SecurityUtils.isValidBase64(keyString)) {
|
||||
throw new Error('Invalid base64 key format');
|
||||
}
|
||||
|
||||
const keyData = Uint8Array.from(atob(keyString), c => c.charCodeAt(0));
|
||||
|
||||
return await window.crypto.subtle.importKey(
|
||||
"spki",
|
||||
keyData,
|
||||
{ name: "RSA-OAEP", hash: "SHA-256" },
|
||||
false,
|
||||
["encrypt"]
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to import public key:", error);
|
||||
throw new SecureChatError("Public key import failed", "KEY_IMPORT_ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
async function encryptSessionKey(sessionKey, publicKey) {
|
||||
try {
|
||||
const keyData = await window.crypto.subtle.exportKey("raw", sessionKey);
|
||||
const encrypted = await window.crypto.subtle.encrypt(
|
||||
{ name: "RSA-OAEP" },
|
||||
publicKey,
|
||||
keyData
|
||||
);
|
||||
return btoa(String.fromCharCode(...new Uint8Array(encrypted)));
|
||||
} catch (error) {
|
||||
console.error("Failed to encrypt session key:", error);
|
||||
throw new SecureChatError("Session key encryption failed", "SESSION_KEY_ENCRYPT_ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
async function decryptSessionKey(encryptedKey) {
|
||||
try {
|
||||
if (!SecurityUtils.isValidBase64(encryptedKey)) {
|
||||
throw new Error('Invalid encrypted key format');
|
||||
}
|
||||
|
||||
const keyData = Uint8Array.from(atob(encryptedKey), c => c.charCodeAt(0));
|
||||
const decrypted = await window.crypto.subtle.decrypt(
|
||||
{ name: "RSA-OAEP" },
|
||||
keyPair.privateKey,
|
||||
keyData
|
||||
);
|
||||
|
||||
return await window.crypto.subtle.importKey(
|
||||
"raw",
|
||||
decrypted,
|
||||
{ name: "AES-GCM" },
|
||||
false,
|
||||
["encrypt", "decrypt"]
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to decrypt session key:", error);
|
||||
throw new SecureChatError("Session key decryption failed", "SESSION_KEY_DECRYPT_ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
async function encryptMessage(message) {
|
||||
if (!sessionKey) {
|
||||
throw new SecureChatError("No session key available", "NO_SESSION_KEY");
|
||||
}
|
||||
|
||||
try {
|
||||
const sanitizedMessage = SecurityUtils.sanitizeInput(message);
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(sanitizedMessage);
|
||||
const iv = window.crypto.getRandomValues(new Uint8Array(12));
|
||||
|
||||
const encrypted = await window.crypto.subtle.encrypt(
|
||||
{ name: "AES-GCM", iv: iv },
|
||||
sessionKey,
|
||||
data
|
||||
);
|
||||
|
||||
return {
|
||||
encrypted: btoa(String.fromCharCode(...new Uint8Array(encrypted))),
|
||||
iv: btoa(String.fromCharCode(...iv))
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Failed to encrypt message:", error);
|
||||
throw new SecureChatError("Message encryption failed", "MESSAGE_ENCRYPT_ERROR");
|
||||
}
|
||||
}
|
||||
|
||||
async function decryptMessage(encryptedData, ivString) {
|
||||
if (!sessionKey) {
|
||||
return "[Encrypted message - no session key]";
|
||||
}
|
||||
|
||||
try {
|
||||
if (!SecurityUtils.isValidBase64(encryptedData) || !SecurityUtils.isValidBase64(ivString)) {
|
||||
return "[Invalid encrypted message format]";
|
||||
}
|
||||
|
||||
const encrypted = Uint8Array.from(atob(encryptedData), c => c.charCodeAt(0));
|
||||
const iv = Uint8Array.from(atob(ivString), c => c.charCodeAt(0));
|
||||
|
||||
const decrypted = await window.crypto.subtle.decrypt(
|
||||
{ name: "AES-GCM", iv: iv },
|
||||
sessionKey,
|
||||
encrypted
|
||||
);
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
const decryptedMessage = decoder.decode(decrypted);
|
||||
|
||||
return SecurityUtils.sanitizeInput(decryptedMessage);
|
||||
} catch (error) {
|
||||
console.error("Failed to decrypt message:", error);
|
||||
return "[Failed to decrypt message]";
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced socket listeners with fixed key exchange
|
||||
function setupSocketListeners() {
|
||||
socket.on('connect', () => {
|
||||
isConnected = true;
|
||||
reconnectAttempts = 0;
|
||||
console.log("Socket connected securely");
|
||||
updateConnectionStatus(true);
|
||||
updateInputState();
|
||||
|
||||
// If user is already in a room, re-emit join_room after reconnect
|
||||
if (currentRoom && keyPair && keysReady) {
|
||||
exportPublicKey(keyPair.publicKey).then((publicKeyString) => {
|
||||
socket.emit('join_room', {
|
||||
room_id: currentRoom,
|
||||
public_key: publicKeyString,
|
||||
password: ''
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('disconnect', (reason) => {
|
||||
isConnected = false;
|
||||
updateStatus("Connection lost. Attempting to reconnect...", true);
|
||||
updateConnectionStatus(false);
|
||||
updateInputState();
|
||||
console.log('Socket disconnected:', reason);
|
||||
|
||||
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
|
||||
reconnectAttempts++;
|
||||
setTimeout(() => {
|
||||
if (!isConnected && reconnectAttempts <= MAX_RECONNECT_ATTEMPTS) {
|
||||
socket.connect();
|
||||
}
|
||||
}, 2000 * Math.pow(2, reconnectAttempts));
|
||||
} else {
|
||||
updateStatus("Connection failed. Please refresh the page.", true);
|
||||
updateInputState();
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('connect_error', (error) => {
|
||||
console.error('Connection error:', error);
|
||||
updateStatus("Connection error. Please check your internet connection.", true);
|
||||
});
|
||||
|
||||
socket.on('user_connected', (data) => {
|
||||
if (data && data.user_id) {
|
||||
currentUserId = data.user_id;
|
||||
console.log("Connected as user:", currentUserId);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('room_joined', async (data) => {
|
||||
try {
|
||||
if (data.error) {
|
||||
updateStatus(data.error, true);
|
||||
updateInputState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.room_id || !data.user_id || !SecurityUtils.isValidRoomId(data.room_id)) {
|
||||
throw new SecureChatError("Invalid room data received", "INVALID_ROOM_DATA");
|
||||
}
|
||||
|
||||
currentRoom = data.room_id;
|
||||
currentUserId = data.user_id;
|
||||
currentDisplayName = data.display_name;
|
||||
roomUsers = data.user_keys || {};
|
||||
|
||||
// Switch to chat screen
|
||||
document.getElementById('welcomeScreen').style.display = 'none';
|
||||
document.getElementById('chatScreen').style.display = 'flex';
|
||||
document.getElementById('roomInfo').style.display = 'flex';
|
||||
document.getElementById('inputSection').style.display = 'block';
|
||||
|
||||
// Update room info
|
||||
document.getElementById('currentRoomId').textContent = currentRoom;
|
||||
document.getElementById('userCounter').textContent = `Users: ${data.users?.length || 0}`;
|
||||
document.getElementById('messageCounter').textContent = `Messages: ${data.message_count || 0}/256`;
|
||||
|
||||
// Handle session key based on user status
|
||||
if (data.is_first_user) {
|
||||
console.log("First user - generating session key");
|
||||
await generateSessionKey();
|
||||
updateStatus("You're the first user! Session key generated. Others will receive it when they join.", false);
|
||||
updateInputState();
|
||||
} else if (sessionKey) {
|
||||
console.log("Rejoining with existing session key");
|
||||
updateStatus("Session key present. You can chat.", false);
|
||||
updateInputState();
|
||||
} else {
|
||||
console.log("Waiting for session key from existing users");
|
||||
updateStatus("Waiting for session key from other users...", false);
|
||||
updateInputState();
|
||||
}
|
||||
|
||||
// Load existing messages
|
||||
if (data.messages && Array.isArray(data.messages)) {
|
||||
for (const msg of data.messages) {
|
||||
if (msg && typeof msg === 'object') {
|
||||
await displayMessage(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("[room_joined] sessionKey:", !!sessionKey, "isFirstUser:", data.is_first_user);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error processing room_joined:", error);
|
||||
updateStatus("Error joining room", true);
|
||||
updateInputState();
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('user_joined', async (data) => {
|
||||
try {
|
||||
if (!data || !data.user_id || !data.public_key) return;
|
||||
|
||||
roomUsers[data.user_id] = data.public_key;
|
||||
updateUsersCount();
|
||||
addSystemMessage(`${data.display_name || 'User'} joined the room`);
|
||||
|
||||
// If we have a session key, share it with the new user
|
||||
if (sessionKey) {
|
||||
console.log("Sharing session key with new user:", data.user_id);
|
||||
try {
|
||||
const publicKey = await importPublicKey(data.public_key);
|
||||
const encryptedKey = await encryptSessionKey(sessionKey, publicKey);
|
||||
|
||||
socket.emit('share_session_key', {
|
||||
room_id: currentRoom,
|
||||
target_user_id: data.user_id,
|
||||
encrypted_key: encryptedKey
|
||||
});
|
||||
|
||||
console.log("Session key shared with new user successfully");
|
||||
} catch (error) {
|
||||
console.error("Failed to share session key with new user:", error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error processing user_joined:", error);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('request_session_key', async (data) => {
|
||||
try {
|
||||
console.log("Session key requested for new user:", data.new_user_id);
|
||||
|
||||
if (!sessionKey || !data.new_user_id || !data.public_key) {
|
||||
console.log("Cannot fulfill session key request - missing data");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const publicKey = await importPublicKey(data.public_key);
|
||||
const encryptedKey = await encryptSessionKey(sessionKey, publicKey);
|
||||
|
||||
socket.emit('share_session_key', {
|
||||
room_id: currentRoom,
|
||||
target_user_id: data.new_user_id,
|
||||
encrypted_key: encryptedKey
|
||||
});
|
||||
|
||||
console.log("Session key shared in response to request");
|
||||
} catch (error) {
|
||||
console.error("Failed to share session key on request:", error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error processing request_session_key:", error);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('session_key_received', async (data) => {
|
||||
try {
|
||||
console.log("Session key received from:", data.from_user_id);
|
||||
|
||||
if (!data || !data.encrypted_key) {
|
||||
console.log("Invalid session key data received");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sessionKey) {
|
||||
try {
|
||||
sessionKey = await decryptSessionKey(data.encrypted_key);
|
||||
console.log("Session key decrypted successfully");
|
||||
updateStatus("Session key received! You can now chat securely.", false);
|
||||
updateInputState();
|
||||
addSystemMessage("🔑 Session key received - you can now chat!");
|
||||
} catch (error) {
|
||||
console.error("Failed to decrypt received session key:", error);
|
||||
updateStatus("Failed to decrypt session key", true);
|
||||
}
|
||||
} else {
|
||||
console.log("Session key already exists, ignoring duplicate");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to process session key:", error);
|
||||
updateStatus("Error: Failed to decrypt session key", true);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('new_message', async (data) => {
|
||||
try {
|
||||
if (!data || typeof data !== 'object') return;
|
||||
|
||||
await displayMessage(data);
|
||||
|
||||
if (data.message_count) {
|
||||
document.getElementById('messageCounter').textContent = `Messages: ${data.message_count}/256`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error processing new message:", error);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('user_left', (data) => {
|
||||
try {
|
||||
if (data && data.user_id) {
|
||||
delete roomUsers[data.user_id];
|
||||
updateUsersCount();
|
||||
addSystemMessage(`User left the room`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error processing user_left:", error);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', (error) => {
|
||||
console.error('Socket error:', error);
|
||||
updateStatus("Connection error occurred", true);
|
||||
});
|
||||
|
||||
socket.on('ping', () => {
|
||||
socket.emit('pong');
|
||||
});
|
||||
}
|
||||
|
||||
async function createRoom() {
|
||||
try {
|
||||
const roomId = SecurityUtils.generateSecureId(6);
|
||||
const password = document.getElementById('roomPasswordInput').value;
|
||||
await joinSpecificRoom(roomId, password);
|
||||
} catch (error) {
|
||||
console.error("Failed to create room:", error);
|
||||
updateStatus("Error creating room", true);
|
||||
}
|
||||
}
|
||||
|
||||
async function joinRoom() {
|
||||
try {
|
||||
const roomInput = document.getElementById('roomInput');
|
||||
const roomId = SecurityUtils.sanitizeInput(roomInput.value);
|
||||
const password = document.getElementById('roomPasswordInput').value;
|
||||
|
||||
if (!roomId) {
|
||||
await createRoom();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!SecurityUtils.isValidRoomId(roomId)) {
|
||||
updateStatus("Invalid room ID format. Use only letters, numbers, hyphens and underscores.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
await joinSpecificRoom(roomId, password);
|
||||
} catch (error) {
|
||||
console.error("Failed to join room:", error);
|
||||
updateStatus("Error joining room", true);
|
||||
}
|
||||
}
|
||||
|
||||
async function joinSpecificRoom(roomId, password = "") {
|
||||
if (!keysReady) {
|
||||
updateStatus("Error: Encryption keys not ready", true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!SecurityUtils.isValidRoomId(roomId)) {
|
||||
updateStatus("Invalid room ID format", true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
updateStatus("Joining room...", false);
|
||||
|
||||
const publicKeyString = await exportPublicKey(keyPair.publicKey);
|
||||
|
||||
socket.emit('join_room', {
|
||||
room_id: roomId,
|
||||
public_key: publicKeyString,
|
||||
password: password
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to join room:", error);
|
||||
updateStatus("Error: Failed to join room", true);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMessage() {
|
||||
try {
|
||||
const input = document.getElementById('messageInput');
|
||||
let message = SecurityUtils.sanitizeInput(input.value);
|
||||
|
||||
if (!message || !sessionKey) return;
|
||||
|
||||
if (!SecurityUtils.canSendMessage()) {
|
||||
updateStatus("Please wait before sending another message", true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.length > MAX_MESSAGE_LENGTH) {
|
||||
updateStatus("Message too long", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const encryptedData = await encryptMessage(message);
|
||||
|
||||
socket.emit('send_message', {
|
||||
encrypted_content: encryptedData.encrypted,
|
||||
iv: encryptedData.iv
|
||||
});
|
||||
|
||||
input.value = '';
|
||||
autoResize(input);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Failed to send message:", error);
|
||||
updateStatus("Error: Failed to send message", true);
|
||||
}
|
||||
}
|
||||
|
||||
async function displayMessage(messageData) {
|
||||
try {
|
||||
if (!messageData || typeof messageData !== 'object') return;
|
||||
|
||||
const container = document.getElementById('messagesContainer');
|
||||
const isOwnMessage = messageData.sender_id === currentUserId;
|
||||
|
||||
let displayText;
|
||||
if (messageData.iv && sessionKey && messageData.encrypted_content) {
|
||||
displayText = await decryptMessage(messageData.encrypted_content, messageData.iv);
|
||||
} else {
|
||||
displayText = "[Encrypted message]";
|
||||
}
|
||||
|
||||
const escapeHtml = (text) => {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
};
|
||||
|
||||
const timestamp = messageData.timestamp ?
|
||||
new Date(messageData.timestamp * 1000).toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
}) :
|
||||
new Date().toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
|
||||
const displayName = messageData.display_name || `User-${messageData.sender_id?.substring(0, 8) || 'Unknown'}`;
|
||||
const avatarText = displayName.substring(0, 2).toUpperCase();
|
||||
|
||||
const messageGroup = document.createElement('div');
|
||||
messageGroup.className = 'message-group';
|
||||
messageGroup.classList.add(isOwnMessage ? 'sent' : 'received');
|
||||
|
||||
messageGroup.innerHTML = `
|
||||
<div class="message-header">
|
||||
<div class="user-avatar ${isOwnMessage ? 'own' : ''}">${escapeHtml(avatarText)}</div>
|
||||
<span class="user-name">${escapeHtml(displayName)}</span>
|
||||
<span class="message-time">${escapeHtml(timestamp)}</span>
|
||||
</div>
|
||||
<div class="message-content ${isOwnMessage ? 'own' : ''}" data-message-id="${messageData.id || ''}"></div>
|
||||
`;
|
||||
|
||||
const messageContentEl = messageGroup.querySelector('.message-content');
|
||||
messageContentEl.textContent = displayText;
|
||||
|
||||
container.appendChild(messageGroup);
|
||||
container.scrollTop = container.scrollHeight;
|
||||
|
||||
const messages = container.children;
|
||||
if (messages.length > MAX_MESSAGES + 50) {
|
||||
for (let i = 0; i < 50; i++) {
|
||||
if (messages[0]) {
|
||||
container.removeChild(messages[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error displaying message:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function addSystemMessage(text) {
|
||||
try {
|
||||
const container = document.getElementById('messagesContainer');
|
||||
const systemMessage = document.createElement('div');
|
||||
systemMessage.className = 'message-group system'; // ✅ add system class
|
||||
|
||||
const timestamp = new Date().toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'});
|
||||
|
||||
systemMessage.innerHTML = `
|
||||
<div class="message-header">
|
||||
<div class="user-avatar">ℹ️</div>
|
||||
<span class="user-name">System</span>
|
||||
<span class="message-time">${timestamp}</span>
|
||||
</div>
|
||||
<div class="message-content"></div>
|
||||
`;
|
||||
|
||||
const messageContentEl = systemMessage.querySelector('.message-content');
|
||||
messageContentEl.textContent = text;
|
||||
|
||||
container.appendChild(systemMessage);
|
||||
container.scrollTop = container.scrollHeight;
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error adding system message:", error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function updateStatus(message, isError = false) {
|
||||
try {
|
||||
const statusEl = document.getElementById('statusText');
|
||||
const statusDiv = document.createElement('div');
|
||||
|
||||
if (isError) {
|
||||
statusDiv.className = 'error-message';
|
||||
} else {
|
||||
statusDiv.className = 'success-message';
|
||||
}
|
||||
|
||||
statusDiv.textContent = message;
|
||||
statusEl.innerHTML = '';
|
||||
statusEl.appendChild(statusDiv);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error updating status:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function updateUsersCount() {
|
||||
try {
|
||||
const count = Object.keys(roomUsers).length;
|
||||
const counterEl = document.getElementById('userCounter');
|
||||
if (counterEl) {
|
||||
counterEl.textContent = `Users: ${count}`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating users count:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function updateInputState() {
|
||||
try {
|
||||
const input = document.getElementById('messageInput');
|
||||
const btn = document.getElementById('sendButton');
|
||||
|
||||
if (!input || !btn) return;
|
||||
|
||||
if (sessionKey && isConnected) {
|
||||
input.disabled = false;
|
||||
btn.disabled = false;
|
||||
input.placeholder = "Message ByteChat…";
|
||||
} else {
|
||||
input.disabled = true;
|
||||
btn.disabled = true;
|
||||
input.placeholder = isConnected ? "Waiting for encryption key..." : "Disconnected...";
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating input state:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function autoResize(textarea) {
|
||||
try {
|
||||
if (!textarea) return;
|
||||
|
||||
textarea.style.height = 'auto';
|
||||
const newHeight = Math.min(Math.max(textarea.scrollHeight, 44), 200);
|
||||
textarea.style.height = newHeight + 'px';
|
||||
} catch (error) {
|
||||
console.error("Error in autoResize:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function updateConnectionStatus(connected) {
|
||||
const encryptionBadge = document.getElementById('encryptionBadge');
|
||||
const securityIndicator = document.getElementById('securityIndicator');
|
||||
|
||||
if (connected) {
|
||||
if (encryptionBadge) encryptionBadge.textContent = 'E2E Encrypted';
|
||||
if (securityIndicator) securityIndicator.textContent = '🔒 End-to-End Encrypted';
|
||||
} else {
|
||||
if (encryptionBadge) encryptionBadge.textContent = 'Disconnected';
|
||||
if (securityIndicator) securityIndicator.textContent = '⚠️ Disconnected';
|
||||
}
|
||||
}
|
||||
|
||||
// Event listeners setup
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
try {
|
||||
const messageInput = document.getElementById('messageInput');
|
||||
const roomInput = document.getElementById('roomInput');
|
||||
const roomPasswordInput = document.getElementById('roomPasswordInput');
|
||||
|
||||
if (messageInput) {
|
||||
messageInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
|
||||
messageInput.addEventListener('input', (e) => {
|
||||
autoResize(e.target);
|
||||
|
||||
if (e.target.value.length > MAX_MESSAGE_LENGTH) {
|
||||
e.target.value = e.target.value.substring(0, MAX_MESSAGE_LENGTH);
|
||||
updateStatus(`Message truncated to ${MAX_MESSAGE_LENGTH} characters`, true);
|
||||
}
|
||||
});
|
||||
|
||||
messageInput.addEventListener('paste', (e) => {
|
||||
setTimeout(() => {
|
||||
if (messageInput.value.length > MAX_MESSAGE_LENGTH) {
|
||||
messageInput.value = messageInput.value.substring(0, MAX_MESSAGE_LENGTH);
|
||||
updateStatus(`Pasted content truncated to ${MAX_MESSAGE_LENGTH} characters`, true);
|
||||
}
|
||||
autoResize(messageInput);
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
|
||||
if (roomInput) {
|
||||
roomInput.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
joinRoom();
|
||||
}
|
||||
});
|
||||
|
||||
roomInput.addEventListener('input', (e) => {
|
||||
const value = e.target.value;
|
||||
if (value && !SecurityUtils.isValidRoomId(value)) {
|
||||
e.target.setCustomValidity('Only letters, numbers, hyphens and underscores allowed');
|
||||
} else {
|
||||
e.target.setCustomValidity('');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (roomPasswordInput) {
|
||||
roomPasswordInput.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
joinRoom();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const joinRoomBtn = document.getElementById('joinRoomBtn');
|
||||
if (joinRoomBtn) joinRoomBtn.addEventListener('click', joinRoom);
|
||||
|
||||
const createRoomBtn = document.getElementById('createRoomBtn');
|
||||
if (createRoomBtn) createRoomBtn.addEventListener('click', createRoom);
|
||||
|
||||
const sendButton = document.getElementById('sendButton');
|
||||
if (sendButton) sendButton.addEventListener('click', sendMessage);
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter' && messageInput && !messageInput.disabled) {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
|
||||
if (e.key === 'Escape' && messageInput) {
|
||||
messageInput.value = '';
|
||||
autoResize(messageInput);
|
||||
}
|
||||
});
|
||||
|
||||
initializeApp();
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error setting up event listeners:", error);
|
||||
updateStatus("Error initializing interface", true);
|
||||
}
|
||||
});
|
||||
|
||||
// Periodic connection health check
|
||||
setInterval(() => {
|
||||
if (socket && isConnected) {
|
||||
socket.volatile.emit('ping');
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
// Prevent clickjacking
|
||||
if (top !== self) {
|
||||
top.location = self.location;
|
||||
}
|
||||
|
||||
// CSP violation reporting
|
||||
document.addEventListener('securitypolicyviolation', (e) => {
|
||||
console.error('CSP Violation:', {
|
||||
violatedDirective: e.violatedDirective,
|
||||
blockedURI: e.blockedURI,
|
||||
lineNumber: e.lineNumber,
|
||||
columnNumber: e.columnNumber
|
||||
});
|
||||
});
|
||||
|
||||
// Expose necessary functions
|
||||
window.SecureChat = {
|
||||
joinRoom,
|
||||
createRoom,
|
||||
sendMessage
|
||||
};
|
||||
|
||||
// Prevent console access in production
|
||||
if (location.hostname !== 'localhost' && location.hostname !== '127.0.0.1') {
|
||||
console.log = console.warn = console.error = () => {};
|
||||
}
|
||||
@@ -1,437 +0,0 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||
background: #0f0f0f;
|
||||
color: #ececec;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
background: #212121;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
background: #0f0f0f;
|
||||
border-bottom: 1px solid #333;
|
||||
padding: 16px 20px;
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.encryption-badge {
|
||||
background: #10a37f;
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.room-section {
|
||||
background: #2d2d2d;
|
||||
padding: 20px;
|
||||
margin: 20px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #404040;
|
||||
}
|
||||
|
||||
.room-input-container {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.room-input {
|
||||
flex: 1;
|
||||
background: #0f0f0f;
|
||||
border: 1px solid #404040;
|
||||
color: #ececec;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.room-input:focus {
|
||||
border-color: #10a37f;
|
||||
box-shadow: 0 0 0 2px rgba(16, 163, 127, 0.1);
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: #10a37f;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #0d8c6a;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #2d2d2d;
|
||||
border: 1px solid #404040;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #3d3d3d;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
color: #b4b4b4;
|
||||
font-size: 14px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.room-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
background: #2d2d2d;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
color: #b4b4b4;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.room-details {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.messages-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
background: #212121;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.messages-container::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.messages-container::-webkit-scrollbar-track {
|
||||
background: #2d2d2d;
|
||||
}
|
||||
|
||||
.messages-container::-webkit-scrollbar-thumb {
|
||||
background: #404040;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
|
||||
.message-group {
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.message-group.sent {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.message-group.received {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.message-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(45deg, #10a37f, #0d8c6a);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.user-avatar.own {
|
||||
background: linear-gradient(45deg, #6366f1, #8b5cf6);
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: #ececec;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
|
||||
.message-content {
|
||||
background: #2d2d2d;
|
||||
padding: 12px 16px;
|
||||
border-radius: 12px;
|
||||
margin-left: 36px;
|
||||
border: 1px solid #404040;
|
||||
line-height: 1.5;
|
||||
word-wrap: break-word;
|
||||
max-width: 70%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.message-group.sent .message-content {
|
||||
background: #0f0f0f;
|
||||
border-color: #333;
|
||||
margin-left: 0;
|
||||
margin-right: 36px;
|
||||
text-align: right;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.message-group.received .message-content {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.input-section {
|
||||
background: #0f0f0f;
|
||||
padding: 20px;
|
||||
border-top: 1px solid #333;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.message-input {
|
||||
width: 100%;
|
||||
background: #2d2d2d;
|
||||
border: 1px solid #404040;
|
||||
color: #ececec;
|
||||
padding: 16px 60px 16px 20px;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
outline: none;
|
||||
resize: none;
|
||||
min-height: 24px;
|
||||
max-height: 200px;
|
||||
font-family: inherit;
|
||||
line-height: 1.5;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.message-input:focus {
|
||||
border-color: #10a37f;
|
||||
box-shadow: 0 0 0 2px rgba(16, 163, 127, 0.1);
|
||||
}
|
||||
|
||||
.message-input:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.send-button {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: #10a37f;
|
||||
border: none;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.send-button:hover:not(:disabled) {
|
||||
background: #0d8c6a;
|
||||
}
|
||||
|
||||
.send-button:disabled {
|
||||
background: #404040;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.welcome-screen {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 16px;
|
||||
background: linear-gradient(45deg, #10a37f, #6366f1);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.welcome-subtitle {
|
||||
font-size: 18px;
|
||||
color: #b4b4b4;
|
||||
margin-bottom: 32px;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 32px;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
background: #2d2d2d;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #404040;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
color: #ececec;
|
||||
}
|
||||
|
||||
.feature-desc {
|
||||
color: #b4b4b4;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.loading-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #b4b4b4;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid #404040;
|
||||
border-top: 2px solid #10a37f;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: #dc2626;
|
||||
color: white;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
margin: 16px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.typing-indicator {
|
||||
color: #888;
|
||||
font-style: italic;
|
||||
font-size: 13px;
|
||||
padding: 8px 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.chat-container {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.room-input-container {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.room-input, .btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.room-details {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
margin-left: 0;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
548
src/static/stylesheet.css
Normal file
548
src/static/stylesheet.css
Normal file
@@ -0,0 +1,548 @@
|
||||
/* Reset & base */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
overflow: hidden; /* prevents double scrollbars */
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||
background: linear-gradient(135deg, #0a0a0a 0%, #1a1a1a 100%);
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Main container */
|
||||
.chat-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
max-width: 100vw;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.chat-header {
|
||||
background: rgba(20, 20, 20, 0.95);
|
||||
border-bottom: 1px solid #333;
|
||||
padding: 1rem 1.5rem;
|
||||
backdrop-filter: blur(10px);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #00ff88;
|
||||
}
|
||||
|
||||
.encryption-badge {
|
||||
background: linear-gradient(45deg, #00ff88, #00cc6a);
|
||||
color: #000;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 20px;
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* Welcome screen */
|
||||
.welcome-screen {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 1rem;
|
||||
background: linear-gradient(45deg, #00ff88, #ffffff);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.welcome-subtitle {
|
||||
font-size: 1.1rem;
|
||||
color: #aaa;
|
||||
margin-bottom: 3rem;
|
||||
max-width: 600px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.room-section {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.room-input-container {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.room-input {
|
||||
padding: 0.75rem 1rem;
|
||||
border: 2px solid #333;
|
||||
border-radius: 8px;
|
||||
background: #1a1a1a;
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.room-input:focus {
|
||||
outline: none;
|
||||
border-color: #00ff88;
|
||||
box-shadow: 0 0 0 3px rgba(0, 255, 136, 0.1);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(45deg, #00ff88, #00cc6a);
|
||||
color: #000;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 255, 136, 0.3);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: linear-gradient(45deg, #333, #555);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Status text */
|
||||
.status-text {
|
||||
color: #aaa;
|
||||
margin-top: 1rem;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.loading-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid #333;
|
||||
border-top: 2px solid #00ff88;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #ff4444;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
color: #00ff88;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Features */
|
||||
.feature-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1.5rem;
|
||||
max-width: 1000px;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid #333;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.feature-desc {
|
||||
color: #aaa;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Room info */
|
||||
.room-info {
|
||||
background: rgba(20, 20, 20, 0.95);
|
||||
border-bottom: 1px solid #333;
|
||||
padding: 0.75rem 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.room-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
align-items: flex-start;
|
||||
flex: 0 0 auto;
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.room-details span {
|
||||
color: #aaa;
|
||||
font-size: 0.85rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.room-details strong {
|
||||
color: #00ff88;
|
||||
}
|
||||
|
||||
.room-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Chat screen & messages */
|
||||
.chat-screen {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.messages-wrapper {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
scroll-behavior: smooth;
|
||||
padding-bottom: 80px; /* leave space for input */
|
||||
}
|
||||
|
||||
.message-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* Align left vs right */
|
||||
.message-group.sent {
|
||||
align-items: flex-end;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.message-group.received {
|
||||
align-items: flex-start;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.message-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 0.8rem;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.message-group.sent .message-header {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
/* System messages */
|
||||
.message-group.system {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0.5rem auto;
|
||||
text-align: center;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.message-group.system .message-content {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid #444;
|
||||
color: #ccc;
|
||||
font-size: 0.85rem;
|
||||
font-style: italic;
|
||||
border-radius: 12px;
|
||||
padding: 0.35rem 0.75rem;
|
||||
width: fit-content; /* 👈 shrink to text */
|
||||
max-width: 90%; /* still wrap if very long */
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(45deg, #00ff88, #00cc6a);
|
||||
color: #000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.user-avatar.own {
|
||||
background: linear-gradient(45deg, #0088ff, #0066cc);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* The bubble */
|
||||
.message-content {
|
||||
display: inline-block;
|
||||
background: #2d2d2d;
|
||||
border: 1px solid #404040;
|
||||
border-radius: 12px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
word-wrap: break-word;
|
||||
max-width: 75%; /* prevents overly long messages */
|
||||
width: auto; /* shrink to fit content */
|
||||
position: relative;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.message-group.sent .message-content {
|
||||
background: #1a4d3a;
|
||||
border-color: #00ff88;
|
||||
text-align: left; /* keep text natural */
|
||||
}
|
||||
|
||||
/* Input section */
|
||||
.input-section {
|
||||
padding: 0.75rem 1rem;
|
||||
background: rgba(20, 20, 20, 0.95);
|
||||
border-top: 1px solid #333;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.message-input {
|
||||
flex: 1;
|
||||
border: 2px solid #333;
|
||||
border-radius: 12px;
|
||||
background: #1a1a1a;
|
||||
color: #fff;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 1rem;
|
||||
font-family: inherit;
|
||||
resize: none;
|
||||
max-height: 120px;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.message-input:focus {
|
||||
outline: none;
|
||||
border-color: #00ff88;
|
||||
box-shadow: 0 0 0 3px rgba(0, 255, 136, 0.1);
|
||||
}
|
||||
|
||||
.message-input:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.send-button {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(45deg, #00ff88, #00cc6a);
|
||||
color: #000;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.send-button:hover:not(:disabled) {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 4px 15px rgba(0, 255, 136, 0.3);
|
||||
}
|
||||
|
||||
.send-button:disabled {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Security indicator */
|
||||
.security-indicator {
|
||||
position: fixed;
|
||||
bottom: calc(1rem + 70px);
|
||||
right: 20px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: #00ff88;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
border: 1px solid #00ff88;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.welcome-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.room-input-container {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.room-input {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.room-info {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.room-details {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.room-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
max-width: 85%;
|
||||
}
|
||||
|
||||
.input-section {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.room-details span {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.6rem 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
max-width: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Accessibility */
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.btn:focus,
|
||||
.room-input:focus,
|
||||
.message-input:focus,
|
||||
.send-button:focus {
|
||||
outline: 2px solid #00ff88;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user