WhatsApp-Logger-Self-Hosted-

A privacy-focused, self-hosted...
Log | Files | Refs | README | LICENSE

commit 113af07abdb15f70be11f226e7cbbfe58faadd8c
parent 3142fb8bf510800b9e4972fd9d1bb2a8a5718b26
Author: Amit Dutta <amitdutta4255@gmail.com>
Date:   Mon, 25 May 2026 21:51:36 +0530

feat: ui update v4.1.5
Diffstat:
Mindex.html | 431+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
1 file changed, 314 insertions(+), 117 deletions(-)

diff --git a/index.html b/index.html @@ -147,7 +147,7 @@ #FBFDF9; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 24px; height: 100dvh; - transition: opacity 0.3s; + transition: opacity 0.4s cubic-bezier(0.2, 0, 0, 1); } [data-theme="dark"] #auth-screen { @@ -164,6 +164,7 @@ width: 90%; max-width: 380px; box-shadow: var(--elevation-2); display: flex; flex-direction: column; gap: 20px; + position: relative; } [data-theme="dark"] .auth-card { @@ -177,9 +178,11 @@ } .brand-icon img { width: 100%; height: 100%; object-fit: contain; } - .auth-header { text-align: center; } - .auth-header h1 { font-family: 'Google Sans', sans-serif; font-size: 28px; font-weight: 400; color: var(--md-sys-color-on-surface); margin: 8px 0; } - .auth-header p { font-family: 'Roboto', sans-serif; font-size: 14px; color: var(--md-sys-color-on-surface-variant); margin: 0; } + /* Custom Spinner for the Boot Loader */ + [data-theme="dark"] #authLoader .spinner { + border-color: rgba(255,255,255,0.1) !important; + border-top-color: var(--md-sys-color-primary) !important; + } /* --- LOCK SCREEN (PIN/BIO) --- */ #lock-screen { @@ -192,7 +195,7 @@ .lock-header { text-align: center; margin-top: 4vh; } .lock-icon { width: 64px; height: 64px; margin-bottom: 16px; color: var(--md-sys-color-primary); } - .lock-title { font-family: 'Google Sans'; font-size: 24px; margin-bottom: 8px; } + .lock-title { font-family: 'Lexend', sans-serif; font-size: 24px; margin-bottom: 8px; } .lock-sub { color: var(--md-sys-color-on-surface-variant); font-size: 14px; } /* PIN Dots */ @@ -211,7 +214,7 @@ width: 72px; height: 72px; border-radius: 50%; border: none; background: transparent; font-size: 28px; font-weight: 400; color: var(--md-sys-color-on-surface); display: flex; align-items: center; justify-content: center; cursor: pointer; - transition: background 0.2s; font-family: 'Google Sans', sans-serif; + transition: background 0.2s; font-family: 'Lexend', sans-serif; } .key-btn:active { background: var(--md-sys-color-surface-container-high); } .key-backspace { font-size: 24px; color: var(--md-sys-color-on-surface-variant); } @@ -222,9 +225,9 @@ .lockout-container { display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; width: 100%; animation: fadeIn 0.3s; flex: 1; } .lockout-icon-bg { width: 80px; height: 80px; border-radius: 50%; background: var(--md-sys-color-error-container); color: var(--md-sys-color-on-error-container); display: flex; align-items: center; justify-content: center; margin-bottom: 24px; } .lockout-icon { font-size: 40px; } - .lockout-title { font-family: 'Google Sans'; font-size: 28px; margin-bottom: 8px; color: var(--md-sys-color-on-surface); } + .lockout-title { font-family: 'Lexend', sans-serif; font-size: 28px; margin-bottom: 8px; color: var(--md-sys-color-on-surface); } .lockout-msg { color: var(--md-sys-color-on-surface-variant); margin-bottom: 32px; font-size: 16px; } - .lockout-timer { font-family: 'Google Sans'; font-size: 56px; font-weight: 400; color: var(--md-sys-color-error); font-feature-settings: "tnum"; margin-bottom: 8px; } + .lockout-timer { font-family: 'Lexend', sans-serif; font-size: 56px; font-weight: 400; color: var(--md-sys-color-error); font-feature-settings: "tnum"; margin-bottom: 8px; } .lockout-sub { margin-top: 16px; color: var(--md-sys-color-on-surface-variant); font-size: 14px; } .lockout-actions { margin-top: 24px; display: flex; gap: 12px; } .locked-mode #pinUI, .locked-mode #bioUI, .locked-mode .forgot-pin-btn { display: none !important; } @@ -246,7 +249,7 @@ .m3-btn { width: 100%; height: 48px; border-radius: 24px; border: none; outline: none; cursor: pointer; - font-family: 'Google Sans', sans-serif; font-weight: 500; font-size: 14px; + font-family: 'Lexend', sans-serif; font-weight: 500; font-size: 14px; display: flex; align-items: center; justify-content: center; gap: 8px; transition: 0.2s; position: relative; overflow: hidden; } @@ -284,7 +287,7 @@ /* CHAT AREA */ .chat-area { flex: 1; display: flex; flex-direction: column; position: relative; background-color: var(--chat-bg-color); transition: background-color 0.3s; overflow: hidden; } - .chat-bg-pattern { position: absolute; inset: 0; opacity: var(--chat-bg-image-opacity); z-index: 0; pointer-events: none; background-image: url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 1.79 4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z' fill='%236e7874' fill-opacity='0.08' fill-rule='evenodd'/%3E%3C/svg%3E"); } + .chat-bg-pattern { position: absolute; inset: 0; opacity: var(--chat-bg-image-opacity); z-index: 0; pointer-events: none; background-image: url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z' fill='%236e7874' fill-opacity='0.08' fill-rule='evenodd'/%3E%3C/svg%3E"); } .chat-header { height: 64px; background: var(--md-sys-color-surface); display: flex; align-items: center; padding: 0 16px; border-bottom: 1px solid var(--md-sys-color-outline-variant); z-index: 5; } .chat-content { @@ -316,6 +319,7 @@ .msg-in.group-bot { border-top-left-radius: 4px; margin-top: 0; } .msg-sender { font-size: 12px; font-weight: 700; color: var(--md-sys-color-primary); margin-bottom: 2px; } + .msg-text { white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; } .msg-meta { font-size: 10px; opacity: 0.7; align-self: flex-end; margin-top: 2px; } /* DATE DIVIDERS */ @@ -527,42 +531,53 @@ <!-- AUTH SCREEN --> <div id="auth-screen"> <div class="auth-card"> - <div style="text-align: center;" class="anim-up delay-1"> - <div class="brand-icon"> - <img src="/assets/icon.svg" alt="App Logo"> - </div> - <h1 style="font-family:'Google Sans'; font-size:28px; margin:8px 0; font-weight:400;">Welcome Back</h1> - <p style="color:var(--md-sys-color-on-surface-variant); font-size:14px;">Sign in to your chat log</p> + + <!-- Dynamic Initial Loader --> + <div id="authLoader" style="text-align: center; padding: 24px; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; min-height: 200px;"> + <div class="spinner" style="display:block; position:static; border-color:rgba(0,0,0,0.1); border-top-color:var(--md-sys-color-primary); width:36px; height:36px;"></div> + <p style="margin-top:20px; color:var(--md-sys-color-on-surface-variant); font-size: 15px; font-family:'Lexend', sans-serif;">Authenticating session...</p> </div> - <div class="m3-field anim-up delay-2"> - <input type="text" id="username" class="m3-input" placeholder=" "> - <label class="m3-label">Username</label> - </div> + <!-- Login Fields (Hidden initially) --> + <div id="authForm" class="hidden"> + <div style="text-align: center;" class="anim-up delay-1"> + <div class="brand-icon"> + <img src="/assets/icon.svg" alt="App Logo"> + </div> + <h1 style="font-family:'Lexend', sans-serif; font-size:28px; margin:8px 0; font-weight:400;">Welcome Back</h1> + <p style="color:var(--md-sys-color-on-surface-variant); font-size:14px;">Sign in to your chat log</p> + </div> - <div class="m3-field anim-up delay-3"> - <input type="password" id="password" class="m3-input" placeholder=" "> - <label class="m3-label">Password</label> - <button class="eye-toggle" onclick="togglePasswordVisibility('password', 'passIcon')"> - <span class="material-symbols-rounded" id="passIcon">visibility</span> - </button> - </div> + <div class="m3-field anim-up delay-2" style="margin-top: 12px;"> + <input type="text" id="username" class="m3-input" placeholder=" "> + <label class="m3-label">Username</label> + </div> - <div class="anim-up delay-4" style="display:flex; align-items:center; padding-left:4px;"> - <div style="display:flex; align-items:center; gap:8px;"> - <input type="checkbox" id="rememberMe" style="width:18px; height:18px; accent-color: var(--md-sys-color-primary);"> - <label for="rememberMe" style="font-size: 14px; color: var(--md-sys-color-on-surface);">Remember me</label> + <div class="m3-field anim-up delay-3" style="margin-top: 12px;"> + <input type="password" id="password" class="m3-input" placeholder=" "> + <label class="m3-label">Password</label> + <button class="eye-toggle" onclick="togglePasswordVisibility('password', 'passIcon')"> + <span class="material-symbols-rounded" id="passIcon">visibility</span> + </button> </div> - </div> - <div class="anim-up delay-5" style="display: flex; flex-direction: column; gap: 12px; margin-top: 8px;"> - <button class="m3-btn filled" id="loginBtn" onclick="attemptLogin()"> - <span class="btn-text">Sign In</span> - <span class="material-symbols-rounded btn-icon" style="font-size:20px;">arrow_forward</span> - <div class="spinner"></div> - </button> - <p id="authError" style="color: var(--md-sys-color-error); font-size: 14px; opacity: 0; text-align: center;">Invalid Credentials</p> + <div class="anim-up delay-4" style="display:flex; align-items:center; padding-left:4px; margin-top: 12px;"> + <div style="display:flex; align-items:center; gap:8px;"> + <input type="checkbox" id="rememberMe" style="width:18px; height:18px; accent-color: var(--md-sys-color-primary);"> + <label for="rememberMe" style="font-size: 14px; color: var(--md-sys-color-on-surface);">Remember me</label> + </div> + </div> + + <div class="anim-up delay-5" style="display: flex; flex-direction: column; gap: 12px; margin-top: 16px;"> + <button class="m3-btn filled" id="loginBtn" onclick="attemptLogin()"> + <span class="btn-text">Sign In</span> + <span class="material-symbols-rounded btn-icon" style="font-size:20px;">arrow_forward</span> + <div class="spinner"></div> + </button> + <p id="authError" style="color: var(--md-sys-color-error); font-size: 14px; opacity: 0; text-align: center;">Invalid Credentials</p> + </div> </div> + </div> </div> @@ -621,8 +636,8 @@ <aside class="sidebar" id="sidebar"> <div class="sidebar-header"> <div style="display: flex; align-items: center; gap: 12px; padding: 12px 0;"> - <h2 style="font-family: 'Google Sans', sans-serif; font-size: 20px; font-weight: 500; color: var(--md-sys-color-on-surface); margin: 0;">Chat Log</h2> - <span style="font-family: 'Google Sans', sans-serif; font-size: 12px; font-weight: 500; color: var(--md-sys-color-on-surface-variant); background-color: var(--md-sys-color-surface-container-high); padding: 2px 8px; border-radius: 100px; display: inline-flex; align-items: center;">v4.0</span> + <h2 style="font-family: 'Lexend', sans-serif; font-size: 20px; font-weight: 500; color: var(--md-sys-color-on-surface); margin: 0;">Chat Log</h2> + <span style="font-family: 'Lexend', sans-serif; font-size: 12px; font-weight: 500; color: var(--md-sys-color-on-surface-variant); background-color: var(--md-sys-color-surface-container-high); padding: 2px 8px; border-radius: 100px; display: inline-flex; align-items: center;">v4.1.5</span> </div> <div style="display:flex; gap: 8px;"> <button class="icon-btn ripple-surface" onclick="refreshApp()"><span class="material-symbols-rounded">refresh</span></button> @@ -753,6 +768,9 @@ <div style="margin-top:24px; display:flex; gap:12px;"> <button class="m3-btn filled" style="background:var(--md-sys-color-secondary-container); color:var(--md-sys-color-on-secondary-container); cursor: pointer;" onclick="window.open('https://amit.is-a.dev/logger#faq', '_blank')">View FAQ</button> </div> + <div style="margin-top:24px; display:flex; gap:12px;"> + <button class="m3-btn filled" style="background:var(--md-sys-color-secondary-container); color:var(--md-sys-color-on-secondary-container); cursor: pointer;" onclick="window.open('https://amit.is-a.dev/logger', '_blank')">View Docs</button> + </div> </div> <button class="m3-btn filled" style="background:var(--md-sys-color-error); margin-top: 16px;" onclick="showLogoutModal()">Log Out</button> @@ -790,6 +808,23 @@ </div> </div> + <!-- APP OPTIONS MODAL --> + <div id="appOptionsModal" class="modal-overlay"> + <div class="modal-card utility"> + <h3 class="modal-title">App Options</h3> + <p style="margin:8px 0 16px; color:var(--md-sys-color-on-surface-variant); font-size:14px;">Choose how you want to refresh the app.</p> + <div style="display:flex; flex-direction:column; gap:12px;"> + <button class="m3-btn filled" onclick="executePageRefresh()"> + <span class="material-symbols-rounded" style="font-size:20px;">refresh</span> Reload App + </button> + <button class="m3-btn text" style="color:var(--md-sys-color-error);" onclick="openHardReset()"> + <span class="material-symbols-rounded" style="font-size:20px;">delete_forever</span> Hard Reset + </button> + <button class="m3-btn text" onclick="closeModal('appOptionsModal')">Cancel</button> + </div> + </div> + </div> + <!-- HARD REFRESH MODAL --> <div id="refreshModal" class="modal-overlay"> <div class="modal-card utility"> @@ -863,7 +898,7 @@ </div> <div id="installSheet" class="install-sheet"> - <div class="sheet-header"><h3 style="font-family:'Google Sans'; margin: 0;">Install App</h3><button class="icon-btn" onclick="closeInstallSheet()"><span class="material-symbols-rounded">close</span></button></div> + <div class="sheet-header"><h3 style="font-family:'Lexend', sans-serif; margin: 0;">Install App</h3><button class="icon-btn" onclick="closeInstallSheet()"><span class="material-symbols-rounded">close</span></button></div> <div class="sheet-content"> <div style="width:64px; height:64px; margin:0 auto 16px; display:flex; align-items:center; justify-content:center;"><img src="/assets/icon.svg" alt="App Logo" style="width:100%; height:100%; object-fit: contain;"></div> <p style="color:var(--md-sys-color-on-surface-variant); margin-bottom:24px;">Install WhatsApp Logger App for a better experience, full screen view, and enhanced security.</p> @@ -903,24 +938,63 @@ resolve(); return; } - const req = indexedDB.open(DB_NAME, DB_VERSION); - req.onupgradeneeded = (e) => { - const database = e.target.result; - if (!database.objectStoreNames.contains('messages')) { - const store = database.createObjectStore('messages', { keyPath: 'id' }); - store.createIndex('chatId', 'chatId', { unique: false }); - store.createIndex('timestamp', 'timestamp', { unique: false }); - } - if (!database.objectStoreNames.contains('chats')) { - database.createObjectStore('chats', { keyPath: 'id' }); + + let isResolved = false; + const safeResolve = () => { + if(!isResolved) { + isResolved = true; + resolve(); } }; - req.onsuccess = (e) => { dbInstance = e.target.result; resolve(); }; - req.onerror = (e) => { - console.warn('IDB init error', e); - isIdbSupported = false; - resolve(); - }; + + // Safety timeout: Prevent infinite hangs on mobile browsers + const fallbackTimeout = setTimeout(() => { + console.warn('IDB init timed out'); + isIdbSupported = false; + safeResolve(); + }, 3000); + + try { + const req = indexedDB.open(DB_NAME, DB_VERSION); + req.onupgradeneeded = (e) => { + const database = e.target.result; + if (!database.objectStoreNames.contains('messages')) { + const store = database.createObjectStore('messages', { keyPath: 'id' }); + store.createIndex('chatId', 'chatId', { unique: false }); + store.createIndex('timestamp', 'timestamp', { unique: false }); + } + if (!database.objectStoreNames.contains('chats')) { + database.createObjectStore('chats', { keyPath: 'id' }); + } + }; + req.onsuccess = (e) => { + clearTimeout(fallbackTimeout); + dbInstance = e.target.result; + + // Close connection if another tab/process requests an upgrade, prevents blocking + dbInstance.onversionchange = () => { + dbInstance.close(); + dbInstance = null; + }; + + safeResolve(); + }; + req.onerror = (e) => { + clearTimeout(fallbackTimeout); + console.warn('IDB init error', e); + isIdbSupported = false; + safeResolve(); + }; + req.onblocked = (e) => { + console.warn('IDB init blocked. Waiting for other connections to close...', e); + // We rely on fallbackTimeout if it never unblocks + }; + } catch (e) { + clearTimeout(fallbackTimeout); + console.warn('IDB open exception', e); + isIdbSupported = false; + safeResolve(); + } }); } @@ -1003,6 +1077,20 @@ return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); } + // HTML Sanitizer to prevent Code Injection via chat messages + function escapeHTML(str) { + if (!str) return ''; + return str.replace(/[&<>'"]/g, + tag => ({ + '&': '&amp;', + '<': '&lt;', + '>': '&gt;', + "'": '&#39;', + '"': '&quot;' + }[tag] || tag) + ); + } + // --- PWA INSTALLATION --- if ('serviceWorker' in navigator && (window.location.protocol === 'https:' || window.location.protocol === 'http:')) { navigator.serviceWorker.register('/assets/sw-wpChat.js').catch(err => { @@ -1049,7 +1137,6 @@ } window.addEventListener('online', () => { document.getElementById('offlineIndicator').classList.add('hidden'); - loadChats(); }); window.addEventListener('offline', () => { document.getElementById('offlineIndicator').classList.remove('hidden'); @@ -1065,31 +1152,41 @@ changeBg(bg, false); populateFAQ(); - if(isMobileApp()) { - document.getElementById('mobileSecuritySection').classList.remove('hidden'); - const hasPin = localStorage.getItem('wp_app_pin'); - const useBio = localStorage.getItem('wp_app_bio') === 'true'; - document.getElementById('pinLockToggle').checked = !!hasPin; - document.getElementById('bioLockToggle').checked = useBio; - const authValid = localStorage.getItem('wp_auth_expiry') > Date.now(); + // Setup Auth & App Routing with smooth transitions + const authValid = localStorage.getItem('wp_auth_expiry') > Date.now(); + + if (authValid) { + // If auth is valid, fade out the auth screen completely and show the app + document.getElementById('auth-screen').style.opacity = '0'; - if (authValid && (hasPin || useBio)) { - document.getElementById('auth-screen').style.display = 'none'; - initLockScreen(hasPin, useBio); - } else if (authValid) { + setTimeout(() => { document.getElementById('auth-screen').style.display = 'none'; - document.getElementById('app-layout').classList.add('visible'); - loadChats(); - } else { - document.getElementById('auth-screen').style.opacity = '1'; - } + if (isMobileApp()) { + const hasPin = localStorage.getItem('wp_app_pin'); + const useBio = localStorage.getItem('wp_app_bio') === 'true'; + document.getElementById('mobileSecuritySection').classList.remove('hidden'); + document.getElementById('pinLockToggle').checked = !!hasPin; + document.getElementById('bioLockToggle').checked = useBio; + + if (hasPin || useBio) { + initLockScreen(hasPin, useBio); + } else { + document.getElementById('app-layout').classList.add('visible'); + loadChats(); + } + } else { + document.getElementById('app-layout').classList.add('visible'); + loadChats(); + } + }, 400); // Wait for CSS opacity transition } else { - if(localStorage.getItem('wp_auth_expiry') > Date.now()) { - document.getElementById('auth-screen').style.display = 'none'; - document.getElementById('app-layout').classList.add('visible'); - loadChats(); - } else { - document.getElementById('auth-screen').style.opacity = '1'; + // If not valid, hide the spinner and reveal the login form + document.getElementById('authLoader').classList.add('hidden'); + document.getElementById('authForm').classList.remove('hidden'); + document.getElementById('authForm').classList.add('fade-in'); + + if (isMobileApp()) { + document.getElementById('mobileSecuritySection').classList.remove('hidden'); } } @@ -1333,12 +1430,21 @@ const data = await response.json(); if(data.success) { if(document.getElementById('rememberMe').checked) localStorage.setItem('wp_auth_expiry', Date.now() + (3 * 24 * 60 * 60 * 1000)); + else localStorage.setItem('wp_auth_expiry', Date.now() + (1 * 60 * 60 * 1000)); // default 1 hr fallback revealApp(); } else throw new Error(); } catch (e) { err.style.opacity = '1'; btn.classList.remove('loading'); btn.disabled = false; } } - function revealApp() { document.getElementById('auth-screen').style.display = 'none'; document.getElementById('app-layout').classList.add('visible'); loadChats(); } + function revealApp() { + document.getElementById('auth-screen').style.opacity = '0'; + setTimeout(() => { + document.getElementById('auth-screen').style.display = 'none'; + document.getElementById('app-layout').classList.add('visible'); + loadChats(); + }, 400); + } + function openSettings() { document.getElementById('settingsSidebar').classList.add('active'); calculateLocalStorageUsage(); @@ -1378,7 +1484,10 @@ const faqs = [ {q: "Is this secure?", a: "Yes, data lives in your private Firebase. No one else can access it."}, {q: "Does it track deleted messages?", a: "Yes, once a message is received by the server, it is logged forever, even if the sender deletes it."}, - {q: "Does my phone need to be online?", a: "No. Once linked, your Render server handles logging even if your phone is powered off."} + {q: "Does my phone need to be online?", a: "No. Once linked, your Render server handles logging even if your phone is powered off."}, + {q: "Is this legal?", a: "The tool uses standard WhatsApp Web protocols to function as a linked device. It is intended for personal archiving of your own conversations."}, + {q: "Does it save photos/videos?", a: "No. To keep the project free (Firebase offers limited storage), the logger only saves text content. Media files would fill up your free tier storage very quickly."}, + {q: "Does it save View Once messages?", a: "No. WhatsApp does not send the content of `View Once` media to linked devices/web clients, so the logger cannot see them."} ]; const container = document.getElementById('faqContainer'); faqs.forEach(item => { @@ -1391,24 +1500,47 @@ function toggleChatMenu(event) { if(event) event.stopPropagation(); document.getElementById('chatMenu').classList.toggle('show'); } document.addEventListener('click', function(e) { const menu = document.getElementById('chatMenu'); if(menu.classList.contains('show') && !e.target.closest('.menu-container')) menu.classList.remove('show'); }); - function toggleChatSearch() { document.getElementById('inChatSearch').classList.toggle('hidden'); document.getElementById('chatMenu').classList.remove('show'); document.getElementById('msgSearchInput').focus(); } + function toggleChatSearch() { + const searchContainer = document.getElementById('inChatSearch'); + searchContainer.classList.toggle('hidden'); + document.getElementById('chatMenu').classList.remove('show'); + + const input = document.getElementById('msgSearchInput'); + if (searchContainer.classList.contains('hidden')) { + // Clear and trigger input event to reset highlights when closed + input.value = ''; + input.dispatchEvent(new Event('input')); + } else { + input.focus(); + } + } document.getElementById('msgSearchInput').addEventListener('input', (e) => { const term = e.target.value.toLowerCase(); let count = 0; - document.querySelectorAll('.highlight').forEach(span => { const parent = span.parentNode; parent.innerHTML = parent.textContent; }); document.querySelectorAll('.msg-bubble').forEach(b => { - const textDiv = b.querySelector('div:not(.msg-sender):not(.msg-meta)'); + const textDiv = b.querySelector('.msg-text'); if(textDiv) { - const originalText = textDiv.textContent; - if(term && originalText.toLowerCase().includes(term)) { + // Back up the original HTML on first search so we don't destroy structure + if (!textDiv.hasAttribute('data-orig')) { + textDiv.setAttribute('data-orig', textDiv.innerHTML); + } + + const originalHTML = textDiv.getAttribute('data-orig'); + const rawText = textDiv.textContent || ""; + + if(term && rawText.toLowerCase().includes(term)) { b.style.display = 'flex'; - textDiv.innerHTML = originalText.replace(new RegExp(`(${term})`, 'gi'), '<span class="highlight">$1</span>'); + // Negative lookahead regex: Highlights term ONLY if it's outside of HTML tags (<...>) + const safeTerm = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const regex = new RegExp(`(?![^<]*>)(${safeTerm})`, 'gi'); + + textDiv.innerHTML = originalHTML.replace(regex, '<span class="highlight">$1</span>'); count++; } else { b.style.display = term ? 'none' : 'flex'; - textDiv.textContent = originalText; + textDiv.innerHTML = originalHTML; // Restore clean HTML } } }); @@ -1416,19 +1548,28 @@ }); async function loadChats() { - const list = document.getElementById('contactList'); list.innerHTML = ''; - for(let i=0; i<5; i++) list.innerHTML += `<div class="skel-contact fade-in"><div class="skeleton skel-avatar"></div><div class="skel-text-col"><div class="skeleton" style="width:60%; height:16px;"></div><div class="skeleton" style="width:40%; height:12px;"></div></div></div>`; - - // Load from cache instantly + const list = document.getElementById('contactList'); + + // Render initial cache instantly before networking kicks in const cachedChats = await getCachedChats(); - if (cachedChats && cachedChats.length > 0) renderChatList(cachedChats); + if (cachedChats && cachedChats.length > 0) { + // Ensure it's correctly sorted descending by activity in cache + cachedChats.sort((a,b) => b.lastActive - a.lastActive); + renderChatList(cachedChats); + } else { + list.innerHTML = ''; + for(let i=0; i<5; i++) list.innerHTML += `<div class="skel-contact fade-in"><div class="skeleton skel-avatar"></div><div class="skel-text-col"><div class="skeleton" style="width:60%; height:16px;"></div><div class="skeleton" style="width:40%; height:12px;"></div></div></div>`; + } - if (!navigator.onLine) return; // Skip network fetch if offline + if (!navigator.onLine) return; // Skip snapshot if offline - try { - const snap = await db.collection('Chats').get(); + // Prevent duplicating listeners + if(window.chatListUnsub) window.chatListUnsub(); + + // Use real-time listener instead of one-time .get() + window.chatListUnsub = db.collection('Chats').onSnapshot(async snap => { const groups = {}; - const excluded = ['917278779512@s.whatsapp.net', '201554426618024@lid']; + const excluded = ['']; snap.forEach(doc => { const d = doc.data(); @@ -1441,11 +1582,14 @@ } }); + // Sort descending by time const chats = Object.values(groups).map(g => ({...g.main, ids: g.ids})).sort((a,b) => b.lastActive - a.lastActive); await saveCachedChats(chats); renderChatList(chats); - } catch (err) { console.error(err); } + }, err => { + console.error("Chat list sync error:", err); + }); } function renderChatList(chats) { @@ -1460,13 +1604,13 @@ function renderChatList(chats) { const primaryName = c.customName || c.displayName || c.phoneNumber || c.id; const isGroup = c.id.includes('@g.us'); - // Structure: Avatar | [ Name Time ] - // | [ Checkmark/Sender + Msg ] + // Structure: Avatar | [ Name Time ] + // | [ Checkmark/Sender + Msg ] el.innerHTML = ` - <div class="avatar">${isGroup ? '<span class="material-symbols-rounded">group</span>' : primaryName[0].toUpperCase()}</div> + <div class="avatar">${isGroup ? '<span class="material-symbols-rounded">group</span>' : escapeHTML(primaryName[0]).toUpperCase()}</div> <div class="contact-info" style="min-width: 0;"> <div class="contact-name" style="display:flex; justify-content:space-between; align-items:baseline; width:100%;"> - <span style="white-space:nowrap; overflow:hidden; text-overflow:ellipsis; flex:1;">${primaryName}</span> + <span style="white-space:nowrap; overflow:hidden; text-overflow:ellipsis; flex:1;">${escapeHTML(primaryName)}</span> <span class="chat-time" style="font-size: 12px; color: var(--md-sys-color-on-surface-variant); font-weight: 400; margin-left: 8px; flex-shrink: 0;"></span> </div> <div class="contact-sub preview-text" style="display:flex; align-items:center; margin-top:2px; color:var(--md-sys-color-on-surface-variant);"> @@ -1488,14 +1632,38 @@ function renderChatList(chats) { allMsgs = allMsgs.concat(msgs); } + // Sort descending to grab the absolute latest message in local cache + allMsgs.sort((a, b) => b.timestamp - a.timestamp); + let lastMsg = allMsgs.length > 0 ? allMsgs[0] : null; + + // REAL-TIME PREVIEW SYNC: + // If the server tells us this chat was active *after* our cached latest message, fetch the missing real-time snippet + if (navigator.onLine && (!lastMsg || c.lastActive > lastMsg.timestamp)) { + try { + let latestOnline = null; + for (const id of c.ids) { + const snap = await db.collection('Chats').doc(id).collection('Messages') + .orderBy('timestamp', 'desc').limit(1).get(); + + if (!snap.empty) { + const data = snap.docs[0].data(); + if (!latestOnline || data.timestamp > latestOnline.timestamp) { + latestOnline = data; + } + } + } + if (latestOnline && (!lastMsg || latestOnline.timestamp > lastMsg.timestamp)) { + lastMsg = latestOnline; + } + } catch(err) { + console.warn("Failed fetching realtime preview snippet", err); + } + } + const contentSpan = el.querySelector('.preview-content'); const timeSpan = el.querySelector('.chat-time'); - if (allMsgs.length > 0) { - // Sort descending to grab the absolute latest message - allMsgs.sort((a, b) => b.timestamp - a.timestamp); - const lastMsg = allMsgs[0]; - + if (lastMsg) { // Format Timestamp: Time if today, Date if older const msgDate = new Date(lastMsg.timestamp * 1000); const today = new Date(); @@ -1510,11 +1678,12 @@ function renderChatList(chats) { if (lastMsg.fromMe) { prefix = `<span class="material-symbols-rounded" style="font-size: 16px; vertical-align: text-bottom; margin-right: 4px; color: var(--md-sys-color-outline);">done_all</span>`; } else if (isGroup && lastMsg.senderName && lastMsg.senderName !== "Unknown") { - prefix = `<span style="font-weight: 500;">${lastMsg.senderName}:</span> `; + prefix = `<span style="font-weight: 500;">${escapeHTML(lastMsg.senderName)}:</span> `; } - // Construct the final line (falling back to "Media" if it's an image/video without text) - contentSpan.innerHTML = `${prefix}${lastMsg.text || 'Media'}`; + // Construct the final line properly escaped + const safePreviewText = escapeHTML(lastMsg.text || 'Media'); + contentSpan.innerHTML = `${prefix}${safePreviewText}`; } else { contentSpan.innerHTML = `<span style="font-style: italic; opacity: 0.7;">No messages</span>`; } @@ -1531,7 +1700,7 @@ function renderChatList(chats) { document.getElementById('headerName').innerText = name; document.getElementById('chatHeaderContent').style.opacity = '1'; document.getElementById('headerId').innerText = displayId; - document.getElementById('headerAvatar').innerHTML = isGroup ? `<span class="material-symbols-rounded">group</span>` : name[0].toUpperCase(); + document.getElementById('headerAvatar').innerHTML = isGroup ? `<span class="material-symbols-rounded">group</span>` : escapeHTML(name[0]).toUpperCase(); if (window.matchMedia('(max-width: 768px)').matches) { window.history.pushState({chatOpen: true}, ""); @@ -1670,11 +1839,13 @@ function renderChatList(chats) { const showSenderName = !m.fromMe && isCurrentChatGroup && m.senderName && m.senderName !== "Unknown" && !isSamePrev; div.id = `msg-${m.timestamp}`; div.dataset.date = dateStr; - // Regex to find URLs and wrap them in an anchor tag + // Escape all text first so HTML code isn't rendered + const safeText = escapeHTML(m.text || ''); + // Find URLs and wrap them in an anchor tag safely const urlRegex = /(https?:\/\/[^\s]+)/g; - const formattedText = m.text ? m.text.replace(urlRegex, url => `<a href="${url}" target="_blank" rel="noopener noreferrer" style="color: var(--md-sys-color-primary); text-decoration: underline;">${url}</a>`) : ''; + const formattedText = safeText ? safeText.replace(urlRegex, url => `<a href="${url}" target="_blank" rel="noopener noreferrer" style="color: var(--md-sys-color-primary); text-decoration: underline;">${url}</a>`) : ''; - div.innerHTML = `${showSenderName ? `<div class="msg-sender">${m.senderName}</div>` : ''}<div>${formattedText || 'Media'}</div><div class="msg-meta">${date.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'})}</div>`; + div.innerHTML = `${showSenderName ? `<div class="msg-sender">${escapeHTML(m.senderName)}</div>` : ''}<div class="msg-text">${formattedText || 'Media'}</div><div class="msg-meta">${date.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'})}</div>`; if (currentGroup) currentGroup.appendChild(div); }); @@ -1739,6 +1910,15 @@ function renderChatList(chats) { }); function refreshApp() { + document.getElementById('appOptionsModal').classList.add('active'); + } + + function executePageRefresh() { + window.location.reload(true); + } + + function openHardReset() { + closeModal('appOptionsModal'); document.getElementById('refreshAuthStep').classList.remove('hidden'); document.getElementById('refreshProgressStep').classList.add('hidden'); document.getElementById('refreshUsername').value = ''; @@ -1794,7 +1974,24 @@ function renderChatList(chats) { updateRefreshProgress(15, "Wiping database..."); if(dbInstance) { dbInstance.close(); dbInstance = null; } - await new Promise(res => { const req = indexedDB.deleteDatabase(DB_NAME); req.onsuccess = res; req.onerror = res; req.onblocked = res; }); + + // Slight delay helps mobile OS clear file locks before deletion + await new Promise(r => setTimeout(r, 100)); + + await new Promise(res => { + try { + const req = indexedDB.deleteDatabase(DB_NAME); + req.onsuccess = res; + req.onerror = res; + req.onblocked = () => { + console.warn("Delete DB blocked by another process."); + res(); + }; + } catch(e) { res(); } + }); + + // Slight delay before reopening + await new Promise(r => setTimeout(r, 200)); updateRefreshProgress(25, "Rebuilding database..."); isIdbSupported = true;
© notamitgamer • Site Built: 2026-07-21 13:58:23 UTC • git-mirror commit: 1037f62 [view raw info]
Originally created with stagit • modified by notamitgamer
Forked from github.com/notamitgamer/git-mirror