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:
| M | index.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 => ({
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ "'": ''',
+ '"': '"'
+ }[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;