Spaces:
Running
Running
| <html lang="fa" dir="rtl"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>چت روم پیشرفته | ارتباط واقعی</title> | |
| <!-- Persian Font: Vazirmatn --> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@100;300;400;500;700;900&display=swap" | |
| rel="stylesheet"> | |
| <!-- Icons --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| :root { | |
| --primary: #6366f1; | |
| --primary-dark: #4f46e5; | |
| --secondary: #ec4899; | |
| --background: #0f172a; | |
| --surface: rgba(30, 41, 59, 0.7); | |
| --surface-solid: #1e293b; | |
| --text: #f8fafc; | |
| --text-muted: #94a3b8; | |
| --border: rgba(148, 163, 184, 0.1); | |
| --success: #10b981; | |
| --warning: #f59e0b; | |
| --glass: rgba(255, 255, 255, 0.05); | |
| --shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37); | |
| --message-sent: linear-gradient(135deg, var(--primary), var(--primary-dark)); | |
| --message-received: #334155; | |
| } | |
| [data-theme="light"] { | |
| --background: #f1f5f9; | |
| --surface: rgba(255, 255, 255, 0.8); | |
| --surface-solid: #ffffff; | |
| --text: #1e293b; | |
| --text-muted: #64748b; | |
| --border: rgba(148, 163, 184, 0.2); | |
| --glass: rgba(255, 255, 255, 0.6); | |
| --message-received: #e2e8f0; | |
| --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| outline: none; | |
| } | |
| html { | |
| scroll-behavior: smooth; | |
| } | |
| body { | |
| font-family: 'Vazirmatn', sans-serif; | |
| background: var(--background); | |
| color: var(--text); | |
| min-height: 100vh; | |
| overflow: hidden; /* App-like feel */ | |
| transition: background 0.3s ease, color 0.3s ease; | |
| } | |
| /* Animated Background */ | |
| .bg-animation { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| z-index: -1; | |
| overflow: hidden; | |
| pointer-events: none; | |
| } | |
| .bg-animation .circle { | |
| position: absolute; | |
| border-radius: 50%; | |
| filter: blur(80px); | |
| opacity: 0.3; | |
| animation: float 20s infinite ease-in-out; | |
| } | |
| .circle:nth-child(1) { | |
| width: 400px; | |
| height: 400px; | |
| background: var(--primary); | |
| top: -100px; | |
| right: -100px; | |
| animation-delay: 0s; | |
| } | |
| .circle:nth-child(2) { | |
| width: 300px; | |
| height: 300px; | |
| background: var(--secondary); | |
| bottom: -50px; | |
| left: -50px; | |
| animation-delay: 5s; | |
| } | |
| @keyframes float { | |
| 0%, 100% { transform: translate(0, 0) scale(1); } | |
| 50% { transform: translate(-30px, 30px) scale(1.1); } | |
| } | |
| /* Header */ | |
| header { | |
| position: fixed; | |
| top: 0; | |
| width: 100%; | |
| padding: 0.8rem 2rem; | |
| background: var(--glass); | |
| backdrop-filter: blur(12px); | |
| border-bottom: 1px solid var(--border); | |
| z-index: 1000; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| height: 70px; | |
| } | |
| .logo { | |
| font-size: 1.4rem; | |
| font-weight: 900; | |
| background: linear-gradient(135deg, var(--primary), var(--secondary)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .header-actions { | |
| display: flex; | |
| gap: 1rem; | |
| align-items: center; | |
| } | |
| .built-with { | |
| font-size: 0.75rem; | |
| color: var(--text-muted); | |
| text-decoration: none; | |
| padding: 0.5rem 1rem; | |
| border-radius: 2rem; | |
| background: var(--glass); | |
| border: 1px solid var(--border); | |
| transition: all 0.3s ease; | |
| } | |
| .built-with:hover { | |
| background: var(--primary); | |
| color: white; | |
| transform: translateY(-2px); | |
| } | |
| .theme-toggle { | |
| background: var(--glass); | |
| border: 1px solid var(--border); | |
| color: var(--text); | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 50%; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: all 0.3s ease; | |
| } | |
| .theme-toggle:hover { | |
| transform: rotate(180deg); | |
| background: var(--primary); | |
| } | |
| /* Main Container */ | |
| .container { | |
| width: 100%; | |
| height: 100vh; | |
| padding-top: 70px; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| /* Auth Screens */ | |
| .auth-container { | |
| flex: 1; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| padding: 2rem; | |
| animation: fadeIn 0.5s ease; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(20px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .auth-box { | |
| background: var(--surface); | |
| backdrop-filter: blur(16px); | |
| border: 1px solid var(--border); | |
| border-radius: 2rem; | |
| padding: 3rem; | |
| width: 100%; | |
| max-width: 420px; | |
| text-align: center; | |
| box-shadow: var(--shadow); | |
| } | |
| .auth-box h2 { | |
| margin-bottom: 2rem; | |
| font-size: 1.8rem; | |
| } | |
| .form-group { | |
| margin-bottom: 1.5rem; | |
| text-align: right; | |
| } | |
| .form-group label { | |
| display: block; | |
| margin-bottom: 0.5rem; | |
| color: var(--text-muted); | |
| font-size: 0.9rem; | |
| } | |
| .form-group input { | |
| width: 100%; | |
| padding: 0.9rem 1rem; | |
| border: 2px solid var(--border); | |
| border-radius: 1rem; | |
| background: var(--glass); | |
| color: var(--text); | |
| font-family: inherit; | |
| font-size: 1rem; | |
| transition: all 0.3s; | |
| } | |
| .form-group input:focus { | |
| border-color: var(--primary); | |
| box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.1); | |
| } | |
| .btn { | |
| width: 100%; | |
| padding: 1rem; | |
| border-radius: 1rem; | |
| font-family: inherit; | |
| font-size: 1rem; | |
| font-weight: 700; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| border: none; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 0.5rem; | |
| } | |
| .btn-primary { | |
| background: var(--primary); | |
| color: white; | |
| box-shadow: 0 4px 15px rgba(99, 102, 241, 0.4); | |
| } | |
| .btn-primary:hover { | |
| background: var(--primary-dark); | |
| transform: translateY(-2px); | |
| } | |
| .switch-auth { | |
| margin-top: 1.5rem; | |
| font-size: 0.9rem; | |
| color: var(--text-muted); | |
| } | |
| .switch-auth a { | |
| color: var(--primary); | |
| text-decoration: none; | |
| font-weight: 700; | |
| cursor: pointer; | |
| } | |
| /* Chat Layout */ | |
| #chat-app { | |
| display: none; | |
| width: 100%; | |
| height: calc(100vh - 70px); | |
| max-width: 1600px; | |
| margin: 0 auto; | |
| padding: 1rem; | |
| gap: 1rem; | |
| } | |
| .chat-layout { | |
| display: grid; | |
| grid-template-columns: 320px 1fr; | |
| height: 100%; | |
| gap: 1rem; | |
| } | |
| /* Sidebar */ | |
| .sidebar { | |
| background: var(--surface); | |
| backdrop-filter: blur(16px); | |
| border: 1px solid var(--border); | |
| border-radius: 1.5rem; | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| transition: transform 0.3s ease; | |
| } | |
| .sidebar-header { | |
| padding: 1.5rem; | |
| border-bottom: 1px solid var(--border); | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| } | |
| .my-avatar { | |
| width: 50px; | |
| height: 50px; | |
| border-radius: 50%; | |
| background: linear-gradient(135deg, var(--secondary), var(--primary)); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 1.2rem; | |
| font-weight: bold; | |
| color: white; | |
| flex-shrink: 0; | |
| } | |
| .user-info h4 { | |
| font-size: 1rem; | |
| margin-bottom: 0.2rem; | |
| } | |
| .user-status { | |
| font-size: 0.8rem; | |
| color: var(--success); | |
| display: flex; | |
| align-items: center; | |
| gap: 0.3rem; | |
| } | |
| .user-status::before { | |
| content: ''; | |
| width: 8px; | |
| height: 8px; | |
| background: var(--success); | |
| border-radius: 50%; | |
| display: inline-block; | |
| } | |
| .rooms-section { | |
| padding: 1rem; | |
| flex: 1; | |
| overflow-y: auto; | |
| } | |
| .section-title { | |
| font-size: 0.8rem; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| color: var(--text-muted); | |
| margin-bottom: 1rem; | |
| padding-right: 0.5rem; | |
| } | |
| .room-btn { | |
| width: 100%; | |
| padding: 1rem; | |
| margin-bottom: 0.5rem; | |
| border-radius: 1rem; | |
| background: transparent; | |
| border: none; | |
| color: var(--text); | |
| text-align: right; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| position: relative; | |
| } | |
| .room-btn:hover { | |
| background: var(--glass); | |
| } | |
| .room-btn.active { | |
| background: rgba(99, 102, 241, 0.1); | |
| color: var(--primary); | |
| } | |
| .room-btn.active::before { | |
| content: ''; | |
| position: absolute; | |
| right: 0; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| width: 4px; | |
| height: 60%; | |
| background: var(--primary); | |
| border-radius: 4px 0 0 4px; | |
| } | |
| .logout-btn { | |
| margin: 1rem; | |
| padding: 1rem; | |
| background: var(--glass); | |
| border: 1px solid var(--border); | |
| color: var(--text-muted); | |
| border-radius: 1rem; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 0.5rem; | |
| } | |
| .logout-btn:hover { | |
| background: #ef4444; | |
| color: white; | |
| border-color: #ef4444; | |
| } | |
| /* Main Chat Area */ | |
| .chat-area { | |
| background: var(--surface); | |
| backdrop-filter: blur(16px); | |
| border: 1px solid var(--border); | |
| border-radius: 1.5rem; | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| .chat-area-header { | |
| padding: 1rem 1.5rem; | |
| border-bottom: 1px solid var(--border); | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| background: rgba(0,0,0,0.02); | |
| } | |
| .chat-title h3 { | |
| font-size: 1.1rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .messages-container { | |
| flex: 1; | |
| padding: 1.5rem; | |
| overflow-y: auto; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1rem; | |
| } | |
| /* Messages */ | |
| .message { | |
| max-width: 70%; | |
| padding: 0.8rem 1.2rem; | |
| border-radius: 1.5rem; | |
| position: relative; | |
| line-height: 1.5; | |
| font-size: 0.95rem; | |
| word-wrap: break-word; | |
| animation: popIn 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); | |
| } | |
| @keyframes popIn { | |
| from { opacity: 0; transform: scale(0.9); } | |
| to { opacity: 1; transform: scale(1); } | |
| } | |
| /* RTL Specifics */ | |
| .message.sent { | |
| align-self: flex-end; /* Right side in RTL */ | |
| background: var(--message-sent); | |
| color: white; | |
| border-bottom-right-radius: 0.25rem; | |
| box-shadow: 0 4px 15px rgba(99, 102, 241, 0.2); | |
| } | |
| .message.received { | |
| align-self: flex-start; /* Left side in RTL */ | |
| background: var(--message-received); | |
| color: var(--text); | |
| border-bottom-left-radius: 0.25rem; | |
| border: 1px solid var(--border); | |
| } | |
| .message-meta { | |
| font-size: 0.7rem; | |
| margin-top: 0.3rem; | |
| opacity: 0.7; | |
| display: flex; | |
| align-items: center; | |
| justify-content: flex-end; | |
| gap: 0.3rem; | |
| } | |
| .message-author { | |
| font-weight: 700; | |
| font-size: 0.8rem; | |
| margin-bottom: 0.2rem; | |
| display: block; | |
| } | |
| .date-divider { | |
| text-align: center; | |
| margin: 1.5rem 0; | |
| position: relative; | |
| } | |
| .date-divider span { | |
| background: var(--glass); | |
| padding: 0.3rem 1rem; | |
| border-radius: 1rem; | |
| font-size: 0.75rem; | |
| color: var(--text-muted); | |
| border: 1px solid var(--border); | |
| } | |
| /* Input Area */ | |
| .input-area { | |
| padding: 1rem 1.5rem; | |
| background: var(--glass); | |
| border-top: 1px solid var(--border); | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| } | |
| .input-wrapper { | |
| flex: 1; | |
| position: relative; | |
| } | |
| .input-wrapper input { | |
| width: 100%; | |
| padding: 0.9rem 3.5rem 0.9rem 1.2rem; | |
| border-radius: 2rem; | |
| border: 2px solid var(--border); | |
| background: var(--background); | |
| color: var(--text); | |
| font-family: inherit; | |
| transition: all 0.3s; | |
| } | |
| .input-wrapper input:focus { | |
| border-color: var(--primary); | |
| } | |
| .send-btn { | |
| width: 50px; | |
| height: 50px; | |
| border-radius: 50%; | |
| background: var(--primary); | |
| color: white; | |
| border: none; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: all 0.3s; | |
| flex-shrink: 0; | |
| } | |
| .send-btn:hover { | |
| background: var(--primary-dark); | |
| transform: scale(1.1) rotate(-10deg); | |
| } | |
| .send-btn:active { | |
| transform: scale(0.95); | |
| } | |
| /* Typing Indicator */ | |
| .typing-indicator-wrapper { | |
| padding: 0 1.5rem 0.5rem; | |
| height: 24px; | |
| position: absolute; | |
| bottom: 70px; | |
| right: 20px; | |
| pointer-events: none; | |
| } | |
| .typing-indicator { | |
| display: inline-flex; | |
| gap: 4px; | |
| background: var(--message-received); | |
| padding: 0.5rem 1rem; | |
| border-radius: 1rem; | |
| border: 1px solid var(--border); | |
| align-items: center; | |
| font-size: 0.75rem; | |
| color: var(--text-muted); | |
| } | |
| .typing-indicator span { | |
| width: 6px; | |
| height: 6px; | |
| background: var(--text-muted); | |
| border-radius: 50%; | |
| animation: bounce 1.4s infinite ease-in-out both; | |
| } | |
| .typing-indicator span:nth-child(1) { animation-delay: -0.32s; } | |
| .typing-indicator span:nth-child(2) { animation-delay: -0.16s; } | |
| @keyframes bounce { | |
| 0%, 80%, 100% { transform: scale(0); } | |
| 40% { transform: scale(1); } | |
| } | |
| /* Toast Notification */ | |
| .toast-container { | |
| position: fixed; | |
| top: 90px; | |
| left: 20px; /* Left in RTL to not cover chat */ | |
| z-index: 2000; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| } | |
| .toast { | |
| background: var(--surface-solid); | |
| border: 1px solid var(--border); | |
| padding: 1rem 1.5rem; | |
| border-radius: 1rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.8rem; | |
| box-shadow: var(--shadow); | |
| animation: slideInLeft 0.3s ease; | |
| max-width: 300px; | |
| } | |
| @keyframes slideInLeft { | |
| from { opacity: 0; transform: translateX(-50px); } | |
| to { opacity: 1; transform: translateX(0); } | |
| } | |
| /* Responsive */ | |
| @media (max-width: 768px) { | |
| .chat-layout { | |
| grid-template-columns: 1fr; | |
| } | |
| .sidebar { | |
| position: absolute; | |
| top: 0; | |
| right: 0; | |
| width: 85%; | |
| height: 100%; | |
| z-index: 100; | |
| transform: translateX(100%); | |
| border-radius: 0; | |
| } | |
| .sidebar.active { | |
| transform: translateX(0); | |
| box-shadow: -10px 0 30px rgba(0,0,0,0.5); | |
| } | |
| .sidebar-overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0,0,0,0.5); | |
| z-index: 90; | |
| display: none; | |
| backdrop-filter: blur(2px); | |
| } | |
| .sidebar-overlay.active { | |
| display: block; | |
| } | |
| .mobile-toggle { | |
| display: flex ; | |
| } | |
| .message { | |
| max-width: 85%; | |
| } | |
| } | |
| .mobile-toggle { | |
| display: none; | |
| background: none; | |
| border: none; | |
| color: var(--text); | |
| font-size: 1.2rem; | |
| cursor: pointer; | |
| margin-left: 1rem; | |
| } | |
| /* Scrollbar */ | |
| ::-webkit-scrollbar { width: 6px; } | |
| ::-webkit-scrollbar-track { background: transparent; } | |
| ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; } | |
| ::-webkit-scrollbar-thumb:hover { background: var(--text-muted); } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Animated Background --> | |
| <div class="bg-animation"> | |
| <div class="circle"></div> | |
| <div class="circle"></div> | |
| </div> | |
| <!-- Header --> | |
| <header> | |
| <div class="logo"> | |
| <i class="fas fa-comments"></i> | |
| <span>چت روم واقعی</span> | |
| </div> | |
| <div class="header-actions"> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="built-with"> | |
| <i class="fas fa-code"></i> Built with anycoder | |
| </a> | |
| <button class="theme-toggle" onclick="toggleTheme()" title="تغییر تم"> | |
| <i class="fas fa-moon"></i> | |
| </button> | |
| </div> | |
| </header> | |
| <div class="container"> | |
| <!-- Auth View --> | |
| <div id="auth-view" class="auth-container"> | |
| <div class="auth-box"> | |
| <h2 id="auth-title">ورود به حساب</h2> | |
| <form id="auth-form" onsubmit="handleAuth(event)"> | |
| <div class="form-group"> | |
| <label>نام کاربری</label> | |
| <input type="text" id="username-input" placeholder="مثلاً: علی" required autocomplete="off"> | |
| </div> | |
| <button type="submit" class="btn btn-primary"> | |
| <i class="fas fa-arrow-left"></i> | |
| <span id="auth-btn-text">ورود</span> | |
| </button> | |
| </form> | |
| <div class="switch-auth"> | |
| <a onclick="toggleAuthMode()" id="auth-switch-link">ثبتنام کنید</a> | |
| </div> | |
| <p style="margin-top: 2rem; font-size: 0.8rem; color: var(--text-muted); opacity: 0.7;"> | |
| <i class="fas fa-info-circle"></i> | |
| برای تست واقعی، این صفحه را در دو تب مختلف باز کنید. | |
| </p> | |
| </div> | |
| </div> | |
| <!-- Chat View --> | |
| <div id="chat-app"> | |
| <div class="chat-layout"> | |
| <!-- Mobile Overlay --> | |
| <div class="sidebar-overlay" onclick="toggleSidebar()"></div> | |
| <!-- Sidebar --> | |
| <aside class="sidebar" id="sidebar"> | |
| <div class="sidebar-header"> | |
| <div class="my-avatar" id="current-user-avatar">A</div> | |
| <div class="user-info"> | |
| <h4 id="current-user-name">نام کاربر</h4> | |
| <span class="user-status">آنلاین</span> | |
| </div> | |
| </div> | |
| <div class="rooms-section"> | |
| <div class="section-title">اتاقهای گفتگو</div> | |
| <div id="rooms-list"> | |
| <!-- Rooms will be injected here --> | |
| </div> | |
| </div> | |
| <button class="logout-btn" onclick="logout()"> | |
| <i class="fas fa-sign-out-alt"></i> | |
| خروج از حساب | |
| </button> | |
| </aside> | |
| <!-- Main Chat --> | |
| <main class="chat-area"> | |
| <div class="chat-area-header"> | |
| <div class="chat-title"> | |
| <button class="mobile-toggle" onclick="toggleSidebar()"> | |
| <i class="fas fa-bars"></i> | |
| </button> | |
| <h3 id="room-title"> | |
| <i class="fas fa-hashtag"></i> | |
| <span>عمومی</span> | |
| </h3> | |
| </div> | |
| <div style="display: flex; gap: 0.5rem;"> | |
| <button class="theme-toggle" style="width: 32px; height: 32px;" onclick="clearHistory()"> | |
| <i class="fas fa-trash-alt" title="پاک کردن تاریخچه"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="messages-container" id="messages-container"> | |
| <!-- Messages will be injected here --> | |
| </div> | |
| <div class="typing-indicator-wrapper" id="typing-indicator"> | |
| <div class="typing-indicator"> | |
| <span></span><span></span><span></span> | |
| <span style="margin-right: 5px;" id="typer-name">کاربر</span> در حال نوشتن... | |
| </div> | |
| </div> | |
| <div class="input-area"> | |
| <div class="input-wrapper"> | |
| <input type="text" id="message-input" placeholder="پیام خود را بنویسید..." autocomplete="off"> | |
| </div> | |
| <button class="send-btn" onclick="sendMessage()"> | |
| <i class="fas fa-paper-plane"></i> | |
| </button> | |
| </div> | |
| </main> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Toast Container --> | |
| <div class="toast-container" id="toast-container"></div> | |
| <!-- Sound Effect (Base64 for simplicity) --> | |
| <script> | |
| // Short pop sound for messages | |
| const audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
| function playNotificationSound() { | |
| if (audioContext.state === 'suspended') audioContext.resume(); | |
| const oscillator = audioContext.createOscillator(); | |
| const gainNode = audioContext.createGain(); | |
| oscillator.type = 'sine'; | |
| oscillator.frequency.setValueAtTime(500, audioContext.currentTime); | |
| oscillator.frequency.exponentialRampToValueAtTime(1000, audioContext.currentTime + 0.1); | |
| gainNode.gain.setValueAtTime(0.1, audioContext.currentTime); | |
| gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3); | |
| oscillator.connect(gainNode); | |
| gainNode.connect(audioContext.destination); | |
| oscillator.start(); | |
| oscillator.stop(audioContext.currentTime + 0.3); | |
| } | |
| </script> | |
| <script> | |
| // --- State Management --- | |
| const STORE_KEY = 'persian_chat_v1'; | |
| const CHANNEL_NAME = 'persian_chat_realtime'; | |
| // Default Data Structure | |
| const defaultData = { | |
| users: [], // { username, color, joinedAt } | |
| messages: [], // { id, roomId, username, text, timestamp, type } | |
| rooms: [ | |
| { id: 'general', name: 'عمومی', icon: 'fa-globe' }, | |
| { id: 'tech', name: 'فناوری', icon: 'fa-laptop-code' }, | |
| { id: 'random', name: 'گفتگوی آزاد', icon: 'fa-comments' } | |
| ] | |
| }; | |
| let currentState = JSON.parse(localStorage.getItem(STORE_KEY)) || defaultData; | |
| let currentUser = JSON.parse(sessionStorage.getItem('currentUser')) || null; | |
| let currentRoomId = 'general'; | |
| let broadcastChannel = new BroadcastChannel(CHANNEL_NAME); | |
| let typingTimeout = null; | |
| // --- Core Logic --- | |
| function saveState() { | |
| localStorage.setItem(STORE_KEY, JSON.stringify(currentState)); | |
| } | |
| function generateId() { | |
| return Date.now().toString(36) + Math.random().toString(36).substr(2); | |
| } | |
| function getCurrentUserColor(username) { | |
| const user = currentState.users.find(u => u.username === username); | |
| return user ? user.color : '#6366f1'; | |
| } | |
| // --- Auth Functions --- | |
| function handleAuth(e) { | |
| e.preventDefault(); | |
| const username = document.getElementById('username-input').value.trim(); | |
| if (!username) return showToast('لطفا نام کاربری را وارد کنید', 'error'); | |
| if (username.length < 3) return showToast('نام کاربری باید حداقل ۳ حرف باشد', 'error'); | |
| // Simulate login/register combined logic | |
| const existingUser = currentState.users.find(u => u.username === username); | |
| const userObj = existingUser || { | |
| username: username, | |
| color: `hsl(${Math.random() * 360}, 70%, 60%)`, | |
| joinedAt: Date.now() | |
| }; | |
| if (!existingUser) { | |
| currentState.users.push(userObj); | |
| saveState(); | |
| } | |
| currentUser = userObj; | |
| sessionStorage.setItem('currentUser', JSON.stringify(currentUser)); | |
| showToast(`خوش آمدید ${username}!`, 'success'); | |
| initChatApp(); | |
| } | |
| function logout() { | |
| currentUser = null; | |
| sessionStorage.removeItem('currentUser'); | |
| document.getElementById('chat-app').style.display = 'none'; | |
| document.getElementById('auth-view').style.display = 'flex'; | |
| document.getElementById('username-input').value = ''; | |
| } | |
| function initChatApp() { | |
| if (!currentUser) return; | |
| // UI Updates | |
| document.getElementById('auth-view').style.display = 'none'; | |
| document.getElementById('chat-app').style.display = 'block'; | |
| document.getElementById('current-user-name').textContent = currentUser.username; | |
| document.getElementById('current-user-avatar').textContent = currentUser.username.charAt(0).toUpperCase(); | |
| document.getElementById('current-user-avatar').style.background = currentUser.color; | |
| renderRooms(); | |
| switchRoom('general'); | |
| // Broadcast Join Event | |
| broadcastChannel.postMessage({ | |
| type: 'USER_JOINED', | |
| payload: { username: currentUser.username } | |
| }); | |
| } | |
| // --- Chat Functions --- | |
| function renderRooms() { | |
| const container = document.getElementById('rooms-list'); | |
| container.innerHTML = currentState.rooms.map(room => ` | |
| <button class="room-btn ${room.id === currentRoomId ? 'active' : ''}" onclick="switchRoom('${room.id}')"> | |
| <i class="fas ${room.icon}" style="width: 20px; text-align: center;"></i> | |
| <span>${room.name}</span> | |
| </button> | |
| `).join(''); | |
| } | |
| function switchRoom(roomId) { | |
| currentRoomId = roomId; | |
| const room = currentState.rooms.find(r => r.id === roomId); | |
| document.getElementById('room-title').innerHTML = `<i class="fas ${room.icon}"></i> ${room.name}`; | |
| renderRooms(); | |
| renderMessages(); | |
| // Mobile: close sidebar | |
| if (window.innerWidth <= 768) { | |
| document.getElementById('sidebar').classList.remove('active'); | |
| document.querySelector('.sidebar-overlay').classList.remove('active'); | |
| } | |
| } | |
| function renderMessages() { | |
| const container = document.getElementById('messages-container'); | |
| const roomMessages = currentState.messages.filter(m => m.roomId === currentRoomId); | |
| container.innerHTML = ''; | |
| if (roomMessages.length === 0) { | |
| container.innerHTML = ` | |
| <div style="text-align:center; padding: 2rem; color: var(--text-muted);"> | |
| <i class="fas fa-comments" style="font-size: 3rem; margin-bottom: 1rem; opacity: 0.3;"></i> | |
| <p>هنوز پیامی ارسال نشده است.</p> | |
| <p style="font-size: 0.8rem;">اولین نفری باشید که سلام میکند!</p> | |
| </div> | |
| `; | |
| return; | |
| } | |
| let lastDate = null; | |
| roomMessages.forEach((msg, index) => { | |
| const msgDate = new Date(msg.timestamp).toLocaleDateString('fa-IR'); | |
| // Date Divider | |
| if (msgDate !== lastDate) { | |
| container.innerHTML += ` | |
| <div class="date-divider"><span>${msgDate}</span></div> | |
| `; | |
| lastDate = msgDate; | |
| } | |
| const isMe = msg.username === currentUser.username; | |
| const time = new Date(msg.timestamp).toLocaleTimeString('fa-IR', { hour: '2-digit', minute: '2-digit' }); | |
| const msgEl = document.createElement('div'); | |
| msgEl.className = `message ${isMe ? 'sent' : 'received'}`; | |
| msgEl.innerHTML = ` | |
| ${!isMe ? `<span class="message-author" style="color: ${getCurrentUserColor(msg.username)}">${msg.username}</span>` : ''} | |
| ${escapeHtml(msg.text)} | |
| <div class="message-meta"> | |
| <span>${time}</span> | |
| ${isMe ? '<i class="fas fa-check-double"></i>' : ''} | |
| </div> | |
| `; | |
| container.appendChild(msgEl); | |
| }); | |
| scrollToBottom(); | |
| } | |
| function sendMessage() { | |
| const input = document.getElementById('message-input'); | |
| const text = input.value.trim(); | |
| if (!text) return; | |
| const newMessage = { | |
| id: generateId(), | |
| roomId: currentRoomId, | |
| username: currentUser.username, | |
| text: text, | |
| timestamp: Date.now(), | |
| type: 'text' | |
| }; | |
| // Update Local State | |
| currentState.messages.push(newMessage); | |
| saveState(); | |
| // Update UI | |
| input.value = ''; | |
| renderMessages(); | |
| // Broadcast to other tabs | |
| broadcastChannel.postMessage({ | |
| type: 'NEW_MESSAGE', | |
| payload: newMessage | |
| }); | |
| // Stop typing indicator | |
| stopTyping(); | |
| } | |
| function scrollToBottom() { | |
| const container = document.getElementById('messages-container'); | |
| container.scrollTop = container.scrollHeight; | |
| } | |
| function clearHistory() { | |
| if(confirm('آیا مطمئن هستید که میخواهید تاریخچه این اتاق را پاک کنید؟')) { | |
| currentState.messages = currentState.messages.filter(m => m.roomId !== currentRoomId); | |
| saveState(); | |
| renderMessages(); | |
| showToast('تاریخچه پاک شد', 'success'); | |
| } | |
| } | |
| // --- Real-time Features (BroadcastChannel) --- | |
| broadcastChannel.onmessage = (event) => { | |
| const { type, payload } = event.data; | |
| if (type === 'NEW_MESSAGE') { | |
| // Only add if not exists (deduplication) | |
| if (!currentState.messages.find(m => m.id === payload.id)) { | |
| currentState.messages.push(payload); | |
| saveState(); | |
| if (payload.roomId === currentRoomId) { | |
| renderMessages(); | |
| if (payload.username !== currentUser.username) { | |
| playNotificationSound(); | |
| showToast(`پیام جدید از ${payload.username}`, 'info'); | |
| } | |
| } | |
| } | |
| } | |
| else if (type === 'TYPING_START') { | |
| if (payload.roomId === currentRoomId && payload.username !== currentUser.username) { | |
| showTypingIndicator(payload.username); | |
| } | |
| } | |
| else if (type |