Spaces:
Running
Running
| <html lang="fa" dir="rtl"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
| <title>چت روم مینیمال | Minimal Chat</title> | |
| <!-- فونت وزیرمتن --> | |
| <link href="https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@v33.003/Vazirmatn-font-face.css" rel="stylesheet" type="text/css" /> | |
| <!-- آیکونها --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| :root { | |
| --primary-color: #6c5ce7; | |
| --secondary-color: #a29bfe; | |
| --bg-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| --glass-bg: rgba(255, 255, 255, 0.15); | |
| --glass-border: rgba(255, 255, 255, 0.2); | |
| --glass-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37); | |
| --text-color: #ffffff; | |
| --text-secondary: #e0e0e0; | |
| --danger: #ff7675; | |
| --success: #00b894; | |
| --msg-me: rgba(108, 92, 231, 0.6); | |
| --msg-other: rgba(255, 255, 255, 0.1); | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| outline: none; | |
| -webkit-tap-highlight-color: transparent; | |
| } | |
| body { | |
| font-family: 'Vazirmatn', sans-serif; | |
| background: var(--bg-gradient); | |
| color: var(--text-color); | |
| height: 100vh; | |
| overflow: hidden; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| } | |
| /* --- Glassmorphism Utilities --- */ | |
| .glass { | |
| background: var(--glass-bg); | |
| backdrop-filter: blur(12px); | |
| -webkit-backdrop-filter: blur(12px); | |
| border: 1px solid var(--glass-border); | |
| box-shadow: var(--glass-shadow); | |
| border-radius: 16px; | |
| } | |
| .glass-panel { | |
| background: rgba(0, 0, 0, 0.2); | |
| border-radius: 12px; | |
| padding: 10px; | |
| } | |
| /* --- App Container --- */ | |
| #app { | |
| width: 100%; | |
| height: 100%; | |
| max-width: 480px; /* Mobile view simulation on desktop */ | |
| position: relative; | |
| overflow: hidden; | |
| background: rgba(255, 255, 255, 0.05); | |
| } | |
| @media (min-width: 481px) { | |
| #app { | |
| height: 95vh; | |
| border-radius: 24px; | |
| } | |
| } | |
| /* --- Screens --- */ | |
| .screen { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| display: flex; | |
| flex-direction: column; | |
| transition: transform 0.3s cubic-bezier(0.4, 0.0, 0.2, 1), opacity 0.3s; | |
| opacity: 0; | |
| pointer-events: none; | |
| transform: scale(0.95); | |
| z-index: 1; | |
| } | |
| .screen.active { | |
| opacity: 1; | |
| pointer-events: all; | |
| transform: scale(1); | |
| z-index: 10; | |
| } | |
| /* --- Header --- */ | |
| header { | |
| padding: 15px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| z-index: 20; | |
| } | |
| .logo { | |
| font-weight: bold; | |
| font-size: 1.2rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .anycoder-link { | |
| font-size: 0.7rem; | |
| color: rgba(255,255,255,0.6); | |
| text-decoration: none; | |
| } | |
| /* --- Auth Screen --- */ | |
| #auth-screen { | |
| justify-content: center; | |
| align-items: center; | |
| text-align: center; | |
| padding: 30px; | |
| } | |
| .auth-input { | |
| width: 100%; | |
| padding: 15px; | |
| margin: 10px 0; | |
| border-radius: 12px; | |
| border: 1px solid var(--glass-border); | |
| background: rgba(255, 255, 255, 0.1); | |
| color: white; | |
| font-size: 1rem; | |
| text-align: center; | |
| transition: 0.3s; | |
| } | |
| .auth-input:focus { | |
| background: rgba(255, 255, 255, 0.2); | |
| border-color: var(--secondary-color); | |
| } | |
| .btn { | |
| padding: 12px 24px; | |
| border-radius: 12px; | |
| border: none; | |
| background: var(--primary-color); | |
| color: white; | |
| font-family: inherit; | |
| font-weight: bold; | |
| cursor: pointer; | |
| transition: transform 0.1s, background 0.3s; | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 8px; | |
| } | |
| .btn:active { | |
| transform: scale(0.96); | |
| } | |
| .btn-full { | |
| width: 100%; | |
| margin-top: 20px; | |
| } | |
| .btn-icon { | |
| width: 40px; | |
| height: 40px; | |
| padding: 0; | |
| border-radius: 50%; | |
| } | |
| /* --- Rooms List --- */ | |
| #rooms-screen { | |
| padding-top: 60px; /* Space for header */ | |
| } | |
| .room-list { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 15px; | |
| } | |
| .room-card { | |
| display: flex; | |
| align-items: center; | |
| padding: 15px; | |
| margin-bottom: 12px; | |
| cursor: pointer; | |
| transition: 0.2s; | |
| } | |
| .room-card:active { | |
| background: rgba(255, 255, 255, 0.1); | |
| transform: scale(0.98); | |
| } | |
| .room-avatar { | |
| width: 50px; | |
| height: 50px; | |
| border-radius: 50%; | |
| margin-left: 15px; | |
| object-fit: cover; | |
| border: 2px solid var(--secondary-color); | |
| } | |
| .room-info { | |
| flex: 1; | |
| } | |
| .room-name { | |
| font-weight: bold; | |
| font-size: 1.1rem; | |
| margin-bottom: 4px; | |
| } | |
| .room-last-msg { | |
| font-size: 0.85rem; | |
| color: var(--text-secondary); | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| max-width: 200px; | |
| } | |
| .fab { | |
| position: absolute; | |
| bottom: 30px; | |
| left: 30px; | |
| width: 60px; | |
| height: 60px; | |
| border-radius: 50%; | |
| background: var(--success); | |
| color: white; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| font-size: 1.5rem; | |
| box-shadow: 0 4px 15px rgba(0,0,0,0.3); | |
| cursor: pointer; | |
| z-index: 100; | |
| transition: transform 0.2s; | |
| } | |
| .fab:hover { | |
| transform: rotate(90deg); | |
| } | |
| /* --- Chat Screen --- */ | |
| #chat-screen { | |
| padding-top: 0; | |
| } | |
| .chat-header { | |
| height: 70px; | |
| display: flex; | |
| align-items: center; | |
| padding: 0 10px; | |
| border-bottom: 1px solid var(--glass-border); | |
| } | |
| .chat-body { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 15px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| } | |
| .message { | |
| max-width: 75%; | |
| padding: 10px 14px; | |
| border-radius: 18px; | |
| position: relative; | |
| animation: popIn 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); | |
| font-size: 0.95rem; | |
| line-height: 1.5; | |
| } | |
| @keyframes popIn { | |
| from { opacity: 0; transform: translateY(10px) scale(0.9); } | |
| to { opacity: 1; transform: translateY(0) scale(1); } | |
| } | |
| .message.me { | |
| align-self: flex-start; /* RTL: Start is Right */ | |
| background: var(--msg-me); | |
| border-bottom-right-radius: 4px; | |
| } | |
| .message.other { | |
| align-self: flex-end; /* RTL: End is Left */ | |
| background: var(--msg-other); | |
| border-bottom-left-radius: 4px; | |
| } | |
| .msg-meta { | |
| font-size: 0.7rem; | |
| opacity: 0.7; | |
| margin-top: 4px; | |
| text-align: left; /* Always left for time in bubbles usually, or right in RTL */ | |
| display: flex; | |
| justify-content: flex-end; | |
| gap: 5px; | |
| align-items: center; | |
| } | |
| .reply-preview { | |
| background: rgba(0,0,0,0.2); | |
| padding: 5px 8px; | |
| border-radius: 8px; | |
| margin-bottom: 8px; | |
| font-size: 0.8rem; | |
| border-right: 3px solid var(--secondary-color); | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .reply-owner { | |
| font-size: 0.7rem; | |
| font-weight: bold; | |
| color: var(--secondary-color); | |
| } | |
| .message-actions { | |
| position: absolute; | |
| top: -30px; | |
| left: 0; | |
| background: rgba(0,0,0,0.8); | |
| border-radius: 20px; | |
| padding: 5px; | |
| display: none; /* Shown on long press */ | |
| gap: 10px; | |
| white-space: nowrap; | |
| } | |
| .message.selected .message-actions { | |
| display: flex; | |
| } | |
| .chat-footer { | |
| padding: 10px; | |
| background: rgba(0, 0, 0, 0.2); | |
| display: flex; | |
| align-items: flex-end; | |
| gap: 10px; | |
| } | |
| .chat-input-wrapper { | |
| flex: 1; | |
| background: rgba(255, 255, 255, 0.1); | |
| border-radius: 20px; | |
| padding: 8px 15px; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .reply-bar { | |
| display: none; | |
| border-bottom: 1px solid rgba(255,255,255,0.1); | |
| padding-bottom: 5px; | |
| margin-bottom: 5px; | |
| font-size: 0.8rem; | |
| color: var(--secondary-color); | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .chat-input { | |
| width: 100%; | |
| background: transparent; | |
| border: none; | |
| color: white; | |
| resize: none; | |
| max-height: 100px; | |
| min-height: 24px; | |
| font-family: inherit; | |
| } | |
| /* --- Reactions --- */ | |
| .reaction-bar { | |
| display: flex; | |
| gap: 5px; | |
| margin-top: 5px; | |
| justify-content: flex-end; | |
| } | |
| .reaction-badge { | |
| font-size: 0.7rem; | |
| background: rgba(0,0,0,0.3); | |
| border-radius: 10px; | |
| padding: 2px 6px; | |
| display: flex; | |
| align-items: center; | |
| gap: 2px; | |
| } | |
| /* --- Modals & Overlays --- */ | |
| .modal-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0,0,0,0.6); | |
| backdrop-filter: blur(5px); | |
| z-index: 1000; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: 0.3s; | |
| } | |
| .modal-overlay.open { | |
| opacity: 1; | |
| pointer-events: all; | |
| } | |
| .modal { | |
| width: 85%; | |
| max-width: 350px; | |
| padding: 20px; | |
| transform: translateY(20px); | |
| transition: 0.3s; | |
| } | |
| .modal-overlay.open .modal { | |
| transform: translateY(0); | |
| } | |
| .modal h3 { | |
| margin-bottom: 15px; | |
| text-align: center; | |
| } | |
| .reaction-picker { | |
| display: flex; | |
| justify-content: space-around; | |
| margin-top: 15px; | |
| } | |
| .emoji-btn { | |
| font-size: 1.5rem; | |
| background: none; | |
| border: none; | |
| cursor: pointer; | |
| transition: transform 0.2s; | |
| } | |
| .emoji-btn:hover { | |
| transform: scale(1.3); | |
| } | |
| /* --- Toast Notification --- */ | |
| #toast { | |
| position: fixed; | |
| bottom: 80px; | |
| left: 50%; | |
| transform: translateX(-50%) translateY(20px); | |
| background: rgba(0,0,0,0.8); | |
| color: white; | |
| padding: 10px 20px; | |
| border-radius: 20px; | |
| font-size: 0.9rem; | |
| opacity: 0; | |
| transition: 0.3s; | |
| pointer-events: none; | |
| z-index: 2000; | |
| white-space: nowrap; | |
| } | |
| #toast.show { | |
| opacity: 1; | |
| transform: translateX(-50%) translateY(0); | |
| } | |
| /* Scrollbar */ | |
| ::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: rgba(255,255,255,0.2); | |
| border-radius: 3px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="app" class="glass"> | |
| <!-- Header (Global) --> | |
| <header class="glass-panel" style="margin: 10px; border-radius: 12px; position: absolute; top:0; width: calc(100% - 20px); z-index: 50;"> | |
| <div class="logo"> | |
| <i class="fa-solid fa-comments"></i> | |
| <span>مینیمال چت</span> | |
| </div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link"> | |
| Built with anycoder <i class="fa-solid fa-arrow-up-right-from-square"></i> | |
| </a> | |
| </header> | |
| <!-- 1. Auth Screen --> | |
| <section id="auth-screen" class="screen active"> | |
| <div style="text-align: center; margin-bottom: 30px;"> | |
| <i class="fa-solid fa-lock" style="font-size: 3rem; margin-bottom: 20px; opacity: 0.8;"></i> | |
| <h2>ورود به حساب</h2> | |
| <p style="opacity: 0.7; font-size: 0.9rem;">برای شروع شماره موبایل خود را وارد کنید</p> | |
| </div> | |
| <div id="step-phone"> | |
| <input type="tel" id="phone-input" class="auth-input" placeholder="0912..." maxlength="11"> | |
| <button class="btn btn-full" onclick="Auth.sendCode()">دریافت کد تایید</button> | |
| </div> | |
| <div id="step-code" style="display: none;"> | |
| <input type="number" id="code-input" class="auth-input" placeholder="کد 4 رقمی (تست: 1234)" maxlength="4"> | |
| <button class="btn btn-full" onclick="Auth.verifyCode()">تایید و ورود</button> | |
| <p style="margin-top: 15px; font-size: 0.8rem; cursor: pointer; opacity: 0.8;" onclick="Auth.reset()">ویرایش شماره</p> | |
| </div> | |
| </section> | |
| <!-- 2. Rooms List Screen --> | |
| <section id="rooms-screen" class="screen"> | |
| <div class="room-list" id="room-list-container"> | |
| <!-- Rooms injected here --> | |
| </div> | |
| <!-- Create Room FAB --> | |
| <div class="fab" onclick="UI.openModal('create-room-modal')"> | |
| <i class="fa-solid fa-plus"></i> | |
| </div> | |
| </section> | |
| <!-- 3. Chat Screen --> | |
| <section id="chat-screen" class="screen"> | |
| <div class="chat-header glass-panel" style="border-radius: 0; margin: 0; width: 100%;"> | |
| <button class="btn btn-icon" onclick="UI.showScreen('rooms-screen')"> | |
| <i class="fa-solid fa-arrow-right"></i> | |
| </button> | |
| <div class="room-info" style="margin-right: 10px; margin-left: 10px;"> | |
| <div class="room-name" id="chat-title">نام اتاق</div> | |
| <div style="font-size: 0.7rem; opacity: 0.7;">آنلاین</div> | |
| </div> | |
| <button class="btn btn-icon" onclick="Chat.copyLink()"> | |
| <i class="fa-solid fa-link"></i> | |
| </button> | |
| </div> | |
| <div class="chat-body" id="chat-container" ontouchstart="Gestures.handleTouchStart(event)" ontouchend="Gestures.handleTouchEnd(event)"> | |
| <!-- Messages injected here --> | |
| </div> | |
| <div class="chat-footer"> | |
| <button class="btn btn-icon" style="background: rgba(255,255,255,0.1);" onclick="UI.openModal('reactions-modal')"> | |
| <i class="fa-regular fa-face-smile"></i> | |
| </button> | |
| <div class="chat-input-wrapper"> | |
| <div class="reply-bar" id="reply-bar"> | |
| <span id="reply-text-preview">پاسخ به...</span> | |
| <i class="fa-solid fa-xmark" style="cursor: pointer;" onclick="Chat.cancelReply()"></i> | |
| </div> | |
| <textarea id="message-input" class="chat-input" rows="1" placeholder="پیام خود را بنویسید..."></textarea> | |
| </div> | |
| <button class="btn btn-icon" style="background: var(--primary-color);" onclick="Chat.sendMessage()"> | |
| <i class="fa-solid fa-paper-plane"></i> | |
| </button> | |
| </div> | |
| </section> | |
| </div> | |
| <!-- Modals --> | |
| <!-- Create Room Modal --> | |
| <div id="create-room-modal" class="modal-overlay"> | |
| <div class="modal glass"> | |
| <h3>ایجاد اتاق جدید</h3> | |
| <input type="text" id="new-room-name" class="auth-input" placeholder="نام اتاق"> | |
| <div style="display: flex; gap: 10px; margin-top: 20px;"> | |
| <button class="btn" style="flex: 1; background: rgba(255,255,255,0.2);" onclick="UI.closeModal('create-room-modal')">لغو</button> | |
| <button class="btn" style="flex: 1; background: var(--success);" onclick="RoomManager.createRoom()">ساخت</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Reaction Picker Modal (Contextual) --> | |
| <div id="reactions-modal" class="modal-overlay"> | |
| <div class="modal glass"> | |
| <h3>انتخاب ایموجی</h3> | |
| <div class="reaction-picker"> | |
| <button class="emoji-btn" onclick="Chat.addReaction('❤️')">❤️</button> | |
| <button class="emoji-btn" onclick="Chat.addReaction('👍')">👍</button> | |
| <button class="emoji-btn" onclick="Chat.addReaction('😂')">😂</button> | |
| <button class="emoji-btn" onclick="Chat.addReaction('😮')">😮</button> | |
| <button class="emoji-btn" onclick="Chat.addReaction('😢')">😢</button> | |
| <button class="emoji-btn" onclick="Chat.addReaction('😡')">😡</button> | |
| </div> | |
| <div style="text-align: center; margin-top: 15px;"> | |
| <button class="btn" style="background: rgba(255,255,255,0.2); font-size: 0.8rem; padding: 5px 15px;" onclick="UI.closeModal('reactions-modal')">انصراف</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Context Menu (Long Press) --> | |
| <div id="context-menu" class="modal-overlay" style="z-index: 3000;"> | |
| <div class="modal glass" style="position: absolute; bottom: 100px; width: auto; min-width: 150px; padding: 5px;"> | |
| <button class="btn" style="width: 100%; justify-content: flex-start; background: transparent;" onclick="Chat.replyToMsg()"> | |
| <i class="fa-solid fa-reply"></i> پاسخ | |
| </button> | |
| <button class="btn" style="width: 100%; justify-content: flex-start; background: transparent;" onclick="Chat.deleteMsg()"> | |
| <i class="fa-solid fa-trash"></i> حذف | |
| </button> | |
| <button class="btn" style="width: 100%; justify-content: flex-start; background: transparent;" onclick="UI.closeModal('context-menu')"> | |
| <i class="fa-solid fa-xmark"></i> لغو | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Toast --> | |
| <div id="toast">پیام سیستم</div> | |
| <script> | |
| // --- State Management --- | |
| const State = { | |
| user: null, | |
| rooms: [], | |
| activeRoomId: null, | |
| messages: {}, // { roomId: [msgs] } | |
| selectedMsgId: null, | |
| replyToMsgId: null | |
| }; | |
| // --- Data Mockup --- | |
| const MockData = { | |
| avatars: [ | |
| 'https://picsum.photos/seed/room1/100/100', | |
| 'https://picsum.photos/seed/room2/100/100', | |
| 'https://picsum.photos/seed/room3/100/100' | |
| ] | |
| }; | |
| // --- Utility --- | |
| const Utils = { | |
| id: () => Math.random().toString(36).substr(2, 9), | |
| time: () => { | |
| const d = new Date(); | |
| return d.getHours().toString().padStart(2, '0') + ':' + d.getMinutes().toString().padStart(2, '0'); | |
| }, | |
| save: () => localStorage.setItem('minimalChatState', JSON.stringify(State)), | |
| load: () => { | |
| const data = localStorage.getItem('minimalChatState'); | |
| if (data) { | |
| const parsed = JSON.parse(data); | |
| State.user = parsed.user; | |
| State.rooms = parsed.rooms; | |
| State.messages = parsed.messages; | |
| } | |
| } | |
| }; | |
| // --- Auth Module --- | |
| const Auth = { | |
| sendCode: () => { | |
| const phone = document.getElementById('phone-input').value; | |
| if (phone.length < 10 || !phone.startsWith('09')) { | |
| UI.toast('شماره موبایل نامعتبر است'); | |
| return; | |
| } | |
| UI.toast('کد تایید ارسال شد: 1234'); | |
| document.getElementById('step-phone').style.display = 'none'; | |
| document.getElementById('step-code').style.display = 'block'; | |
| }, | |
| verifyCode: () => { | |
| const code = document.getElementById('code-input').value; | |
| if (code === '1234') { | |
| const phone = document.getElementById('phone-input').value; | |
| State.user = { phone: phone, name: 'کاربر' }; | |
| Utils.save(); | |
| UI.toast('خوش آمدید!'); | |
| UI.showScreen('rooms-screen'); | |
| RoomManager.render(); | |
| } else { | |
| UI.toast('کد اشتباه است'); | |
| } | |
| }, | |
| reset: () => { | |
| document.getElementById('step-phone').style.display = 'block'; | |
| document.getElementById('step-code').style.display = 'none'; | |
| }, | |
| check: () => { | |
| Utils.load(); | |
| if (State.user) { | |
| UI.showScreen('rooms-screen'); | |
| RoomManager.render(); | |
| } | |
| } | |
| }; | |
| // --- Room Manager Module --- | |
| const RoomManager = { | |
| createRoom: () => { | |
| const name = document.getElementById('new-room-name').value; | |
| if (!name) return UI.toast('نام اتاق الزامی است'); | |
| if (State.rooms.length >= 3) { | |
| UI.closeModal('create-room-modal'); | |
| return UI.toast('حداکثر ۳ اتاق مجاز است'); | |
| } | |
| const newRoom = { | |
| id: Utils.id(), | |
| name: name, | |
| avatar: MockData.avatars[State.rooms.length % 3], | |
| lastMsg: 'اتاق جدید ایجاد شد' | |
| }; | |
| State.rooms.unshift(newRoom); | |
| State.messages[newRoom.id] = []; | |
| Utils.save(); | |
| document.getElementById('new-room-name').value = ''; | |
| UI.closeModal('create-room-modal'); | |
| RoomManager.render(); | |
| UI.toast('اتاق ایجاد شد'); | |
| }, | |
| joinRoom: (id) => { | |
| State.activeRoomId = id; | |
| const room = State.rooms.find(r => r.id === id); | |
| document.getElementById('chat-title').innerText = room.name; | |
| Chat.render(); | |
| UI.showScreen('chat-screen'); | |
| }, | |
| render: () => { | |
| const container = document.getElementById('room-list-container'); | |
| container.innerHTML = ''; | |
| if (State.rooms.length === 0) { | |
| container.innerHTML = '<div style="text-align:center; opacity:0.6; margin-top:50px;">هنوز اتاقی ندارید.<br>برای ساخت دکمه + را بزنید.</div>'; | |
| return; | |
| } | |
| State.rooms.forEach(room => { | |
| const el = document.createElement('div'); | |
| el.className = 'room-card glass'; | |
| el.onclick = () => RoomManager.joinRoom(room.id); | |
| el.innerHTML = ` | |
| <img src="${room.avatar}" class="room-avatar" alt="Avatar"> | |
| <div class="room-info"> | |
| <div class="room-name">${room.name}</div> | |
| <div class="room-last-msg">${room.lastMsg}</div> | |
| </div> | |
| <div style="font-size:0.7rem; opacity:0.5;">${Utils.time()}</div> | |
| `; | |
| container.appendChild(el); | |
| }); | |
| } | |
| }; | |
| // --- Chat System Module --- | |
| const Chat = { | |
| sendMessage: () => { | |
| const input = document.getElementById('message-input'); | |
| const text = input.value.trim(); | |
| if (!text) return; | |
| const msg = { | |
| id: Utils.id(), | |
| text: text, | |
| sender: 'me', | |
| time: Utils.time(), | |
| replyTo: State.replyToMsgId ? Chat.findMsg(State.replyToMsgId) : null, | |
| reactions: [] | |
| }; | |
| if (!State.messages[State.activeRoomId]) State.messages[State.activeRoomId] = []; | |
| State.messages[State.activeRoomId].push(msg); | |
| // Update last message in room list | |
| const room = State.rooms.find(r => r.id === State.activeRoomId); | |
| room.lastMsg = text; | |
| Chat.clearReply(); | |
| input.value = ''; | |
| Chat.render(); | |
| Utils.save(); | |
| // Simulate reply | |
| setTimeout(() => { | |
| Chat.receiveMockReply(); | |
| }, 2000); | |
| }, | |
| receiveMockReply: () => { | |
| const replies = ['خیلی جالبه!', 'باشه حتما', 'میفهمم 😊', 'خوبه', 'فعلاً سرم شلوغه']; | |
| const text = replies[Math.floor(Math.random() * replies.length)]; | |
| const msg = { | |
| id: Utils.id(), | |
| text: text, | |
| sender: 'other', | |
| time: Utils.time(), | |
| replyTo: null, | |
| reactions: [] | |
| }; | |
| State.messages[State.activeRoomId].push(msg); | |
| Chat.render(); | |
| Utils.save(); | |
| }, | |
| findMsg: (id) => { | |
| return State.messages[State.activeRoomId].find(m => m.id === id); | |
| }, | |
| render: () => { | |
| const container = document.getElementById('chat-container'); | |
| container.innerHTML = ''; | |
| const msgs = State.messages[State.activeRoomId] || []; | |
| msgs.forEach(msg => { | |
| const el = document.createElement('div'); | |
| el.className = `message ${msg.sender}`; | |
| el.dataset.id = msg.id; | |
| // Long press event attached to element | |
| el.addEventListener('touchstart', (e) => Gestures.msgTouchStart(e, msg.id), {passive: true}); | |
| el.addEventListener('touchend', (e) => Gestures.msgTouchEnd(e), {passive: true}); | |
| let html = ''; | |
| // Reply Preview | |
| if (msg.replyTo) { | |
| html += ` | |
| <div class="reply-preview"> | |
| <span class="reply-owner">${msg.replyTo.sender === 'me' ? 'شما' : 'دیگران'}</span> | |
| <span>${msg.replyTo.text}</span> | |
| </div> | |
| `; | |
| } | |
| html += `<div>${msg.text}</div>`; | |
| // Reactions | |
| if (msg.reactions && msg.reactions.length > 0) { | |
| const uniqueReactions = [...new Set(msg.reactions)]; | |
| html += `<div class="reaction-bar">`; | |
| uniqueReactions.forEach(r => { | |
| html += `<span class="reaction-badge">${r}</span>`; | |
| }); | |
| html += `</div>`; | |
| } | |
| html += `<div class="msg-meta">${msg.time} <i class="fa-solid fa-check-double" style="font-size:0.6rem"></i></div>`; | |
| // Context Actions (Hidden by default) | |
| html += ` | |
| <div class="message-actions"> | |
| <button class="btn-icon" style="width:30px; height:30px; font-size:0.8rem;" onclick="Chat.prepareReply('${msg.id}')">پاسخ</button> | |
| <button class="btn-icon" style="width:30px; height:30px; font-size:0.8rem; color:var(--danger)" onclick="Chat.deleteMsg('${msg.id}')">حذف</button> | |
| </div> | |
| `; | |
| el.innerHTML = html; | |
| container.appendChild(el); | |
| }); | |
| container.scrollTop = container.scrollHeight; | |
| }, | |
| prepareReply: (id) => { | |
| const msg = Chat.findMsg(id); | |
| if (!msg) return; | |
| State.replyToMsgId = id; | |
| const preview = document.getElementById('reply-bar'); | |
| const text = document.getElementById('reply-text-preview'); | |
| preview.style.display = 'flex'; | |
| text.innerText = `پاسخ به: ${msg.text}`; | |
| UI.closeModal('context-menu'); | |
| document.getElementById('message-input').focus(); | |
| }, | |
| clearReply: () => { | |
| State.replyToMsgId = null; | |
| document.getElementById('reply-bar').style.display = 'none'; | |
| }, | |
| deleteMsg: (id) => { | |
| if(!id) id = State.selectedMsgId; | |
| const msgs = State.messages[State.activeRoomId]; | |
| const idx = msgs.findIndex(m => m.id === id); | |
| if (idx > -1) { | |
| msgs.splice(idx, 1); | |
| Chat.render(); | |
| Utils.save(); | |
| UI.toast('پیام حذف شد'); | |
| } | |
| UI.closeModal('context-menu'); | |
| }, | |
| copyLink: () => { | |
| // Simulate copying a room link | |
| const dummyLink = `https://minimalchat.app/room/${State.activeRoomId}`; | |
| navigator.clipboard.writeText(dummyLink).then(() => { | |
| UI.toast('لینک اتاق کپی شد'); | |
| }); | |
| }, | |
| addReaction: (emoji) => { | |
| const msg = Chat.findMsg(State.selectedMsgId); | |
| if (msg) { | |
| if (!msg.reactions) msg.reactions = []; | |
| msg.reactions.push(emoji); | |
| Chat.render(); | |
| Utils.save(); | |
| } | |
| UI.closeModal('reactions-modal'); | |
| UI.closeModal('context-menu'); | |
| }, | |
| cancelReply: () => Chat.clearReply() | |
| }; | |
| // --- UI Module --- | |
| const UI = { | |
| showScreen: (screenId) => { | |
| document.querySelectorAll('.screen').forEach(s => s.classList.remove('active')); | |
| document.getElementById(screenId).classList.add('active'); | |
| }, | |
| toast: (msg) => { | |
| const el = document.getElementById('toast'); | |
| el.innerText = msg; | |
| el.classList.add('show'); | |
| setTimeout(() => el.classList.remove('show'), 3000); | |
| }, | |
| openModal: (id) => { | |
| document.getElementById(id).classList.add('open'); | |
| }, | |
| closeModal: (id) => { | |
| document.getElementById(id).classList.remove('open'); | |
| } | |
| }; | |
| // --- Gestures Module --- | |
| const Gestures = { | |
| touchStartX: 0, | |
| touchStartY: 0, | |
| longPressTimer: null, | |
| isLongPress: false, | |
| handleTouchStart: (e) => { | |
| Gestures.touchStartX = e.changedTouches[0].screenX; | |
| }, | |
| handleTouchEnd: (e) => { | |
| const touchEndX = e.changedTouches[0].screenX; | |
| // Swipe Right (in RTL, Right is Back) -> Actually Swipe Left is back visually in LTR, but in RTL: | |
| // To go "Back", usually swipe from Right to Left. | |
| // Let's implement: Swipe from Right to Left (negative delta) to go back. | |
| if (touchEndX < Gestures.touchStartX - 50) { | |
| // Swipe detected (Right to Left) | |
| const currentScreen = document.querySelector('.screen.active').id; | |
| if (currentScreen === 'chat-screen') { | |
| UI.showScreen('rooms-screen'); | |
| } | |
| } | |
| }, | |
| msgTouchStart: (e, id) => { | |
| Gestures.isLongPress = false; | |
| Gestures.longPressTimer = setTimeout(() => { | |
| Gestures.isLongPress = true; | |
| State.selectedMsgId = id; | |
| UI.openModal('context-menu'); | |
| // Visual feedback | |
| e.target.closest('.message').classList.add('selected'); | |
| }, 600); | |
| }, | |
| msgTouchEnd: (e) => { | |
| clearTimeout(Gestures.longPressTimer); | |
| if (Gestures.isLongPress) { | |
| e.target.closest('.message').classList.remove('selected'); | |
| // Prevent click if it was a long press | |
| // e.preventDefault(); | |
| } | |
| } | |
| }; | |
| // --- Init --- | |
| window.addEventListener('DOMContentLoaded', () => { | |
| Auth.check(); | |
| // Textarea auto-resize | |
| const tx = document.getElementById('message-input'); | |
| tx.addEventListener('input', function() { | |
| this.style.height = 'auto'; | |
| this.style.height = (this.scrollHeight) + 'px'; | |
| }); | |
| }); | |
| </script> | |
| </body> | |
| </html> |