Files
msgs/web/index.html
T

2167 lines
73 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>
:root {
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--bg-tertiary: #e9ecef;
--text-primary: #2c3e50;
--text-secondary: #7f8c8d;
--border-color: #e9ecef;
--primary-color: #3498db;
--primary-hover: #2980b9;
--success-color: #27ae60;
--error-color: #e74c3c;
--warning-color: #f39c12;
--shadow: rgba(0, 0, 0, 0.1);
}
[data-theme="dark"] {
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--bg-tertiary: #3d3d3d;
--text-primary: #ffffff;
--text-secondary: #b0b0b0;
--border-color: #444444;
--primary-color: #2980b9;
--primary-hover: #3498db;
--success-color: #2ecc71;
--error-color: #e74c3c;
--warning-color: #f39c12;
--shadow: rgba(0, 0, 0, 0.3);
}
* {
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;
overflow: hidden;
transition: background 0.3s ease;
}
[data-theme="dark"] body {
background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);
}
/* Auth Container */
.auth-container {
width: 100%;
max-width: 400px;
margin: 0 auto;
background: var(--bg-primary);
padding: 30px 20px;
border-radius: 0;
box-shadow: 0 0 0 rgba(0, 0, 0, 0);
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
transition: all 0.3s ease;
}
.auth-tabs {
display: flex;
margin-bottom: 25px;
border-bottom: 1px solid var(--border-color);
}
.auth-tab {
flex: 1;
padding: 15px 10px;
text-align: center;
cursor: pointer;
border-bottom: 3px solid transparent;
transition: all 0.3s ease;
font-size: 14px;
color: var(--text-primary);
}
.auth-tab.active {
border-bottom-color: var(--primary-color);
color: var(--primary-color);
font-weight: 600;
}
.auth-form {
display: none;
}
.auth-form.active {
display: block;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 6px;
font-weight: 500;
color: var(--text-primary);
font-size: 14px;
}
.form-control {
width: 100%;
padding: 12px 15px;
border: 2px solid var(--border-color);
border-radius: 8px;
font-size: 16px;
outline: none;
transition: border-color 0.3s ease;
background: var(--bg-primary);
color: var(--text-primary);
}
.form-control:focus {
border-color: var(--primary-color);
}
.btn {
width: 100%;
padding: 12px;
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: white;
border: none;
border-radius: 8px;
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: 10px 12px;
border-radius: 6px;
margin-bottom: 15px;
display: none;
font-size: 14px;
}
.alert-success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
/* Chat Container */
.chat-container {
width: 100%;
height: 100vh;
background: var(--bg-primary);
display: none;
flex-direction: column;
transition: background 0.3s ease;
}
/* Mobile Header */
.mobile-header {
display: none;
padding: 15px;
background: var(--bg-secondary);
color: var(--text-primary);
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--border-color);
position: sticky;
top: 0;
z-index: 100;
}
.menu-btn {
background: none;
border: none;
color: var(--text-primary);
font-size: 20px;
cursor: pointer;
padding: 5px;
}
.mobile-title {
font-weight: 600;
font-size: 18px;
}
.theme-toggle {
background: none;
border: none;
color: var(--text-primary);
font-size: 18px;
cursor: pointer;
padding: 5px;
margin-left: 10px;
}
.chats-panel {
width: 350px;
background: var(--bg-secondary);
color: var(--text-primary);
display: flex;
flex-direction: column;
height: 100vh;
transition: transform 0.3s ease;
border-right: 1px solid var(--border-color);
}
.chats-header {
padding: 20px;
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
}
.chats-tabs {
display: flex;
border-bottom: 1px solid var(--border-color);
}
.chat-tab {
flex: 1;
padding: 15px 10px;
text-align: center;
cursor: pointer;
border-bottom: 3px solid transparent;
transition: all 0.3s ease;
font-size: 14px;
color: var(--text-primary);
}
.chat-tab.active {
border-bottom-color: var(--primary-color);
color: var(--primary-color);
font-weight: 600;
}
.search-container {
padding: 15px;
border-bottom: 1px solid var(--border-color);
display: none;
}
.search-input {
width: 100%;
padding: 10px 12px;
border: 1px solid var(--border-color);
border-radius: 20px;
background: var(--bg-tertiary);
color: var(--text-primary);
outline: none;
font-size: 14px;
}
.search-input::placeholder {
color: var(--text-secondary);
}
.search-results {
max-height: 300px;
overflow-y: auto;
}
.chat-list {
list-style: none;
flex: 1;
overflow-y: auto;
}
.chat-item {
padding: 12px 15px;
border-bottom: 1px solid var(--border-color);
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
}
.chat-item:hover {
background: var(--bg-tertiary);
}
.chat-item.active {
background: var(--primary-color);
color: white;
}
.chat-avatar {
width: 45px;
height: 45px;
border-radius: 50%;
margin-right: 12px;
object-fit: cover;
background: var(--bg-tertiary);
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
font-size: 18px;
flex-shrink: 0;
}
.chat-avatar img {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
}
.chat-info {
flex: 1;
min-width: 0;
}
.chat-username {
font-weight: 600;
margin-bottom: 3px;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.chat-phone {
font-size: 11px;
color: var(--text-secondary);
margin-bottom: 3px;
}
.chat-last-message {
font-size: 12px;
color: var(--text-secondary);
opacity: 0.8;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.chat-meta {
display: flex;
flex-direction: column;
align-items: flex-end;
flex-shrink: 0;
margin-left: 10px;
}
.chat-time {
font-size: 10px;
color: var(--text-secondary);
margin-bottom: 5px;
}
.unread-badge {
background: var(--error-color);
color: white;
border-radius: 50%;
width: 18px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: 600;
}
.chat-panel {
flex: 1;
display: flex;
flex-direction: column;
background: var(--bg-primary);
height: 100vh;
position: relative;
}
.chat-header {
padding: 12px 15px;
background: var(--bg-primary);
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
z-index: 10;
min-height: 64px;
}
.chat-header-info {
display: flex;
align-items: center;
flex: 1;
}
.back-btn {
display: none;
background: none;
border: none;
font-size: 18px;
margin-right: 10px;
cursor: pointer;
color: var(--primary-color);
}
.chat-header-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
margin-right: 10px;
object-fit: cover;
background: var(--bg-tertiary);
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
font-size: 16px;
flex-shrink: 0;
}
.chat-header-avatar img {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
}
.chat-header-text {
display: flex;
flex-direction: column;
min-width: 0;
flex: 1;
}
.chat-header-username {
font-weight: 600;
font-size: 16px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: var(--text-primary);
}
.chat-header-phone {
font-size: 12px;
color: var(--text-secondary);
}
.header-actions {
display: flex;
align-items: center;
gap: 8px;
}
.avatar-upload-btn {
padding: 6px 10px;
background: var(--primary-color);
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 11px;
white-space: nowrap;
}
.avatar-upload-btn:hover {
background: var(--primary-hover);
}
.logout-btn {
padding: 6px 12px;
background: var(--error-color);
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 12px;
}
.logout-btn:hover {
background: #c0392b;
}
.chat-messages {
flex: 1;
padding: 15px;
overflow-y: auto;
background: var(--bg-primary);
display: flex;
flex-direction: column;
min-height: 0;
}
.message {
max-width: 85%;
margin: 8px 0;
padding: 12px 15px;
border-radius: 18px;
position: relative;
word-wrap: break-word;
overflow-wrap: break-word;
word-break: break-word;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.incoming {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
align-self: flex-start;
border-bottom-left-radius: 5px;
box-shadow: 0 2px 5px var(--shadow);
color: var(--text-primary);
}
.outgoing {
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
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: 11px;
font-weight: 600;
margin-bottom: 4px;
opacity: 0.8;
}
.message-content {
margin: 6px 0;
line-height: 1.4;
font-size: 14px;
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
}
.message-time {
font-size: 10px;
text-align: right;
opacity: 0.7;
margin-top: 4px;
}
.chat-input-container {
position: sticky;
bottom: 0;
background: var(--bg-primary);
border-top: 1px solid var(--border-color);
padding: 15px;
z-index: 10;
}
.chat-input-area {
display: flex;
gap: 10px;
align-items: flex-end;
max-height: 120px;
}
.message-input {
flex: 1;
padding: 12px 15px;
border: 2px solid var(--border-color);
border-radius: 25px;
outline: none;
font-size: 14px;
transition: border-color 0.3s ease;
background: var(--bg-primary);
color: var(--text-primary);
resize: none;
min-height: 44px;
max-height: 100px;
font-family: inherit;
line-height: 1.4;
}
.message-input:focus {
border-color: var(--primary-color);
}
.send-button {
padding: 12px 20px;
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
color: white;
border: none;
border-radius: 25px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.3s ease;
flex-shrink: 0;
min-height: 44px;
}
.send-button:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(52, 152, 219, 0.4);
}
.send-button:disabled {
background: var(--bg-tertiary);
color: var(--text-secondary);
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.system-message {
text-align: center;
color: var(--text-secondary);
font-style: italic;
margin: 10px 0;
font-size: 12px;
padding: 8px;
background: var(--bg-secondary);
border-radius: 10px;
align-self: center;
max-width: 80%;
}
.empty-state {
text-align: center;
color: var(--text-secondary);
padding: 30px 15px;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.empty-state h3 {
margin-bottom: 8px;
font-weight: 300;
font-size: 16px;
}
.empty-state p {
font-size: 14px;
}
/* Стили для кнопки удаления сообщения */
.message-actions {
position: absolute;
top: 5px;
right: 8px;
opacity: 0;
transition: opacity 0.3s ease;
}
.message:hover .message-actions {
opacity: 1;
}
.delete-btn {
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
font-size: 14px;
padding: 4px;
border-radius: 3px;
}
.delete-btn:hover {
background: rgba(0, 0, 0, 0.1);
color: var(--error-color);
}
/* Стили для модального окна удаления */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal {
background: var(--bg-primary);
border-radius: 10px;
padding: 20px;
max-width: 350px;
width: 90%;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
margin: 20px;
}
.modal h3 {
margin-bottom: 12px;
color: var(--text-primary);
font-size: 18px;
}
.modal p {
margin-bottom: 18px;
color: var(--text-secondary);
font-size: 14px;
}
.modal-buttons {
display: flex;
gap: 8px;
justify-content: flex-end;
flex-wrap: wrap;
}
.modal-btn {
padding: 8px 16px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
}
.modal-btn-cancel {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.modal-btn-cancel:hover {
background: var(--border-color);
}
.modal-btn-delete {
background: var(--error-color);
color: white;
}
.modal-btn-delete:hover {
background: #c0392b;
}
.modal-btn-delete-for-me {
background: var(--primary-color);
color: white;
}
.modal-btn-delete-for-me:hover {
background: var(--primary-hover);
}
/* Стили для скрытого input file */
.avatar-input {
display: none;
}
/* Scrollbars */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: var(--bg-tertiary);
border-radius: 3px;
}
::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-secondary);
}
/* Desktop Layout */
.desktop-layout {
display: flex;
height: 100vh;
width: 100%;
}
/* Connection status */
.connection-status {
position: fixed;
top: 10px;
right: 10px;
padding: 8px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
z-index: 1000;
transition: all 0.3s ease;
}
.connection-status.connected {
background: var(--success-color);
color: white;
}
.connection-status.disconnected {
background: var(--error-color);
color: white;
}
.connection-status.connecting {
background: var(--warning-color);
color: white;
}
/* Mobile Styles */
@media (max-width: 768px) {
.auth-container {
max-width: 100%;
padding: 20px;
border-radius: 0;
}
.desktop-layout {
position: relative;
height: 100%;
}
.chats-panel {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
z-index: 100;
transform: translateX(-100%);
}
.chats-panel.active {
transform: translateX(0);
}
.chat-panel {
width: 100%;
height: 100vh;
}
.mobile-header {
display: flex;
}
.back-btn {
display: block;
}
.header-actions {
flex-wrap: wrap;
justify-content: flex-end;
}
.avatar-upload-btn,
.logout-btn {
font-size: 12px;
padding: 6px 8px;
}
.message {
max-width: 90%;
margin: 6px 0;
}
.modal {
margin: 15px;
}
.modal-buttons {
flex-direction: column;
}
.modal-btn {
width: 100%;
}
.chat-input-container {
padding: 10px;
padding-bottom: max(10px, env(safe-area-inset-bottom));
}
.chat-input-area {
gap: 8px;
}
.message-input {
padding: 10px 12px;
min-height: 44px;
font-size: 16px;
}
.send-button {
padding: 10px 16px;
min-height: 44px;
font-size: 14px;
}
.connection-status {
top: 60px;
right: 10px;
}
}
@media (max-width: 480px) {
.auth-container {
padding: 15px;
}
.auth-tab {
padding: 12px 8px;
font-size: 13px;
}
.form-control {
padding: 10px 12px;
font-size: 16px;
}
.btn {
padding: 12px;
}
.chat-header {
padding: 10px 12px;
min-height: 60px;
}
.chat-messages {
padding: 10px;
padding-bottom: 70px;
}
.message {
padding: 10px 12px;
max-width: 95%;
margin: 4px 0;
}
.message-content {
font-size: 14px;
}
.chat-input-container {
padding: 8px;
padding-bottom: max(8px, env(safe-area-inset-bottom));
}
.message-input {
padding: 8px 12px;
font-size: 16px;
}
.send-button {
padding: 8px 14px;
font-size: 14px;
}
.mobile-title {
font-size: 16px;
}
.chat-header-username {
font-size: 15px;
}
}
/* Very small screens */
@media (max-width: 320px) {
.chat-avatar {
width: 40px;
height: 40px;
font-size: 16px;
margin-right: 8px;
}
.chat-header-avatar {
width: 35px;
height: 35px;
font-size: 14px;
}
.chat-item {
padding: 10px 12px;
}
.header-actions {
gap: 5px;
}
.avatar-upload-btn,
.logout-btn {
font-size: 11px;
padding: 5px 6px;
}
.chat-input-area {
gap: 6px;
}
.message-input {
padding: 8px 10px;
}
.send-button {
padding: 8px 12px;
min-width: 60px;
}
}
/* Safe area support for notches */
@supports(padding: max(0px)) {
.chat-input-container {
padding-bottom: max(15px, env(safe-area-inset-bottom));
}
@media (max-width: 768px) {
.chat-input-container {
padding-bottom: max(10px, env(safe-area-inset-bottom));
}
}
@media (max-width: 480px) {
.chat-input-container {
padding-bottom: max(8px, env(safe-area-inset-bottom));
}
}
}
/* Prevent body scroll when modal is open */
body.modal-open {
overflow: hidden;
}
/* Theme transition */
* {
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
}
/* Ensure messages are always fully visible */
.chat-messages-container {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
position: relative;
}
</style>
</head>
<body>
<!-- Connection Status -->
<div id="connectionStatus" class="connection-status" style="display: none;"></div>
<!-- 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="chat-container" id="chatContainer">
<!-- Mobile Header -->
<div class="mobile-header">
<button class="menu-btn" onclick="toggleChatsPanel()">☰</button>
<div class="mobile-title" id="mobileChatTitle">Secure Chat</div>
<button class="theme-toggle" onclick="toggleTheme()">🌓</button>
</div>
<div class="desktop-layout">
<!-- Chats Panel -->
<div class="chats-panel" id="chatsPanel">
<div class="chats-header">
<h2>Chats</h2>
<button class="theme-toggle" onclick="toggleTheme()">🌓</button>
</div>
<div class="chats-tabs">
<div class="chat-tab active" onclick="showChatTab('existing')">My Chats</div>
<div class="chat-tab" onclick="showChatTab('search')">New Chat</div>
</div>
<!-- Search Container -->
<div class="search-container" id="searchContainer">
<input type="text" id="searchInput" class="search-input" placeholder="Search by phone or username...">
<div class="search-results" id="searchResults"></div>
</div>
<!-- Existing Chats List -->
<ul class="chat-list" id="chatsList">
<!-- Chats will be populated here -->
</ul>
</div>
<!-- Chat Panel -->
<div class="chat-panel">
<div class="chat-header">
<div class="chat-header-info">
<button class="back-btn" onclick="showChatsPanel()">←</button>
<div class="chat-header-avatar" id="currentUserAvatar">
<!-- Avatar will be populated here -->
</div>
<div class="chat-header-text">
<div class="chat-header-username" id="currentUsername">Select a chat</div>
<div class="chat-header-phone" id="currentUserPhone">Choose someone to message</div>
</div>
</div>
<div class="header-actions">
<input type="file" id="avatarInput" class="avatar-input" accept="image/*">
<button class="avatar-upload-btn" onclick="document.getElementById('avatarInput').click()">Upload Avatar</button>
<button class="logout-btn" onclick="logout()">Logout</button>
</div>
</div>
<div class="chat-messages-container">
<div class="chat-messages" id="chatMessages">
<div class="empty-state">
<h3>Welcome to Secure Chat</h3>
<p>Select a chat to start messaging</p>
</div>
</div>
</div>
<div class="chat-input-container">
<div class="chat-input-area">
<textarea id="messageInput" class="message-input" placeholder="Type a message..." disabled rows="1"></textarea>
<button onclick="sendMessage()" class="send-button" id="sendButton" disabled>Send</button>
</div>
</div>
</div>
</div>
</div>
<!-- Modal for delete confirmation -->
<div id="deleteModal" class="modal-overlay" style="display: none;">
<div class="modal">
<h3>Delete Message</h3>
<p>How would you like to delete this message?</p>
<div class="modal-buttons">
<button class="modal-btn modal-btn-cancel" onclick="closeDeleteModal()">Cancel</button>
<button class="modal-btn modal-btn-delete-for-me" onclick="deleteMessageForMe()">Delete for me</button>
<button class="modal-btn modal-btn-delete" onclick="deleteMessageForEveryone()">Delete for everyone</button>
</div>
</div>
</div>
<script>
let ws = null;
let currentUser = '';
let currentToken = '';
let currentRecipient = '';
let currentUserAvatar = '';
let chats = [];
let searchResults = [];
let messageHistory = {};
let currentChatTab = 'existing';
let messageToDelete = null;
let isMobile = window.innerWidth <= 768;
let currentTheme = localStorage.getItem('theme') || 'light';
let reconnectTimeout = null;
let reconnectAttempts = 0;
const MAX_RECONNECT_ATTEMPTS = 5;
const RECONNECT_DELAY = 3000;
// Initialize theme
document.body.setAttribute('data-theme', currentTheme);
// 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);
}
// Auto-resize textarea
const messageInput = document.getElementById('messageInput');
messageInput.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = Math.min(this.scrollHeight, 100) + 'px';
});
// Update mobile detection on resize
window.addEventListener('resize', function() {
isMobile = window.innerWidth <= 768;
if (!isMobile) {
// On desktop, ensure chats panel is visible
document.getElementById('chatsPanel').classList.remove('active');
}
});
});
function toggleTheme() {
currentTheme = currentTheme === 'light' ? 'dark' : 'light';
document.body.setAttribute('data-theme', currentTheme);
localStorage.setItem('theme', currentTheme);
}
function updateConnectionStatus(status, message = '') {
const statusElement = document.getElementById('connectionStatus');
statusElement.textContent = message || status;
statusElement.className = `connection-status ${status}`;
statusElement.style.display = 'block';
if (status === 'connected') {
setTimeout(() => {
statusElement.style.display = 'none';
}, 3000);
}
}
function showTab(tabName) {
document.querySelectorAll('.auth-tab').forEach(tab => tab.classList.remove('active'));
document.querySelectorAll('.auth-form').forEach(form => form.classList.remove('active'));
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');
}
hideAlert();
}
function showAlert(message, type) {
const alert = document.getElementById('alert');
alert.textContent = message;
alert.className = `alert alert-${type}`;
alert.style.display = 'block';
// Auto hide after 5 seconds
setTimeout(hideAlert, 5000);
}
function hideAlert() {
document.getElementById('alert').style.display = 'none';
}
// Mobile navigation functions
function toggleChatsPanel() {
const chatsPanel = document.getElementById('chatsPanel');
chatsPanel.classList.toggle('active');
}
function showChatsPanel() {
const chatsPanel = document.getElementById('chatsPanel');
chatsPanel.classList.add('active');
}
function hideChatsPanel() {
const chatsPanel = document.getElementById('chatsPanel');
chatsPanel.classList.remove('active');
}
function reconnectWebSocket() {
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
reconnectAttempts++;
updateConnectionStatus('connecting', `Reconnecting... (${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
reconnectTimeout = setTimeout(() => {
console.log(`Attempting to reconnect... (${reconnectAttempts})`);
connectWebSocket();
}, RECONNECT_DELAY);
} else {
updateConnectionStatus('disconnected', 'Failed to connect. Please refresh the page.');
showAlert('Connection lost. Please refresh the page.', 'error');
}
}
function clearReconnection() {
if (reconnectTimeout) {
clearTimeout(reconnectTimeout);
reconnectTimeout = null;
}
reconnectAttempts = 0;
}
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;
currentUserAvatar = data.avatar_url;
localStorage.setItem('chatToken', token);
localStorage.setItem('chatUsername', data.username);
document.getElementById('authContainer').style.display = 'none';
document.getElementById('chatContainer').style.display = 'flex';
updateUserProfile();
loadChats();
connectWebSocket();
} else {
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');
}
}
function updateUserProfile() {
document.getElementById('currentUsername').textContent = currentUser;
const avatarElement = document.getElementById('currentUserAvatar');
if (currentUserAvatar) {
avatarElement.innerHTML = `<img src="${currentUserAvatar}" alt="${currentUser}">`;
} else {
avatarElement.innerHTML = currentUser.charAt(0).toUpperCase();
}
}
async function loadChats() {
try {
console.log('Loading chats for user:', currentUser);
const response = await fetch('/api/chats', {
headers: { 'Authorization': `Bearer ${currentToken}` }
});
console.log('Chats response status:', response.status);
if (response.ok) {
chats = await response.json();
console.log('Loaded chats:', chats);
renderChatsList();
} else {
console.error('Failed to load chats, status:', response.status);
}
} catch (error) {
console.error('Error loading chats:', error);
}
}
// Function to mark messages as read
async function markMessagesAsRead(fromUser) {
try {
const response = await fetch('/api/mark-read', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${currentToken}`
},
body: JSON.stringify({
from_user: fromUser,
to_user: currentUser
})
});
const data = await response.json();
if (data.success) {
console.log(`✅ Marked messages as read from ${fromUser}`);
// Обновляем список чатов чтобы убрать красный badge
loadChats();
} else {
console.error('❌ Failed to mark messages as read:', data.error);
}
} catch (error) {
console.error('❌ Error marking messages as read:', error);
}
}
// Function to delete message
async function deleteMessage(messageId, forEveryone) {
try {
const response = await fetch('/api/delete-message', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${currentToken}`
},
body: JSON.stringify({
message_id: messageId,
for_everyone: forEveryone
})
});
const data = await response.json();
if (data.success) {
console.log(`✅ Message deleted: ${data.message}`);
showAlert(data.message, 'success');
// Remove message from UI
const messageElement = document.querySelector(`[data-message-id="${messageId}"]`);
if (messageElement) {
messageElement.remove();
}
// Update chat history
if (messageHistory[currentRecipient]) {
messageHistory[currentRecipient] = messageHistory[currentRecipient].filter(
msg => msg.id !== messageId
);
}
// Reload chats to update last message
loadChats();
} else {
console.error('❌ Failed to delete message:', data.message);
showAlert(data.message, 'error');
}
} catch (error) {
console.error('❌ Error deleting message:', error);
showAlert('Error deleting message', 'error');
}
}
// Function to upload avatar
async function uploadAvatar(file) {
const formData = new FormData();
formData.append('avatar', file);
try {
const response = await fetch('/api/upload-avatar', {
method: 'POST',
headers: {
'Authorization': `Bearer ${currentToken}`
},
body: formData
});
const data = await response.json();
if (data.success) {
currentUserAvatar = data.avatar_url;
updateUserProfile();
showAlert('Avatar uploaded successfully!', 'success');
loadChats(); // Refresh chats to show updated avatar
} else {
showAlert(data.message, 'error');
}
} catch (error) {
console.error('Error uploading avatar:', error);
showAlert('Error uploading avatar', 'error');
}
}
// Modal functions
function showDeleteModal(messageId) {
messageToDelete = messageId;
document.getElementById('deleteModal').style.display = 'flex';
document.body.classList.add('modal-open');
}
function closeDeleteModal() {
messageToDelete = null;
document.getElementById('deleteModal').style.display = 'none';
document.body.classList.remove('modal-open');
}
function deleteMessageForMe() {
if (messageToDelete) {
deleteMessage(messageToDelete, false);
closeDeleteModal();
}
}
function deleteMessageForEveryone() {
if (messageToDelete) {
deleteMessage(messageToDelete, true);
closeDeleteModal();
}
}
function renderChatsList() {
const chatsList = document.getElementById('chatsList');
chatsList.innerHTML = '';
console.log('Rendering chats:', chats);
if (chats.length === 0) {
const emptyState = document.createElement('div');
emptyState.className = 'empty-state';
emptyState.innerHTML = `
<h3>No chats yet</h3>
<p>Start a new conversation by clicking "New Chat"</p>
`;
chatsList.appendChild(emptyState);
return;
}
chats.forEach(chat => {
const li = document.createElement('li');
li.className = 'chat-item';
if (chat.username === currentRecipient) {
li.classList.add('active');
}
const lastMessageTime = chat.last_message_time ?
formatTime(chat.last_message_time) : '';
const lastMessage = chat.last_message || 'No messages yet';
const avatarContent = chat.avatar_url ?
`<img src="${chat.avatar_url}" alt="${chat.username}">` :
chat.username.charAt(0).toUpperCase();
li.innerHTML = `
<div class="chat-avatar">${avatarContent}</div>
<div class="chat-info">
<div class="chat-username">${chat.username}</div>
<div class="chat-phone">${chat.phone}</div>
<div class="chat-last-message">${lastMessage}</div>
</div>
<div class="chat-meta">
<div class="chat-time">${lastMessageTime}</div>
${chat.unread_count > 0 ? `<div class="unread-badge">${chat.unread_count}</div>` : ''}
</div>
`;
li.onclick = () => {
selectChat(chat.username, chat.phone, chat.avatar_url);
if (isMobile) {
hideChatsPanel();
}
};
chatsList.appendChild(li);
});
}
function showChatTab(tab) {
currentChatTab = tab;
document.querySelectorAll('.chat-tab').forEach(tab => tab.classList.remove('active'));
if (tab === 'existing') {
document.querySelector('.chat-tab:nth-child(1)').classList.add('active');
document.getElementById('searchContainer').style.display = 'none';
renderChatsList();
} else {
document.querySelector('.chat-tab:nth-child(2)').classList.add('active');
document.getElementById('searchContainer').style.display = 'block';
document.getElementById('searchResults').innerHTML = '<div class="empty-state"><p>Search for users by phone or username</p></div>';
document.getElementById('searchInput').value = '';
}
}
let searchTimeout;
document.getElementById('searchInput').addEventListener('input', function(e) {
clearTimeout(searchTimeout);
const query = e.target.value.trim();
if (query.length < 2) {
document.getElementById('searchResults').innerHTML = '<div class="empty-state"><p>Enter at least 2 characters to search</p></div>';
return;
}
searchTimeout = setTimeout(() => searchUsers(query), 300);
});
async function searchUsers(query) {
try {
const response = await fetch(`/api/search-users?q=${encodeURIComponent(query)}`, {
headers: { 'Authorization': `Bearer ${currentToken}` }
});
if (response.ok) {
searchResults = await response.json();
renderSearchResults();
} else {
console.error('Search failed');
}
} catch (error) {
console.error('Error searching users:', error);
}
}
function renderSearchResults() {
const searchResultsContainer = document.getElementById('searchResults');
searchResultsContainer.innerHTML = '';
if (searchResults.length === 0) {
searchResultsContainer.innerHTML = '<div class="empty-state"><p>No users found</p></div>';
return;
}
searchResults.forEach(user => {
const div = document.createElement('div');
div.className = 'chat-item';
const avatarContent = user.avatar_url ?
`<img src="${user.avatar_url}" alt="${user.username}">` :
user.username.charAt(0).toUpperCase();
div.innerHTML = `
<div class="chat-avatar">${avatarContent}</div>
<div class="chat-info">
<div class="chat-username">${user.username}</div>
<div class="chat-phone">${user.phone}</div>
<div class="chat-last-message">Click to start chatting</div>
</div>
`;
div.onclick = () => {
selectChat(user.username, user.phone, user.avatar_url);
showChatTab('existing');
document.getElementById('searchInput').value = '';
if (isMobile) {
hideChatsPanel();
}
};
searchResultsContainer.appendChild(div);
});
}
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) {
localStorage.setItem('chatToken', data.token);
localStorage.setItem('chatUsername', data.username);
currentUser = data.username;
currentToken = data.token;
document.getElementById('authContainer').style.display = 'none';
document.getElementById('chatContainer').style.display = 'flex';
updateUserProfile();
loadChats();
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) {
localStorage.setItem('chatToken', data.token);
localStorage.setItem('chatUsername', data.username);
currentUser = data.username;
currentToken = data.token;
// Get user profile to load avatar
const profileResponse = await fetch('/api/profile', {
headers: { 'Authorization': `Bearer ${currentToken}` }
});
if (profileResponse.ok) {
const profile = await profileResponse.json();
currentUserAvatar = profile.avatar_url;
}
document.getElementById('authContainer').style.display = 'none';
document.getElementById('chatContainer').style.display = 'flex';
updateUserProfile();
loadChats();
connectWebSocket();
} else {
showAlert(data.message, 'error');
}
} catch (error) {
showAlert('Login failed. Please try again.', 'error');
}
}
function logout() {
localStorage.removeItem('chatToken');
localStorage.removeItem('chatUsername');
if (ws) {
ws.close();
ws = null;
}
clearReconnection();
currentUser = '';
currentToken = '';
currentRecipient = '';
currentUserAvatar = '';
chats = [];
searchResults = [];
messageHistory = {};
document.getElementById('authContainer').style.display = 'block';
document.getElementById('chatContainer').style.display = 'none';
document.getElementById('loginPhone').value = '';
document.getElementById('loginPassword').value = '';
document.getElementById('registerPhone').value = '';
document.getElementById('registerUsername').value = '';
document.getElementById('registerPassword').value = '';
showTab('login');
}
function connectWebSocket() {
// Clear any existing reconnection attempts
clearReconnection();
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws`;
updateConnectionStatus('connecting', 'Connecting...');
ws = new WebSocket(wsUrl);
ws.onopen = () => {
console.log('WebSocket connected');
updateConnectionStatus('connected', 'Connected');
clearReconnection();
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 === 'error') {
showAlert(data.content, 'error');
if (data.content.includes('Invalid') || data.content.includes('expired') || data.content.includes('auth')) {
logout();
}
} else if (data.type === 'system') {
addSystemMessage(data.content);
} else if (data.type === 'message_deleted') {
// Handle message deletion from server
const messageElement = document.querySelector(`[data-message-id="${data.message_id}"]`);
if (messageElement) {
messageElement.remove();
}
// Update chat history
if (messageHistory[currentRecipient]) {
messageHistory[currentRecipient] = messageHistory[currentRecipient].filter(
msg => msg.id !== data.message_id
);
}
// Reload chats to update last message
loadChats();
} else {
handleChatMessage(data);
}
} catch (e) {
console.error('Error parsing message:', e, 'Raw data:', event.data);
}
};
ws.onclose = (event) => {
console.log('WebSocket disconnected:', event.code, event.reason);
updateConnectionStatus('disconnected', 'Disconnected');
addSystemMessage('Disconnected from server');
document.getElementById('sendButton').disabled = true;
document.getElementById('messageInput').disabled = true;
// Attempt to reconnect if this wasn't a normal closure
if (event.code !== 1000 && event.code !== 1001) {
reconnectWebSocket();
}
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
updateConnectionStatus('disconnected', 'Connection error');
addSystemMessage('Connection error');
};
}
async function selectChat(username, phone, avatarUrl) {
if (!username) return;
currentRecipient = username;
// Update mobile header
document.getElementById('mobileChatTitle').textContent = username;
// Update chat header with recipient info
const chatHeaderInfo = document.querySelector('.chat-header-info');
const avatarContent = avatarUrl ?
`<img src="${avatarUrl}" alt="${username}">` :
username.charAt(0).toUpperCase();
chatHeaderInfo.innerHTML = `
<button class="back-btn" onclick="showChatsPanel()">←</button>
<div class="chat-header-avatar">${avatarContent}</div>
<div class="chat-header-text">
<div class="chat-header-username">${username}</div>
<div class="chat-header-phone">${phone}</div>
</div>
`;
// Помечаем сообщения как прочитанные при открытии чата
await markMessagesAsRead(username);
// Update active chat in list
document.querySelectorAll('.chat-item').forEach(item => {
item.classList.remove('active');
const usernameElement = item.querySelector('.chat-username');
if (usernameElement && usernameElement.textContent === username) {
item.classList.add('active');
}
});
// Enable message input
document.getElementById('sendButton').disabled = false;
document.getElementById('messageInput').disabled = false;
// Focus on input after a short delay to ensure UI is updated
setTimeout(() => {
document.getElementById('messageInput').focus();
}, 100);
displayChatHistory(username);
}
function displayChatHistory(username) {
const chatMessages = document.getElementById('chatMessages');
chatMessages.innerHTML = '';
if (messageHistory[username] && messageHistory[username].length > 0) {
messageHistory[username].forEach(msg => displayMessage(msg, false));
// Scroll to bottom after rendering
setTimeout(() => {
chatMessages.scrollTop = chatMessages.scrollHeight;
}, 50);
} else {
chatMessages.innerHTML = `
<div class="empty-state">
<h3>Start a conversation</h3>
<p>Send a message to start chatting with ${username}</p>
</div>
`;
}
}
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] = [];
}
const messageExists = messageHistory[otherUser].some(
existingMsg => existingMsg.id === msg.id
);
if (!messageExists) {
messageHistory[otherUser].push(msg);
}
loadChats();
if (otherUser === currentRecipient) {
displayMessage(msg, true);
// Если сообщение пришло в открытый чат, сразу помечаем как прочитанное
if (msg.from_user === currentRecipient) {
markMessagesAsRead(currentRecipient);
}
}
}
function displayMessage(msg, scrollToBottom) {
const chatMessages = document.getElementById('chatMessages');
// Remove empty state if it exists
const emptyState = chatMessages.querySelector('.empty-state');
if (emptyState) {
emptyState.remove();
}
const messageDiv = document.createElement('div');
messageDiv.className = `message ${msg.from_user === currentUser ? 'outgoing' : 'incoming'}`;
messageDiv.setAttribute('data-message-id', msg.id);
if (msg.message_type === 'system') {
messageDiv.className = 'system-message';
messageDiv.textContent = msg.content;
} else {
const isOutgoing = msg.from_user === currentUser;
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);
// Add delete button for user's own messages or all messages
if (msg.from_user === currentUser) {
const actionsDiv = document.createElement('div');
actionsDiv.className = 'message-actions';
const deleteBtn = document.createElement('button');
deleteBtn.className = 'delete-btn';
deleteBtn.innerHTML = '×';
deleteBtn.title = 'Delete message';
deleteBtn.onclick = (e) => {
e.stopPropagation();
showDeleteModal(msg.id);
};
actionsDiv.appendChild(deleteBtn);
messageDiv.appendChild(actionsDiv);
}
}
chatMessages.appendChild(messageDiv);
if (scrollToBottom) {
setTimeout(() => {
chatMessages.scrollTop = chatMessages.scrollHeight;
}, 100);
}
}
function addSystemMessage(content) {
const chatMessages = document.getElementById('chatMessages');
// Remove empty state if it exists
const emptyState = chatMessages.querySelector('.empty-state');
if (emptyState) {
emptyState.remove();
}
const systemDiv = document.createElement('div');
systemDiv.className = 'system-message';
systemDiv.textContent = content;
chatMessages.appendChild(systemDiv);
setTimeout(() => {
chatMessages.scrollTop = chatMessages.scrollHeight;
}, 100);
}
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,
to_user: currentRecipient,
content: content,
timestamp: Date.now(),
message_type: 'text'
};
console.log('Sending message:', message);
ws.send(JSON.stringify(message));
input.value = '';
input.style.height = 'auto';
setTimeout(() => loadChats(), 100);
}
function generateId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
function formatTime(timestamp) {
if (!timestamp) return '';
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
if (diff < 24 * 60 * 60 * 1000) {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
} else {
return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
}
}
// Event listeners
document.getElementById('messageInput').addEventListener('keydown', function(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
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();
});
// Avatar upload handler
document.getElementById('avatarInput').addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
// Validate file type
if (!file.type.startsWith('image/')) {
showAlert('Please select an image file', 'error');
return;
}
// Validate file size (5MB)
if (file.size > 5 * 1024 * 1024) {
showAlert('Image must be less than 5MB', 'error');
return;
}
uploadAvatar(file);
e.target.value = ''; // Reset input
}
});
// Close modal when clicking outside
document.getElementById('deleteModal').addEventListener('click', function(e) {
if (e.target === this) {
closeDeleteModal();
}
});
// Close chats panel when clicking on overlay (mobile)
document.getElementById('chatsPanel').addEventListener('click', function(e) {
if (e.target === this && isMobile) {
hideChatsPanel();
}
});
document.getElementById('loginPhone').focus();
</script>
</body>
</html>