logger

Self-hosted WhatsApp logger us...
Log | Files | Refs | README | LICENSE

commit 6c1829dbf8bfc1da954e9dc287f52cba27e0b26c
parent b347ec327a036b1ec3df5313801d43a5d023ed9f
Author: Amit Dutta <amitdutta4255@gmail.com>
Date:   Sun, 24 May 2026 20:40:29 +0530

new

Diffstat:
Aindex.html | 2075+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 2075 insertions(+), 0 deletions(-)

diff --git a/index.html b/index.html @@ -0,0 +1,2074 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> + <title>WhatsApp Logger</title> + + <!-- PWA Manifest --> + <link rel="manifest" href="/assets/manifest-wpChat.json"> + <meta name="theme-color" content="#006C4C"> + + <!-- SVG Favicon --> + <link rel="icon" href="/assets/icon.svg"> + + <!-- Fonts & Icons --> + <link rel="preconnect" href="https://fonts.googleapis.com"> + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> + <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&family=Google+Sans:wght@400;500;700&display=swap" rel="stylesheet"> + <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@24,400,0,0" /> + + <!-- Firebase SDKs --> + <script src="https://www.gstatic.com/firebasejs/10.8.0/firebase-app-compat.js"></script> + <script src="https://www.gstatic.com/firebasejs/10.8.0/firebase-firestore-compat.js"></script> + + <style> + :root { + /* Light Theme Tokens */ + --md-sys-color-primary: #006C4C; + --md-sys-color-on-primary: #FFFFFF; + --md-sys-color-primary-container: #89F8C7; + --md-sys-color-on-primary-container: #002114; + + --md-sys-color-secondary: #4D6357; + --md-sys-color-on-secondary: #FFFFFF; + --md-sys-color-secondary-container: #CFE9D9; + --md-sys-color-on-secondary-container: #092016; + + --md-sys-color-surface: #FBFDF9; + --md-sys-color-surface-dim: #D9DBD9; + --md-sys-color-surface-container: #ECEEEB; + --md-sys-color-surface-container-high: #E6E8E5; + + --md-sys-color-on-surface: #191C1A; + --md-sys-color-on-surface-variant: #404944; + --md-sys-color-outline: #707974; + --md-sys-color-outline-variant: #BFC9C2; + + --md-sys-color-error: #BA1A1A; + --md-sys-color-error-container: #FFDAD6; + --md-sys-color-on-error-container: #410002; + + /* WhatsApp Specifics */ + --chat-bubble-me: #E2FFEC; + --chat-bubble-them: #FFFFFF; + --chat-bg-color: #EFE7DE; + --chat-bg-image-opacity: 0.08; + --search-highlight: #ffeb3b; + + /* Elevations & Shapes */ + --elevation-1: 0px 1px 3px 1px rgba(0,0,0,0.15), 0px 1px 2px 0px rgba(0,0,0,0.3); + --elevation-2: 0px 2px 6px 2px rgba(0,0,0,0.15), 0px 1px 2px 0px rgba(0,0,0,0.3); + + /* Settings */ + --base-font-size: 15px; + } + + /* Dark Theme Tokens */ + [data-theme="dark"] { + --md-sys-color-primary: #89F8C7; + --md-sys-color-on-primary: #003828; + --md-sys-color-primary-container: #005138; + --md-sys-color-on-primary-container: #89F8C7; + + --md-sys-color-secondary: #BACCB3; + --md-sys-color-on-secondary: #25342D; + --md-sys-color-secondary-container: #3B4B43; + --md-sys-color-on-secondary-container: #D6E8D8; + + --md-sys-color-surface: #111412; + --md-sys-color-surface-dim: #111412; + --md-sys-color-surface-container: #1E211F; + --md-sys-color-surface-container-high: #282B29; + + --md-sys-color-on-surface: #E2E3DD; + --md-sys-color-on-surface-variant: #C2C8C5; + --md-sys-color-outline: #8C938F; + --md-sys-color-outline-variant: #424945; + + --md-sys-color-error: #FFB4AB; + --md-sys-color-error-container: #93000A; + --md-sys-color-on-error-container: #FFDAD6; + + --chat-bubble-me: #005C4B; + --chat-bubble-them: #202C33; + --chat-bg-color: #0B141A; + --chat-bg-image-opacity: 0.05; + --search-highlight: #6b6200; + } + + * { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; } + + body { + font-family: 'Roboto', sans-serif; + background-color: var(--md-sys-color-surface); + color: var(--md-sys-color-on-surface); + height: 100vh; + overflow: hidden; + display: flex; + justify-content: center; + transition: background-color 0.3s, color 0.3s; + } + + /* --- ANIMATIONS --- */ + .hidden { display: none !important; } + .fade-in { animation: fadeIn 0.4s cubic-bezier(0.2, 0.0, 0.0, 1.0); } + @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } + .ripple-surface { position: relative; overflow: hidden; transform: translate3d(0, 0, 0); transition: transform 0.1s; } + .ripple-surface:active { transform: scale(0.98); } + + @keyframes scaleIn { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } } + @keyframes slideUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } + .animate-enter { animation: scaleIn 0.3s cubic-bezier(0.2, 0.0, 0, 1.0); } + .shake { animation: shake 0.4s cubic-bezier(.36,.07,.19,.97) both; } + @keyframes shake { 10%, 90% { transform: translate3d(-1px, 0, 0); } 20%, 80% { transform: translate3d(2px, 0, 0); } 30%, 50%, 70% { transform: translate3d(-4px, 0, 0); } 40%, 60% { transform: translate3d(4px, 0, 0); } } + + /* --- M3 STAGGERED ENTRANCE --- */ + @keyframes fadeInUp { + from { opacity: 0; transform: translateY(30px); } + to { opacity: 1; transform: translateY(0); } + } + .anim-up { opacity: 0; animation: fadeInUp 0.6s cubic-bezier(0.2, 0, 0, 1) forwards; } + .delay-1 { animation-delay: 0.1s; } + .delay-2 { animation-delay: 0.2s; } + .delay-3 { animation-delay: 0.3s; } + .delay-4 { animation-delay: 0.4s; } + .delay-5 { animation-delay: 0.5s; } + + /* --- AUTH SCREEN (UPDATED to M3 Classic) --- */ + #auth-screen { + position: fixed; inset: 0; z-index: 999; + /* Static blue-green background */ + background: radial-gradient(circle at 0% 0%, #C1E8FB 0%, transparent 50%), + radial-gradient(circle at 100% 100%, #89F8C7 0%, transparent 50%), + #FBFDF9; + display: flex; flex-direction: column; align-items: center; justify-content: center; + padding: 24px; height: 100dvh; + transition: opacity 0.3s; + } + + /* Dark Mode Override for Auth Background */ + [data-theme="dark"] #auth-screen { + background: radial-gradient(circle at 0% 0%, #001F29 0%, transparent 50%), + radial-gradient(circle at 100% 100%, #002114 0%, transparent 50%), + #111412; + } + + /* Updated M3 Auth Card (Classic Style) */ + .auth-card { + background: rgba(251, 253, 249, 0.7); /* Translucent */ + backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); + border-radius: 28px; + padding: 32px 24px; + width: 90%; max-width: 380px; + box-shadow: var(--elevation-2); + display: flex; flex-direction: column; gap: 20px; + } + + [data-theme="dark"] .auth-card { + background: rgba(30, 33, 31, 0.7); + } + + .brand-icon { + width: 64px; height: 64px; + display: flex; align-items: center; justify-content: center; + margin: 0 auto 8px; + } + .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; + } + + /* --- LOCK SCREEN (PIN/BIO) --- */ + #lock-screen { + position: fixed; inset: 0; z-index: 2000; + background: var(--md-sys-color-surface); + display: flex; flex-direction: column; align-items: center; justify-content: space-between; + padding: 48px 24px; opacity: 0; visibility: hidden; transition: opacity 0.3s; + } + #lock-screen.active { opacity: 1; visibility: visible; } + + .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-sub { color: var(--md-sys-color-on-surface-variant); font-size: 14px; } + + /* PIN Dots */ + .pin-dots { display: flex; gap: 16px; margin: 32px 0; justify-content: center; } + .pin-dot { + width: 14px; height: 14px; border-radius: 50%; + border: 1px solid var(--md-sys-color-outline); + transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1); + } + .pin-dot.filled { background: var(--md-sys-color-on-surface); border-color: var(--md-sys-color-on-surface); transform: scale(1.1); } + .pin-dot.error { border-color: var(--md-sys-color-error); background: var(--md-sys-color-error); } + + /* Native Keypad */ + .keypad { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px 32px; margin-bottom: 2vh; } + .key-btn { + 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; + } + .key-btn:active { background: var(--md-sys-color-surface-container-high); } + .key-backspace { font-size: 24px; color: var(--md-sys-color-on-surface-variant); } + + .forgot-pin-btn { + margin-top: 16px; color: var(--md-sys-color-primary); background: none; border: none; + font-weight: 500; font-size: 14px; padding: 8px 16px; border-radius: 20px; cursor: pointer; + } + + /* --- LOCKOUT UI --- */ + .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-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-sub { margin-top: 16px; color: var(--md-sys-color-on-surface-variant); font-size: 14px; } + + /* Utility to hide PIN/BIO when locked */ + .locked-mode #pinUI, + .locked-mode #bioUI, + .locked-mode .forgot-pin-btn { + display: none !important; + } + + /* --- UI ELEMENTS --- */ + .m3-field { + position: relative; margin-bottom: 0; + background: var(--md-sys-color-surface-container-highest); + border-radius: 4px 4px 0 0; + border-bottom: 1px solid var(--md-sys-color-outline); + transition: background 0.3s; + height: 56px; + } + .m3-field:hover { + background: rgba(29, 25, 43, 0.08); + } + .m3-field:focus-within { + border-bottom: 2px solid var(--md-sys-color-primary); + } + .m3-input { + width: 100%; height: 100%; padding: 24px 16px 8px 16px; + border: none; background: transparent; outline: none; + font-size: 16px; color: var(--md-sys-color-on-surface); font-family: inherit; + } + .m3-label { + position: absolute; left: 16px; top: 18px; + color: var(--md-sys-color-on-surface-variant); + font-size: 16px; pointer-events: none; transition: 0.2s cubic-bezier(0.2, 0, 0, 1); + } + .m3-input:focus ~ .m3-label, .m3-input:not(:placeholder-shown) ~ .m3-label { + top: 8px; font-size: 12px; color: var(--md-sys-color-primary); + } + .eye-toggle { + position: absolute; right: 12px; top: 50%; transform: translateY(-50%); + border: none; background: none; cursor: pointer; color: var(--md-sys-color-on-surface-variant); + padding: 8px; border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: background 0.2s; + } + .eye-toggle:hover { background: rgba(0,0,0,0.05); color: var(--md-sys-color-on-surface); } + + .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; + display: flex; align-items: center; justify-content: center; gap: 8px; + transition: 0.2s; position: relative; overflow: hidden; + } + .m3-btn.filled { + background: var(--md-sys-color-primary); + color: var(--md-sys-color-on-primary); + box-shadow: none; + } + .m3-btn.filled:hover { + box-shadow: var(--elevation-1); + } + .m3-btn:disabled { opacity: 0.38; cursor: not-allowed; background: var(--md-sys-color-on-surface); color: var(--md-sys-color-surface); box-shadow: none; } + + /* Spinner */ + .spinner { + width: 20px; height: 20px; border: 2px solid rgba(255,255,255,0.3); + border-radius: 50%; border-top-color: white; + animation: spin 0.8s linear infinite; display: none; + } + .m3-btn.loading .btn-text { visibility: hidden; } + .m3-btn.loading .btn-icon { display: none; } + .m3-btn.loading .spinner { display: block; position: absolute; } + @keyframes spin { to { transform: rotate(360deg); } } + + /* --- APP LAYOUT --- */ + #app-layout { + display: flex; width: 100%; height: 100%; max-width: 1600px; + background: var(--md-sys-color-surface); opacity: 0; transition: opacity 0.6s ease-in-out; + visibility: hidden; + } + #app-layout.visible { opacity: 1; visibility: visible; } + + /* SIDEBAR */ + .sidebar { + width: 400px; display: flex; flex-direction: column; + border-right: 1px solid var(--md-sys-color-outline-variant); + background: var(--md-sys-color-surface); z-index: 10; + } + .sidebar-header { padding: 16px 20px; display: flex; justify-content: space-between; align-items: center; } + .icon-btn { + width: 40px; height: 40px; border-radius: 50%; border: none; background: transparent; cursor: pointer; + color: var(--md-sys-color-on-surface-variant); display: flex; align-items: center; justify-content: center; transition: 0.2s; + } + .icon-btn:hover { background: rgba(128,128,128,0.1); color: var(--md-sys-color-on-surface); } + + .search-container { padding: 0 16px 12px; } + .search-bar { + background: var(--md-sys-color-surface-container-high); + border-radius: 28px; height: 48px; display: flex; align-items: center; padding: 0 16px; + } + .search-bar input { + border: none; background: transparent; outline: none; flex: 1; margin-left: 12px; + font-size: 16px; color: var(--md-sys-color-on-surface); + } + .contact-list { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 8px; } + .contact-item { + display: flex; align-items: center; padding: 12px 16px; border-radius: 16px; cursor: pointer; margin-bottom: 2px; + animation: slideUp 0.3s cubic-bezier(0.2, 0.0, 0, 1.0) backwards; + } + .contact-item:hover { background: rgba(128,128,128,0.05); } + .contact-item.active { background: var(--md-sys-color-secondary-container); } + .contact-item.active .contact-name { color: var(--md-sys-color-on-secondary-container); font-weight: 700; } + .avatar { + width: 48px; height: 48px; border-radius: 50%; + background: var(--md-sys-color-primary-container); + color: var(--md-sys-color-on-primary-container); + display: flex; align-items: center; justify-content: center; + font-size: 18px; font-weight: 500; flex-shrink: 0; margin-right: 16px; + } + .contact-info { flex: 1; min-width: 0; } + .contact-name { font-size: 16px; font-weight: 500; color: var(--md-sys-color-on-surface); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + .contact-sub { font-size: 14px; color: var(--md-sys-color-on-surface-variant); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + + /* CHAT AREA */ + .chat-area { + flex: 1; display: flex; flex-direction: column; position: relative; + background-color: var(--chat-bg-color); + transition: background-color 0.3s; + } + .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-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 { + flex: 1; overflow-y: auto; padding: 16px; z-index: 1; + display: flex; flex-direction: column; gap: 4px; + scroll-behavior: auto; /* Removed smooth scrolling for initial load */ + scroll-behavior: smooth; + } + /* Override specifically to remove default smooth behavior that causes load animation */ + .chat-content { + scroll-behavior: auto !important; + } + + .msg-bubble { + max-width: 80%; padding: 8px 12px; position: relative; + font-size: var(--base-font-size); line-height: 1.4; + box-shadow: 0 1px 2px rgba(0,0,0,0.1); + display: flex; flex-direction: column; + animation: scaleIn 0.2s cubic-bezier(0.2, 0.0, 0, 1.0) backwards; + } + .msg-in { + align-self: flex-start; background: var(--chat-bubble-them); + border-radius: 0 16px 16px 16px; color: var(--md-sys-color-on-surface); + } + .msg-out { + align-self: flex-end; background: var(--chat-bubble-me); + border-radius: 16px 0 16px 16px; color: var(--md-sys-color-on-surface); + } + + /* Message Grouping - WhatsApp Style */ + .msg-out.group-top { border-bottom-right-radius: 4px; margin-bottom: 2px; } + .msg-out.group-mid { border-top-right-radius: 4px; border-bottom-right-radius: 4px; margin-bottom: 2px; } + .msg-out.group-bot { border-top-right-radius: 4px; margin-top: 0; } + + .msg-in.group-top { border-bottom-left-radius: 4px; margin-bottom: 2px; } + .msg-in.group-mid { border-top-left-radius: 4px; border-bottom-left-radius: 4px; margin-bottom: 2px; } + .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-meta { font-size: 10px; opacity: 0.7; align-self: flex-end; margin-top: 2px; } + + /* DATE DIVIDERS */ + .chat-date-group { + display: flex; flex-direction: column; gap: 4px; + position: relative; /* Scope for sticky */ + } + .date-divider { + align-self: center; margin: 16px 0; + background: var(--md-sys-color-surface-container-high); + color: var(--md-sys-color-on-surface-variant); + font-size: 12px; font-weight: 500; + padding: 6px 12px; border-radius: 12px; + box-shadow: 0 1px 2px rgba(0,0,0,0.1); + position: sticky; top: 8px; z-index: 2; + } + + .highlight { background-color: var(--search-highlight); color: black; border-radius: 2px; padding: 0 2px; } + + /* Search In Chat */ + .in-chat-search { + background: var(--md-sys-color-surface-container); + padding: 8px 16px; display: flex; align-items: center; gap: 10px; + border-bottom: 1px solid var(--md-sys-color-outline-variant); + animation: slideUp 0.3s cubic-bezier(0.2, 0.0, 0, 1.0); + } + .in-chat-search input { + border: none; background: transparent; outline: none; flex: 1; + color: var(--md-sys-color-on-surface); + } + + /* Menu */ + .menu-container { position: relative; } + .dropdown-menu { + position: absolute; top: 100%; right: 0; + background: var(--md-sys-color-surface-container-high); + border-radius: 12px; padding: 8px 0; min-width: 200px; + display: none; flex-direction: column; z-index: 100; + box-shadow: 0 4px 8px rgba(0,0,0,0.2); + animation: scaleIn 0.2s cubic-bezier(0.2, 0.0, 0, 1.0); transform-origin: top right; + } + .dropdown-menu.show { display: flex; } + .menu-item { + padding: 12px 16px; display: flex; align-items: center; gap: 12px; + color: var(--md-sys-color-on-surface); background: none; border: none; + font-family: inherit; font-size: 14px; cursor: pointer; text-align: left; + } + .menu-item:hover { background: rgba(128,128,128,0.1); } + + /* Scroll To Bottom FAB */ + .scroll-fab { + position: fixed; bottom: 24px; right: 24px; + width: 40px; height: 40px; border-radius: 50%; + background: var(--md-sys-color-surface-container-high); + color: var(--md-sys-color-on-surface-variant); + border: 1px solid var(--md-sys-color-outline-variant); + box-shadow: 0 2px 6px rgba(0,0,0,0.15); + display: flex; align-items: center; justify-content: center; + cursor: pointer; z-index: 20; opacity: 0; transform: translateY(20px); pointer-events: none; + transition: 0.3s; + } + .scroll-fab.visible { opacity: 1; transform: translateY(0); pointer-events: auto; } + + /* Calendar Modal */ + .calendar-card { + background: var(--md-sys-color-surface); + padding: 0; border-radius: 28px; width: 320px; + overflow: hidden; display: flex; flex-direction: column; + } + .cal-header { + background: var(--md-sys-color-surface-container-high); + padding: 16px 24px; + } + .cal-header h3 { font-size: 14px; color: var(--md-sys-color-on-surface-variant); font-weight: 500; } + .cal-header h2 { font-size: 32px; color: var(--md-sys-color-on-surface); font-weight: 400; margin-top: 4px; } + + .cal-grid { padding: 16px 12px 0; } + .cal-nav { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; padding: 0 12px; } + .cal-days { display: grid; grid-template-columns: repeat(7, 1fr); text-align: center; margin-bottom: 8px; } + .cal-day-label { font-size: 12px; color: var(--md-sys-color-on-surface-variant); width: 36px; } + + .cal-dates { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; margin-bottom: 16px; } + .cal-date { + width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; + border-radius: 50%; font-size: 14px; cursor: pointer; color: var(--md-sys-color-on-surface); + } + .cal-date:hover { background: var(--md-sys-color-surface-container-high); } + .cal-date.active { background: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary); } + .cal-date.empty { pointer-events: none; } + + /* Settings Modal */ + .modal-overlay { + position: fixed; inset: 0; z-index: 2000; + background: rgba(0,0,0,0.6); backdrop-filter: blur(4px); + display: flex; align-items: center; justify-content: center; + opacity: 0; visibility: hidden; transition: 0.3s; + } + .modal-overlay.active { opacity: 1; visibility: visible; } + .settings-card { + background: var(--md-sys-color-surface); + width: 90%; max-width: 600px; height: 80vh; + border-radius: 28px; display: flex; flex-direction: column; + overflow: hidden; animation: scaleIn 0.3s var(--anim-enter); + } + .settings-header { + padding: 20px 24px; border-bottom: 1px solid var(--md-sys-color-outline-variant); + display: flex; justify-content: space-between; align-items: center; + } + .settings-content { flex: 1; overflow-y: auto; padding: 24px; } + .setting-section { margin-bottom: 32px; } + .setting-title { font-size: 14px; font-weight: 500; color: var(--md-sys-color-primary); margin-bottom: 16px; text-transform: uppercase; letter-spacing: 1px; } + .setting-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; } + .setting-label h4 { font-size: 16px; font-weight: 400; margin-bottom: 4px; } + .setting-label p { font-size: 14px; color: var(--md-sys-color-on-surface-variant); } + + /* Toggle Switch */ + .switch { position: relative; display: inline-block; width: 50px; height: 28px; } + .switch input { opacity: 0; width: 0; height: 0; } + .slider { + position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; + background-color: var(--md-sys-color-surface-container-high); border-radius: 34px; transition: .4s; + border: 1px solid var(--md-sys-color-outline); + } + .slider:before { + position: absolute; content: ""; height: 20px; width: 20px; left: 4px; bottom: 3px; + background-color: var(--md-sys-color-outline); border-radius: 50%; transition: .4s; + } + input:checked + .slider { background-color: var(--md-sys-color-primary); border-color: var(--md-sys-color-primary); } + input:checked + .slider:before { transform: translateX(20px); background-color: var(--md-sys-color-on-primary); } + + /* Accordion */ + .faq-item { border-bottom: 1px solid var(--md-sys-color-outline-variant); } + .faq-question { + width: 100%; padding: 16px 0; text-align: left; background: none; border: none; + font-size: 16px; color: var(--md-sys-color-on-surface); cursor: pointer; + display: flex; justify-content: space-between; + } + .faq-answer { display: none; padding-bottom: 16px; color: var(--md-sys-color-on-surface-variant); line-height: 1.5; } + .faq-item.open .faq-answer { display: block; } + + /* Background Selector */ + .bg-selector { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 10px; } + .bg-option { + height: 60px; border-radius: 8px; cursor: pointer; border: 2px solid transparent; + background-size: cover; position: relative; overflow: hidden; + } + .bg-option.selected { border-color: var(--md-sys-color-primary); } + .bg-1 { background-color: #EFE7DE; } + .bg-2 { background-color: #d1d7db; } + .bg-3 { background-color: #e5ddd5; } + .bg-4 { background-color: #ffffff; } + + /* Install Sheet - Hidden by default on PC */ + .install-sheet { display: none; } + + /* Settings Install Card */ + .install-card { + background: var(--md-sys-color-primary-container); + color: var(--md-sys-color-on-primary-container); + padding: 16px; border-radius: 12px; margin-bottom: 32px; + display: flex; align-items: center; justify-content: space-between; + } + .install-card h4 { margin-bottom: 4px; } + .install-card p { font-size: 12px; } + .install-card .m3-btn { + width: auto; height: 36px; padding: 0 16px; + font-size: 14px; white-space: nowrap; flex-shrink: 0; + box-shadow: none; + } + + @media (max-width: 768px) { + .sidebar { + width: 100%; + flex: 1; + border-right: none; + /* Mobile-only background gradient */ + background: radial-gradient(circle at 0% 0%, #c1e8fb49 0%, transparent 50%), + radial-gradient(circle at 100% 100%, #89f8c86b 0%, transparent 50%), + #fbfdf96c; + } + + /* Dark mode override for sidebar on mobile */ + [data-theme="dark"] .sidebar { + background: radial-gradient(circle at 0% 0%, #001F29 0%, transparent 50%), + radial-gradient(circle at 100% 100%, #002114 0%, transparent 50%), + #111412; + } + + .chat-area { position: fixed; inset: 0; z-index: 20; transform: translateX(100%); transition: transform 0.3s; background-color: var(--chat-bg-color); } + .chat-area.active { transform: translateX(0); } + .back-btn { display: flex !important; margin-right: 8px; } + + /* Bottom Sheet Install Prompt - Only visible on Mobile */ + .install-sheet { + display: block; /* Enable it for mobile logic */ + position: fixed; bottom: 0; left: 0; right: 0; z-index: 3000; + background: var(--md-sys-color-surface); + border-top-left-radius: 28px; border-top-right-radius: 28px; + padding: 24px; box-shadow: 0 -4px 20px rgba(0,0,0,0.15); + transform: translateY(100%); transition: transform 0.3s cubic-bezier(0.2, 0, 0, 1); + } + .install-sheet.visible { transform: translateY(0); } + .sheet-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; } + .sheet-content { text-align: center; } + .sheet-actions { display: flex; flex-direction: column; gap: 12px; margin-top: 24px; } + } + .back-btn { display: none; margin-right: 8px; } + + /* UTILITY MODALS STYLING FIX */ + .modal-card { + background-color: var(--md-sys-color-surface); + padding: 24px; + border-radius: 28px; + width: 90%; + max-width: 320px; + transform: scale(0.9); transition: 0.3s cubic-bezier(0.2, 0, 0, 1); + animation: scaleIn 0.3s var(--anim-enter); + } + .modal-overlay.active .modal-card { transform: scale(1); } + + .modal-card.utility { + width: 320px; + padding: 24px; + border-radius: 28px; + } + .modal-actions { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 24px; + } + .modal-actions .m3-btn { + width: auto; + min-width: 80px; + height: 40px; + padding: 0 16px; + } + + /* Empty State Centering */ + .empty-state { + display: flex; flex-direction: column; align-items: center; justify-content: center; + height: 100%; width: 100%; color: var(--md-sys-color-outline); + text-align: center; animation: fadeIn 0.5s; + } + + /* Skeleton */ + .skeleton { + background: linear-gradient(90deg, var(--md-sys-color-surface-container-high) 25%, var(--md-sys-color-surface-container) 50%, var(--md-sys-color-surface-container-high) 75%); + background-size: 200% 100%; animation: shimmer 1.5s infinite linear; border-radius: 4px; + } + @keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } } + .skel-contact { display: flex; align-items: center; padding: 12px 16px; gap: 16px; width: 100%; } + .skel-avatar { width: 48px; height: 48px; border-radius: 50%; } + .skel-text-col { flex: 1; display: flex; flex-direction: column; gap: 8px; } + .skel-msg-group { display: flex; flex-direction: column; gap: 12px; padding: 20px; width: 100%; } + .skel-bubble { height: 40px; border-radius: 12px; width: 40%; margin-bottom: 8px;} + .skel-left { align-self: flex-start; border-bottom-left-radius: 0; } + .skel-right { align-self: flex-end; border-bottom-right-radius: 0; width: 50%; } + + </style> +</head> +<body> + + <!-- 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> + </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> + + <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="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> + </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> + </div> + </div> + + <!-- LOCK SCREEN (PIN / BIO / LOCKOUT) --> + <div id="lock-screen"> + <!-- LOCKOUT UI --> + <div id="lockoutUI" class="hidden lockout-container"> + <div class="lockout-icon-bg"> + <span class="material-symbols-rounded lockout-icon" id="lockoutIcon">lock_clock</span> + </div> + <h2 class="lockout-title" id="lockoutTitle">Locked</h2> + <p class="lockout-msg" id="lockoutMsg">Too many attempts.</p> + <div class="lockout-timer" id="lockoutTimer">00:30</div> + <p class="lockout-sub" id="lockoutSub">Try again later</p> + <div id="permanentLockActions" class="hidden" style="margin-top:24px; display:flex; gap:12px;"> + <button class="m3-btn filled" onclick="showForgotPin()" style="background:var(--md-sys-color-error); width:auto; padding:0 32px;">Forgot PIN</button> + </div> + </div> + + <!-- HEADER (Hidden during lockout) --> + <div class="lock-header" id="lockHeader"> + <span class="material-symbols-rounded lock-icon" id="lockIcon">lock</span> + <div class="lock-title" id="lockTitle">Enter PIN</div> + <div class="lock-sub" id="lockSub">Protected by App Lock</div> + </div> + + <!-- PIN UI --> + <div id="pinUI"> + <div class="pin-dots" id="pinDots"> + <div class="pin-dot"></div><div class="pin-dot"></div><div class="pin-dot"></div> + <div class="pin-dot"></div><div class="pin-dot"></div><div class="pin-dot"></div> + </div> + + <div class="keypad"> + <button class="key-btn ripple-surface" onclick="pressKey(1)">1</button> + <button class="key-btn ripple-surface" onclick="pressKey(2)">2</button> + <button class="key-btn ripple-surface" onclick="pressKey(3)">3</button> + <button class="key-btn ripple-surface" onclick="pressKey(4)">4</button> + <button class="key-btn ripple-surface" onclick="pressKey(5)">5</button> + <button class="key-btn ripple-surface" onclick="pressKey(6)">6</button> + <button class="key-btn ripple-surface" onclick="pressKey(7)">7</button> + <button class="key-btn ripple-surface" onclick="pressKey(8)">8</button> + <button class="key-btn ripple-surface" onclick="pressKey(9)">9</button> + <button class="key-btn ripple-surface" style="visibility:hidden"> </button> + <button class="key-btn ripple-surface" onclick="pressKey(0)">0</button> + <button class="key-btn ripple-surface" onclick="pressKey(-1)"><span class="material-symbols-rounded key-backspace">backspace</span></button> + </div> + </div> + + <!-- BIO ONLY UI --> + <div id="bioUI" class="hidden" style="flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center;"> + <div class="ripple-surface" onclick="triggerBiometricAuth()" style="width:96px; height:96px; border-radius:50%; background:var(--md-sys-color-secondary-container); color:var(--md-sys-color-on-secondary-container); display:flex; align-items:center; justify-content:center; cursor:pointer;"> + <span class="material-symbols-rounded" style="font-size:48px;">fingerprint</span> + </div> + <p style="margin-top:16px; color:var(--md-sys-color-on-surface-variant);">Tap to Unlock</p> + </div> + + <div style="text-align: center;"> + <button class="forgot-pin-btn" onclick="showForgotPin()">Forgot PIN / Use Password</button> + </div> + </div> + + <!-- MAIN APP --> + <div id="app-layout"> + <aside class="sidebar" id="sidebar"> + <div class="sidebar-header"> + <div style="display: flex; align-items: center; gap: 12px; padding: 12px 0;"> + <!-- Title --> + <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> + + <!-- Version (Tighter Gap) --> + <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;"> + v3.10.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> + <button class="icon-btn ripple-surface" onclick="openSettings()" title="Settings"><span class="material-symbols-rounded">settings</span></button> + </div> + </div> + <div class="search-container"> + <div class="search-bar"> + <span class="material-symbols-rounded" style="color: var(--md-sys-color-on-surface-variant);">search</span> + <input type="text" id="searchInput" placeholder="Search..."> + </div> + </div> + <div class="contact-list" id="contactList"></div> + </aside> + + <main class="chat-area" id="chatArea"> + <header class="chat-header"> + <button class="icon-btn back-btn ripple-surface" onclick="closeChatMobile()"><span class="material-symbols-rounded">arrow_back</span></button> + <div id="chatHeaderContent" style="display:flex; align-items:center; gap:12px; flex:1; opacity:0;"> + <div class="avatar" id="headerAvatar">A</div> + <div style="flex:1;"> + <div id="headerName" style="font-weight: 500; font-size: 16px;">Alice</div> + <div id="headerId" style="font-size: 12px; opacity: 0.7;">Online</div> + </div> + <div class="menu-container"> + <button class="icon-btn ripple-surface" onclick="toggleChatMenu(event)"><span class="material-symbols-rounded">more_vert</span></button> + <div class="dropdown-menu" id="chatMenu"> + <button class="menu-item" onclick="toggleChatSearch()"><span class="material-symbols-rounded">search</span>Search in Chat</button> + <button class="menu-item" onclick="openDateSearch()"><span class="material-symbols-rounded">calendar_month</span>Search by Date</button> + <button class="menu-item" onclick="showRenameModal()"><span class="material-symbols-rounded">edit</span>Rename</button> + <button class="menu-item" onclick="downloadChat()"><span class="material-symbols-rounded">download</span>Download</button> + </div> + </div> + </div> + </header> + + <div id="inChatSearch" class="in-chat-search hidden"> + <input type="text" id="msgSearchInput" placeholder="Find message..."> + <div id="searchCount" style="font-size:12px; margin-right:8px;"></div> + <button class="icon-btn" style="width:30px; height:30px;" onclick="toggleChatSearch()"><span class="material-symbols-rounded">close</span></button> + </div> + + <div class="chat-bg-pattern"></div> + <div class="chat-content" id="messagesContainer"> + <div class="empty-state"> + <span class="material-symbols-rounded" style="font-size: 64px; opacity: 0.2;">chat</span> + <p>Select a conversation</p> + </div> + </div> + + <!-- Scroll to Bottom FAB - Now triggers SMOOTH scroll manually --> + <button class="scroll-fab" id="scrollFab" onclick="scrollToBottom('smooth')"> + <span class="material-symbols-rounded">arrow_downward</span> + </button> + </main> + </div> + + <!-- SETTINGS MODAL --> + <div id="settingsModal" class="modal-overlay"> + <div class="settings-card"> + <div class="settings-header"> + <h2 style="font-family:'Google Sans'">Settings</h2> + <button class="icon-btn" onclick="closeModal('settingsModal')"><span class="material-symbols-rounded">close</span></button> + </div> + <div class="settings-content"> + + <!-- Install App --> + <div id="installAppSection" class="install-card hidden"> + <div> + <h4>Install App</h4> + <p>Get the app for better experience</p> + </div> + <button class="m3-btn filled" onclick="installPWA()">Install</button> + </div> + + <!-- Security (Only visible on Mobile App) --> + <div class="setting-section hidden" id="mobileSecuritySection"> + <div class="setting-title">App Security</div> + <div class="setting-row"> + <div class="setting-label"><h4>PIN Lock</h4><p>Require PIN on startup</p></div> + <label class="switch"><input type="checkbox" id="pinLockToggle" onchange="togglePinSetting()"><span class="slider"></span></label> + </div> + <div class="setting-row"> + <div class="setting-label"><h4>Fingerprint Lock</h4><p>Use biometrics</p></div> + <label class="switch"><input type="checkbox" id="bioLockToggle" onchange="toggleBioSetting()"><span class="slider"></span></label> + </div> + </div> + + <!-- Profile --> + <div class="setting-section"> + <div class="setting-title">Profile</div> + <div class="m3-field"><input type="text" id="profileName" class="m3-input" placeholder=" " onchange="saveProfile()"><label class="m3-label">Your Name</label></div> + <div class="m3-field"><input type="text" id="profilePhone" class="m3-input" placeholder=" " onchange="saveProfile()"><label class="m3-label">Phone Number</label></div> + </div> + + <!-- Appearance --> + <div class="setting-section"> + <div class="setting-title">Appearance</div> + <div class="setting-row"> + <div class="setting-label"><h4>Dark Mode</h4><p>Easier on the eyes</p></div> + <label class="switch"><input type="checkbox" id="darkModeToggle" onchange="toggleTheme()"><span class="slider"></span></label> + </div> + <div class="setting-row"> + <div class="setting-label"><h4>Text Size</h4><p>Adjust message font size</p></div> + <input type="range" min="12" max="20" value="15" oninput="changeFontSize(this.value)"> + </div> + + <div class="setting-label" style="margin-bottom:8px"><h4>Chat Wallpaper</h4></div> + <div class="bg-selector"> + <div class="bg-option bg-1 selected" onclick="changeBg(1)"></div> + <div class="bg-option bg-2" onclick="changeBg(2)"></div> + <div class="bg-option bg-3" onclick="changeBg(3)"></div> + <div class="bg-option bg-4" onclick="changeBg(4)"></div> + </div> + </div> + + <!-- Help & Legal --> + <div class="setting-section"> + <div class="setting-title">Help & Support</div> + <div id="faqContainer"> + <!-- FAQs injected via JS --> + </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#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)" onclick="window.open('mailto:mail@amit.is-a.dev')">Email Support</button> + </div> + </div> + + <div class="setting-section"> + <div class="setting-title">Legal</div> + <p style="font-size:12px; color:var(--md-sys-color-on-surface-variant); line-height:1.5;"> + <strong>MIT License</strong><br> + <strong>Security:</strong> Logs are stored strictly in your private Firebase. No external access. + <br>Check out this project on <a href="https://amit.is-a.dev/logger">amit.is-a.dev</a> or <a href="https://github.com/notamitgamer/WhatsApp-Logger-Self-Hosted-">Github</a> + <br>Developed by <a href="https://github.com/notamitgamer">@notamitgamer</a> + </p> + </div> + + <button class="m3-btn filled" style="background:var(--md-sys-color-error);" onclick="showLogoutModal()">Log Out</button> + </div> + </div> + </div> + + <!-- UTILITY MODALS --> + <div id="logoutModal" class="modal-overlay"> + <div class="modal-card utility"> + <h3 class="modal-title">Log Out?</h3> + <p style="margin:16px 0; color:var(--md-sys-color-on-surface-variant);">Session will end.</p> + <div class="modal-actions"> + <button class="m3-btn text" onclick="closeModal('logoutModal')">Cancel</button> + <button class="m3-btn filled" onclick="handleLogout()">Logout</button> + </div> + </div> + </div> + + <!-- FORGOT PIN MODAL --> + <div id="forgotPinModal" class="modal-overlay"> + <div class="modal-card utility"> + <h3 class="modal-title">Reset App Lock</h3> + <p style="margin-top:8px; color:var(--md-sys-color-on-surface-variant); font-size:14px;">Enter your server credentials to reset local security.</p> + + <div class="m3-field" style="margin-top:16px;"> + <input type="text" id="resetUsername" class="m3-input" placeholder=" "><label class="m3-label">Username</label> + </div> + <div class="m3-field"> + <input type="password" id="resetPassword" class="m3-input" placeholder=" "> + <label class="m3-label">Password</label> + <button class="eye-toggle" onclick="togglePasswordVisibility('resetPassword', 'resetPassIcon')"> + <span class="material-symbols-rounded" id="resetPassIcon">visibility</span> + </button> + </div> + + <p id="resetError" style="color:var(--md-sys-color-error); font-size:12px; margin-bottom:12px; display:none;">Invalid credentials</p> + + <div class="modal-actions"> + <button class="m3-btn text" onclick="closeModal('forgotPinModal')">Cancel</button> + <button class="m3-btn filled" id="resetBtn" onclick="attemptPinReset()"> + <span class="btn-text">Reset</span> + <div class="spinner"></div> + </button> + </div> + </div> + </div> + + <!-- RENAME MODAL --> + <div id="renameModal" class="modal-overlay"> + <div class="modal-card utility"> + <h3 class="modal-title">Rename Chat</h3> + <div class="m3-field" style="margin-top:16px;"><input type="text" id="renameInput" class="m3-input"><label class="m3-label">New Name</label></div> + <div class="modal-actions"> + <button class="m3-btn text" onclick="closeModal('renameModal')">Cancel</button> + <button class="m3-btn filled" onclick="saveRename()">Save</button> + </div> + </div> + </div> + + <!-- NOT FOUND MODAL --> + <div id="notFoundModal" class="modal-overlay"> + <div class="modal-card utility"> + <h3 class="modal-title">No Messages Found</h3> + <p style="margin:16px 0; color:var(--md-sys-color-on-surface-variant);">There are no messages from this date onwards.</p> + <div class="modal-actions"> + <button class="m3-btn filled" onclick="closeModal('notFoundModal')">OK</button> + </div> + </div> + </div> + + <!-- PIN SETUP MODAL --> + <div id="pinSetupModal" class="modal-overlay"> + <div class="modal-card utility" style="text-align:center;"> + <h3 class="modal-title" id="pinSetupTitle">Set 6-Digit PIN</h3> + <div class="pin-dots" id="setupDots" style="margin: 24px 0;"> + <div class="pin-dot"></div><div class="pin-dot"></div><div class="pin-dot"></div> + <div class="pin-dot"></div><div class="pin-dot"></div><div class="pin-dot"></div> + </div> + <p style="color:var(--md-sys-color-on-surface-variant); font-size:12px;" id="pinSetupMsg">Use the number pad below</p> + + <div class="keypad" style="transform: scale(0.9); margin-bottom:0;"> + <button class="key-btn ripple-surface" onclick="pressSetupKey(1)">1</button> + <button class="key-btn ripple-surface" onclick="pressSetupKey(2)">2</button> + <button class="key-btn ripple-surface" onclick="pressSetupKey(3)">3</button> + <button class="key-btn ripple-surface" onclick="pressSetupKey(4)">4</button> + <button class="key-btn ripple-surface" onclick="pressSetupKey(5)">5</button> + <button class="key-btn ripple-surface" onclick="pressSetupKey(6)">6</button> + <button class="key-btn ripple-surface" onclick="pressSetupKey(7)">7</button> + <button class="key-btn ripple-surface" onclick="pressSetupKey(8)">8</button> + <button class="key-btn ripple-surface" onclick="pressSetupKey(9)">9</button> + <button class="key-btn ripple-surface" style="visibility:hidden"> </button> + <button class="key-btn ripple-surface" onclick="pressSetupKey(0)">0</button> + <button class="key-btn ripple-surface" onclick="pressSetupKey(-1)"><span class="material-symbols-rounded key-backspace">backspace</span></button> + </div> + <div class="modal-actions" style="justify-content:center; margin-top:8px;"> + <button class="m3-btn text" onclick="cancelPinSetup()">Cancel</button> + </div> + </div> + </div> + + <!-- CALENDAR MODAL --> + <div id="calendarModal" class="modal-overlay"> + <div class="calendar-card animate-enter"> + <div class="cal-header"> + <h3>Select date</h3> + <h2 id="calSelectedDateStr">Tue, Oct 24</h2> + </div> + <div class="cal-grid"> + <div class="cal-nav"> + <span id="calMonthYear" style="font-weight:500">October 2023</span> + <div style="display:flex; gap:12px;"> + <button class="icon-btn" onclick="changeCalMonth(-1)"><span class="material-symbols-rounded">chevron_left</span></button> + <button class="icon-btn" onclick="changeCalMonth(1)"><span class="material-symbols-rounded">chevron_right</span></button> + </div> + </div> + <div class="cal-days"> + <span class="cal-day-label">S</span><span class="cal-day-label">M</span><span class="cal-day-label">T</span> + <span class="cal-day-label">W</span><span class="cal-day-label">T</span><span class="cal-day-label">F</span><span class="cal-day-label">S</span> + </div> + <div class="cal-dates" id="calDatesGrid"></div> + </div> + <div class="modal-actions" style="padding: 16px 24px;"> + <button class="m3-btn text" onclick="closeModal('calendarModal')">Cancel</button> + <button class="m3-btn filled" onclick="jumpToDate()" style="width:auto; padding:0 24px; height:40px;">OK</button> + </div> + </div> + </div> + + <!-- PWA INSTALL SHEET (Mobile Only) --> + <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-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> + <div class="sheet-actions"> + <button class="m3-btn filled" onclick="installPWA()">Install App</button> + <button class="m3-btn text" onclick="closeInstallSheet()">Continue in Browser</button> + </div> + </div> + </div> + + <script> + const RENDER_BACKEND_URL = "https://logger-8i35.onrender.com"; + const firebaseConfig = { + apiKey: "AIzaSyDqOELjpsI7nbPdK7Z6X57rVRyKUlH4bzk", + authDomain: "wp-logger.firebaseapp.com", + projectId: "wp-logger", + storageBucket: "wp-logger.firebasestorage.app", + messagingSenderId: "642661549198", + appId: "1:642661549198:web:ec94fa62aea1c4e2b375df" + }; + + firebase.initializeApp(firebaseConfig); + const db = firebase.firestore(); + let activeChatId = null; + let deferredPrompt; + let currentMessages = []; + + // --- SECURITY VARS --- + let enteredPin = ""; + let setupPin = ""; + let setupStep = 0; + let isLocked = false; + // Lockout Variables + let failAttempts = parseInt(localStorage.getItem('wp_fail_attempts') || '0'); + let lockUntil = parseInt(localStorage.getItem('wp_lock_until') || '0'); + let lockoutTimerInterval; + + // --- CALENDAR VARS --- + let calCurrentDate = new Date(); + let calSelectedDate = new Date(); + + // --- HELPERS --- + const arrayBufferToBase64 = (buffer) => { + let binary = ''; + const bytes = new Uint8Array(buffer); + const len = bytes.byteLength; + for (let i = 0; i < len; i++) binary += String.fromCharCode(bytes[i]); + return window.btoa(binary); + }; + const base64ToArrayBuffer = (base64) => { + const binary_string = window.atob(base64); + const len = binary_string.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) bytes[i] = binary_string.charCodeAt(i); + return bytes.buffer; + }; + + function formatDividerDate(ts) { + const date = new Date(ts * 1000); + const today = new Date(); + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + + if (date.toDateString() === today.toDateString()) return "Today"; + if (date.toDateString() === yesterday.toDateString()) return "Yesterday"; + return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); + } + + // --- PWA INSTALLATION --- + if ('serviceWorker' in navigator && (window.location.protocol === 'https:' || window.location.protocol === 'http:')) { + navigator.serviceWorker.register('/assets/sw-wpChat.js').catch(err => { + console.warn('SW Register Fail:', err); + }); + } + window.addEventListener('beforeinstallprompt', (e) => { + e.preventDefault(); + deferredPrompt = e; + document.getElementById('installAppSection').classList.remove('hidden'); + if (window.innerWidth <= 768) { + setTimeout(() => document.getElementById('installSheet').classList.add('visible'), 2000); + } + }); + async function installPWA() { + if (!deferredPrompt) return; + deferredPrompt.prompt(); + const { outcome } = await deferredPrompt.userChoice; + if (outcome === 'accepted') { + closeInstallSheet(); + document.getElementById('installAppSection').classList.add('hidden'); + } + deferredPrompt = null; + } + + function closeInstallSheet() { + document.getElementById('installSheet').classList.remove('visible'); + } + + // --- AUTH & BOOT SEQUENCE --- + function isMobileApp() { + const isStandalone = (window.matchMedia('(display-mode: standalone)').matches) || (window.navigator.standalone) || document.referrer.includes('android-app://'); + const isMobile = window.innerWidth <= 768; + return isStandalone && isMobile; + } + + window.onload = () => { + if (localStorage.getItem('theme') === 'dark') { + document.body.setAttribute('data-theme', 'dark'); + document.getElementById('darkModeToggle').checked = true; + } + document.getElementById('profileName').value = localStorage.getItem('profileName') || ''; + document.getElementById('profilePhone').value = localStorage.getItem('profilePhone') || ''; + const bg = localStorage.getItem('chatBg') || 1; + 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(); + + if (authValid && (hasPin || useBio)) { + document.getElementById('auth-screen').style.display = 'none'; + initLockScreen(hasPin, useBio); + } else if (authValid) { + document.getElementById('auth-screen').style.display = 'none'; + document.getElementById('app-layout').classList.add('visible'); + loadChats(); + } else { + document.getElementById('auth-screen').style.opacity = '1'; + } + } 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'; + } + } + + // Scroll Listener for FAB + document.getElementById('messagesContainer').addEventListener('scroll', function() { + const fab = document.getElementById('scrollFab'); + const st = this.scrollTop; + const sh = this.scrollHeight; + const ch = this.clientHeight; + + // Show if we are more than 500px from the bottom + if (sh - st - ch > 500) { + fab.classList.add('visible'); + } else { + fab.classList.remove('visible'); + } + }); + + // Fix for mobile keyboard covering inputs + const inputs = document.querySelectorAll('input[type="text"], input[type="password"]'); + inputs.forEach(input => { + input.addEventListener('focus', () => { + if (window.innerWidth <= 768) { + setTimeout(() => { + input.scrollIntoView({ behavior: 'smooth', block: 'center' }); + }, 300); + } + }); + }); + }; + + // --- LOCK SCREEN & LOCKOUT LOGIC --- + function initLockScreen(hasPin, useBio) { + const lockScreen = document.getElementById('lock-screen'); + lockScreen.classList.add('active'); + isLocked = true; + enteredPin = ""; + updatePinDots(0); + + // Check Lockout State First + if (checkLockoutState()) return; + + if(hasPin) { + document.getElementById('pinUI').classList.remove('hidden'); + document.getElementById('bioUI').classList.add('hidden'); + document.getElementById('lockTitle').innerText = "Enter PIN"; + document.getElementById('lockIcon').innerText = "lock"; + } else if (useBio && !hasPin) { + document.getElementById('pinUI').classList.add('hidden'); + document.getElementById('bioUI').classList.remove('hidden'); + document.getElementById('lockTitle').innerText = "Scan Fingerprint"; + document.getElementById('lockIcon').innerText = "fingerprint"; + triggerBiometricAuth(); + } + } + + function checkLockoutState() { + const now = Date.now(); + + // Permanent Lock + if (failAttempts >= 5) { + renderPermanentLock(); + return true; + } + + // Temporary Timer Lock + if (lockUntil > now) { + startLockoutTimer(); + return true; + } + + // Reset if timer expired + if (lockUntil > 0 && lockUntil <= now) { + lockUntil = 0; + localStorage.setItem('wp_lock_until', '0'); + // Don't reset failAttempts to 0 here to keep escalating. + // But we need to hide lockout UI + document.getElementById('lockoutUI').classList.add('hidden'); + document.getElementById('lockHeader').classList.remove('hidden'); + document.getElementById('lock-screen').classList.remove('locked-mode'); + } + + return false; + } + + function handleFailedAttempt() { + failAttempts++; + localStorage.setItem('wp_fail_attempts', failAttempts); + + if (failAttempts === 3 || failAttempts === 4) { + lockUntil = Date.now() + (30 * 1000); // 30 seconds + localStorage.setItem('wp_lock_until', lockUntil); + startLockoutTimer(); + } else if (failAttempts >= 5) { + renderPermanentLock(); + } else { + // Shake animation for attempt 1 and 2 + document.getElementById('pinDots').classList.add('shake'); + updatePinDots(6, true); + setTimeout(() => { + document.getElementById('pinDots').classList.remove('shake'); + enteredPin = ""; + updatePinDots(0); + }, 400); + } + } + + function startLockoutTimer() { + const ui = document.getElementById('lockoutUI'); + const screen = document.getElementById('lock-screen'); + const timerDisplay = document.getElementById('lockoutTimer'); + + ui.classList.remove('hidden'); + document.getElementById('lockHeader').classList.add('hidden'); + screen.classList.add('locked-mode'); // Hides PIN/BIO inputs + + document.getElementById('lockoutTitle').innerText = "Locked"; + document.getElementById('lockoutMsg').innerText = `Too many attempts (${failAttempts})`; + document.getElementById('lockoutSub').innerText = "Try again later"; + document.getElementById('permanentLockActions').classList.add('hidden'); + document.getElementById('lockoutTimer').classList.remove('hidden'); + + if(lockoutTimerInterval) clearInterval(lockoutTimerInterval); + + lockoutTimerInterval = setInterval(() => { + const left = Math.ceil((lockUntil - Date.now()) / 1000); + if (left <= 0) { + clearInterval(lockoutTimerInterval); + timerDisplay.innerText = "00:00"; + // Reset View + initLockScreen(localStorage.getItem('wp_app_pin'), localStorage.getItem('wp_app_bio') === 'true'); + document.getElementById('lockoutUI').classList.add('hidden'); + document.getElementById('lockHeader').classList.remove('hidden'); + screen.classList.remove('locked-mode'); + } else { + const secs = left < 10 ? `0${left}` : left; + timerDisplay.innerText = `00:${secs}`; + } + }, 1000); + } + + function renderPermanentLock() { + const ui = document.getElementById('lockoutUI'); + const screen = document.getElementById('lock-screen'); + + ui.classList.remove('hidden'); + document.getElementById('lockHeader').classList.add('hidden'); + screen.classList.add('locked-mode'); + + document.getElementById('lockoutIcon').innerText = "gpp_bad"; + document.getElementById('lockoutTitle').innerText = "Security Lockout"; + document.getElementById('lockoutMsg').innerText = "Maximum attempts exceeded. App is locked."; + document.getElementById('lockoutSub').innerText = "Use Forgot Password to reset."; + document.getElementById('lockoutTimer').classList.add('hidden'); + document.getElementById('permanentLockActions').classList.remove('hidden'); + } + + function pressKey(num) { + if(num === -1) { + enteredPin = enteredPin.slice(0, -1); + } else if (enteredPin.length < 6) { + enteredPin += num; + } + updatePinDots(enteredPin.length); + + if(enteredPin.length === 6) { + verifyPin(); + } + } + + function updatePinDots(count, isError=false) { + const dots = document.querySelectorAll('#pinDots .pin-dot'); + dots.forEach((d, i) => { + if(isError) { + d.classList.add('error'); + setTimeout(() => d.classList.remove('error'), 400); + } else { + d.classList.remove('error'); + if(i < count) d.classList.add('filled'); + else d.classList.remove('filled'); + } + }); + } + + function verifyPin() { + // Re-check lockout before verifying + if(checkLockoutState()) return; + + const storedPin = localStorage.getItem('wp_app_pin'); + if(storedPin && enteredPin === storedPin) { + // Success - Reset Fail Counters + failAttempts = 0; + localStorage.setItem('wp_fail_attempts', '0'); + localStorage.setItem('wp_lock_until', '0'); + + const useBio = localStorage.getItem('wp_app_bio') === 'true'; + if (useBio) { + document.getElementById('pinUI').classList.add('hidden'); + document.getElementById('bioUI').classList.remove('hidden'); + document.getElementById('lockTitle').innerText = "Verify Identity"; + document.getElementById('lockIcon').innerText = "fingerprint"; + triggerBiometricAuth(); + } else { + unlockApp(); + } + } else { + handleFailedAttempt(); + } + } + + async function triggerBiometricAuth() { + if(checkLockoutState()) return; + + if(document.getElementById('bioUI').classList.contains('hidden') === false) { + document.getElementById('lockTitle').innerText = "Verify Identity"; + } + try { + const storedId = localStorage.getItem('wp_bio_id'); + const options = { + publicKey: { + challenge: new Uint8Array([1, 2, 3, 4]), + timeout: 60000, + userVerification: "required" + } + }; + if (storedId) { + options.publicKey.allowCredentials = [{ + type: 'public-key', + id: base64ToArrayBuffer(storedId), + transports: ['internal'] + }]; + } + const credential = await navigator.credentials.get(options); + if(credential) { + // Success Bio - Reset Counters + failAttempts = 0; + localStorage.setItem('wp_fail_attempts', '0'); + localStorage.setItem('wp_lock_until', '0'); + unlockApp(); + } + } catch (e) { + console.warn("Bio failed", e); + // Note: We generally don't lock on Bio failures here as OS handles retries. + // But if user cancels and goes to PIN, PIN failures will trigger lock. + } + } + + function unlockApp() { + isLocked = false; + document.getElementById('lock-screen').classList.remove('active'); + document.getElementById('app-layout').classList.add('visible'); + loadChats(); + } + + // --- SECURITY SETTINGS --- + function togglePinSetting() { + const chk = document.getElementById('pinLockToggle'); + if(chk.checked) openPinSetup(); + else localStorage.removeItem('wp_app_pin'); + } + + function toggleBioSetting() { + const chk = document.getElementById('bioLockToggle'); + if(chk.checked) registerBiometric(); + else { + localStorage.removeItem('wp_app_bio'); + localStorage.removeItem('wp_bio_id'); + } + } + + function openPinSetup() { + document.getElementById('pinSetupModal').classList.add('active'); + setupStep = 1; + setupPin = ""; + enteredPin = ""; + updateSetupDots(0); + document.getElementById('pinSetupTitle').innerText = "Set 6-Digit PIN"; + document.getElementById('pinSetupMsg').innerText = "Enter a new PIN"; + } + + function updateSetupDots(count) { + const dots = document.querySelectorAll('#setupDots .pin-dot'); + dots.forEach((d, i) => i < count ? d.classList.add('filled') : d.classList.remove('filled')); + } + + function pressSetupKey(num) { + if(num === -1) { + enteredPin = enteredPin.slice(0, -1); + } else if (enteredPin.length < 6) { + enteredPin += num; + } + updateSetupDots(enteredPin.length); + + if(enteredPin.length === 6) { + if(setupStep === 1) { + setupPin = enteredPin; + enteredPin = ""; + setupStep = 2; + setTimeout(() => { + updateSetupDots(0); + document.getElementById('pinSetupTitle').innerText = "Confirm PIN"; + document.getElementById('pinSetupMsg').innerText = "Re-enter to confirm"; + }, 200); + } else if (setupStep === 2) { + if(enteredPin === setupPin) { + localStorage.setItem('wp_app_pin', setupPin); + closeModal('pinSetupModal'); + enteredPin = ""; + } else { + const dots = document.getElementById('setupDots'); + dots.classList.add('shake'); + setTimeout(() => { + dots.classList.remove('shake'); + enteredPin = ""; + setupPin = ""; + setupStep = 1; + updateSetupDots(0); + document.getElementById('pinSetupTitle').innerText = "Mismatch. Try Again"; + document.getElementById('pinSetupMsg').innerText = "Enter a new PIN"; + }, 500); + } + } + } + } + + function cancelPinSetup() { + document.getElementById('pinLockToggle').checked = false; + closeModal('pinSetupModal'); + } + + async function registerBiometric() { + try { + const iconPath = new URL('/assets/icon.svg', window.location.href).toString(); + const credential = await navigator.credentials.create({ + publicKey: { + challenge: new Uint8Array([1, 2, 3, 4]), + rp: { name: "WhatsApp Logger", icon: iconPath }, + user: { id: new Uint8Array([1]), name: "User", displayName: "App User" }, + pubKeyCredParams: [{type: "public-key", alg: -7}], + authenticatorSelection: { authenticatorAttachment: "platform", userVerification: "required" }, + timeout: 60000 + } + }); + + if(credential) { + const idBase64 = arrayBufferToBase64(credential.rawId); + localStorage.setItem('wp_bio_id', idBase64); + localStorage.setItem('wp_app_bio', 'true'); + } else { throw new Error("No cred"); } + } catch (e) { + console.error(e); + document.getElementById('bioLockToggle').checked = false; + } + } + + function showForgotPin() { + document.getElementById('forgotPinModal').classList.add('active'); + } + + async function attemptPinReset() { + const btn = document.getElementById('resetBtn'); + const err = document.getElementById('resetError'); + const u = document.getElementById('resetUsername').value; + const p = document.getElementById('resetPassword').value; + + btn.classList.add('loading'); + btn.disabled = true; + err.style.display = 'none'; + + try { + const response = await fetch(`${RENDER_BACKEND_URL}/api/verify`, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ username: u, password: p }) + }); + const data = await response.json(); + + if(data.success) { + // RESET EVERYTHING + localStorage.removeItem('wp_app_pin'); + localStorage.removeItem('wp_app_bio'); + localStorage.removeItem('wp_bio_id'); + localStorage.removeItem('wp_fail_attempts'); + localStorage.removeItem('wp_lock_until'); + failAttempts = 0; + lockUntil = 0; + + document.getElementById('pinLockToggle').checked = false; + document.getElementById('bioLockToggle').checked = false; + + // Reset UI State + document.getElementById('lockoutUI').classList.add('hidden'); + document.getElementById('lockHeader').classList.remove('hidden'); + document.getElementById('lock-screen').classList.remove('locked-mode'); + + closeModal('forgotPinModal'); + unlockApp(); + } else { throw new Error(); } + } catch (e) { + err.style.display = 'block'; + } finally { + btn.classList.remove('loading'); + btn.disabled = false; + } + } + + function togglePasswordVisibility(id, iconId) { + const input = document.getElementById(id); + const icon = document.getElementById(iconId); + if (input.type === 'password') { + input.type = 'text'; + icon.innerText = 'visibility_off'; + } else { + input.type = 'password'; + icon.innerText = 'visibility'; + } + } + + async function attemptLogin() { + const btn = document.getElementById('loginBtn'); + const err = document.getElementById('authError'); + btn.classList.add('loading'); + btn.disabled = true; + err.style.opacity = '0'; + + try { + const response = await fetch(`${RENDER_BACKEND_URL}/api/verify`, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + username: document.getElementById('username').value, + password: document.getElementById('password').value + }) + }); + 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)); + } + 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 openSettings() { document.getElementById('settingsModal').classList.add('active'); } + function closeModal(id) { document.getElementById(id).classList.remove('active'); } + function toggleTheme() { + const isDark = document.getElementById('darkModeToggle').checked; + document.body.setAttribute('data-theme', isDark ? 'dark' : 'light'); + localStorage.setItem('theme', isDark ? 'dark' : 'light'); + } + function changeFontSize(val) { document.documentElement.style.setProperty('--base-font-size', `${val}px`); } + function changeBg(id, save=true) { + document.querySelectorAll('.bg-option').forEach(el => el.classList.remove('selected')); + const colors = ['#EFE7DE', '#d1d7db', '#e5ddd5', '#ffffff']; + document.documentElement.style.setProperty('--chat-bg-color', colors[id-1]); + const option = document.querySelector(`.bg-${id}`); + if(option) option.classList.add('selected'); + if(save) localStorage.setItem('chatBg', id); + } + function saveProfile() { + localStorage.setItem('profileName', document.getElementById('profileName').value); + localStorage.setItem('profilePhone', document.getElementById('profilePhone').value); + } + function populateFAQ() { + 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: "Can I reply from here?", a: "No, this is a read-only archive designed for logging and privacy, not active chatting."}, + {q: "What is the 'Pinger' for?", a: "It keeps your free Render server awake so it never misses a message due to inactivity."} + ]; + const container = document.getElementById('faqContainer'); + faqs.forEach(item => { + const div = document.createElement('div'); + div.className = 'faq-item'; + div.innerHTML = `<button class="faq-question" onclick="this.parentElement.classList.toggle('open')">${item.q} <span class="material-symbols-rounded">expand_more</span></button><div class="faq-answer">${item.a}</div>`; + container.appendChild(div); + }); + } + + 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(); + } + + document.getElementById('msgSearchInput').addEventListener('input', (e) => { + const term = e.target.value.toLowerCase(); + const bubbles = document.querySelectorAll('.msg-bubble'); + let count = 0; + document.querySelectorAll('.highlight').forEach(span => { const parent = span.parentNode; parent.innerHTML = parent.textContent; }); + + bubbles.forEach(b => { + const textDiv = b.querySelector('div:not(.msg-sender):not(.msg-meta)'); + if(textDiv) { + const originalText = textDiv.textContent; + if(term && originalText.toLowerCase().includes(term)) { + b.style.display = 'flex'; + const regex = new RegExp(`(${term})`, 'gi'); + textDiv.innerHTML = originalText.replace(regex, '<span class="highlight">$1</span>'); + count++; + } else { + b.style.display = term ? 'none' : 'flex'; + textDiv.textContent = originalText; + } + } + }); + document.getElementById('searchCount').innerText = term ? `${count} found` : ''; + }); + + 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>`; + } + + try { + const snap = await db.collection('Chats').get(); + const groups = {}; + const excluded = ['917278779512@s.whatsapp.net', '201554426618024@lid']; + + snap.forEach(doc => { + const d = doc.data(); + if (!d.lastActive || excluded.includes(doc.id)) return; + const phone = d.phoneNumber || doc.id.split('@')[0]; + if (!groups[phone]) groups[phone] = { main: {id:doc.id, ...d}, ids: [doc.id] }; + else { + groups[phone].ids.push(doc.id); + if (d.lastActive > groups[phone].main.lastActive) groups[phone].main = {id:doc.id, ...d}; + } + }); + + const chats = Object.values(groups).map(g => ({...g.main, ids: g.ids})).sort((a,b) => b.lastActive - a.lastActive); + + list.innerHTML = ''; + if(chats.length === 0) { + list.innerHTML = `<div style="text-align:center; padding:32px; color:var(--md-sys-color-outline);">No chats found</div>`; + return; + } + + chats.forEach(c => { + const el = document.createElement('div'); + el.className = 'contact-item'; + const name = c.customName || c.displayName || c.phoneNumber || c.id; + const isGroup = c.id.includes('@g.us'); + const avatarContent = isGroup ? `<span class="material-symbols-rounded">group</span>` : name[0].toUpperCase(); + + el.innerHTML = `<div class="avatar">${avatarContent}</div><div class="contact-info"><div class="contact-name">${name}</div><div class="contact-sub">${c.phoneNumber || c.id}</div></div>`; + el.onclick = () => openChat(c.ids, name, el, isGroup, c.phoneNumber || c.id); + list.appendChild(el); + }); + } catch (err) { console.error(err); } + } + + function openChat(ids, name, el, isGroup, displayId) { + activeChatId = ids[0]; + isCurrentChatGroup = isGroup; + 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(); + + if (window.matchMedia('(max-width: 768px)').matches) { + window.history.pushState({chatOpen: true}, ""); + document.getElementById('sidebar').classList.add('inactive'); + document.getElementById('chatArea').classList.add('active'); + } + + if(window.unsub) window.unsub.forEach(u => u()); + window.unsub = []; + + const container = document.getElementById('messagesContainer'); + container.innerHTML = `<div class="skel-msg-group"><div class="skeleton skel-bubble skel-left"></div><div class="skeleton skel-bubble skel-right" style="width:60%"></div><div class="skeleton skel-bubble skel-left" style="width:30%"></div></div>`; + + const msgs = new Map(); + ids.forEach(id => { + window.unsub.push(db.collection('Chats').doc(id).collection('Messages').orderBy('timestamp').onSnapshot(snap => { + snap.forEach(d => msgs.set(d.id, d.data())); + renderMsgs(msgs); + })); + }); + } + + // --- UPDATED RENDER MESSAGES (Date Groups for Sticky Header Fix) --- + function renderMsgs(map) { + const container = document.getElementById('messagesContainer'); + container.innerHTML = ''; + currentMessages = Array.from(map.values()).sort((a,b) => a.timestamp - b.timestamp); + + if(currentMessages.length === 0) { + container.innerHTML = '<div class="empty-state"><p>No messages recorded</p></div>'; + return; + } + + let lastDateStr = ""; + let currentGroup = null; + + currentMessages.forEach((m, index) => { + const date = new Date(m.timestamp * 1000); + const dateStr = date.toDateString(); + + // Start New Date Group if date changed + if(dateStr !== lastDateStr) { + // Close previous group if exists + if(currentGroup) container.appendChild(currentGroup); + + // Create new group container + currentGroup = document.createElement('div'); + currentGroup.className = 'chat-date-group'; + + // Add Sticky Divider inside group + const div = document.createElement('div'); + div.className = 'date-divider'; + div.innerText = formatDividerDate(m.timestamp); + currentGroup.appendChild(div); + + lastDateStr = dateStr; + } + + // 2. GROUPING LOGIC (Using previous logic but appending to group) + const div = document.createElement('div'); + div.className = `msg-bubble ${m.fromMe ? 'msg-out' : 'msg-in'}`; + + const nextMsg = currentMessages[index + 1]; + const prevMsg = currentMessages[index - 1]; + + const isSamePrev = (prevMsg && prevMsg.fromMe === m.fromMe && (!isCurrentChatGroup || prevMsg.senderName === m.senderName) && new Date(prevMsg.timestamp*1000).toDateString() === dateStr); + const isSameNext = (nextMsg && nextMsg.fromMe === m.fromMe && (!isCurrentChatGroup || nextMsg.senderName === m.senderName) && new Date(nextMsg.timestamp*1000).toDateString() === dateStr); + + if (isSamePrev && isSameNext) div.classList.add('group-mid'); + else if (isSamePrev) div.classList.add('group-bot'); + else if (isSameNext) div.classList.add('group-top'); + + const showSenderName = !m.fromMe && isCurrentChatGroup && m.senderName && m.senderName !== "Unknown" && !isSamePrev; + + div.id = `msg-${m.timestamp}`; + div.dataset.date = dateStr; + + div.innerHTML = ` + ${showSenderName ? `<div class="msg-sender">${m.senderName}</div>` : ''} + <div>${m.text}</div> + <div class="msg-meta">${date.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'})}</div> + `; + + // Append message to current GROUP + if (currentGroup) currentGroup.appendChild(div); + }); + + // Append the final group + if(currentGroup) container.appendChild(currentGroup); + + // Force scroll to bottom on every update/load (now instant) + setTimeout(() => scrollToBottom('auto'), 50); + } + + // --- SCROLL TO BOTTOM --- + function scrollToBottom(behavior = 'auto') { + const c = document.getElementById('messagesContainer'); + c.scrollTo({ top: c.scrollHeight, behavior: behavior }); + } + + // --- CALENDAR LOGIC --- + function openDateSearch() { + document.getElementById('chatMenu').classList.remove('show'); + document.getElementById('calendarModal').classList.add('active'); + renderCalendar(calCurrentDate); + } + + function renderCalendar(date) { + const year = date.getFullYear(); + const month = date.getMonth(); + + // Header + document.getElementById('calMonthYear').innerText = new Intl.DateTimeFormat('en-US', { month: 'long', year: 'numeric' }).format(date); + + // Grid + const firstDay = new Date(year, month, 1).getDay(); + const daysInMonth = new Date(year, month + 1, 0).getDate(); + const grid = document.getElementById('calDatesGrid'); + grid.innerHTML = ''; + + // Empty slots for prev month + for(let i=0; i<firstDay; i++) { + grid.innerHTML += `<div class="cal-date empty"></div>`; + } + + // Days + for(let d=1; d<=daysInMonth; d++) { + const dayEl = document.createElement('div'); + dayEl.className = 'cal-date ripple-surface'; + dayEl.innerText = d; + + // Check if selected + if(calSelectedDate && calSelectedDate.getDate() === d && calSelectedDate.getMonth() === month && calSelectedDate.getFullYear() === year) { + dayEl.classList.add('active'); + } + + dayEl.onclick = () => selectDate(new Date(year, month, d)); + grid.appendChild(dayEl); + } + } + + function changeCalMonth(delta) { + calCurrentDate.setMonth(calCurrentDate.getMonth() + delta); + renderCalendar(calCurrentDate); + } + + function selectDate(date) { + calSelectedDate = date; + document.getElementById('calSelectedDateStr').innerText = date.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' }); + renderCalendar(calCurrentDate); // Re-render to show active state + } + + function jumpToDate() { + if(!currentMessages.length) { + closeModal('calendarModal'); + return; + } + + // Find first message >= selected date + // Set time to 00:00:00 for comparison + const targetTs = calSelectedDate.setHours(0,0,0,0) / 1000; + const targetDateStr = calSelectedDate.toDateString(); + + // Try to find exact match or next closest + const msg = currentMessages.find(m => m.timestamp >= targetTs); + + closeModal('calendarModal'); + + if(msg) { + const el = document.getElementById(`msg-${msg.timestamp}`); + if(el) { + el.scrollIntoView({behavior: 'smooth', block: 'center'}); + el.style.backgroundColor = 'var(--search-highlight)'; // Flash highlight + setTimeout(() => el.style.backgroundColor = '', 2000); + } + } else { + document.getElementById('notFoundModal').classList.add('active'); + } + } + + function handleLogout() { + localStorage.removeItem('wp_auth_expiry'); + localStorage.removeItem('wp_app_pin'); + localStorage.removeItem('wp_app_bio'); + location.reload(); + } + + function closeChatMobile() { + window.history.back(); + } + + window.addEventListener('popstate', () => { + document.getElementById('chatArea').classList.remove('remove'); + document.getElementById('chatArea').classList.remove('active'); + }); + + function downloadChat() { + if(!currentMessages.length) return; + const text = currentMessages.map(m => `[${new Date(m.timestamp*1000).toLocaleString()}] ${m.senderName}: ${m.text}`).join('\n'); + const blob = new Blob([text], {type:'text/plain'}); + const a = document.createElement('a'); + a.href = URL.createObjectURL(blob); + a.download = 'chat.txt'; + a.click(); + } + + function showRenameModal() { + if(!activeChatId) return; + document.getElementById('renameModal').classList.add('active'); + document.getElementById('renameInput').value = document.getElementById('headerName').innerText; + document.getElementById('chatMenu').classList.remove('show'); + } + + async function saveRename() { + const name = document.getElementById('renameInput').value; + if(name) { + await db.collection('Chats').doc(activeChatId).set({customName: name}, {merge:true}); + document.getElementById('headerName').innerText = name; + closeModal('renameModal'); + loadChats(); + } + } + + function showLogoutModal() { + document.getElementById('logoutModal').classList.add('active'); + } + + document.getElementById('searchInput').addEventListener('input', (e) => { + const val = e.target.value.toLowerCase(); + document.querySelectorAll('.contact-item').forEach(el => { + const name = el.innerText.toLowerCase(); + el.style.display = name.includes(val) ? 'flex' : 'none'; + }); + }); + + function promptAddChat() { + const id = prompt("Enter WhatsApp ID (e.g. 12345@s.whatsapp.net)"); + if(id) { + db.collection('Chats').doc(id).set({displayName:'Manual Add', id:id, lastActive: Math.floor(Date.now()/1000)}, {merge:true}); + setTimeout(loadChats, 1000); + } + } + + async function refreshApp() { + const btn = document.querySelector('.icon-btn span[class*="refresh"]'); + if(btn) btn.style.animation = 'spin 1s infinite linear'; + if ('caches' in window) { + try { + const keys = await caches.keys(); + await Promise.all(keys.map(key => caches.delete(key))); + } catch(e) { console.log("Cache clear error", e); } + } + if ('serviceWorker' in navigator) { + const registrations = await navigator.serviceWorker.getRegistrations(); + for (let registration of registrations) { + await registration.unregister(); + } + } + window.location.reload(); + } + </script> +</body> +</html>+ \ No newline at end of file
© 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