Spaces:
Sleeping
Sleeping
| <html lang="he" dir="rtl"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Chat View - Telegram Style</title> | |
| <style> | |
| /* ===== Telegram-like Chat Viewer ===== */ | |
| :root { | |
| --bg-primary: #0e1621; | |
| --bg-secondary: #17212b; | |
| --bg-message: #182533; | |
| --bg-hover: #1e2c3a; | |
| --bg-reply: rgba(77, 184, 255, 0.08); | |
| --bg-forward: rgba(100, 191, 71, 0.08); | |
| --text-primary: #f5f5f5; | |
| --text-secondary: #8b9fad; | |
| --text-link: #6ab2f2; | |
| --accent-blue: #6ab2f2; | |
| --accent-green: #6dc264; | |
| --border-reply: #6ab2f2; | |
| --border-forward: #6dc264; | |
| --date-badge: #1b2a38; | |
| --nav-bg: #17212b; | |
| --nav-border: #0e1621; | |
| } | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; | |
| font-size: 14px; | |
| line-height: 1.5; | |
| background-color: var(--bg-primary); | |
| color: var(--text-primary); | |
| } | |
| /* ===== Navigation ===== */ | |
| .nav-bar { | |
| position: fixed; | |
| top: 0; left: 0; right: 0; | |
| z-index: 100; | |
| background-color: var(--nav-bg); | |
| border-bottom: 1px solid var(--nav-border); | |
| padding: 0 16px; | |
| } | |
| .nav-content { | |
| max-width: 800px; | |
| margin: 0 auto; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| height: 56px; | |
| } | |
| .nav-title { | |
| font-size: 18px; | |
| font-weight: 700; | |
| color: var(--text-primary); | |
| } | |
| .nav-links { display: flex; gap: 4px; } | |
| .nav-links a { | |
| color: var(--accent-blue); | |
| text-decoration: none; | |
| padding: 8px 14px; | |
| border-radius: 8px; | |
| font-size: 13px; | |
| transition: background 0.15s; | |
| } | |
| .nav-links a:hover { background-color: var(--bg-hover); } | |
| .nav-links a.active { | |
| background-color: var(--accent-blue); | |
| color: var(--bg-primary); | |
| } | |
| /* ===== Chat Area ===== */ | |
| .chat-wrap { | |
| padding-top: 56px; | |
| min-height: 100vh; | |
| } | |
| .chat-body { | |
| max-width: 680px; | |
| margin: 0 auto; | |
| padding: 0 12px 80px; | |
| } | |
| .history { padding: 8px 0; } | |
| /* ===== Load More ===== */ | |
| .load-more { | |
| text-align: center; | |
| padding: 16px; | |
| } | |
| .load-more button { | |
| padding: 10px 24px; | |
| background-color: var(--bg-secondary); | |
| color: var(--accent-blue); | |
| border: 1px solid rgba(106, 178, 242, 0.3); | |
| border-radius: 20px; | |
| cursor: pointer; | |
| font-size: 14px; | |
| transition: all 0.15s; | |
| } | |
| .load-more button:hover { | |
| background-color: var(--bg-hover); | |
| border-color: var(--accent-blue); | |
| } | |
| .load-more button:disabled { opacity: 0.4; cursor: not-allowed; } | |
| /* ===== Date Separator ===== */ | |
| .date-separator { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 12px 0; | |
| position: sticky; | |
| top: 60px; | |
| z-index: 10; | |
| } | |
| .date-badge { | |
| padding: 4px 12px; | |
| background-color: var(--date-badge); | |
| border-radius: 12px; | |
| color: var(--text-secondary); | |
| font-size: 13px; | |
| font-weight: 500; | |
| box-shadow: 0 1px 4px rgba(0,0,0,0.2); | |
| } | |
| /* ===== Message ===== */ | |
| .msg { | |
| display: flex; | |
| align-items: flex-start; | |
| gap: 10px; | |
| padding: 3px 8px; | |
| border-radius: 8px; | |
| transition: background 0.15s; | |
| } | |
| .msg:hover { background-color: var(--bg-hover); } | |
| .msg.joined { padding-top: 1px; } | |
| .msg.joined .avatar-wrap { visibility: hidden; height: 0; } | |
| /* ===== Avatar ===== */ | |
| .avatar-wrap { flex-shrink: 0; padding-top: 2px; } | |
| .avatar { | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-weight: 600; | |
| font-size: 15px; | |
| color: #fff; | |
| cursor: pointer; | |
| } | |
| .avatar:hover { filter: brightness(1.15); } | |
| /* 8 Telegram avatar colors */ | |
| .c1 { background: #ff5555; } | |
| .c2 { background: #64bf47; } | |
| .c3 { background: #ffab00; } | |
| .c4 { background: #4f9cd9; } | |
| .c5 { background: #9884e8; } | |
| .c6 { background: #e671a5; } | |
| .c7 { background: #47bcd1; } | |
| .c8 { background: #ff8c44; } | |
| /* Name colors to match avatars */ | |
| .name-c1 { color: #ff5555; } | |
| .name-c2 { color: #64bf47; } | |
| .name-c3 { color: #ffab00; } | |
| .name-c4 { color: #4f9cd9; } | |
| .name-c5 { color: #9884e8; } | |
| .name-c6 { color: #e671a5; } | |
| .name-c7 { color: #47bcd1; } | |
| .name-c8 { color: #ff8c44; } | |
| /* ===== Message Body ===== */ | |
| .msg-body { | |
| flex: 1; | |
| min-width: 0; | |
| } | |
| /* Header: name + time */ | |
| .msg-header { | |
| display: flex; | |
| align-items: baseline; | |
| gap: 8px; | |
| margin-bottom: 2px; | |
| } | |
| .msg-name { | |
| font-weight: 600; | |
| font-size: 14px; | |
| cursor: pointer; | |
| } | |
| .msg-name:hover { text-decoration: underline; } | |
| .msg-time { | |
| color: var(--text-secondary); | |
| font-size: 12px; | |
| white-space: nowrap; | |
| } | |
| .msg-edited { | |
| color: var(--text-secondary); | |
| font-size: 11px; | |
| font-style: italic; | |
| } | |
| /* ===== Reply Block ===== */ | |
| .reply-block { | |
| display: flex; | |
| gap: 0; | |
| margin: 4px 0 6px; | |
| padding: 6px 10px; | |
| border-radius: 6px; | |
| border-right: 3px solid var(--border-reply); | |
| background: var(--bg-reply); | |
| cursor: pointer; | |
| overflow: hidden; | |
| transition: background 0.15s; | |
| } | |
| .reply-block:hover { background: rgba(106, 178, 242, 0.15); } | |
| .reply-content { min-width: 0; } | |
| .reply-name { | |
| font-weight: 600; | |
| font-size: 13px; | |
| color: var(--accent-blue); | |
| } | |
| .reply-text { | |
| font-size: 13px; | |
| color: var(--text-secondary); | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| max-width: 400px; | |
| } | |
| /* ===== Forward Block ===== */ | |
| .forward-block { | |
| margin: 4px 0 6px; | |
| padding: 6px 10px; | |
| border-radius: 6px; | |
| border-right: 3px solid var(--border-forward); | |
| background: var(--bg-forward); | |
| } | |
| .forward-label { | |
| font-size: 12px; | |
| color: var(--text-secondary); | |
| } | |
| .forward-name { | |
| font-weight: 600; | |
| font-size: 13px; | |
| color: var(--accent-green); | |
| } | |
| /* ===== Message Text ===== */ | |
| .msg-text { | |
| word-wrap: break-word; | |
| overflow-wrap: break-word; | |
| line-height: 1.55; | |
| unicode-bidi: plaintext; | |
| text-align: start; | |
| white-space: pre-wrap; | |
| } | |
| .msg-text a { | |
| color: var(--text-link); | |
| text-decoration: none; | |
| } | |
| .msg-text a:hover { text-decoration: underline; } | |
| /* Mention */ | |
| .mention { | |
| color: var(--accent-blue); | |
| font-weight: 500; | |
| cursor: pointer; | |
| } | |
| .mention:hover { text-decoration: underline; } | |
| /* Hashtag */ | |
| .hashtag { | |
| color: var(--accent-blue); | |
| cursor: pointer; | |
| } | |
| /* Code */ | |
| .msg-text code { | |
| font-family: 'Consolas', 'Monaco', 'Courier New', monospace; | |
| background: rgba(255,255,255,0.06); | |
| padding: 1px 5px; | |
| border-radius: 4px; | |
| font-size: 13px; | |
| } | |
| .msg-text pre { | |
| background: rgba(0,0,0,0.3); | |
| padding: 10px 12px; | |
| border-radius: 8px; | |
| margin: 6px 0; | |
| overflow-x: auto; | |
| font-family: 'Consolas', 'Monaco', 'Courier New', monospace; | |
| font-size: 13px; | |
| line-height: 1.4; | |
| } | |
| /* ===== Entities (links, media) ===== */ | |
| .entity-links { | |
| margin-top: 6px; | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 6px; | |
| } | |
| .entity-link { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 5px; | |
| padding: 4px 10px; | |
| background: rgba(106, 178, 242, 0.1); | |
| border-radius: 8px; | |
| font-size: 13px; | |
| color: var(--text-link); | |
| text-decoration: none; | |
| max-width: 350px; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| transition: background 0.15s; | |
| } | |
| .entity-link:hover { | |
| background: rgba(106, 178, 242, 0.2); | |
| text-decoration: none; | |
| } | |
| .entity-link .link-icon { font-size: 11px; } | |
| .entity-link .link-domain { | |
| opacity: 0.7; | |
| font-size: 12px; | |
| } | |
| /* ===== Media Badge ===== */ | |
| .media-badge { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| padding: 5px 10px; | |
| background: var(--bg-secondary); | |
| border-radius: 8px; | |
| margin-top: 6px; | |
| font-size: 13px; | |
| color: var(--text-secondary); | |
| } | |
| .media-badge .media-icon { font-size: 14px; } | |
| /* ===== Time for joined messages ===== */ | |
| .msg-time-inline { | |
| color: var(--text-secondary); | |
| font-size: 12px; | |
| margin-top: 2px; | |
| opacity: 0; | |
| transition: opacity 0.15s; | |
| } | |
| .msg:hover .msg-time-inline { opacity: 1; } | |
| /* ===== Selected (highlight on go-to) ===== */ | |
| .msg.selected { | |
| background-color: rgba(106, 178, 242, 0.15); | |
| transition: background-color 2s ease; | |
| } | |
| /* ===== Scroll-to-bottom ===== */ | |
| .scroll-btn { | |
| position: fixed; | |
| bottom: 24px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| width: 44px; | |
| height: 44px; | |
| background: var(--bg-secondary); | |
| color: var(--accent-blue); | |
| border: 1px solid rgba(106, 178, 242, 0.3); | |
| border-radius: 50%; | |
| cursor: pointer; | |
| font-size: 20px; | |
| display: none; | |
| align-items: center; | |
| justify-content: center; | |
| box-shadow: 0 2px 12px rgba(0,0,0,0.4); | |
| z-index: 80; | |
| transition: all 0.15s; | |
| } | |
| .scroll-btn.visible { display: flex; } | |
| .scroll-btn:hover { | |
| background: var(--accent-blue); | |
| color: var(--bg-primary); | |
| } | |
| /* ===== Loading ===== */ | |
| .loading { | |
| text-align: center; | |
| padding: 24px; | |
| color: var(--text-secondary); | |
| } | |
| .spinner { | |
| display: inline-block; | |
| width: 24px; height: 24px; | |
| border: 3px solid var(--bg-secondary); | |
| border-top-color: var(--accent-blue); | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| margin-bottom: 8px; | |
| } | |
| @keyframes spin { to { transform: rotate(360deg); } } | |
| /* ===== Toast ===== */ | |
| .toast { | |
| position: fixed; | |
| bottom: 80px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| background: rgba(0,0,0,0.85); | |
| color: #fff; | |
| padding: 10px 24px; | |
| border-radius: 20px; | |
| z-index: 200; | |
| opacity: 0; | |
| transition: opacity 0.3s; | |
| font-size: 13px; | |
| } | |
| .toast.visible { opacity: 1; } | |
| /* ===== Responsive ===== */ | |
| @media (max-width: 700px) { | |
| .nav-links a { padding: 6px 8px; font-size: 12px; } | |
| .chat-body { padding: 0 4px 80px; } | |
| .reply-text { max-width: 200px; } | |
| .entity-link { max-width: 250px; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <nav class="nav-bar"> | |
| <div class="nav-content"> | |
| <div class="nav-title">Chat View</div> | |
| <div class="nav-links"> | |
| <a href="/">Overview</a> | |
| <a href="/users">Users</a> | |
| <a href="/chat" class="active">Chat</a> | |
| <a href="/search">Search</a> | |
| <a href="/ai-search">AI Search</a> | |
| <a href="/moderation">Moderation</a> | |
| <a href="/settings">Settings</a> | |
| <a href="/maintenance">🔒</a> | |
| </div> | |
| </div> | |
| </nav> | |
| <div class="chat-wrap"> | |
| <div class="chat-body"> | |
| <div class="history" id="history"> | |
| <div class="load-more" id="load-more-top"> | |
| <button onclick="loadOlderMessages()" id="load-older-btn">↑ Load earlier messages</button> | |
| </div> | |
| <div id="messages-container"></div> | |
| <div class="loading" id="loading"> | |
| <div class="spinner"></div> | |
| <div>Loading messages...</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <button class="scroll-btn" id="scroll-bottom" onclick="scrollToBottom()">↓</button> | |
| <div class="toast" id="toast"></div> | |
| <script> | |
| // ===== State ===== | |
| let allMessages = []; | |
| let oldestOffset = 0; | |
| let totalMessages = 0; | |
| let loading = false; | |
| let initialLoad = true; | |
| const BATCH_SIZE = 100; | |
| const userColors = {}; | |
| // ===== Utilities ===== | |
| function getUserColor(userId) { | |
| if (!userColors[userId]) { | |
| let hash = 0; | |
| const str = String(userId); | |
| for (let i = 0; i < str.length; i++) { | |
| hash = str.charCodeAt(i) + ((hash << 5) - hash); | |
| } | |
| userColors[userId] = (Math.abs(hash) % 8) + 1; | |
| } | |
| return userColors[userId]; | |
| } | |
| function getInitials(name) { | |
| if (!name) return '?'; | |
| const parts = name.trim().split(/\s+/); | |
| if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase(); | |
| return name.substring(0, 2).toUpperCase(); | |
| } | |
| function formatDate(dateStr) { | |
| if (!dateStr) return ''; | |
| // Add 'Z' to treat as UTC (Telegram stores dates in UTC without timezone indicator) | |
| if (!dateStr.endsWith('Z') && !dateStr.includes('+')) dateStr += 'Z'; | |
| const d = new Date(dateStr); | |
| return d.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'Asia/Jerusalem' }); | |
| } | |
| function formatTime(dateStr) { | |
| if (!dateStr) return ''; | |
| // Add 'Z' to treat as UTC (Telegram stores dates in UTC without timezone indicator) | |
| if (!dateStr.endsWith('Z') && !dateStr.includes('+')) dateStr += 'Z'; | |
| const d = new Date(dateStr); | |
| return d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false, timeZone: 'Asia/Jerusalem' }); | |
| } | |
| function escapeHtml(text) { | |
| if (!text) return ''; | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| } | |
| function getDomain(url) { | |
| try { | |
| return new URL(url).hostname.replace('www.', ''); | |
| } catch { | |
| return url.substring(0, 30); | |
| } | |
| } | |
| // ===== Text Formatting ===== | |
| function formatMessageText(text, entities) { | |
| if (!text) return ''; | |
| let html = escapeHtml(text); | |
| // Auto-linkify URLs in text | |
| html = html.replace( | |
| /(https?:\/\/[^\s<]+)/g, | |
| '<a href="$1" target="_blank" rel="noopener">$1</a>' | |
| ); | |
| // Highlight @mentions | |
| html = html.replace( | |
| /@(\w{3,})/g, | |
| '<span class="mention">@$1</span>' | |
| ); | |
| // Highlight #hashtags | |
| html = html.replace( | |
| /#(\w{2,})/g, | |
| '<span class="hashtag">#$1</span>' | |
| ); | |
| // Convert newlines to <br> | |
| html = html.replace(/\n/g, '<br>'); | |
| return html; | |
| } | |
| // ===== Render Message ===== | |
| function renderMessage(msg, prevMsg) { | |
| const frag = document.createDocumentFragment(); | |
| // Date separator | |
| const msgDate = msg.date ? msg.date.split('T')[0] : ''; | |
| const prevDate = prevMsg && prevMsg.date ? prevMsg.date.split('T')[0] : ''; | |
| if (msgDate !== prevDate) { | |
| const sep = document.createElement('div'); | |
| sep.className = 'date-separator'; | |
| sep.innerHTML = `<div class="date-badge">${formatDate(msg.date)}</div>`; | |
| frag.appendChild(sep); | |
| } | |
| // Joined message? (same user, same day, within 5 minutes) | |
| const isJoined = prevMsg && | |
| prevMsg.from_id === msg.from_id && | |
| msgDate === prevDate && | |
| !msg.forwarded_from && | |
| !prevMsg.forwarded_from && | |
| timeDiffMinutes(prevMsg.date, msg.date) < 5; | |
| const colorNum = getUserColor(msg.from_id); | |
| const el = document.createElement('div'); | |
| el.className = `msg${isJoined ? ' joined' : ''}`; | |
| el.id = `message${msg.message_id || msg.id}`; | |
| let html = ''; | |
| // Avatar | |
| html += `<div class="avatar-wrap"> | |
| <div class="avatar c${colorNum}">${getInitials(msg.from_name)}</div> | |
| </div>`; | |
| // Body | |
| html += '<div class="msg-body">'; | |
| // Header (name + time) - only for first message in group | |
| if (!isJoined) { | |
| html += `<div class="msg-header"> | |
| <span class="msg-name name-c${colorNum}">${escapeHtml(msg.from_name || 'Unknown')}</span> | |
| <span class="msg-time">${formatTime(msg.date)}</span> | |
| ${msg.is_edited ? '<span class="msg-edited">edited</span>' : ''} | |
| </div>`; | |
| } | |
| // Forward block | |
| if (msg.forwarded_from) { | |
| html += `<div class="forward-block"> | |
| <div class="forward-label">Forwarded message</div> | |
| <div class="forward-name">${escapeHtml(msg.forwarded_from)}</div> | |
| </div>`; | |
| } | |
| // Reply block | |
| if (msg.reply_to_message_id && msg.reply_to_name) { | |
| html += `<div class="reply-block" onclick="goToMessage(${msg.reply_to_message_id})"> | |
| <div class="reply-content"> | |
| <div class="reply-name">${escapeHtml(msg.reply_to_name)}</div> | |
| <div class="reply-text">${escapeHtml(msg.reply_to_text || '')}</div> | |
| </div> | |
| </div>`; | |
| } | |
| // Message text | |
| if (msg.text) { | |
| html += `<div class="msg-text">${formatMessageText(msg.text, msg.entities)}</div>`; | |
| } | |
| // Entity links (extracted from DB) | |
| const links = (msg.entities || []).filter(e => e.type === 'link' || e.type === 'text_link'); | |
| if (links.length > 0) { | |
| html += '<div class="entity-links">'; | |
| const seen = new Set(); | |
| for (const link of links) { | |
| const url = link.value; | |
| if (seen.has(url)) continue; | |
| seen.add(url); | |
| // Skip if the link is already visible in the text | |
| if (msg.text && msg.text.includes(url)) continue; | |
| const domain = getDomain(url); | |
| html += `<a class="entity-link" href="${escapeHtml(url)}" target="_blank" rel="noopener"> | |
| <span class="link-icon">🔗</span> | |
| <span class="link-domain">${escapeHtml(domain)}</span> | |
| </a>`; | |
| } | |
| html += '</div>'; | |
| } | |
| // Media badge | |
| if (msg.has_media) { | |
| const icon = msg.has_photo ? '📷' : '📎'; | |
| const label = msg.has_photo ? 'Photo' : 'Media'; | |
| html += `<div class="media-badge"><span class="media-icon">${icon}</span> ${label}</div>`; | |
| } | |
| // Time for joined messages (shown on hover) | |
| if (isJoined) { | |
| html += `<div class="msg-time-inline">${formatTime(msg.date)}${msg.is_edited ? ' · edited' : ''}</div>`; | |
| } | |
| html += '</div>'; // close msg-body | |
| el.innerHTML = html; | |
| frag.appendChild(el); | |
| return frag; | |
| } | |
| function timeDiffMinutes(dateStr1, dateStr2) { | |
| if (!dateStr1 || !dateStr2) return 999; | |
| return Math.abs(new Date(dateStr2) - new Date(dateStr1)) / 60000; | |
| } | |
| // ===== Render All ===== | |
| function renderAllMessages() { | |
| const container = document.getElementById('messages-container'); | |
| container.innerHTML = ''; | |
| for (let i = 0; i < allMessages.length; i++) { | |
| container.appendChild(renderMessage(allMessages[i], i > 0 ? allMessages[i-1] : null)); | |
| } | |
| } | |
| // ===== Load Messages ===== | |
| async function loadInitialMessages() { | |
| if (loading) return; | |
| loading = true; | |
| document.getElementById('loading').style.display = 'block'; | |
| try { | |
| const countRes = await fetch('/api/chat/messages?limit=1&offset=0'); | |
| const countData = await countRes.json(); | |
| totalMessages = countData.total || 0; | |
| if (totalMessages === 0) { | |
| document.getElementById('loading').style.display = 'none'; | |
| document.getElementById('messages-container').innerHTML = | |
| '<div class="date-separator"><div class="date-badge">No messages found</div></div>'; | |
| loading = false; | |
| return; | |
| } | |
| const startOffset = Math.max(0, totalMessages - BATCH_SIZE); | |
| oldestOffset = startOffset; | |
| const res = await fetch(`/api/chat/messages?limit=${BATCH_SIZE}&offset=${startOffset}`); | |
| const data = await res.json(); | |
| if (data.messages && data.messages.length > 0) { | |
| allMessages = data.messages; | |
| renderAllMessages(); | |
| setTimeout(() => { scrollToBottom(); initialLoad = false; }, 100); | |
| if (oldestOffset <= 0) { | |
| document.getElementById('load-more-top').style.display = 'none'; | |
| } | |
| } | |
| } catch (e) { | |
| console.error('Error loading messages:', e); | |
| showToast('Error loading messages'); | |
| } | |
| loading = false; | |
| document.getElementById('loading').style.display = 'none'; | |
| } | |
| async function loadOlderMessages() { | |
| if (loading || oldestOffset <= 0) return; | |
| loading = true; | |
| document.getElementById('load-older-btn').disabled = true; | |
| try { | |
| const newOffset = Math.max(0, oldestOffset - BATCH_SIZE); | |
| const limit = oldestOffset - newOffset; | |
| const res = await fetch(`/api/chat/messages?limit=${limit}&offset=${newOffset}`); | |
| const data = await res.json(); | |
| if (data.messages && data.messages.length > 0) { | |
| const container = document.getElementById('messages-container'); | |
| const scrollBefore = container.scrollHeight; | |
| allMessages = [...data.messages, ...allMessages]; | |
| oldestOffset = newOffset; | |
| renderAllMessages(); | |
| const scrollAfter = container.scrollHeight; | |
| window.scrollBy(0, scrollAfter - scrollBefore); | |
| if (oldestOffset <= 0) { | |
| document.getElementById('load-more-top').style.display = 'none'; | |
| } | |
| } | |
| } catch (e) { | |
| console.error('Error loading older messages:', e); | |
| showToast('Error loading messages'); | |
| } | |
| loading = false; | |
| document.getElementById('load-older-btn').disabled = false; | |
| } | |
| // ===== Navigation ===== | |
| function goToMessage(messageId) { | |
| const el = document.getElementById(`message${messageId}`); | |
| if (el) { | |
| el.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
| el.classList.add('selected'); | |
| setTimeout(() => el.classList.remove('selected'), 2500); | |
| } else { | |
| showToast('Message not in current view'); | |
| } | |
| } | |
| function scrollToBottom() { | |
| window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); | |
| } | |
| function showToast(message) { | |
| const toast = document.getElementById('toast'); | |
| toast.textContent = message; | |
| toast.classList.add('visible'); | |
| setTimeout(() => toast.classList.remove('visible'), 3000); | |
| } | |
| // Scroll button visibility | |
| window.addEventListener('scroll', () => { | |
| const btn = document.getElementById('scroll-bottom'); | |
| const dist = document.body.scrollHeight - window.scrollY - window.innerHeight; | |
| btn.classList.toggle('visible', dist > 500); | |
| }); | |
| // ===== Init ===== | |
| loadInitialMessages(); | |
| </script> | |
| </body> | |
| </html> | |