WhatsApp-Logger-Self-Hosted-

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

index.html (126785B)


      1 <!DOCTYPE html>
      2 <html lang="en">
      3 <head>
      4     <meta charset="UTF-8">
      5     <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
      6     <title>WhatsApp Logger</title>
      7     
      8     <!-- PWA Manifest -->
      9     <link rel="manifest" href="/assets/manifest-wpChat.json">
     10     <meta name="theme-color" content="#006C4C">
     11     
     12     <!-- SVG Favicon -->
     13     <link rel="icon" href="/assets/icon.svg">
     14 
     15     <!-- Fonts & Icons -->
     16     <link rel="preconnect" href="https://fonts.googleapis.com">
     17     <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
     18     <link href="https://fonts.googleapis.com/css2?family=Lexend:wght@300;400;500;600;700&display=swap" rel="stylesheet">
     19     <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@24,400,0,0" />
     20 
     21     <!-- Firebase SDKs -->
     22     <script src="https://www.gstatic.com/firebasejs/10.8.0/firebase-app-compat.js"></script>
     23     <script src="https://www.gstatic.com/firebasejs/10.8.0/firebase-firestore-compat.js"></script>
     24 
     25     <style>
     26         :root {
     27             /* Light Theme Tokens */
     28             --md-sys-color-primary: #006C4C;
     29             --md-sys-color-on-primary: #FFFFFF;
     30             --md-sys-color-primary-container: #89F8C7;
     31             --md-sys-color-on-primary-container: #002114;
     32             
     33             --md-sys-color-secondary: #4D6357;
     34             --md-sys-color-on-secondary: #FFFFFF;
     35             --md-sys-color-secondary-container: #CFE9D9;
     36             --md-sys-color-on-secondary-container: #092016;
     37             
     38             --md-sys-color-surface: #FBFDF9;
     39             --md-sys-color-surface-dim: #D9DBD9;
     40             --md-sys-color-surface-container: #ECEEEB;
     41             --md-sys-color-surface-container-high: #E6E8E5;
     42             --md-sys-color-surface-container-highest: #E1E3E0;
     43             
     44             --md-sys-color-on-surface: #191C1A;
     45             --md-sys-color-on-surface-variant: #404944;
     46             --md-sys-color-outline: #707974;
     47             --md-sys-color-outline-variant: #BFC9C2;
     48 
     49             --md-sys-color-error: #BA1A1A;
     50             --md-sys-color-error-container: #FFDAD6;
     51             --md-sys-color-on-error-container: #410002;
     52             
     53             /* WhatsApp Authentic Colors */
     54             --chat-bubble-me: #DCF8C6;
     55             --chat-bubble-them: #FFFFFF;
     56             --chat-bg-color: #ECE5DD;
     57             --chat-bg-image-opacity: 0.08;
     58             --search-highlight: #ffeb3b;
     59             
     60             /* Elevations & Shapes */
     61             --elevation-1: 0px 1px 3px 1px rgba(0,0,0,0.15), 0px 1px 2px 0px rgba(0,0,0,0.3);
     62             --elevation-2: 0px 2px 6px 2px rgba(0,0,0,0.15), 0px 1px 2px 0px rgba(0,0,0,0.3);
     63             
     64             /* Settings */
     65             --base-font-size: 15px;
     66         }
     67 
     68         /* Dark Theme Tokens */
     69         [data-theme="dark"] {
     70             --md-sys-color-primary: #89F8C7;
     71             --md-sys-color-on-primary: #003828;
     72             --md-sys-color-primary-container: #005138;
     73             --md-sys-color-on-primary-container: #89F8C7;
     74             
     75             --md-sys-color-secondary: #BACCB3;
     76             --md-sys-color-on-secondary: #25342D;
     77             --md-sys-color-secondary-container: #3B4B43;
     78             --md-sys-color-on-secondary-container: #D6E8D8;
     79             
     80             --md-sys-color-surface: #111412;
     81             --md-sys-color-surface-dim: #111412;
     82             --md-sys-color-surface-container: #1E211F;
     83             --md-sys-color-surface-container-high: #282B29;
     84             --md-sys-color-surface-container-highest: #333534;
     85             
     86             --md-sys-color-on-surface: #E2E3DD;
     87             --md-sys-color-on-surface-variant: #C2C8C5;
     88             --md-sys-color-outline: #8C938F;
     89             --md-sys-color-outline-variant: #424945;
     90 
     91             --md-sys-color-error: #FFB4AB;
     92             --md-sys-color-error-container: #93000A;
     93             --md-sys-color-on-error-container: #FFDAD6;
     94             
     95             --chat-bubble-me: #005C4B;
     96             --chat-bubble-them: #202C33;
     97             --chat-bg-color: #0B141A;
     98             --chat-bg-image-opacity: 0.05;
     99             --search-highlight: #6b6200;
    100         }
    101 
    102         * { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
    103 
    104         body, *:not(.material-symbols-rounded) {
    105             font-family: 'Lexend', sans-serif !important;
    106         }
    107         body {
    108             background-color: var(--md-sys-color-surface);
    109             color: var(--md-sys-color-on-surface);
    110             height: 100vh;
    111             overflow: hidden;
    112             display: flex;
    113             justify-content: center;
    114             transition: background-color 0.3s, color 0.3s;
    115         }
    116 
    117         /* --- ANIMATIONS --- */
    118         .hidden { display: none !important; }
    119         .fade-in { animation: fadeIn 0.4s cubic-bezier(0.2, 0.0, 0.0, 1.0); }
    120         @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
    121         .ripple-surface { position: relative; overflow: hidden; transform: translate3d(0, 0, 0); transition: transform 0.1s; }
    122         .ripple-surface:active { transform: scale(0.98); }
    123         
    124         @keyframes scaleIn { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } }
    125         @keyframes slideUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
    126         .animate-enter { animation: scaleIn 0.3s cubic-bezier(0.2, 0.0, 0, 1.0); }
    127         .shake { animation: shake 0.4s cubic-bezier(.36,.07,.19,.97) both; }
    128         @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); } }
    129 
    130         /* --- M3 STAGGERED ENTRANCE --- */
    131         @keyframes fadeInUp {
    132             from { opacity: 0; transform: translateY(30px); }
    133             to { opacity: 1; transform: translateY(0); }
    134         }
    135         .anim-up { opacity: 0; animation: fadeInUp 0.6s cubic-bezier(0.2, 0, 0, 1) forwards; }
    136         .delay-1 { animation-delay: 0.1s; }
    137         .delay-2 { animation-delay: 0.2s; }
    138         .delay-3 { animation-delay: 0.3s; }
    139         .delay-4 { animation-delay: 0.4s; }
    140         .delay-5 { animation-delay: 0.5s; }
    141 
    142         /* --- AUTH SCREEN --- */
    143         #auth-screen {
    144             position: fixed; inset: 0; z-index: 999;
    145             background: radial-gradient(circle at 0% 0%, #C1E8FB 0%, transparent 50%), 
    146                         radial-gradient(circle at 100% 100%, #89F8C7 0%, transparent 50%), 
    147                         #FBFDF9;
    148             display: flex; flex-direction: column; align-items: center; justify-content: center;
    149             padding: 24px; height: 100dvh;
    150             transition: opacity 0.4s cubic-bezier(0.2, 0, 0, 1);
    151         }
    152         
    153         [data-theme="dark"] #auth-screen {
    154              background: radial-gradient(circle at 0% 0%, #001F29 0%, transparent 50%), 
    155                         radial-gradient(circle at 100% 100%, #002114 0%, transparent 50%), 
    156                         #111412;
    157         }
    158         
    159         .auth-card { 
    160             background: rgba(251, 253, 249, 0.7);
    161             backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
    162             border-radius: 28px;
    163             padding: 32px 24px; 
    164             width: 90%; max-width: 380px;
    165             box-shadow: var(--elevation-2);
    166             display: flex; flex-direction: column; gap: 20px;
    167             position: relative;
    168         }
    169         
    170         [data-theme="dark"] .auth-card {
    171             background: rgba(30, 33, 31, 0.7);
    172         }
    173 
    174         .brand-icon { 
    175             width: 64px; height: 64px; 
    176             display: flex; align-items: center; justify-content: center; 
    177             margin: 0 auto 8px;
    178         }
    179         .brand-icon img { width: 100%; height: 100%; object-fit: contain; }
    180 
    181         /* Custom Spinner for the Boot Loader */
    182         [data-theme="dark"] #authLoader .spinner { 
    183              border-color: rgba(255,255,255,0.1) !important; 
    184              border-top-color: var(--md-sys-color-primary) !important; 
    185         }
    186 
    187         /* --- LOCK SCREEN (PIN/BIO) --- */
    188         #lock-screen {
    189             position: fixed; inset: 0; z-index: 2000;
    190             background: var(--md-sys-color-surface);
    191             display: flex; flex-direction: column; align-items: center; justify-content: space-between;
    192             padding: 48px 24px; opacity: 0; visibility: hidden; transition: opacity 0.3s;
    193         }
    194         #lock-screen.active { opacity: 1; visibility: visible; }
    195         
    196         .lock-header { text-align: center; margin-top: 4vh; }
    197         .lock-icon { width: 64px; height: 64px; margin-bottom: 16px; color: var(--md-sys-color-primary); }
    198         .lock-title { font-family: 'Lexend', sans-serif; font-size: 24px; margin-bottom: 8px; }
    199         .lock-sub { color: var(--md-sys-color-on-surface-variant); font-size: 14px; }
    200 
    201         /* PIN Dots */
    202         .pin-dots { display: flex; gap: 16px; margin: 32px 0; justify-content: center; }
    203         .pin-dot {
    204             width: 14px; height: 14px; border-radius: 50%;
    205             border: 1px solid var(--md-sys-color-outline);
    206             transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
    207         }
    208         .pin-dot.filled { background: var(--md-sys-color-on-surface); border-color: var(--md-sys-color-on-surface); transform: scale(1.1); }
    209         .pin-dot.error { border-color: var(--md-sys-color-error); background: var(--md-sys-color-error); }
    210 
    211         /* Native Keypad */
    212         .keypad { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px 32px; margin-bottom: 2vh; }
    213         .key-btn {
    214             width: 72px; height: 72px; border-radius: 50%; border: none; background: transparent;
    215             font-size: 28px; font-weight: 400; color: var(--md-sys-color-on-surface);
    216             display: flex; align-items: center; justify-content: center; cursor: pointer;
    217             transition: background 0.2s; font-family: 'Lexend', sans-serif;
    218         }
    219         .key-btn:active { background: var(--md-sys-color-surface-container-high); }
    220         .key-backspace { font-size: 24px; color: var(--md-sys-color-on-surface-variant); }
    221 
    222         .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; }
    223 
    224         /* --- LOCKOUT UI --- */
    225         .lockout-container { display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; width: 100%; animation: fadeIn 0.3s; flex: 1; }
    226         .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; }
    227         .lockout-icon { font-size: 40px; }
    228         .lockout-title { font-family: 'Lexend', sans-serif; font-size: 28px; margin-bottom: 8px; color: var(--md-sys-color-on-surface); }
    229         .lockout-msg { color: var(--md-sys-color-on-surface-variant); margin-bottom: 32px; font-size: 16px; }
    230         .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; }
    231         .lockout-sub { margin-top: 16px; color: var(--md-sys-color-on-surface-variant); font-size: 14px; }
    232         .lockout-actions { margin-top: 24px; display: flex; gap: 12px; }
    233         .locked-mode #pinUI, .locked-mode #bioUI, .locked-mode .forgot-pin-btn { display: none !important; }
    234 
    235         /* --- UI ELEMENTS --- */
    236         .m3-field {
    237             position: relative; margin-bottom: 0;
    238             background: var(--md-sys-color-surface-container-highest);
    239             border-radius: 4px 4px 0 0; border-bottom: 1px solid var(--md-sys-color-outline);
    240             transition: background 0.3s; height: 56px;
    241         }
    242         .m3-field:hover { background: rgba(29, 25, 43, 0.08); }
    243         .m3-field:focus-within { border-bottom: 2px solid var(--md-sys-color-primary); }
    244         .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; }
    245         .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); }
    246         .m3-input:focus ~ .m3-label, .m3-input:not(:placeholder-shown) ~ .m3-label { top: 8px; font-size: 12px; color: var(--md-sys-color-primary); }
    247         .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; }
    248         .eye-toggle:hover { background: rgba(0,0,0,0.05); color: var(--md-sys-color-on-surface); }
    249 
    250         .m3-btn {
    251             width: 100%; height: 48px; border-radius: 24px; border: none; outline: none; cursor: pointer;
    252             font-family: 'Lexend', sans-serif; font-weight: 500; font-size: 14px;
    253             display: flex; align-items: center; justify-content: center; gap: 8px;
    254             transition: 0.2s; position: relative; overflow: hidden;
    255         }
    256         .m3-btn.filled { background: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary); box-shadow: none; }
    257         .m3-btn.filled:hover { box-shadow: var(--elevation-1); }
    258         .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; }
    259 
    260         .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; }
    261         .m3-btn.loading .btn-text { visibility: hidden; }
    262         .m3-btn.loading .btn-icon { display: none; }
    263         .m3-btn.loading .spinner { display: block; position: absolute; }
    264         @keyframes spin { to { transform: rotate(360deg); } }
    265 
    266         /* --- APP LAYOUT --- */
    267         #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; }
    268         #app-layout.visible { opacity: 1; visibility: visible; }
    269 
    270         /* SIDEBAR */
    271         .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; }
    272         .sidebar-header { padding: 16px 20px; display: flex; justify-content: space-between; align-items: center; }
    273         .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; }
    274         .icon-btn:hover { background: rgba(128,128,128,0.1); color: var(--md-sys-color-on-surface); }
    275         .search-container { padding: 0 16px 12px; }
    276         .search-bar { background: var(--md-sys-color-surface-container-high); border-radius: 28px; height: 48px; display: flex; align-items: center; padding: 0 16px; }
    277         .search-bar input { border: none; background: transparent; outline: none; flex: 1; margin-left: 12px; font-size: 16px; color: var(--md-sys-color-on-surface); }
    278         .contact-list { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 8px; }
    279         .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; }
    280         .contact-item:hover { background: rgba(128,128,128,0.05); }
    281         .contact-item.active { background: var(--md-sys-color-secondary-container); }
    282         .contact-item.active .contact-name { color: var(--md-sys-color-on-secondary-container); font-weight: 700; }
    283         .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; }
    284         .contact-info { flex: 1; min-width: 0; }
    285         .contact-name { font-size: 16px; font-weight: 500; color: var(--md-sys-color-on-surface); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    286         .contact-sub { font-size: 14px; color: var(--md-sys-color-on-surface-variant); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    287 
    288         /* CHAT AREA */
    289         .chat-area { flex: 1; display: flex; flex-direction: column; position: relative; background-color: var(--chat-bg-color); transition: background-color 0.3s; overflow: hidden; }
    290         .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"); }
    291         .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; }
    292         
    293         .chat-content {
    294             flex: 1; overflow-y: auto; padding: 16px; z-index: 1;
    295             display: flex; flex-direction: column; gap: 4px;
    296             scroll-behavior: auto !important; 
    297             transition: padding-bottom 0.4s cubic-bezier(0.2, 0, 0, 1);
    298         }
    299         
    300         /* Forces bottom-up alignment when content is sparse */
    301         .chat-content::before {
    302             content: '';
    303             flex: 1 1 auto;
    304         }
    305 
    306         .chat-content.syncing {
    307             padding-bottom: 72px; /* Cinematic push up */
    308         }
    309 
    310         .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; }
    311         .msg-in { align-self: flex-start; background: var(--chat-bubble-them); border-radius: 0 16px 16px 16px; color: var(--md-sys-color-on-surface); }
    312         .msg-out { align-self: flex-end; background: var(--chat-bubble-me); border-radius: 16px 0 16px 16px; color: var(--md-sys-color-on-surface); }
    313         
    314         .msg-out.group-top { border-bottom-right-radius: 4px; margin-bottom: 2px; }
    315         .msg-out.group-mid { border-top-right-radius: 4px; border-bottom-right-radius: 4px; margin-bottom: 2px; }
    316         .msg-out.group-bot { border-top-right-radius: 4px; margin-top: 0; }
    317         .msg-in.group-top { border-bottom-left-radius: 4px; margin-bottom: 2px; }
    318         .msg-in.group-mid { border-top-left-radius: 4px; border-bottom-left-radius: 4px; margin-bottom: 2px; }
    319         .msg-in.group-bot { border-top-left-radius: 4px; margin-top: 0; }
    320 
    321         .msg-sender { font-size: 12px; font-weight: 700; color: var(--md-sys-color-primary); margin-bottom: 2px; }
    322         .msg-text { white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; }
    323         .msg-meta { font-size: 10px; opacity: 0.7; align-self: flex-end; margin-top: 2px; }
    324         
    325         /* DATE DIVIDERS */
    326         .chat-date-group { display: flex; flex-direction: column; gap: 4px; position: relative; }
    327         .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; }
    328 
    329         .highlight { background-color: var(--search-highlight); color: black; border-radius: 2px; padding: 0 2px; }
    330 
    331         /* Sync Loading Spinner (Cinematic UI) */
    332         .sync-spinner-wrapper {
    333             position: absolute; bottom: 16px; left: 0; right: 0;
    334             display: flex; justify-content: center;
    335             pointer-events: none; z-index: 10;
    336             transform: translateY(150%); opacity: 0;
    337             transition: all 0.4s cubic-bezier(0.2, 0, 0, 1);
    338         }
    339         .sync-spinner-wrapper.active {
    340             transform: translateY(0); opacity: 1;
    341         }
    342         .sync-chip {
    343             background: var(--md-sys-color-surface-container-highest);
    344             color: var(--md-sys-color-on-surface);
    345             padding: 8px 16px; border-radius: 24px;
    346             display: flex; align-items: center; gap: 10px;
    347             font-size: 13px; font-weight: 500;
    348             box-shadow: var(--elevation-2);
    349             border: 1px solid var(--md-sys-color-outline-variant);
    350         }
    351         .sync-chip .spinner {
    352             border-color: rgba(0,0,0,0.1);
    353             border-top-color: var(--md-sys-color-primary);
    354             width: 16px; height: 16px; 
    355             display: block; position: static;
    356         }
    357         [data-theme="dark"] .sync-chip .spinner {
    358              border-color: rgba(255,255,255,0.1);
    359              border-top-color: var(--md-sys-color-primary);
    360         }
    361 
    362         /* Search In Chat */
    363         .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); }
    364         .in-chat-search input { border: none; background: transparent; outline: none; flex: 1; color: var(--md-sys-color-on-surface); }
    365 
    366         /* Menu */
    367         .menu-container { position: relative; }
    368         .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; }
    369         .dropdown-menu.show { display: flex; }
    370         .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; }
    371         .menu-item:hover { background: rgba(128,128,128,0.1); }
    372         
    373         /* Scroll To Bottom FAB */
    374         .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; }
    375         .scroll-fab.visible { opacity: 1; transform: translateY(0); pointer-events: auto; }
    376         .new-msg-badge { position: absolute; top: -2px; right: -2px; width: 12px; height: 12px; background: var(--md-sys-color-error); border-radius: 50%; border: 2px solid var(--md-sys-color-surface-container-high); }
    377 
    378         /* Offline Indicator */
    379         .offline-banner { position: fixed; top: 0; left: 50%; transform: translateX(-50%); background: var(--md-sys-color-error); color: var(--md-sys-color-on-error-container); padding: 4px 16px; border-radius: 0 0 8px 8px; font-size: 12px; z-index: 9999; display: flex; align-items: center; gap: 6px; box-shadow: var(--elevation-1); }
    380 
    381         /* Calendar Modal */
    382         .calendar-card { background: var(--md-sys-color-surface); padding: 0; border-radius: 28px; width: 320px; overflow: hidden; display: flex; flex-direction: column; }
    383         .cal-header { background: var(--md-sys-color-surface-container-high); padding: 16px 24px; }
    384         .cal-header h3 { font-size: 14px; color: var(--md-sys-color-on-surface-variant); font-weight: 500; }
    385         .cal-header h2 { font-size: 32px; color: var(--md-sys-color-on-surface); font-weight: 400; margin-top: 4px; }
    386         .cal-grid { padding: 16px 12px 0; }
    387         .cal-nav { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; padding: 0 12px; }
    388         .cal-days { display: grid; grid-template-columns: repeat(7, 1fr); text-align: center; margin-bottom: 8px; }
    389         .cal-day-label { font-size: 12px; color: var(--md-sys-color-on-surface-variant); width: 36px; }
    390         .cal-dates { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; margin-bottom: 16px; }
    391         .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); }
    392         .cal-date:hover { background: var(--md-sys-color-surface-container-high); }
    393         .cal-date.active { background: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary); }
    394         .cal-date.empty { pointer-events: none; }
    395 
    396         /* Settings Modal */
    397         .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; }
    398         .modal-overlay.active { opacity: 1; visibility: visible; }
    399         .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); }
    400         .settings-header { padding: 20px 24px; border-bottom: 1px solid var(--md-sys-color-outline-variant); display: flex; justify-content: space-between; align-items: center; }
    401         .settings-content { flex: 1; overflow-y: auto; padding: 24px; }
    402         .setting-section { margin-bottom: 32px; }
    403         .setting-title { font-size: 14px; font-weight: 500; color: var(--md-sys-color-primary); margin-bottom: 16px; text-transform: uppercase; letter-spacing: 1px; }
    404         .setting-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; }
    405         .setting-label h4 { font-size: 16px; font-weight: 400; margin-bottom: 4px; }
    406         .setting-label p { font-size: 14px; color: var(--md-sys-color-on-surface-variant); }
    407         
    408         .switch { position: relative; display: inline-block; width: 50px; height: 28px; }
    409         .switch input { opacity: 0; width: 0; height: 0; }
    410         .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); }
    411         .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; }
    412         input:checked + .slider { background-color: var(--md-sys-color-primary); border-color: var(--md-sys-color-primary); }
    413         input:checked + .slider:before { transform: translateX(20px); background-color: var(--md-sys-color-on-primary); }
    414 
    415         .faq-item { border-bottom: 1px solid var(--md-sys-color-outline-variant); }
    416         .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; }
    417         .faq-answer { display: none; padding-bottom: 16px; color: var(--md-sys-color-on-surface-variant); line-height: 1.5; }
    418         .faq-item.open .faq-answer { display: block; }
    419 
    420         .bg-selector { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 10px; }
    421         .bg-option { height: 60px; border-radius: 8px; cursor: pointer; border: 2px solid transparent; background-size: cover; position: relative; overflow: hidden; }
    422         .bg-option.selected { border-color: var(--md-sys-color-primary); }
    423         .bg-1 { background-color: #ECE5DD; }
    424         .bg-2 { background-color: #d1d7db; }
    425         .bg-3 { background-color: #e5ddd5; }
    426         .bg-4 { background-color: #ffffff; }
    427 
    428         .install-sheet { display: none; }
    429         .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; }
    430         .install-card h4 { margin-bottom: 4px; }
    431         .install-card p { font-size: 12px; }
    432         .install-card .m3-btn { width: auto; height: 36px; padding: 0 16px; font-size: 14px; white-space: nowrap; flex-shrink: 0; box-shadow: none; }
    433 
    434         @media (max-width: 768px) {
    435             .sidebar { width: 100%; flex: 1; border-right: none; background: radial-gradient(circle at 0% 0%, #c1e8fb49 0%, transparent 50%), radial-gradient(circle at 100% 100%, #89f8c86b 0%, transparent 50%), #fbfdf96c; }
    436             [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; }
    437             .chat-area { position: fixed; inset: 0; z-index: 20; transform: translateX(100%); transition: transform 0.3s; background-color: var(--chat-bg-color); }
    438             .chat-area.active { transform: translateX(0); }
    439             .back-btn { display: flex !important; margin-right: 8px; }
    440             .install-sheet { display: block; 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); }
    441             .install-sheet.visible { transform: translateY(0); }
    442             .sheet-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
    443             .sheet-content { text-align: center; }
    444             .sheet-actions { display: flex; flex-direction: column; gap: 12px; margin-top: 24px; }
    445         }
    446         .back-btn { display: none; margin-right: 8px; }
    447 
    448         .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); }
    449         .modal-overlay.active .modal-card { transform: scale(1); }
    450         .modal-card.utility { width: 320px; padding: 24px; border-radius: 28px; }
    451         .modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 24px; }
    452         .modal-actions .m3-btn { width: auto; min-width: 80px; height: 40px; padding: 0 16px; }
    453         .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; }
    454         .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; }
    455         @keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
    456         .skel-contact { display: flex; align-items: center; padding: 12px 16px; gap: 16px; width: 100%; }
    457         .skel-avatar { width: 48px; height: 48px; border-radius: 50%; }
    458         .skel-text-col { flex: 1; display: flex; flex-direction: column; gap: 8px; }
    459         .skel-msg-group { display: flex; flex-direction: column; gap: 12px; padding: 20px; width: 100%; }
    460         .skel-bubble { height: 40px; border-radius: 12px; width: 40%; margin-bottom: 8px;}
    461         .skel-left { align-self: flex-start; border-bottom-left-radius: 0; }
    462         .skel-right { align-self: flex-end; border-bottom-right-radius: 0; width: 50%; }
    463 
    464         /* --- SETTINGS SIDEBAR --- */
    465         .settings-sidebar {
    466             position: fixed;
    467             top: 0; left: 0; bottom: 0;
    468             width: 400px;
    469             background: var(--md-sys-color-surface);
    470             z-index: 2000;
    471             display: flex;
    472             flex-direction: column;
    473             border-right: 1px solid var(--md-sys-color-outline-variant);
    474             box-shadow: var(--elevation-2);
    475             transform: translateX(-100%);
    476             transition: transform 0.4s cubic-bezier(0.2, 0, 0, 1);
    477         }
    478 
    479         .settings-sidebar.active {
    480             transform: translateX(0);
    481         }
    482 
    483         /* Mobile full-width override */
    484         @media (max-width: 768px) {
    485             .settings-sidebar {
    486                 width: 100%;
    487                 border-right: none;
    488             }
    489         }
    490 
    491         @media (max-width: 768px) {
    492         /* Pin all utility modals to the bottom */
    493         .modal-card.utility {
    494             position: absolute;
    495             bottom: 0;
    496             width: 100% !important;
    497             max-width: 100%;
    498             border-radius: 28px 28px 0 0;
    499             padding: 32px 24px 24px;
    500             transform: translateY(100%) scale(1);
    501             animation: none; /* Override the desktop scaleIn */
    502             transition: transform 0.4s cubic-bezier(0.2, 0, 0, 1);
    503         }
    504         
    505         /* Add the M3 drag handle indicator */
    506         .modal-card.utility::before {
    507             content: '';
    508             position: absolute;
    509             top: 12px;
    510             left: 50%;
    511             transform: translateX(-50%);
    512             width: 32px;
    513             height: 4px;
    514             border-radius: 2px;
    515             background: var(--md-sys-color-outline-variant);
    516         }
    517 
    518         .modal-overlay.active .modal-card.utility {
    519             transform: translateY(0) scale(1);
    520         }
    521     }
    522 
    523     </style>
    524 </head>
    525 <body>
    526 
    527     <div id="offlineIndicator" class="offline-banner hidden">
    528         <span class="material-symbols-rounded" style="font-size: 16px;">cloud_off</span> You are offline
    529     </div>
    530 
    531     <!-- AUTH SCREEN -->
    532     <div id="auth-screen">
    533         <div class="auth-card">
    534             
    535             <!-- Dynamic Initial Loader -->
    536             <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;">
    537                 <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>
    538                 <p style="margin-top:20px; color:var(--md-sys-color-on-surface-variant); font-size: 15px; font-family:'Lexend', sans-serif;">Authenticating session...</p>
    539             </div>
    540 
    541             <!-- Login Fields (Hidden initially) -->
    542             <div id="authForm" class="hidden">
    543                 <div style="text-align: center;" class="anim-up delay-1">
    544                     <div class="brand-icon">
    545                         <img src="/assets/icon.svg" alt="App Logo">
    546                     </div>
    547                     <h1 style="font-family:'Lexend', sans-serif; font-size:28px; margin:8px 0; font-weight:400;">Welcome Back</h1>
    548                     <p style="color:var(--md-sys-color-on-surface-variant); font-size:14px;">Sign in to your chat log</p>
    549                 </div>
    550 
    551                 <div class="m3-field anim-up delay-2" style="margin-top: 12px;">
    552                     <input type="text" id="username" class="m3-input" placeholder=" ">
    553                     <label class="m3-label">Username</label>
    554                 </div>
    555 
    556                 <div class="m3-field anim-up delay-3" style="margin-top: 12px;">
    557                     <input type="password" id="password" class="m3-input" placeholder=" ">
    558                     <label class="m3-label">Password</label>
    559                     <button class="eye-toggle" onclick="togglePasswordVisibility('password', 'passIcon')">
    560                         <span class="material-symbols-rounded" id="passIcon">visibility</span>
    561                     </button>
    562                 </div>
    563 
    564                 <div class="anim-up delay-4" style="display:flex; align-items:center; padding-left:4px; margin-top: 12px;">
    565                     <div style="display:flex; align-items:center; gap:8px;">
    566                         <input type="checkbox" id="rememberMe" style="width:18px; height:18px; accent-color: var(--md-sys-color-primary);">
    567                         <label for="rememberMe" style="font-size: 14px; color: var(--md-sys-color-on-surface);">Remember me</label>
    568                     </div>
    569                 </div>
    570 
    571                 <div class="anim-up delay-5" style="display: flex; flex-direction: column; gap: 12px; margin-top: 16px;">
    572                     <button class="m3-btn filled" id="loginBtn" onclick="attemptLogin()">
    573                         <span class="btn-text">Sign In</span>
    574                         <span class="material-symbols-rounded btn-icon" style="font-size:20px;">arrow_forward</span>
    575                         <div class="spinner"></div>
    576                     </button>
    577                     <p id="authError" style="color: var(--md-sys-color-error); font-size: 14px; opacity: 0; text-align: center;">Invalid Credentials</p>
    578                 </div>
    579             </div>
    580 
    581         </div>
    582     </div>
    583 
    584     <!-- LOCK SCREEN -->
    585     <div id="lock-screen">
    586         <div id="lockoutUI" class="hidden lockout-container">
    587             <div class="lockout-icon-bg"><span class="material-symbols-rounded lockout-icon" id="lockoutIcon">lock_clock</span></div>
    588             <h2 class="lockout-title" id="lockoutTitle">Locked</h2>
    589             <p class="lockout-msg" id="lockoutMsg">Too many attempts.</p>
    590             <div class="lockout-timer" id="lockoutTimer">00:30</div>
    591             <p class="lockout-sub" id="lockoutSub">Try again later</p>
    592             <div id="permanentLockActions" class="hidden lockout-actions">
    593                  <button class="m3-btn filled" onclick="showForgotPin()" style="background:var(--md-sys-color-error); width:auto; padding:0 32px;">Forgot PIN</button>
    594             </div>
    595         </div>
    596 
    597         <div class="lock-header" id="lockHeader">
    598             <span class="material-symbols-rounded lock-icon" id="lockIcon">lock</span>
    599             <div class="lock-title" id="lockTitle">Enter PIN</div>
    600             <div class="lock-sub" id="lockSub">Protected by App Lock</div>
    601         </div>
    602         
    603         <div id="pinUI">
    604             <div class="pin-dots" id="pinDots">
    605                 <div class="pin-dot"></div><div class="pin-dot"></div><div class="pin-dot"></div>
    606                 <div class="pin-dot"></div><div class="pin-dot"></div><div class="pin-dot"></div>
    607             </div>
    608             <div class="keypad">
    609                 <button class="key-btn ripple-surface" onclick="pressKey(1)">1</button>
    610                 <button class="key-btn ripple-surface" onclick="pressKey(2)">2</button>
    611                 <button class="key-btn ripple-surface" onclick="pressKey(3)">3</button>
    612                 <button class="key-btn ripple-surface" onclick="pressKey(4)">4</button>
    613                 <button class="key-btn ripple-surface" onclick="pressKey(5)">5</button>
    614                 <button class="key-btn ripple-surface" onclick="pressKey(6)">6</button>
    615                 <button class="key-btn ripple-surface" onclick="pressKey(7)">7</button>
    616                 <button class="key-btn ripple-surface" onclick="pressKey(8)">8</button>
    617                 <button class="key-btn ripple-surface" onclick="pressKey(9)">9</button>
    618                 <button class="key-btn ripple-surface" style="visibility:hidden"> </button>
    619                 <button class="key-btn ripple-surface" onclick="pressKey(0)">0</button>
    620                 <button class="key-btn ripple-surface" onclick="pressKey(-1)"><span class="material-symbols-rounded key-backspace">backspace</span></button>
    621             </div>
    622         </div>
    623 
    624         <div id="bioUI" class="hidden" style="flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center;">
    625              <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;">
    626                 <span class="material-symbols-rounded" style="font-size:48px;">fingerprint</span>
    627             </div>
    628             <p style="margin-top:16px; color:var(--md-sys-color-on-surface-variant);">Tap to Unlock</p>
    629         </div>
    630 
    631         <div style="text-align: center;"><button class="forgot-pin-btn" onclick="showForgotPin()">Forgot PIN / Use Password</button></div>
    632     </div>
    633 
    634     <!-- MAIN APP -->
    635     <div id="app-layout">
    636         <aside class="sidebar" id="sidebar">
    637             <div class="sidebar-header">
    638                 <div style="display: flex; align-items: center; gap: 12px; padding: 12px 0;">
    639                     <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>
    640                     <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>
    641                 </div>
    642                 <div style="display:flex; gap: 8px;">
    643                     <button class="icon-btn ripple-surface" onclick="refreshApp()"><span class="material-symbols-rounded">refresh</span></button>
    644                     <button class="icon-btn ripple-surface" onclick="openSettings()" title="Settings"><span class="material-symbols-rounded">settings</span></button>
    645                 </div>
    646             </div>
    647             <div class="search-container">
    648                 <div class="search-bar">
    649                     <span class="material-symbols-rounded" style="color: var(--md-sys-color-on-surface-variant);">search</span>
    650                     <input type="text" id="searchInput" placeholder="Search...">
    651                 </div>
    652             </div>
    653             <div class="contact-list" id="contactList"></div>
    654         </aside>
    655 
    656         <main class="chat-area" id="chatArea">
    657             <header class="chat-header">
    658                 <button class="icon-btn back-btn ripple-surface" onclick="closeChatMobile()"><span class="material-symbols-rounded">arrow_back</span></button>
    659                 <div id="chatHeaderContent" style="display:flex; align-items:center; gap:12px; flex:1; opacity:0;">
    660                     <div class="avatar" id="headerAvatar">A</div>
    661                     <div style="flex:1;">
    662                         <div id="headerName" style="font-weight: 500; font-size: 16px;">Alice</div>
    663                         <div id="headerId" style="font-size: 12px; opacity: 0.7;">Online</div>
    664                     </div>
    665                     <div class="menu-container">
    666                         <button class="icon-btn ripple-surface" onclick="toggleChatMenu(event)"><span class="material-symbols-rounded">more_vert</span></button>
    667                         <div class="dropdown-menu" id="chatMenu">
    668                             <button class="menu-item" onclick="toggleChatSearch()"><span class="material-symbols-rounded">search</span>Search in Chat</button>
    669                             <button class="menu-item" onclick="openDateSearch()"><span class="material-symbols-rounded">calendar_month</span>Search by Date</button>
    670                             <button class="menu-item" onclick="showRenameModal()"><span class="material-symbols-rounded">edit</span>Rename</button>
    671                             <button class="menu-item" onclick="downloadChat()"><span class="material-symbols-rounded">download</span>Download</button>
    672                         </div>
    673                     </div>
    674                 </div>
    675             </header>
    676             
    677             <div id="inChatSearch" class="in-chat-search hidden">
    678                 <input type="text" id="msgSearchInput" placeholder="Find message...">
    679                 <div id="searchCount" style="font-size:12px; margin-right:8px;"></div>
    680                 <button class="icon-btn" style="width:30px; height:30px;" onclick="toggleChatSearch()"><span class="material-symbols-rounded">close</span></button>
    681             </div>
    682 
    683             <!-- Cinematic Loading UI -->
    684             <div class="sync-spinner-wrapper" id="syncSpinnerWrap">
    685                 <div class="sync-chip">
    686                     <div class="spinner"></div>
    687                     <span>Syncing messages...</span>
    688                 </div>
    689             </div>
    690 
    691             <div class="chat-bg-pattern"></div>
    692             
    693             <div class="chat-content" id="messagesContainer">
    694                 <div class="empty-state">
    695                     <span class="material-symbols-rounded" style="font-size: 64px; opacity: 0.2;">chat</span>
    696                     <p>Select a conversation</p>
    697                 </div>
    698             </div>
    699             
    700             <button class="scroll-fab" id="scrollFab" onclick="scrollToBottom('smooth')">
    701                 <span class="material-symbols-rounded">arrow_downward</span>
    702                 <span id="newMsgBadge" class="new-msg-badge hidden"></span>
    703             </button>
    704         </main>
    705     </div>
    706 
    707     <!-- MODALS SECTION -->
    708     <!-- SETTINGS SIDEBAR -->
    709     <div id="settingsSidebar" class="settings-sidebar">
    710         <div class="settings-header" style="height: 64px; padding: 0 16px; justify-content: flex-start; gap: 24px; background: var(--md-sys-color-surface-container-high);">
    711             <button class="icon-btn ripple-surface" onclick="closeSettingsSidebar()">
    712                 <span class="material-symbols-rounded">arrow_back</span>
    713             </button>
    714             <h2 style="font-family:'Lexend'; font-size: 20px; font-weight: 500; margin: 0;">Settings</h2>
    715         </div>
    716         
    717         <div class="settings-content">
    718             <div id="installAppSection" class="install-card hidden">
    719                 <div><h4>Install App</h4><p>Get the app for better experience</p></div>
    720                 <button class="m3-btn filled" onclick="installPWA()">Install</button>
    721             </div>
    722             
    723             <div class="setting-section hidden" id="mobileSecuritySection">
    724                 <div class="setting-title">App Security</div>
    725                 <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>
    726                 <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>
    727             </div>
    728             
    729             <div class="setting-section">
    730                 <div class="setting-title">Profile</div>
    731                 <div class="m3-field"><input type="text" id="profileName" class="m3-input" placeholder=" " onchange="saveProfile()"><label class="m3-label">Your Name</label></div>
    732                 <div class="m3-field"><input type="text" id="profilePhone" class="m3-input" placeholder=" " onchange="saveProfile()"><label class="m3-label">Phone Number</label></div>
    733             </div>
    734             
    735             <div class="setting-section">
    736                 <div class="setting-title">Appearance</div>
    737                 <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>
    738                 <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>
    739                 <div class="setting-label" style="margin-bottom:8px"><h4>Chat Wallpaper</h4></div>
    740                 <div class="bg-selector">
    741                     <div class="bg-option bg-1 selected" onclick="changeBg(1)"></div>
    742                     <div class="bg-option bg-2" onclick="changeBg(2)"></div>
    743                     <div class="bg-option bg-3" onclick="changeBg(3)"></div>
    744                     <div class="bg-option bg-4" onclick="changeBg(4)"></div>
    745                 </div>
    746             </div>
    747             
    748             <div class="setting-section">
    749                 <div class="setting-title">Storage and Data</div>
    750                 <div class="setting-row">
    751                     <div class="setting-label">
    752                         <h4>Local Storage Used</h4>
    753                         <p id="localStorageSizeDisplay">Calculating...</p>
    754                     </div>
    755                 </div>
    756                 <div class="setting-row">
    757                     <div class="setting-label">
    758                         <h4>Clear Cache</h4>
    759                         <p>Delete all messages, cached chats, and media records locally.</p>
    760                     </div>
    761                     <button class="m3-btn filled" style="background: var(--md-sys-color-error); width: auto; padding: 0 16px; height: 36px; font-size: 13px;" onclick="clearAppCacheData()">Clear</button>
    762                 </div>
    763             </div>
    764             
    765             <div class="setting-section">
    766                 <div class="setting-title">Help & Support</div>
    767                 <div id="faqContainer"></div>
    768                 <div style="margin-top:24px; display:flex; gap:12px;">
    769                     <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>
    770                 </div>
    771                 <div style="margin-top:24px; display:flex; gap:12px;">
    772                     <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>
    773                 </div>
    774             </div>
    775             
    776             <button class="m3-btn filled" style="background:var(--md-sys-color-error); margin-top: 16px;" onclick="showLogoutModal()">Log Out</button>
    777         </div>
    778     </div>
    779 
    780     <!-- UTILITY MODALS -->
    781     <div id="logoutModal" class="modal-overlay">
    782         <div class="modal-card utility">
    783             <h3 class="modal-title">Log Out?</h3>
    784             <p style="margin:16px 0; color:var(--md-sys-color-on-surface-variant);">Session will end.</p>
    785             <div class="modal-actions">
    786                 <button class="m3-btn text" onclick="closeModal('logoutModal')">Cancel</button>
    787                 <button class="m3-btn filled" onclick="handleLogout()">Logout</button>
    788             </div>
    789         </div>
    790     </div>
    791     
    792     <div id="forgotPinModal" class="modal-overlay">
    793         <div class="modal-card utility">
    794             <h3 class="modal-title">Reset App Lock</h3>
    795             <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>
    796             <div class="m3-field" style="margin-top:16px;"><input type="text" id="resetUsername" class="m3-input" placeholder=" "><label class="m3-label">Username</label></div>
    797             <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>
    798             <p id="resetError" style="color:var(--md-sys-color-error); font-size:12px; margin-bottom:12px; display:none;">Invalid credentials</p>
    799             <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>
    800         </div>
    801     </div>
    802 
    803     <div id="renameModal" class="modal-overlay">
    804         <div class="modal-card utility">
    805             <h3 class="modal-title">Rename Chat</h3>
    806             <div class="m3-field" style="margin-top:16px;"><input type="text" id="renameInput" class="m3-input"><label class="m3-label">New Name</label></div>
    807             <div class="modal-actions"><button class="m3-btn text" onclick="closeModal('renameModal')">Cancel</button><button class="m3-btn filled" onclick="saveRename()">Save</button></div>
    808         </div>
    809     </div>
    810     
    811     <!-- APP OPTIONS MODAL -->
    812     <div id="appOptionsModal" class="modal-overlay">
    813         <div class="modal-card utility">
    814             <h3 class="modal-title">App Options</h3>
    815             <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>
    816             <div style="display:flex; flex-direction:column; gap:12px;">
    817                 <button class="m3-btn filled" onclick="executePageRefresh()">
    818                     <span class="material-symbols-rounded" style="font-size:20px;">refresh</span> Reload App
    819                 </button>
    820                 <button class="m3-btn text" style="color:var(--md-sys-color-error);" onclick="openHardReset()">
    821                     <span class="material-symbols-rounded" style="font-size:20px;">delete_forever</span> Hard Reset
    822                 </button>
    823                 <button class="m3-btn text" onclick="closeModal('appOptionsModal')">Cancel</button>
    824             </div>
    825         </div>
    826     </div>
    827 
    828     <!-- HARD REFRESH MODAL -->
    829     <div id="refreshModal" class="modal-overlay">
    830         <div class="modal-card utility">
    831             <h3 class="modal-title">System Sync</h3>
    832             
    833             <div id="refreshAuthStep">
    834                 <p style="margin-top:8px; color:var(--md-sys-color-on-surface-variant); font-size:14px;">Enter credentials to perform a full hard sync. This will clear the cache and pull all data.</p>
    835                 <div class="m3-field" style="margin-top:16px;">
    836                     <input type="text" id="refreshUsername" class="m3-input" placeholder=" "><label class="m3-label">Username</label>
    837                 </div>
    838                 <div class="m3-field">
    839                     <input type="password" id="refreshPassword" class="m3-input" placeholder=" ">
    840                     <label class="m3-label">Password</label>
    841                     <button class="eye-toggle" onclick="togglePasswordVisibility('refreshPassword', 'refreshPassIcon')"><span class="material-symbols-rounded" id="refreshPassIcon">visibility</span></button>
    842                 </div>
    843                 <p id="refreshError" style="color:var(--md-sys-color-error); font-size:12px; margin-bottom:12px; display:none;">Invalid credentials</p>
    844                 <div class="modal-actions">
    845                     <button class="m3-btn text" onclick="closeModal('refreshModal')">Cancel</button>
    846                     <button class="m3-btn filled" id="refreshBtn" onclick="startHardRefresh()">
    847                         <span class="btn-text">Sync</span><div class="spinner"></div>
    848                     </button>
    849                 </div>
    850             </div>
    851 
    852             <div id="refreshProgressStep" class="hidden" style="text-align:center; padding: 16px 0;">
    853                 <p id="refreshStatusText" style="font-size:14px; font-weight:500; margin-bottom:16px; color:var(--md-sys-color-on-surface);">Starting sync...</p>
    854                 <div style="width:100%; height:8px; background:var(--md-sys-color-surface-container-highest); border-radius:4px; overflow:hidden;">
    855                     <div id="refreshProgressBar" style="width:0%; height:100%; background:var(--md-sys-color-primary); transition:width 0.3s ease;"></div>
    856                 </div>
    857                 <p id="refreshPercentText" style="font-size:12px; color:var(--md-sys-color-on-surface-variant); margin-top:8px;">0%</p>
    858             </div>
    859         </div>
    860     </div>
    861 
    862     <div id="notFoundModal" class="modal-overlay">
    863         <div class="modal-card utility">
    864             <h3 class="modal-title">No Messages Found</h3>
    865             <p style="margin:16px 0; color:var(--md-sys-color-on-surface-variant);">There are no messages from this date onwards.</p>
    866             <div class="modal-actions"><button class="m3-btn filled" onclick="closeModal('notFoundModal')">OK</button></div>
    867         </div>
    868     </div>
    869     
    870     <div id="pinSetupModal" class="modal-overlay">
    871         <div class="modal-card utility" style="text-align:center;">
    872              <h3 class="modal-title" id="pinSetupTitle">Set 6-Digit PIN</h3>
    873              <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>
    874             <p style="color:var(--md-sys-color-on-surface-variant); font-size:12px;" id="pinSetupMsg">Use the number pad below</p>
    875             <div class="keypad" style="transform: scale(0.9); margin-bottom:0;">
    876                 <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>
    877                 <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>
    878                 <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>
    879                 <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>
    880             </div>
    881              <div class="modal-actions" style="justify-content:center; margin-top:8px;"><button class="m3-btn text" onclick="cancelPinSetup()">Cancel</button></div>
    882         </div>
    883     </div>
    884     
    885     <div id="calendarModal" class="modal-overlay">
    886         <div class="calendar-card animate-enter">
    887             <div class="cal-header"><h3>Select date</h3><h2 id="calSelectedDateStr">Tue, Oct 24</h2></div>
    888             <div class="cal-grid">
    889                 <div class="cal-nav">
    890                     <span id="calMonthYear" style="font-weight:500">October 2023</span>
    891                     <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>
    892                 </div>
    893                 <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>
    894                 <div class="cal-dates" id="calDatesGrid"></div>
    895             </div>
    896             <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>
    897         </div>
    898     </div>
    899 
    900     <div id="installSheet" class="install-sheet">
    901         <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>
    902         <div class="sheet-content">
    903             <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>
    904             <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>
    905             <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>
    906         </div>
    907     </div>
    908 
    909     <!-- MAIN APP SCRIPT -->
    910     <script>
    911         const RENDER_BACKEND_URL = ""; 
    912         const firebaseConfig = {
    913             apiKey: "",
    914             authDomain: "",
    915             projectId: "",
    916             storageBucket: "",
    917             messagingSenderId: "",
    918             appId: ""
    919         };
    920         
    921         firebase.initializeApp(firebaseConfig);
    922         const db = firebase.firestore();
    923         let activeChatId = null;
    924         let isCurrentChatGroup = false;
    925         let deferredPrompt;
    926         let currentMessages = [];
    927         
    928         // --- INDEXED DB CACHING ---
    929         const DB_NAME = 'WPLoggerDB';
    930         const DB_VERSION = 2; // Incremented for chat list cache
    931         let dbInstance = null;
    932         let isIdbSupported = true;
    933 
    934         function initIndexedDB() {
    935             return new Promise((resolve) => {
    936                 if (!window.indexedDB) {
    937                     isIdbSupported = false;
    938                     resolve();
    939                     return;
    940                 }
    941 
    942                 let isResolved = false;
    943                 const safeResolve = () => {
    944                     if(!isResolved) {
    945                         isResolved = true;
    946                         resolve();
    947                     }
    948                 };
    949 
    950                 // Safety timeout: Prevent infinite hangs on mobile browsers
    951                 const fallbackTimeout = setTimeout(() => {
    952                     console.warn('IDB init timed out');
    953                     isIdbSupported = false;
    954                     safeResolve();
    955                 }, 3000);
    956 
    957                 try {
    958                     const req = indexedDB.open(DB_NAME, DB_VERSION);
    959                     req.onupgradeneeded = (e) => {
    960                         const database = e.target.result;
    961                         if (!database.objectStoreNames.contains('messages')) {
    962                             const store = database.createObjectStore('messages', { keyPath: 'id' });
    963                             store.createIndex('chatId', 'chatId', { unique: false });
    964                             store.createIndex('timestamp', 'timestamp', { unique: false });
    965                         }
    966                         if (!database.objectStoreNames.contains('chats')) {
    967                             database.createObjectStore('chats', { keyPath: 'id' });
    968                         }
    969                     };
    970                     req.onsuccess = (e) => { 
    971                         clearTimeout(fallbackTimeout);
    972                         dbInstance = e.target.result; 
    973                         
    974                         // Close connection if another tab/process requests an upgrade, prevents blocking
    975                         dbInstance.onversionchange = () => {
    976                             dbInstance.close();
    977                             dbInstance = null;
    978                         };
    979                         
    980                         safeResolve(); 
    981                     };
    982                     req.onerror = (e) => { 
    983                         clearTimeout(fallbackTimeout);
    984                         console.warn('IDB init error', e); 
    985                         isIdbSupported = false; 
    986                         safeResolve(); 
    987                     };
    988                     req.onblocked = (e) => {
    989                         console.warn('IDB init blocked. Waiting for other connections to close...', e);
    990                         // We rely on fallbackTimeout if it never unblocks
    991                     };
    992                 } catch (e) {
    993                     clearTimeout(fallbackTimeout);
    994                     console.warn('IDB open exception', e);
    995                     isIdbSupported = false;
    996                     safeResolve();
    997                 }
    998             });
    999         }
   1000 
   1001         async function getCachedMessages(chatId) {
   1002             if(!isIdbSupported || !dbInstance) return [];
   1003             return new Promise((resolve) => {
   1004                 try {
   1005                     const tx = dbInstance.transaction('messages', 'readonly');
   1006                     const store = tx.objectStore('messages');
   1007                     const index = store.index('chatId');
   1008                     const req = index.getAll(chatId);
   1009                     req.onsuccess = () => resolve(req.result || []);
   1010                     req.onerror = () => resolve([]);
   1011                 } catch(e) { resolve([]); }
   1012             });
   1013         }
   1014 
   1015         async function saveCachedMessages(messages) {
   1016             if(!isIdbSupported || !dbInstance) return;
   1017             return new Promise((resolve) => {
   1018                 try {
   1019                     const tx = dbInstance.transaction('messages', 'readwrite');
   1020                     const store = tx.objectStore('messages');
   1021                     messages.forEach(m => store.put(m));
   1022                     tx.oncomplete = () => resolve();
   1023                     tx.onerror = () => resolve();
   1024                 } catch(e) { resolve(); }
   1025             });
   1026         }
   1027 
   1028         async function getCachedChats() {
   1029             if(!isIdbSupported || !dbInstance) return [];
   1030             return new Promise((resolve) => {
   1031                 try {
   1032                     const tx = dbInstance.transaction('chats', 'readonly');
   1033                     const store = tx.objectStore('chats');
   1034                     const req = store.getAll();
   1035                     req.onsuccess = () => resolve(req.result || []);
   1036                     req.onerror = () => resolve([]);
   1037                 } catch(e) { resolve([]); }
   1038             });
   1039         }
   1040 
   1041         async function saveCachedChats(chats) {
   1042             if(!isIdbSupported || !dbInstance) return;
   1043             return new Promise((resolve) => {
   1044                 try {
   1045                     const tx = dbInstance.transaction('chats', 'readwrite');
   1046                     const store = tx.objectStore('chats');
   1047                     chats.forEach(c => store.put(c));
   1048                     tx.oncomplete = () => resolve();
   1049                     tx.onerror = () => resolve();
   1050                 } catch(e) { resolve(); }
   1051             });
   1052         }
   1053         
   1054         // --- SECURITY VARS ---
   1055         let enteredPin = ""; let setupPin = ""; let setupStep = 0; let isLocked = false;
   1056         let failAttempts = parseInt(localStorage.getItem('wp_fail_attempts') || '0');
   1057         let lockUntil = parseInt(localStorage.getItem('wp_lock_until') || '0');
   1058         let lockoutTimerInterval;
   1059         let calCurrentDate = new Date(); let calSelectedDate = new Date();
   1060 
   1061         const arrayBufferToBase64 = (buffer) => {
   1062             let binary = ''; const bytes = new Uint8Array(buffer); const len = bytes.byteLength;
   1063             for (let i = 0; i < len; i++) binary += String.fromCharCode(bytes[i]);
   1064             return window.btoa(binary);
   1065         };
   1066         const base64ToArrayBuffer = (base64) => {
   1067             const binary_string = window.atob(base64); const len = binary_string.length; const bytes = new Uint8Array(len);
   1068             for (let i = 0; i < len; i++) bytes[i] = binary_string.charCodeAt(i);
   1069             return bytes.buffer;
   1070         };
   1071         
   1072         function formatDividerDate(ts) {
   1073             const date = new Date(ts * 1000); const today = new Date(); const yesterday = new Date();
   1074             yesterday.setDate(yesterday.getDate() - 1);
   1075             if (date.toDateString() === today.toDateString()) return "Today";
   1076             if (date.toDateString() === yesterday.toDateString()) return "Yesterday";
   1077             return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
   1078         }
   1079 
   1080         // HTML Sanitizer to prevent Code Injection via chat messages
   1081         function escapeHTML(str) {
   1082             if (!str) return '';
   1083             return str.replace(/[&<>'"]/g, 
   1084                 tag => ({
   1085                     '&': '&amp;',
   1086                     '<': '&lt;',
   1087                     '>': '&gt;',
   1088                     "'": '&#39;',
   1089                     '"': '&quot;'
   1090                 }[tag] || tag)
   1091             );
   1092         }
   1093 
   1094         // --- PWA INSTALLATION ---
   1095         if ('serviceWorker' in navigator && (window.location.protocol === 'https:' || window.location.protocol === 'http:')) {
   1096             navigator.serviceWorker.register('/assets/sw-wpChat.js').catch(err => {
   1097                 console.warn('SW Register Fail:', err);
   1098             });
   1099             // Reconnect Sync trigger
   1100             window.addEventListener('online', () => {
   1101                 if (navigator.serviceWorker.controller) navigator.serviceWorker.controller.postMessage('sync_on_reconnect');
   1102             });
   1103         }
   1104         
   1105         window.addEventListener('beforeinstallprompt', (e) => {
   1106             e.preventDefault(); deferredPrompt = e;
   1107             document.getElementById('installAppSection').classList.remove('hidden');
   1108             if (window.innerWidth <= 768) {
   1109                 setTimeout(() => document.getElementById('installSheet').classList.add('visible'), 2000);
   1110             }
   1111         });
   1112         
   1113         async function installPWA() {
   1114             if (!deferredPrompt) return;
   1115             deferredPrompt.prompt();
   1116             const { outcome } = await deferredPrompt.userChoice;
   1117             if (outcome === 'accepted') {
   1118                 closeInstallSheet();
   1119                 document.getElementById('installAppSection').classList.add('hidden');
   1120             }
   1121             deferredPrompt = null;
   1122         }
   1123 
   1124         function closeInstallSheet() { document.getElementById('installSheet').classList.remove('visible'); }
   1125 
   1126         // --- BOOT SEQUENCE ---
   1127         function isMobileApp() {
   1128             const isStandalone = (window.matchMedia('(display-mode: standalone)').matches) || (window.navigator.standalone) || document.referrer.includes('android-app://');
   1129             return isStandalone && window.innerWidth <= 768;
   1130         }
   1131 
   1132         window.onload = async () => {
   1133             await initIndexedDB();
   1134 
   1135             if (!navigator.onLine) {
   1136                 document.getElementById('offlineIndicator').classList.remove('hidden');
   1137             }
   1138             window.addEventListener('online', () => {
   1139                 document.getElementById('offlineIndicator').classList.add('hidden');
   1140             });
   1141             window.addEventListener('offline', () => {
   1142                 document.getElementById('offlineIndicator').classList.remove('hidden');
   1143             });
   1144 
   1145             if (localStorage.getItem('theme') === 'dark') {
   1146                 document.body.setAttribute('data-theme', 'dark');
   1147                 document.getElementById('darkModeToggle').checked = true;
   1148             }
   1149             document.getElementById('profileName').value = localStorage.getItem('profileName') || '';
   1150             document.getElementById('profilePhone').value = localStorage.getItem('profilePhone') || '';
   1151             const bg = localStorage.getItem('chatBg') || 1;
   1152             changeBg(bg, false);
   1153             populateFAQ();
   1154 
   1155             // Setup Auth & App Routing with smooth transitions
   1156             const authValid = localStorage.getItem('wp_auth_expiry') > Date.now();
   1157             
   1158             if (authValid) {
   1159                 // If auth is valid, fade out the auth screen completely and show the app
   1160                 document.getElementById('auth-screen').style.opacity = '0';
   1161                 
   1162                 setTimeout(() => {
   1163                     document.getElementById('auth-screen').style.display = 'none';
   1164                     if (isMobileApp()) {
   1165                         const hasPin = localStorage.getItem('wp_app_pin');
   1166                         const useBio = localStorage.getItem('wp_app_bio') === 'true';
   1167                         document.getElementById('mobileSecuritySection').classList.remove('hidden');
   1168                         document.getElementById('pinLockToggle').checked = !!hasPin;
   1169                         document.getElementById('bioLockToggle').checked = useBio;
   1170                         
   1171                         if (hasPin || useBio) {
   1172                             initLockScreen(hasPin, useBio);
   1173                         } else {
   1174                             document.getElementById('app-layout').classList.add('visible');
   1175                             loadChats();
   1176                         }
   1177                     } else {
   1178                         document.getElementById('app-layout').classList.add('visible');
   1179                         loadChats();
   1180                     }
   1181                 }, 400); // Wait for CSS opacity transition
   1182             } else {
   1183                 // If not valid, hide the spinner and reveal the login form
   1184                 document.getElementById('authLoader').classList.add('hidden');
   1185                 document.getElementById('authForm').classList.remove('hidden');
   1186                 document.getElementById('authForm').classList.add('fade-in');
   1187                 
   1188                 if (isMobileApp()) {
   1189                     document.getElementById('mobileSecuritySection').classList.remove('hidden');
   1190                 }
   1191             }
   1192             
   1193             document.getElementById('messagesContainer').addEventListener('scroll', function() {
   1194                 const fab = document.getElementById('scrollFab');
   1195                 if (this.scrollHeight - this.scrollTop - this.clientHeight > 500) {
   1196                     fab.classList.add('visible');
   1197                 } else {
   1198                     fab.classList.remove('visible');
   1199                     document.getElementById('newMsgBadge').classList.add('hidden');
   1200                 }
   1201             });
   1202 
   1203             document.querySelectorAll('input[type="text"], input[type="password"]').forEach(input => {
   1204                 input.addEventListener('focus', () => {
   1205                     if (window.innerWidth <= 768) {
   1206                         setTimeout(() => input.scrollIntoView({ behavior: 'smooth', block: 'center' }), 300);
   1207                     }
   1208                 });
   1209             });
   1210         };
   1211 
   1212         // --- LOCK SCREEN LOGIC ---
   1213         function initLockScreen(hasPin, useBio) {
   1214             const lockScreen = document.getElementById('lock-screen');
   1215             lockScreen.classList.add('active'); isLocked = true; enteredPin = "";
   1216             updatePinDots(0);
   1217             if (checkLockoutState()) return;
   1218 
   1219             if(hasPin) {
   1220                 document.getElementById('pinUI').classList.remove('hidden'); document.getElementById('bioUI').classList.add('hidden');
   1221                 document.getElementById('lockTitle').innerText = "Enter PIN"; document.getElementById('lockIcon').innerText = "lock";
   1222             } else if (useBio && !hasPin) {
   1223                 document.getElementById('pinUI').classList.add('hidden'); document.getElementById('bioUI').classList.remove('hidden');
   1224                 document.getElementById('lockTitle').innerText = "Scan Fingerprint"; document.getElementById('lockIcon').innerText = "fingerprint";
   1225                 triggerBiometricAuth();
   1226             }
   1227         }
   1228 
   1229         function checkLockoutState() {
   1230             const now = Date.now();
   1231             if (failAttempts >= 5) { renderPermanentLock(); return true; }
   1232             if (lockUntil > now) { startLockoutTimer(); return true; }
   1233             if (lockUntil > 0 && lockUntil <= now) {
   1234                 lockUntil = 0; localStorage.setItem('wp_lock_until', '0');
   1235                 document.getElementById('lockoutUI').classList.add('hidden');
   1236                 document.getElementById('lockHeader').classList.remove('hidden');
   1237                 document.getElementById('lock-screen').classList.remove('locked-mode');
   1238             }
   1239             return false;
   1240         }
   1241 
   1242         function handleFailedAttempt() {
   1243             failAttempts++; localStorage.setItem('wp_fail_attempts', failAttempts);
   1244             if (failAttempts === 3 || failAttempts === 4) {
   1245                 lockUntil = Date.now() + (30 * 1000); localStorage.setItem('wp_lock_until', lockUntil);
   1246                 startLockoutTimer();
   1247             } else if (failAttempts >= 5) {
   1248                 renderPermanentLock();
   1249             } else {
   1250                 document.getElementById('pinDots').classList.add('shake'); updatePinDots(6, true);
   1251                 setTimeout(() => { document.getElementById('pinDots').classList.remove('shake'); enteredPin = ""; updatePinDots(0); }, 400);
   1252             }
   1253         }
   1254 
   1255         function startLockoutTimer() {
   1256             const ui = document.getElementById('lockoutUI'); const screen = document.getElementById('lock-screen'); const timerDisplay = document.getElementById('lockoutTimer');
   1257             ui.classList.remove('hidden'); document.getElementById('lockHeader').classList.add('hidden'); screen.classList.add('locked-mode'); 
   1258             document.getElementById('lockoutTitle').innerText = "Locked"; document.getElementById('lockoutMsg').innerText = `Too many attempts (${failAttempts})`;
   1259             document.getElementById('lockoutSub').innerText = "Try again later"; document.getElementById('permanentLockActions').classList.add('hidden');
   1260             document.getElementById('lockoutTimer').classList.remove('hidden');
   1261             if(lockoutTimerInterval) clearInterval(lockoutTimerInterval);
   1262             
   1263             lockoutTimerInterval = setInterval(() => {
   1264                 const left = Math.ceil((lockUntil - Date.now()) / 1000);
   1265                 if (left <= 0) {
   1266                     clearInterval(lockoutTimerInterval); timerDisplay.innerText = "00:00";
   1267                     initLockScreen(localStorage.getItem('wp_app_pin'), localStorage.getItem('wp_app_bio') === 'true');
   1268                     document.getElementById('lockoutUI').classList.add('hidden'); document.getElementById('lockHeader').classList.remove('hidden');
   1269                     screen.classList.remove('locked-mode');
   1270                 } else {
   1271                     timerDisplay.innerText = `00:${left < 10 ? '0'+left : left}`;
   1272                 }
   1273             }, 1000);
   1274         }
   1275 
   1276         function renderPermanentLock() {
   1277             document.getElementById('lockoutUI').classList.remove('hidden');
   1278             document.getElementById('lockHeader').classList.add('hidden');
   1279             document.getElementById('lock-screen').classList.add('locked-mode');
   1280             document.getElementById('lockoutIcon').innerText = "gpp_bad";
   1281             document.getElementById('lockoutTitle').innerText = "Security Lockout";
   1282             document.getElementById('lockoutMsg').innerText = "Maximum attempts exceeded. App is locked.";
   1283             document.getElementById('lockoutSub').innerText = "Use Forgot Password to reset.";
   1284             document.getElementById('lockoutTimer').classList.add('hidden');
   1285             document.getElementById('permanentLockActions').classList.remove('hidden');
   1286         }
   1287 
   1288         function pressKey(num) {
   1289             if(num === -1) enteredPin = enteredPin.slice(0, -1);
   1290             else if (enteredPin.length < 6) enteredPin += num;
   1291             updatePinDots(enteredPin.length);
   1292             if(enteredPin.length === 6) verifyPin();
   1293         }
   1294 
   1295         function updatePinDots(count, isError=false) {
   1296             document.querySelectorAll('#pinDots .pin-dot').forEach((d, i) => {
   1297                 if(isError) { d.classList.add('error'); setTimeout(() => d.classList.remove('error'), 400); } 
   1298                 else { d.classList.remove('error'); if(i < count) d.classList.add('filled'); else d.classList.remove('filled'); }
   1299             });
   1300         }
   1301 
   1302         function verifyPin() {
   1303             if(checkLockoutState()) return;
   1304             const storedPin = localStorage.getItem('wp_app_pin');
   1305             if(storedPin && enteredPin === storedPin) {
   1306                 failAttempts = 0; localStorage.setItem('wp_fail_attempts', '0'); localStorage.setItem('wp_lock_until', '0');
   1307                 if (localStorage.getItem('wp_app_bio') === 'true') {
   1308                     document.getElementById('pinUI').classList.add('hidden'); document.getElementById('bioUI').classList.remove('hidden');
   1309                     document.getElementById('lockTitle').innerText = "Verify Identity"; document.getElementById('lockIcon').innerText = "fingerprint";
   1310                     triggerBiometricAuth();
   1311                 } else unlockApp();
   1312             } else handleFailedAttempt();
   1313         }
   1314 
   1315         async function triggerBiometricAuth() {
   1316             if(checkLockoutState()) return;
   1317             if(!document.getElementById('bioUI').classList.contains('hidden')) document.getElementById('lockTitle').innerText = "Verify Identity";
   1318             try {
   1319                 const storedId = localStorage.getItem('wp_bio_id');
   1320                 const options = { publicKey: { challenge: new Uint8Array([1, 2, 3, 4]), timeout: 60000, userVerification: "required" } };
   1321                 if (storedId) options.publicKey.allowCredentials = [{ type: 'public-key', id: base64ToArrayBuffer(storedId), transports: ['internal'] }];
   1322                 const credential = await navigator.credentials.get(options);
   1323                 if(credential) { failAttempts = 0; localStorage.setItem('wp_fail_attempts', '0'); localStorage.setItem('wp_lock_until', '0'); unlockApp(); }
   1324             } catch (e) { console.warn("Bio failed", e); }
   1325         }
   1326 
   1327         function unlockApp() {
   1328             isLocked = false;
   1329             document.getElementById('lock-screen').classList.remove('active');
   1330             document.getElementById('app-layout').classList.add('visible');
   1331             loadChats();
   1332         }
   1333 
   1334         // --- SECURITY SETTINGS ---
   1335         function togglePinSetting() {
   1336             if(document.getElementById('pinLockToggle').checked) openPinSetup(); else localStorage.removeItem('wp_app_pin');
   1337         }
   1338 
   1339         function toggleBioSetting() {
   1340             if(document.getElementById('bioLockToggle').checked) registerBiometric();
   1341             else { localStorage.removeItem('wp_app_bio'); localStorage.removeItem('wp_bio_id'); }
   1342         }
   1343 
   1344         function openPinSetup() {
   1345             document.getElementById('pinSetupModal').classList.add('active');
   1346             setupStep = 1; setupPin = ""; enteredPin = ""; updateSetupDots(0);
   1347             document.getElementById('pinSetupTitle').innerText = "Set 6-Digit PIN";
   1348             document.getElementById('pinSetupMsg').innerText = "Enter a new PIN";
   1349         }
   1350         
   1351         function updateSetupDots(count) {
   1352             document.querySelectorAll('#setupDots .pin-dot').forEach((d, i) => i < count ? d.classList.add('filled') : d.classList.remove('filled'));
   1353         }
   1354 
   1355         function pressSetupKey(num) {
   1356             if(num === -1) enteredPin = enteredPin.slice(0, -1);
   1357             else if (enteredPin.length < 6) enteredPin += num;
   1358             updateSetupDots(enteredPin.length);
   1359 
   1360             if(enteredPin.length === 6) {
   1361                 if(setupStep === 1) {
   1362                     setupPin = enteredPin; enteredPin = ""; setupStep = 2;
   1363                     setTimeout(() => { updateSetupDots(0); document.getElementById('pinSetupTitle').innerText = "Confirm PIN"; document.getElementById('pinSetupMsg').innerText = "Re-enter to confirm"; }, 200);
   1364                 } else if (setupStep === 2) {
   1365                     if(enteredPin === setupPin) {
   1366                         localStorage.setItem('wp_app_pin', setupPin);
   1367                         closeModal('pinSetupModal'); enteredPin = "";
   1368                     } else {
   1369                         const dots = document.getElementById('setupDots'); dots.classList.add('shake');
   1370                         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);
   1371                     }
   1372                 }
   1373             }
   1374         }
   1375 
   1376         function cancelPinSetup() { document.getElementById('pinLockToggle').checked = false; closeModal('pinSetupModal'); }
   1377 
   1378         async function registerBiometric() {
   1379             try {
   1380                 const iconPath = new URL('/assets/icon.svg', window.location.href).toString();
   1381                 const credential = await navigator.credentials.create({
   1382                     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 }
   1383                 });
   1384                 
   1385                 if(credential) {
   1386                     localStorage.setItem('wp_bio_id', arrayBufferToBase64(credential.rawId));
   1387                     localStorage.setItem('wp_app_bio', 'true');
   1388                 } else throw new Error();
   1389             } catch (e) { document.getElementById('bioLockToggle').checked = false; }
   1390         }
   1391 
   1392         function showForgotPin() { document.getElementById('forgotPinModal').classList.add('active'); }
   1393         
   1394         async function attemptPinReset() {
   1395              const btn = document.getElementById('resetBtn'); const err = document.getElementById('resetError');
   1396              btn.classList.add('loading'); btn.disabled = true; err.style.display = 'none';
   1397              
   1398              try {
   1399                 const response = await fetch(`${RENDER_BACKEND_URL}/api/verify`, {
   1400                     method: 'POST', headers: {'Content-Type': 'application/json'},
   1401                     body: JSON.stringify({ username: document.getElementById('resetUsername').value, password: document.getElementById('resetPassword').value })
   1402                 });
   1403                 const data = await response.json();
   1404                 
   1405                 if(data.success) {
   1406                     ['wp_app_pin','wp_app_bio','wp_bio_id','wp_fail_attempts','wp_lock_until'].forEach(k => localStorage.removeItem(k));
   1407                     failAttempts = 0; lockUntil = 0;
   1408                     document.getElementById('pinLockToggle').checked = false; document.getElementById('bioLockToggle').checked = false;
   1409                     document.getElementById('lockoutUI').classList.add('hidden'); document.getElementById('lockHeader').classList.remove('hidden');
   1410                     document.getElementById('lock-screen').classList.remove('locked-mode');
   1411                     closeModal('forgotPinModal'); unlockApp();
   1412                 } else throw new Error();
   1413              } catch (e) { err.style.display = 'block'; } finally { btn.classList.remove('loading'); btn.disabled = false; }
   1414         }
   1415 
   1416         function togglePasswordVisibility(id, iconId) {
   1417             const input = document.getElementById(id); const icon = document.getElementById(iconId);
   1418             if (input.type === 'password') { input.type = 'text'; icon.innerText = 'visibility_off'; } 
   1419             else { input.type = 'password'; icon.innerText = 'visibility'; }
   1420         }
   1421 
   1422         async function attemptLogin() {
   1423             const btn = document.getElementById('loginBtn'); const err = document.getElementById('authError');
   1424             btn.classList.add('loading'); btn.disabled = true; err.style.opacity = '0';
   1425             try {
   1426                 const response = await fetch(`${RENDER_BACKEND_URL}/api/verify`, {
   1427                     method: 'POST', headers: {'Content-Type': 'application/json'},
   1428                     body: JSON.stringify({ username: document.getElementById('username').value, password: document.getElementById('password').value })
   1429                 });
   1430                 const data = await response.json();
   1431                 if(data.success) {
   1432                     if(document.getElementById('rememberMe').checked) localStorage.setItem('wp_auth_expiry', Date.now() + (3 * 24 * 60 * 60 * 1000));
   1433                     else localStorage.setItem('wp_auth_expiry', Date.now() + (1 * 60 * 60 * 1000)); // default 1 hr fallback
   1434                     revealApp();
   1435                 } else throw new Error();
   1436             } catch (e) { err.style.opacity = '1'; btn.classList.remove('loading'); btn.disabled = false; }
   1437         }
   1438 
   1439         function revealApp() {
   1440              document.getElementById('auth-screen').style.opacity = '0';
   1441              setTimeout(() => {
   1442                  document.getElementById('auth-screen').style.display = 'none';
   1443                  document.getElementById('app-layout').classList.add('visible'); 
   1444                  loadChats();
   1445              }, 400);
   1446         }
   1447         
   1448         function openSettings() { 
   1449             document.getElementById('settingsSidebar').classList.add('active'); 
   1450             calculateLocalStorageUsage();
   1451         }
   1452 
   1453         function closeSettingsSidebar() {
   1454             document.getElementById('settingsSidebar').classList.remove('active');
   1455         }
   1456 
   1457         async function clearAppCacheData() {
   1458             if (confirm("Are you absolutely sure you want to clear all cached data? This will clear your offline cache databases and instantly reload the workspace application.")) {
   1459                 closeSettingsSidebar(); // Call the new close function
   1460                 document.getElementById('refreshModal').classList.add('active');
   1461                 document.getElementById('refreshAuthStep').classList.add('hidden');
   1462                 document.getElementById('refreshProgressStep').classList.remove('hidden');
   1463                 
   1464                 await executeHardRefresh();
   1465             }
   1466         }
   1467         function closeModal(id) { document.getElementById(id).classList.remove('active'); }
   1468         function toggleTheme() {
   1469             const isDark = document.getElementById('darkModeToggle').checked;
   1470             document.body.setAttribute('data-theme', isDark ? 'dark' : 'light');
   1471             localStorage.setItem('theme', isDark ? 'dark' : 'light');
   1472         }
   1473         function changeFontSize(val) { document.documentElement.style.setProperty('--base-font-size', `${val}px`); }
   1474         function changeBg(id, save=true) {
   1475             document.querySelectorAll('.bg-option').forEach(el => el.classList.remove('selected'));
   1476             const colors = ['#ECE5DD', '#d1d7db', '#e5ddd5', '#ffffff'];
   1477             document.documentElement.style.setProperty('--chat-bg-color', colors[id-1]);
   1478             const option = document.querySelector(`.bg-${id}`);
   1479             if(option) option.classList.add('selected');
   1480             if(save) localStorage.setItem('chatBg', id);
   1481         }
   1482         function saveProfile() { localStorage.setItem('profileName', document.getElementById('profileName').value); localStorage.setItem('profilePhone', document.getElementById('profilePhone').value); }
   1483         function populateFAQ() {
   1484             const faqs = [
   1485                 {q: "Is this secure?", a: "Yes, data lives in your private Firebase. No one else can access it."},
   1486                 {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."},
   1487                 {q: "Does my phone need to be online?", a: "No. Once linked, your Render server handles logging even if your phone is powered off."},
   1488                 {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."},
   1489                 {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."},
   1490                 {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."}
   1491             ];
   1492             const container = document.getElementById('faqContainer');
   1493             faqs.forEach(item => {
   1494                 const div = document.createElement('div'); div.className = 'faq-item';
   1495                 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>`;
   1496                 container.appendChild(div);
   1497             });
   1498         }
   1499 
   1500         function toggleChatMenu(event) { if(event) event.stopPropagation(); document.getElementById('chatMenu').classList.toggle('show'); }
   1501         document.addEventListener('click', function(e) { const menu = document.getElementById('chatMenu'); if(menu.classList.contains('show') && !e.target.closest('.menu-container')) menu.classList.remove('show'); });
   1502 
   1503         function toggleChatSearch() { 
   1504             const searchContainer = document.getElementById('inChatSearch');
   1505             searchContainer.classList.toggle('hidden'); 
   1506             document.getElementById('chatMenu').classList.remove('show'); 
   1507             
   1508             const input = document.getElementById('msgSearchInput');
   1509             if (searchContainer.classList.contains('hidden')) {
   1510                 // Clear and trigger input event to reset highlights when closed
   1511                 input.value = '';
   1512                 input.dispatchEvent(new Event('input'));
   1513             } else {
   1514                 input.focus(); 
   1515             }
   1516         }
   1517         
   1518         document.getElementById('msgSearchInput').addEventListener('input', (e) => {
   1519             const term = e.target.value.toLowerCase();
   1520             let count = 0;
   1521 
   1522             document.querySelectorAll('.msg-bubble').forEach(b => {
   1523                 const textDiv = b.querySelector('.msg-text'); 
   1524                 if(textDiv) {
   1525                     // Back up the original HTML on first search so we don't destroy structure
   1526                     if (!textDiv.hasAttribute('data-orig')) {
   1527                         textDiv.setAttribute('data-orig', textDiv.innerHTML);
   1528                     }
   1529                     
   1530                     const originalHTML = textDiv.getAttribute('data-orig');
   1531                     const rawText = textDiv.textContent || "";
   1532                     
   1533                     if(term && rawText.toLowerCase().includes(term)) {
   1534                         b.style.display = 'flex';
   1535                         // Negative lookahead regex: Highlights term ONLY if it's outside of HTML tags (<...>)
   1536                         const safeTerm = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
   1537                         const regex = new RegExp(`(?![^<]*>)(${safeTerm})`, 'gi');
   1538                         
   1539                         textDiv.innerHTML = originalHTML.replace(regex, '<span class="highlight">$1</span>');
   1540                         count++;
   1541                     } else {
   1542                         b.style.display = term ? 'none' : 'flex';
   1543                         textDiv.innerHTML = originalHTML; // Restore clean HTML
   1544                     }
   1545                 }
   1546             });
   1547             document.getElementById('searchCount').innerText = term ? `${count} found` : '';
   1548         });
   1549 
   1550         async function loadChats() {
   1551             const list = document.getElementById('contactList'); 
   1552             
   1553             // Render initial cache instantly before networking kicks in
   1554             const cachedChats = await getCachedChats();
   1555             if (cachedChats && cachedChats.length > 0) {
   1556                 // Ensure it's correctly sorted descending by activity in cache
   1557                 cachedChats.sort((a,b) => b.lastActive - a.lastActive);
   1558                 renderChatList(cachedChats);
   1559             } else {
   1560                 list.innerHTML = '';
   1561                 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>`;
   1562             }
   1563 
   1564             if (!navigator.onLine) return; // Skip snapshot if offline
   1565 
   1566             // Prevent duplicating listeners
   1567             if(window.chatListUnsub) window.chatListUnsub();
   1568 
   1569             // Use real-time listener instead of one-time .get()
   1570             window.chatListUnsub = db.collection('Chats').onSnapshot(async snap => {
   1571                 const groups = {};
   1572                 const excluded = [''];
   1573 
   1574                 snap.forEach(doc => {
   1575                     const d = doc.data();
   1576                     if (!d.lastActive || excluded.includes(doc.id)) return;
   1577                     const phone = d.phoneNumber || doc.id.split('@')[0];
   1578                     if (!groups[phone]) groups[phone] = { main: {id:doc.id, ...d}, ids: [doc.id] };
   1579                     else {
   1580                         groups[phone].ids.push(doc.id);
   1581                         if (d.lastActive > groups[phone].main.lastActive) groups[phone].main = {id:doc.id, ...d};
   1582                     }
   1583                 });
   1584 
   1585                 // Sort descending by time
   1586                 const chats = Object.values(groups).map(g => ({...g.main, ids: g.ids})).sort((a,b) => b.lastActive - a.lastActive);
   1587                 
   1588                 await saveCachedChats(chats);
   1589                 renderChatList(chats);
   1590             }, err => { 
   1591                 console.error("Chat list sync error:", err); 
   1592             });
   1593         }
   1594 
   1595 function renderChatList(chats) {
   1596     const list = document.getElementById('contactList');
   1597     list.innerHTML = chats.length === 0 ? `<div style="text-align:center; padding:32px; color:var(--md-sys-color-outline);">No chats found</div>` : '';
   1598 
   1599     chats.forEach(c => {
   1600         const el = document.createElement('div'); 
   1601         el.className = 'contact-item ripple-surface';
   1602         
   1603         // Priority: Custom Name > Display Name > Phone Number > Raw ID
   1604         const primaryName = c.customName || c.displayName || c.phoneNumber || c.id;
   1605         const isGroup = c.id.includes('@g.us');
   1606         
   1607         // Structure: Avatar | [ Name                  Time ]
   1608         //                   | [ Checkmark/Sender + Msg     ]
   1609         el.innerHTML = `
   1610             <div class="avatar">${isGroup ? '<span class="material-symbols-rounded">group</span>' : escapeHTML(primaryName[0]).toUpperCase()}</div>
   1611             <div class="contact-info" style="min-width: 0;">
   1612                 <div class="contact-name" style="display:flex; justify-content:space-between; align-items:baseline; width:100%;">
   1613                     <span style="white-space:nowrap; overflow:hidden; text-overflow:ellipsis; flex:1;">${escapeHTML(primaryName)}</span>
   1614                     <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>
   1615                 </div>
   1616                 <div class="contact-sub preview-text" style="display:flex; align-items:center; margin-top:2px; color:var(--md-sys-color-on-surface-variant);">
   1617                     <span class="preview-content" style="white-space:nowrap; overflow:hidden; text-overflow:ellipsis; width:100%;">
   1618                         <span style="opacity:0.5; font-size: 13px;">Syncing...</span>
   1619                     </span>
   1620                 </div>
   1621             </div>
   1622         `;
   1623         
   1624         el.onclick = () => openChat(c.ids, primaryName, el, isGroup, c.phoneNumber || c.id);
   1625         list.appendChild(el);
   1626 
   1627         // Fetch the latest message asynchronously to prevent blocking the UI render
   1628         const updatePreview = async () => {
   1629             let allMsgs = [];
   1630             for (const id of c.ids) {
   1631                 const msgs = await getCachedMessages(id);
   1632                 allMsgs = allMsgs.concat(msgs);
   1633             }
   1634             
   1635             // Sort descending to grab the absolute latest message in local cache
   1636             allMsgs.sort((a, b) => b.timestamp - a.timestamp);
   1637             let lastMsg = allMsgs.length > 0 ? allMsgs[0] : null;
   1638 
   1639             // REAL-TIME PREVIEW SYNC: 
   1640             // If the server tells us this chat was active *after* our cached latest message, fetch the missing real-time snippet
   1641             if (navigator.onLine && (!lastMsg || c.lastActive > lastMsg.timestamp)) {
   1642                 try {
   1643                     let latestOnline = null;
   1644                     for (const id of c.ids) {
   1645                         const snap = await db.collection('Chats').doc(id).collection('Messages')
   1646                             .orderBy('timestamp', 'desc').limit(1).get();
   1647                         
   1648                         if (!snap.empty) {
   1649                             const data = snap.docs[0].data();
   1650                             if (!latestOnline || data.timestamp > latestOnline.timestamp) {
   1651                                 latestOnline = data;
   1652                             }
   1653                         }
   1654                     }
   1655                     if (latestOnline && (!lastMsg || latestOnline.timestamp > lastMsg.timestamp)) {
   1656                         lastMsg = latestOnline;
   1657                     }
   1658                 } catch(err) {
   1659                     console.warn("Failed fetching realtime preview snippet", err);
   1660                 }
   1661             }
   1662             
   1663             const contentSpan = el.querySelector('.preview-content');
   1664             const timeSpan = el.querySelector('.chat-time');
   1665 
   1666             if (lastMsg) {
   1667                 // Format Timestamp: Time if today, Date if older
   1668                 const msgDate = new Date(lastMsg.timestamp * 1000);
   1669                 const today = new Date();
   1670                 const isToday = msgDate.toDateString() === today.toDateString();
   1671                 
   1672                 timeSpan.innerText = isToday 
   1673                     ? msgDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) 
   1674                     : msgDate.toLocaleDateString([], { month: 'short', day: 'numeric' });
   1675 
   1676                 // Format Prefix: Double ticks for outgoing, Sender Name for incoming groups
   1677                 let prefix = '';
   1678                 if (lastMsg.fromMe) {
   1679                     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>`;
   1680                 } else if (isGroup && lastMsg.senderName && lastMsg.senderName !== "Unknown") {
   1681                     prefix = `<span style="font-weight: 500;">${escapeHTML(lastMsg.senderName)}:</span> `;
   1682                 }
   1683                 
   1684                 // Construct the final line properly escaped
   1685                 const safePreviewText = escapeHTML(lastMsg.text || 'Media');
   1686                 contentSpan.innerHTML = `${prefix}${safePreviewText}`;
   1687             } else {
   1688                 contentSpan.innerHTML = `<span style="font-style: italic; opacity: 0.7;">No messages</span>`;
   1689             }
   1690         };
   1691         
   1692         updatePreview();
   1693     });
   1694 }
   1695 
   1696         // --- UPDATED OPEN CHAT: DELTA SYNC + IDB + CINEMATIC UI ---
   1697         async function openChat(ids, name, el, isGroup, displayId) {
   1698             activeChatId = ids[0];
   1699             isCurrentChatGroup = isGroup;
   1700             document.getElementById('headerName').innerText = name;
   1701             document.getElementById('chatHeaderContent').style.opacity = '1';
   1702             document.getElementById('headerId').innerText = displayId;
   1703             document.getElementById('headerAvatar').innerHTML = isGroup ? `<span class="material-symbols-rounded">group</span>` : escapeHTML(name[0]).toUpperCase();
   1704 
   1705             if (window.matchMedia('(max-width: 768px)').matches) {
   1706                 window.history.pushState({chatOpen: true}, "");
   1707                 document.getElementById('sidebar').classList.add('inactive');
   1708                 document.getElementById('chatArea').classList.add('active');
   1709             }
   1710 
   1711             if(window.unsub) window.unsub.forEach(u => u());
   1712             window.unsub = [];
   1713             
   1714             const container = document.getElementById('messagesContainer');
   1715             const spinnerWrap = document.getElementById('syncSpinnerWrap');
   1716             
   1717             // Show cinematic sync UI
   1718             spinnerWrap.classList.add('active');
   1719             container.classList.add('syncing');
   1720             
   1721             // 1. Fetch from Cache
   1722             let cachedMsgs = [];
   1723             for (const id of ids) {
   1724                 const msgs = await getCachedMessages(id);
   1725                 cachedMsgs = cachedMsgs.concat(msgs);
   1726             }
   1727             
   1728             const msgsMap = new Map();
   1729             cachedMsgs.forEach(m => msgsMap.set(m.id, m));
   1730             
   1731             // Render Cache Immediately
   1732             if (cachedMsgs.length > 0) {
   1733                 renderMsgs(msgsMap);
   1734                 setTimeout(() => scrollToBottom('auto'), 50); // Instantly jump to bottom on load
   1735             } else {
   1736                 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>`;
   1737             }
   1738 
   1739             // 2. Delta Sync with Firebase
   1740             let lastTs = 0;
   1741             if (cachedMsgs.length > 0) {
   1742                 lastTs = Math.max(...cachedMsgs.map(m => m.timestamp));
   1743             }
   1744             
   1745             // Artificial delay to let spinner animation glide smoothly if cache was instant
   1746             await new Promise(r => setTimeout(r, 400));
   1747             
   1748             let pendingSyncs = ids.length;
   1749 
   1750             ids.forEach(id => {
   1751                 let query = db.collection('Chats').doc(id).collection('Messages').orderBy('timestamp');
   1752                 if (lastTs > 0) {
   1753                     query = query.where('timestamp', '>', lastTs);
   1754                 }
   1755 
   1756                 let isFirstSync = true;
   1757                 window.unsub.push(query.onSnapshot(async snap => {
   1758                     let isNew = false;
   1759                     const newToCache = [];
   1760                     snap.forEach(doc => {
   1761                         const data = doc.data();
   1762                         data.chatId = id; 
   1763                         msgsMap.set(data.id, data);
   1764                         newToCache.push(data);
   1765                         isNew = true;
   1766                     });
   1767                     
   1768                     const c = document.getElementById('messagesContainer');
   1769                     const wasAtBottom = (c.scrollHeight - c.scrollTop - c.clientHeight) <= 100;
   1770 
   1771                     if (newToCache.length > 0) await saveCachedMessages(newToCache);
   1772                     if (isNew || cachedMsgs.length === 0) {
   1773                         renderMsgs(msgsMap);
   1774                         
   1775                         if (isFirstSync || wasAtBottom) {
   1776                             setTimeout(() => scrollToBottom(isFirstSync ? 'auto' : 'smooth'), 50);
   1777                         } else if (!isFirstSync && isNew && !wasAtBottom) {
   1778                             document.getElementById('newMsgBadge').classList.remove('hidden');
   1779                             document.getElementById('scrollFab').classList.add('visible');
   1780                         }
   1781                     }
   1782                     
   1783                     if (isFirstSync) {
   1784                         isFirstSync = false;
   1785                         pendingSyncs--;
   1786                         if (pendingSyncs <= 0) {
   1787                             spinnerWrap.classList.remove('active');
   1788                             container.classList.remove('syncing');
   1789                         }
   1790                     }
   1791                 }, err => {
   1792                     console.error("Sync error:", err);
   1793                     if (isFirstSync) {
   1794                         isFirstSync = false;
   1795                         pendingSyncs--;
   1796                         if (pendingSyncs <= 0) {
   1797                             spinnerWrap.classList.remove('active');
   1798                             container.classList.remove('syncing');
   1799                         }
   1800                     }
   1801                 }));
   1802             });
   1803         }
   1804 
   1805         function renderMsgs(map) {
   1806             const container = document.getElementById('messagesContainer');
   1807             container.innerHTML = '';
   1808             currentMessages = Array.from(map.values()).sort((a,b) => a.timestamp - b.timestamp);
   1809             
   1810             if(currentMessages.length === 0) {
   1811                 container.innerHTML = '<div class="empty-state"><p>No messages recorded</p></div>';
   1812                 return;
   1813             }
   1814 
   1815             let lastDateStr = ""; let currentGroup = null;
   1816 
   1817             currentMessages.forEach((m, index) => {
   1818                 const date = new Date(m.timestamp * 1000);
   1819                 const dateStr = date.toDateString();
   1820 
   1821                 if(dateStr !== lastDateStr) {
   1822                     if(currentGroup) container.appendChild(currentGroup);
   1823                     currentGroup = document.createElement('div');
   1824                     currentGroup.className = 'chat-date-group';
   1825                     const div = document.createElement('div'); div.className = 'date-divider'; div.innerText = formatDividerDate(m.timestamp);
   1826                     currentGroup.appendChild(div);
   1827                     lastDateStr = dateStr;
   1828                 }
   1829 
   1830                 const div = document.createElement('div');
   1831                 div.className = `msg-bubble ${m.fromMe ? 'msg-out' : 'msg-in'}`;
   1832                 
   1833                 const nextMsg = currentMessages[index + 1]; const prevMsg = currentMessages[index - 1];
   1834                 const isSamePrev = (prevMsg && prevMsg.fromMe === m.fromMe && (!isCurrentChatGroup || prevMsg.senderName === m.senderName) && new Date(prevMsg.timestamp*1000).toDateString() === dateStr);
   1835                 const isSameNext = (nextMsg && nextMsg.fromMe === m.fromMe && (!isCurrentChatGroup || nextMsg.senderName === m.senderName) && new Date(nextMsg.timestamp*1000).toDateString() === dateStr);
   1836 
   1837                 if (isSamePrev && isSameNext) div.classList.add('group-mid'); else if (isSamePrev) div.classList.add('group-bot'); else if (isSameNext) div.classList.add('group-top');
   1838                 
   1839                 const showSenderName = !m.fromMe && isCurrentChatGroup && m.senderName && m.senderName !== "Unknown" && !isSamePrev; 
   1840                 div.id = `msg-${m.timestamp}`; div.dataset.date = dateStr;
   1841 
   1842                 // Escape all text first so HTML code isn't rendered
   1843                 const safeText = escapeHTML(m.text || '');
   1844                 // Find URLs and wrap them in an anchor tag safely
   1845                 const urlRegex = /(https?:\/\/[^\s]+)/g;
   1846                 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>`) : '';
   1847 
   1848                 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>`;
   1849                 if (currentGroup) currentGroup.appendChild(div);
   1850             });
   1851 
   1852             if(currentGroup) container.appendChild(currentGroup);
   1853             // Auto-scroll logic is now handled strictly in openChat based on user's scroll position
   1854         }
   1855 
   1856         function scrollToBottom(behavior = 'auto') {
   1857             const c = document.getElementById('messagesContainer');
   1858             c.scrollTo({ top: c.scrollHeight, behavior: behavior });
   1859             document.getElementById('newMsgBadge').classList.add('hidden');
   1860         }
   1861 
   1862         // --- CALENDAR LOGIC ---
   1863         function openDateSearch() { document.getElementById('chatMenu').classList.remove('show'); document.getElementById('calendarModal').classList.add('active'); renderCalendar(calCurrentDate); }
   1864 
   1865         function renderCalendar(date) {
   1866             const year = date.getFullYear(); const month = date.getMonth();
   1867             document.getElementById('calMonthYear').innerText = new Intl.DateTimeFormat('en-US', { month: 'long', year: 'numeric' }).format(date);
   1868             const firstDay = new Date(year, month, 1).getDay(); const daysInMonth = new Date(year, month + 1, 0).getDate();
   1869             const grid = document.getElementById('calDatesGrid'); grid.innerHTML = '';
   1870             for(let i=0; i<firstDay; i++) grid.innerHTML += `<div class="cal-date empty"></div>`;
   1871             for(let d=1; d<=daysInMonth; d++) {
   1872                 const dayEl = document.createElement('div'); dayEl.className = 'cal-date ripple-surface'; dayEl.innerText = d;
   1873                 if(calSelectedDate && calSelectedDate.getDate() === d && calSelectedDate.getMonth() === month && calSelectedDate.getFullYear() === year) dayEl.classList.add('active');
   1874                 dayEl.onclick = () => selectDate(new Date(year, month, d)); grid.appendChild(dayEl);
   1875             }
   1876         }
   1877 
   1878         function changeCalMonth(delta) { calCurrentDate.setMonth(calCurrentDate.getMonth() + delta); renderCalendar(calCurrentDate); }
   1879         function selectDate(date) { calSelectedDate = date; document.getElementById('calSelectedDateStr').innerText = date.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' }); renderCalendar(calCurrentDate); }
   1880 
   1881         function jumpToDate() {
   1882             if(!currentMessages.length) { closeModal('calendarModal'); return; }
   1883             const targetTs = calSelectedDate.setHours(0,0,0,0) / 1000;
   1884             const msg = currentMessages.find(m => m.timestamp >= targetTs);
   1885             closeModal('calendarModal');
   1886             if(msg) {
   1887                 const el = document.getElementById(`msg-${msg.timestamp}`);
   1888                 if(el) { el.scrollIntoView({behavior: 'smooth', block: 'center'}); el.style.backgroundColor = 'var(--search-highlight)'; setTimeout(() => el.style.backgroundColor = '', 2000); }
   1889             } else document.getElementById('notFoundModal').classList.add('active');
   1890         }
   1891 
   1892         function handleLogout() { localStorage.removeItem('wp_auth_expiry'); localStorage.removeItem('wp_app_pin'); localStorage.removeItem('wp_app_bio'); location.reload(); }
   1893         function closeChatMobile() { window.history.back(); }
   1894         window.addEventListener('popstate', () => { document.getElementById('chatArea').classList.remove('remove'); document.getElementById('chatArea').classList.remove('active'); });
   1895         
   1896         function downloadChat() {
   1897             if(!currentMessages.length) return;
   1898             const text = currentMessages.map(m => `[${new Date(m.timestamp*1000).toLocaleString()}] ${m.senderName}: ${m.text}`).join('\n');
   1899             const blob = new Blob([text], {type:'text/plain'});
   1900             const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'chat.txt'; a.click();
   1901         }
   1902         
   1903         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'); }
   1904         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(); } }
   1905         function showLogoutModal() { document.getElementById('logoutModal').classList.add('active'); }
   1906 
   1907         document.getElementById('searchInput').addEventListener('input', (e) => {
   1908             const val = e.target.value.toLowerCase();
   1909             document.querySelectorAll('.contact-item').forEach(el => { el.style.display = el.innerText.toLowerCase().includes(val) ? 'flex' : 'none'; });
   1910         });
   1911 
   1912         function refreshApp() {
   1913             document.getElementById('appOptionsModal').classList.add('active');
   1914         }
   1915 
   1916         function executePageRefresh() {
   1917             window.location.reload(true);
   1918         }
   1919 
   1920         function openHardReset() {
   1921             closeModal('appOptionsModal');
   1922             document.getElementById('refreshAuthStep').classList.remove('hidden');
   1923             document.getElementById('refreshProgressStep').classList.add('hidden');
   1924             document.getElementById('refreshUsername').value = '';
   1925             document.getElementById('refreshPassword').value = '';
   1926             document.getElementById('refreshError').style.display = 'none';
   1927             document.getElementById('refreshModal').classList.add('active');
   1928         }
   1929 
   1930         async function startHardRefresh() {
   1931             const btn = document.getElementById('refreshBtn'); const err = document.getElementById('refreshError');
   1932             const u = document.getElementById('refreshUsername').value; const p = document.getElementById('refreshPassword').value;
   1933 
   1934             btn.classList.add('loading'); btn.disabled = true; err.style.display = 'none';
   1935 
   1936             try {
   1937                 const response = await fetch(`${RENDER_BACKEND_URL}/api/verify`, {
   1938                     method: 'POST', headers: {'Content-Type': 'application/json'},
   1939                     body: JSON.stringify({ username: u, password: p })
   1940                 });
   1941                 const data = await response.json();
   1942 
   1943                 if (!data.success) throw new Error();
   1944 
   1945                 document.getElementById('refreshAuthStep').classList.add('hidden');
   1946                 document.getElementById('refreshProgressStep').classList.remove('hidden');
   1947                 
   1948                 await executeHardRefresh();
   1949 
   1950             } catch (e) {
   1951                 err.style.display = 'block';
   1952                 btn.classList.remove('loading'); btn.disabled = false;
   1953             }
   1954         }
   1955 
   1956         function updateRefreshProgress(percent, text) {
   1957             document.getElementById('refreshProgressBar').style.width = `${percent}%`;
   1958             document.getElementById('refreshPercentText').innerText = `${Math.floor(percent)}%`;
   1959             document.getElementById('refreshStatusText').innerText = text;
   1960         }
   1961 
   1962         async function executeHardRefresh() {
   1963             try {
   1964                 updateRefreshProgress(5, "Clearing caches...");
   1965                 if ('caches' in window) { try { const keys = await caches.keys(); await Promise.all(keys.map(key => caches.delete(key))); } catch(e) {} }
   1966                 if ('serviceWorker' in navigator) { const regs = await navigator.serviceWorker.getRegistrations(); for (let r of regs) await r.unregister(); }
   1967                 
   1968                 updateRefreshProgress(10, "Clearing local storage...");
   1969                 const theme = localStorage.getItem('theme');
   1970                 const bg = localStorage.getItem('chatBg');
   1971                 localStorage.clear();
   1972                 if(theme) localStorage.setItem('theme', theme);
   1973                 if(bg) localStorage.setItem('chatBg', bg);
   1974 
   1975                 updateRefreshProgress(15, "Wiping database...");
   1976                 if(dbInstance) { dbInstance.close(); dbInstance = null; }
   1977                 
   1978                 // Slight delay helps mobile OS clear file locks before deletion
   1979                 await new Promise(r => setTimeout(r, 100));
   1980 
   1981                 await new Promise(res => { 
   1982                     try {
   1983                         const req = indexedDB.deleteDatabase(DB_NAME); 
   1984                         req.onsuccess = res; 
   1985                         req.onerror = res; 
   1986                         req.onblocked = () => {
   1987                             console.warn("Delete DB blocked by another process.");
   1988                             res();
   1989                         };
   1990                     } catch(e) { res(); }
   1991                 });
   1992                 
   1993                 // Slight delay before reopening
   1994                 await new Promise(r => setTimeout(r, 200));
   1995                 
   1996                 updateRefreshProgress(25, "Rebuilding database...");
   1997                 isIdbSupported = true;
   1998                 await initIndexedDB();
   1999 
   2000                 updateRefreshProgress(30, "Fetching chat list...");
   2001                 const snap = await db.collection('Chats').get();
   2002                 const groups = {};
   2003                 const excluded = ['917278779512@s.whatsapp.net', '201554426618024@lid'];
   2004 
   2005                 snap.forEach(doc => {
   2006                     const d = doc.data();
   2007                     if (!d.lastActive || excluded.includes(doc.id)) return;
   2008                     const phone = d.phoneNumber || doc.id.split('@')[0];
   2009                     if (!groups[phone]) groups[phone] = { main: {id:doc.id, ...d}, ids: [doc.id] };
   2010                     else {
   2011                         groups[phone].ids.push(doc.id);
   2012                         if (d.lastActive > groups[phone].main.lastActive) groups[phone].main = {id:doc.id, ...d};
   2013                     }
   2014                 });
   2015 
   2016                 const chats = Object.values(groups).map(g => ({...g.main, ids: g.ids})).sort((a,b) => b.lastActive - a.lastActive);
   2017                 await saveCachedChats(chats);
   2018 
   2019                 const total = chats.length;
   2020                 let done = 0;
   2021 
   2022                 for (const chat of chats) {
   2023                     const name = chat.customName || chat.displayName || chat.phoneNumber || chat.id;
   2024                     updateRefreshProgress(30 + ((done / total) * 60), `Fetching: ${name}...`);
   2025                     
   2026                     for (const id of chat.ids) {
   2027                         const msgSnap = await db.collection('Chats').doc(id).collection('Messages').get();
   2028                         const msgs = [];
   2029                         msgSnap.forEach(doc => {
   2030                             const data = doc.data(); data.chatId = id; msgs.push(data);
   2031                         });
   2032                         if (msgs.length > 0) await saveCachedMessages(msgs);
   2033                     }
   2034                     done++;
   2035                 }
   2036 
   2037                 updateRefreshProgress(100, "Sync complete! Restarting...");
   2038                 setTimeout(() => window.location.reload(), 1000);
   2039 
   2040             } catch (e) {
   2041                 console.error("Hard refresh error", e);
   2042                 updateRefreshProgress(100, "Error during sync. Reloading...");
   2043                 setTimeout(() => window.location.reload(), 2000);
   2044             }
   2045         }
   2046         // Function to accurately measure total LocalStorage space occupied
   2047         function calculateLocalStorageUsage() {
   2048             let totalBytes = 0;
   2049             for (let key in localStorage) {
   2050                 if (!localStorage.hasOwnProperty(key)) continue;
   2051                 // Key length + string content value length multiplied by 2 bytes (UTF-16 characters standard layout size)
   2052                 totalBytes += ((key.length + localStorage[key].length) * 2);
   2053             }
   2054             
   2055             // Format into human readable metric layout outputs
   2056             const sizeInKB = (totalBytes / 1024).toFixed(2);
   2057             const displayElement = document.getElementById('localStorageSizeDisplay');
   2058             
   2059             if (displayElement) {
   2060                 if (sizeInKB >= 1024) {
   2061                     displayElement.innerText = `${(sizeInKB / 1024).toFixed(2)} MB occupied`;
   2062                 } else {
   2063                     displayElement.innerText = `${sizeInKB} KB occupied`;
   2064                 }
   2065             }
   2066         }
   2067     </script>
   2068 </body>
   2069 </html>
© 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