commit 62417adb2c64c5eb6652077a9275eefc39cb84b8
parent e98ae72c2bd205ff2450e94fa4872d2b26813d96
Author: Amit Dutta <amitdutta4255@gmail.com>
Date: Sun, 24 May 2026 22:35:16 +0530
feat: ui update v4.0
Diffstat:
| M | index.html | | | 1940 | +++++++++++++++++++++++++++++++++++-------------------------------------------- |
1 file changed, 869 insertions(+), 1071 deletions(-)
diff --git a/index.html b/index.html
@@ -6,16 +6,16 @@
<title>WhatsApp Logger</title>
<!-- PWA Manifest -->
- <link rel="manifest" href="manifest-wpChat.json">
+ <link rel="manifest" href="/assets/manifest-wpChat.json">
<meta name="theme-color" content="#006C4C">
<!-- SVG Favicon -->
- <link rel="icon" href="icon.svg">
+ <link rel="icon" href="/assets/icon.svg">
<!-- Fonts & Icons -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
- <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&family=Google+Sans:wght@400;500;700&display=swap" rel="stylesheet">
+ <link href="https://fonts.googleapis.com/css2?family=Lexend:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@24,400,0,0" />
<!-- Firebase SDKs -->
@@ -39,6 +39,7 @@
--md-sys-color-surface-dim: #D9DBD9;
--md-sys-color-surface-container: #ECEEEB;
--md-sys-color-surface-container-high: #E6E8E5;
+ --md-sys-color-surface-container-highest: #E1E3E0;
--md-sys-color-on-surface: #191C1A;
--md-sys-color-on-surface-variant: #404944;
@@ -49,10 +50,10 @@
--md-sys-color-error-container: #FFDAD6;
--md-sys-color-on-error-container: #410002;
- /* WhatsApp Specifics */
- --chat-bubble-me: #E2FFEC;
+ /* WhatsApp Authentic Colors */
+ --chat-bubble-me: #DCF8C6;
--chat-bubble-them: #FFFFFF;
- --chat-bg-color: #EFE7DE;
+ --chat-bg-color: #ECE5DD;
--chat-bg-image-opacity: 0.08;
--search-highlight: #ffeb3b;
@@ -80,6 +81,7 @@
--md-sys-color-surface-dim: #111412;
--md-sys-color-surface-container: #1E211F;
--md-sys-color-surface-container-high: #282B29;
+ --md-sys-color-surface-container-highest: #333534;
--md-sys-color-on-surface: #E2E3DD;
--md-sys-color-on-surface-variant: #C2C8C5;
@@ -99,8 +101,10 @@
* { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
+ body, *:not(.material-symbols-rounded) {
+ font-family: 'Lexend', sans-serif !important;
+ }
body {
- font-family: 'Roboto', sans-serif;
background-color: var(--md-sys-color-surface);
color: var(--md-sys-color-on-surface);
height: 100vh;
@@ -135,10 +139,9 @@
.delay-4 { animation-delay: 0.4s; }
.delay-5 { animation-delay: 0.5s; }
- /* --- AUTH SCREEN (UPDATED to M3 Classic) --- */
+ /* --- AUTH SCREEN --- */
#auth-screen {
position: fixed; inset: 0; z-index: 999;
- /* Static blue-green background */
background: radial-gradient(circle at 0% 0%, #C1E8FB 0%, transparent 50%),
radial-gradient(circle at 100% 100%, #89F8C7 0%, transparent 50%),
#FBFDF9;
@@ -147,16 +150,14 @@
transition: opacity 0.3s;
}
- /* Dark Mode Override for Auth Background */
[data-theme="dark"] #auth-screen {
background: radial-gradient(circle at 0% 0%, #001F29 0%, transparent 50%),
radial-gradient(circle at 100% 100%, #002114 0%, transparent 50%),
#111412;
}
- /* Updated M3 Auth Card (Classic Style) */
.auth-card {
- background: rgba(251, 253, 249, 0.7); /* Translucent */
+ background: rgba(251, 253, 249, 0.7);
backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
border-radius: 28px;
padding: 32px 24px;
@@ -177,19 +178,8 @@
.brand-icon img { width: 100%; height: 100%; object-fit: contain; }
.auth-header { text-align: center; }
- .auth-header h1 {
- font-family: 'Google Sans', sans-serif;
- font-size: 28px;
- font-weight: 400;
- color: var(--md-sys-color-on-surface);
- margin: 8px 0;
- }
- .auth-header p {
- font-family: 'Roboto', sans-serif;
- font-size: 14px;
- color: var(--md-sys-color-on-surface-variant);
- margin: 0;
- }
+ .auth-header h1 { font-family: 'Google Sans', sans-serif; font-size: 28px; font-weight: 400; color: var(--md-sys-color-on-surface); margin: 8px 0; }
+ .auth-header p { font-family: 'Roboto', sans-serif; font-size: 14px; color: var(--md-sys-color-on-surface-variant); margin: 0; }
/* --- LOCK SCREEN (PIN/BIO) --- */
#lock-screen {
@@ -226,199 +216,101 @@
.key-btn:active { background: var(--md-sys-color-surface-container-high); }
.key-backspace { font-size: 24px; color: var(--md-sys-color-on-surface-variant); }
- .forgot-pin-btn {
- margin-top: 16px; color: var(--md-sys-color-primary); background: none; border: none;
- font-weight: 500; font-size: 14px; padding: 8px 16px; border-radius: 20px; cursor: pointer;
- }
+ .forgot-pin-btn { margin-top: 16px; color: var(--md-sys-color-primary); background: none; border: none; font-weight: 500; font-size: 14px; padding: 8px 16px; border-radius: 20px; cursor: pointer; }
/* --- LOCKOUT UI --- */
- .lockout-container {
- display: flex; flex-direction: column; align-items: center; justify-content: center;
- text-align: center; width: 100%; animation: fadeIn 0.3s; flex: 1;
- }
- .lockout-icon-bg {
- width: 80px; height: 80px; border-radius: 50%;
- background: var(--md-sys-color-error-container);
- color: var(--md-sys-color-on-error-container);
- display: flex; align-items: center; justify-content: center;
- margin-bottom: 24px;
- }
+ .lockout-container { display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; width: 100%; animation: fadeIn 0.3s; flex: 1; }
+ .lockout-icon-bg { width: 80px; height: 80px; border-radius: 50%; background: var(--md-sys-color-error-container); color: var(--md-sys-color-on-error-container); display: flex; align-items: center; justify-content: center; margin-bottom: 24px; }
.lockout-icon { font-size: 40px; }
.lockout-title { font-family: 'Google Sans'; font-size: 28px; margin-bottom: 8px; color: var(--md-sys-color-on-surface); }
.lockout-msg { color: var(--md-sys-color-on-surface-variant); margin-bottom: 32px; font-size: 16px; }
- .lockout-timer {
- font-family: 'Google Sans'; font-size: 56px; font-weight: 400;
- color: var(--md-sys-color-error); font-feature-settings: "tnum";
- margin-bottom: 8px;
- }
+ .lockout-timer { font-family: 'Google Sans'; font-size: 56px; font-weight: 400; color: var(--md-sys-color-error); font-feature-settings: "tnum"; margin-bottom: 8px; }
.lockout-sub { margin-top: 16px; color: var(--md-sys-color-on-surface-variant); font-size: 14px; }
-
- /* Utility to hide PIN/BIO when locked */
- .locked-mode #pinUI,
- .locked-mode #bioUI,
- .locked-mode .forgot-pin-btn {
- display: none !important;
- }
+ .lockout-actions { margin-top: 24px; display: flex; gap: 12px; }
+ .locked-mode #pinUI, .locked-mode #bioUI, .locked-mode .forgot-pin-btn { display: none !important; }
/* --- UI ELEMENTS --- */
.m3-field {
position: relative; margin-bottom: 0;
background: var(--md-sys-color-surface-container-highest);
- border-radius: 4px 4px 0 0;
- border-bottom: 1px solid var(--md-sys-color-outline);
- transition: background 0.3s;
- height: 56px;
- }
- .m3-field:hover {
- background: rgba(29, 25, 43, 0.08);
- }
- .m3-field:focus-within {
- border-bottom: 2px solid var(--md-sys-color-primary);
- }
- .m3-input {
- width: 100%; height: 100%; padding: 24px 16px 8px 16px;
- border: none; background: transparent; outline: none;
- font-size: 16px; color: var(--md-sys-color-on-surface); font-family: inherit;
- }
- .m3-label {
- position: absolute; left: 16px; top: 18px;
- color: var(--md-sys-color-on-surface-variant);
- font-size: 16px; pointer-events: none; transition: 0.2s cubic-bezier(0.2, 0, 0, 1);
- }
- .m3-input:focus ~ .m3-label, .m3-input:not(:placeholder-shown) ~ .m3-label {
- top: 8px; font-size: 12px; color: var(--md-sys-color-primary);
- }
- .eye-toggle {
- position: absolute; right: 12px; top: 50%; transform: translateY(-50%);
- border: none; background: none; cursor: pointer; color: var(--md-sys-color-on-surface-variant);
- padding: 8px; border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: background 0.2s;
- }
+ border-radius: 4px 4px 0 0; border-bottom: 1px solid var(--md-sys-color-outline);
+ transition: background 0.3s; height: 56px;
+ }
+ .m3-field:hover { background: rgba(29, 25, 43, 0.08); }
+ .m3-field:focus-within { border-bottom: 2px solid var(--md-sys-color-primary); }
+ .m3-input { width: 100%; height: 100%; padding: 24px 16px 8px 16px; border: none; background: transparent; outline: none; font-size: 16px; color: var(--md-sys-color-on-surface); font-family: inherit; }
+ .m3-label { position: absolute; left: 16px; top: 18px; color: var(--md-sys-color-on-surface-variant); font-size: 16px; pointer-events: none; transition: 0.2s cubic-bezier(0.2, 0, 0, 1); }
+ .m3-input:focus ~ .m3-label, .m3-input:not(:placeholder-shown) ~ .m3-label { top: 8px; font-size: 12px; color: var(--md-sys-color-primary); }
+ .eye-toggle { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); border: none; background: none; cursor: pointer; color: var(--md-sys-color-on-surface-variant); padding: 8px; border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: background 0.2s; }
.eye-toggle:hover { background: rgba(0,0,0,0.05); color: var(--md-sys-color-on-surface); }
.m3-btn {
- width: 100%; height: 48px; border-radius: 24px;
- border: none; outline: none; cursor: pointer;
+ width: 100%; height: 48px; border-radius: 24px; border: none; outline: none; cursor: pointer;
font-family: 'Google Sans', sans-serif; font-weight: 500; font-size: 14px;
display: flex; align-items: center; justify-content: center; gap: 8px;
transition: 0.2s; position: relative; overflow: hidden;
}
- .m3-btn.filled {
- background: var(--md-sys-color-primary);
- color: var(--md-sys-color-on-primary);
- box-shadow: none;
- }
- .m3-btn.filled:hover {
- box-shadow: var(--elevation-1);
- }
+ .m3-btn.filled { background: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary); box-shadow: none; }
+ .m3-btn.filled:hover { box-shadow: var(--elevation-1); }
.m3-btn:disabled { opacity: 0.38; cursor: not-allowed; background: var(--md-sys-color-on-surface); color: var(--md-sys-color-surface); box-shadow: none; }
- /* Spinner */
- .spinner {
- width: 20px; height: 20px; border: 2px solid rgba(255,255,255,0.3);
- border-radius: 50%; border-top-color: white;
- animation: spin 0.8s linear infinite; display: none;
- }
+ .spinner { width: 20px; height: 20px; border: 2px solid rgba(255,255,255,0.3); border-radius: 50%; border-top-color: white; animation: spin 0.8s linear infinite; display: none; }
.m3-btn.loading .btn-text { visibility: hidden; }
.m3-btn.loading .btn-icon { display: none; }
.m3-btn.loading .spinner { display: block; position: absolute; }
@keyframes spin { to { transform: rotate(360deg); } }
/* --- APP LAYOUT --- */
- #app-layout {
- display: flex; width: 100%; height: 100%; max-width: 1600px;
- background: var(--md-sys-color-surface); opacity: 0; transition: opacity 0.6s ease-in-out;
- visibility: hidden;
- }
+ #app-layout { display: flex; width: 100%; height: 100%; max-width: 1600px; background: var(--md-sys-color-surface); opacity: 0; transition: opacity 0.6s ease-in-out; visibility: hidden; }
#app-layout.visible { opacity: 1; visibility: visible; }
/* SIDEBAR */
- .sidebar {
- width: 400px; display: flex; flex-direction: column;
- border-right: 1px solid var(--md-sys-color-outline-variant);
- background: var(--md-sys-color-surface); z-index: 10;
- }
+ .sidebar { width: 400px; display: flex; flex-direction: column; border-right: 1px solid var(--md-sys-color-outline-variant); background: var(--md-sys-color-surface); z-index: 10; }
.sidebar-header { padding: 16px 20px; display: flex; justify-content: space-between; align-items: center; }
- .icon-btn {
- width: 40px; height: 40px; border-radius: 50%; border: none; background: transparent; cursor: pointer;
- color: var(--md-sys-color-on-surface-variant); display: flex; align-items: center; justify-content: center; transition: 0.2s;
- }
+ .icon-btn { width: 40px; height: 40px; border-radius: 50%; border: none; background: transparent; cursor: pointer; color: var(--md-sys-color-on-surface-variant); display: flex; align-items: center; justify-content: center; transition: 0.2s; }
.icon-btn:hover { background: rgba(128,128,128,0.1); color: var(--md-sys-color-on-surface); }
-
.search-container { padding: 0 16px 12px; }
- .search-bar {
- background: var(--md-sys-color-surface-container-high);
- border-radius: 28px; height: 48px; display: flex; align-items: center; padding: 0 16px;
- }
- .search-bar input {
- border: none; background: transparent; outline: none; flex: 1; margin-left: 12px;
- font-size: 16px; color: var(--md-sys-color-on-surface);
- }
+ .search-bar { background: var(--md-sys-color-surface-container-high); border-radius: 28px; height: 48px; display: flex; align-items: center; padding: 0 16px; }
+ .search-bar input { border: none; background: transparent; outline: none; flex: 1; margin-left: 12px; font-size: 16px; color: var(--md-sys-color-on-surface); }
.contact-list { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 8px; }
- .contact-item {
- display: flex; align-items: center; padding: 12px 16px; border-radius: 16px; cursor: pointer; margin-bottom: 2px;
- animation: slideUp 0.3s cubic-bezier(0.2, 0.0, 0, 1.0) backwards;
- }
+ .contact-item { display: flex; align-items: center; padding: 12px 16px; border-radius: 16px; cursor: pointer; margin-bottom: 2px; animation: slideUp 0.3s cubic-bezier(0.2, 0.0, 0, 1.0) backwards; }
.contact-item:hover { background: rgba(128,128,128,0.05); }
.contact-item.active { background: var(--md-sys-color-secondary-container); }
.contact-item.active .contact-name { color: var(--md-sys-color-on-secondary-container); font-weight: 700; }
- .avatar {
- width: 48px; height: 48px; border-radius: 50%;
- background: var(--md-sys-color-primary-container);
- color: var(--md-sys-color-on-primary-container);
- display: flex; align-items: center; justify-content: center;
- font-size: 18px; font-weight: 500; flex-shrink: 0; margin-right: 16px;
- }
+ .avatar { width: 48px; height: 48px; border-radius: 50%; background: var(--md-sys-color-primary-container); color: var(--md-sys-color-on-primary-container); display: flex; align-items: center; justify-content: center; font-size: 18px; font-weight: 500; flex-shrink: 0; margin-right: 16px; }
.contact-info { flex: 1; min-width: 0; }
.contact-name { font-size: 16px; font-weight: 500; color: var(--md-sys-color-on-surface); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.contact-sub { font-size: 14px; color: var(--md-sys-color-on-surface-variant); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* CHAT AREA */
- .chat-area {
- flex: 1; display: flex; flex-direction: column; position: relative;
- background-color: var(--chat-bg-color);
- transition: background-color 0.3s;
- }
- .chat-bg-pattern {
- position: absolute; inset: 0; opacity: var(--chat-bg-image-opacity); z-index: 0; pointer-events: none;
- background-image: url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 1.79 4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z' fill='%236e7874' fill-opacity='0.08' fill-rule='evenodd'/%3E%3C/svg%3E");
- }
- .chat-header {
- height: 64px; background: var(--md-sys-color-surface);
- display: flex; align-items: center; padding: 0 16px;
- border-bottom: 1px solid var(--md-sys-color-outline-variant); z-index: 5;
- }
+ .chat-area { flex: 1; display: flex; flex-direction: column; position: relative; background-color: var(--chat-bg-color); transition: background-color 0.3s; overflow: hidden; }
+ .chat-bg-pattern { position: absolute; inset: 0; opacity: var(--chat-bg-image-opacity); z-index: 0; pointer-events: none; background-image: url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 1.79 4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z' fill='%236e7874' fill-opacity='0.08' fill-rule='evenodd'/%3E%3C/svg%3E"); }
+ .chat-header { height: 64px; background: var(--md-sys-color-surface); display: flex; align-items: center; padding: 0 16px; border-bottom: 1px solid var(--md-sys-color-outline-variant); z-index: 5; }
+
.chat-content {
flex: 1; overflow-y: auto; padding: 16px; z-index: 1;
display: flex; flex-direction: column; gap: 4px;
- scroll-behavior: auto; /* Removed smooth scrolling for initial load */
- scroll-behavior: smooth;
+ scroll-behavior: auto !important;
+ transition: padding-bottom 0.4s cubic-bezier(0.2, 0, 0, 1);
}
- /* Override specifically to remove default smooth behavior that causes load animation */
- .chat-content {
- scroll-behavior: auto !important;
+
+ /* Forces bottom-up alignment when content is sparse */
+ .chat-content::before {
+ content: '';
+ flex: 1 1 auto;
}
- .msg-bubble {
- max-width: 80%; padding: 8px 12px; position: relative;
- font-size: var(--base-font-size); line-height: 1.4;
- box-shadow: 0 1px 2px rgba(0,0,0,0.1);
- display: flex; flex-direction: column;
- animation: scaleIn 0.2s cubic-bezier(0.2, 0.0, 0, 1.0) backwards;
- }
- .msg-in {
- align-self: flex-start; background: var(--chat-bubble-them);
- border-radius: 0 16px 16px 16px; color: var(--md-sys-color-on-surface);
- }
- .msg-out {
- align-self: flex-end; background: var(--chat-bubble-me);
- border-radius: 16px 0 16px 16px; color: var(--md-sys-color-on-surface);
+ .chat-content.syncing {
+ padding-bottom: 72px; /* Cinematic push up */
}
+
+ .msg-bubble { max-width: 80%; padding: 8px 12px; position: relative; font-size: var(--base-font-size); line-height: 1.4; box-shadow: 0 1px 2px rgba(0,0,0,0.1); display: flex; flex-direction: column; animation: scaleIn 0.2s cubic-bezier(0.2, 0.0, 0, 1.0) backwards; }
+ .msg-in { align-self: flex-start; background: var(--chat-bubble-them); border-radius: 0 16px 16px 16px; color: var(--md-sys-color-on-surface); }
+ .msg-out { align-self: flex-end; background: var(--chat-bubble-me); border-radius: 16px 0 16px 16px; color: var(--md-sys-color-on-surface); }
- /* Message Grouping - WhatsApp Style */
.msg-out.group-top { border-bottom-right-radius: 4px; margin-bottom: 2px; }
.msg-out.group-mid { border-top-right-radius: 4px; border-bottom-right-radius: 4px; margin-bottom: 2px; }
.msg-out.group-bot { border-top-right-radius: 4px; margin-top: 0; }
-
.msg-in.group-top { border-bottom-left-radius: 4px; margin-bottom: 2px; }
.msg-in.group-mid { border-top-left-radius: 4px; border-bottom-left-radius: 4px; margin-bottom: 2px; }
.msg-in.group-bot { border-top-left-radius: 4px; margin-top: 0; }
@@ -427,111 +319,81 @@
.msg-meta { font-size: 10px; opacity: 0.7; align-self: flex-end; margin-top: 2px; }
/* DATE DIVIDERS */
- .chat-date-group {
- display: flex; flex-direction: column; gap: 4px;
- position: relative; /* Scope for sticky */
- }
- .date-divider {
- align-self: center; margin: 16px 0;
- background: var(--md-sys-color-surface-container-high);
- color: var(--md-sys-color-on-surface-variant);
- font-size: 12px; font-weight: 500;
- padding: 6px 12px; border-radius: 12px;
- box-shadow: 0 1px 2px rgba(0,0,0,0.1);
- position: sticky; top: 8px; z-index: 2;
- }
+ .chat-date-group { display: flex; flex-direction: column; gap: 4px; position: relative; }
+ .date-divider { align-self: center; margin: 16px 0; background: var(--md-sys-color-surface-container-high); color: var(--md-sys-color-on-surface-variant); font-size: 12px; font-weight: 500; padding: 6px 12px; border-radius: 12px; box-shadow: 0 1px 2px rgba(0,0,0,0.1); position: sticky; top: 8px; z-index: 2; }
.highlight { background-color: var(--search-highlight); color: black; border-radius: 2px; padding: 0 2px; }
- /* Search In Chat */
- .in-chat-search {
- background: var(--md-sys-color-surface-container);
- padding: 8px 16px; display: flex; align-items: center; gap: 10px;
- border-bottom: 1px solid var(--md-sys-color-outline-variant);
- animation: slideUp 0.3s cubic-bezier(0.2, 0.0, 0, 1.0);
- }
- .in-chat-search input {
- border: none; background: transparent; outline: none; flex: 1;
+ /* Sync Loading Spinner (Cinematic UI) */
+ .sync-spinner-wrapper {
+ position: absolute; bottom: 16px; left: 0; right: 0;
+ display: flex; justify-content: center;
+ pointer-events: none; z-index: 10;
+ transform: translateY(150%); opacity: 0;
+ transition: all 0.4s cubic-bezier(0.2, 0, 0, 1);
+ }
+ .sync-spinner-wrapper.active {
+ transform: translateY(0); opacity: 1;
+ }
+ .sync-chip {
+ background: var(--md-sys-color-surface-container-highest);
color: var(--md-sys-color-on-surface);
+ padding: 8px 16px; border-radius: 24px;
+ display: flex; align-items: center; gap: 10px;
+ font-size: 13px; font-weight: 500;
+ box-shadow: var(--elevation-2);
+ border: 1px solid var(--md-sys-color-outline-variant);
+ }
+ .sync-chip .spinner {
+ border-color: rgba(0,0,0,0.1);
+ border-top-color: var(--md-sys-color-primary);
+ width: 16px; height: 16px;
+ display: block; position: static;
}
+ [data-theme="dark"] .sync-chip .spinner {
+ border-color: rgba(255,255,255,0.1);
+ border-top-color: var(--md-sys-color-primary);
+ }
+
+ /* Search In Chat */
+ .in-chat-search { background: var(--md-sys-color-surface-container); padding: 8px 16px; display: flex; align-items: center; gap: 10px; border-bottom: 1px solid var(--md-sys-color-outline-variant); animation: slideUp 0.3s cubic-bezier(0.2, 0.0, 0, 1.0); }
+ .in-chat-search input { border: none; background: transparent; outline: none; flex: 1; color: var(--md-sys-color-on-surface); }
/* Menu */
.menu-container { position: relative; }
- .dropdown-menu {
- position: absolute; top: 100%; right: 0;
- background: var(--md-sys-color-surface-container-high);
- border-radius: 12px; padding: 8px 0; min-width: 200px;
- display: none; flex-direction: column; z-index: 100;
- box-shadow: 0 4px 8px rgba(0,0,0,0.2);
- animation: scaleIn 0.2s cubic-bezier(0.2, 0.0, 0, 1.0); transform-origin: top right;
- }
+ .dropdown-menu { position: absolute; top: 100%; right: 0; background: var(--md-sys-color-surface-container-high); border-radius: 12px; padding: 8px 0; min-width: 200px; display: none; flex-direction: column; z-index: 100; box-shadow: 0 4px 8px rgba(0,0,0,0.2); animation: scaleIn 0.2s cubic-bezier(0.2, 0.0, 0, 1.0); transform-origin: top right; }
.dropdown-menu.show { display: flex; }
- .menu-item {
- padding: 12px 16px; display: flex; align-items: center; gap: 12px;
- color: var(--md-sys-color-on-surface); background: none; border: none;
- font-family: inherit; font-size: 14px; cursor: pointer; text-align: left;
- }
+ .menu-item { padding: 12px 16px; display: flex; align-items: center; gap: 12px; color: var(--md-sys-color-on-surface); background: none; border: none; font-family: inherit; font-size: 14px; cursor: pointer; text-align: left; }
.menu-item:hover { background: rgba(128,128,128,0.1); }
/* Scroll To Bottom FAB */
- .scroll-fab {
- position: fixed; bottom: 24px; right: 24px;
- width: 40px; height: 40px; border-radius: 50%;
- background: var(--md-sys-color-surface-container-high);
- color: var(--md-sys-color-on-surface-variant);
- border: 1px solid var(--md-sys-color-outline-variant);
- box-shadow: 0 2px 6px rgba(0,0,0,0.15);
- display: flex; align-items: center; justify-content: center;
- cursor: pointer; z-index: 20; opacity: 0; transform: translateY(20px); pointer-events: none;
- transition: 0.3s;
- }
+ .scroll-fab { position: fixed; bottom: 24px; right: 24px; width: 40px; height: 40px; border-radius: 50%; background: var(--md-sys-color-surface-container-high); color: var(--md-sys-color-on-surface-variant); border: 1px solid var(--md-sys-color-outline-variant); box-shadow: 0 2px 6px rgba(0,0,0,0.15); display: flex; align-items: center; justify-content: center; cursor: pointer; z-index: 20; opacity: 0; transform: translateY(20px); pointer-events: none; transition: 0.3s; }
.scroll-fab.visible { opacity: 1; transform: translateY(0); pointer-events: auto; }
+ .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); }
+
+ /* Offline Indicator */
+ .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); }
/* Calendar Modal */
- .calendar-card {
- background: var(--md-sys-color-surface);
- padding: 0; border-radius: 28px; width: 320px;
- overflow: hidden; display: flex; flex-direction: column;
- }
- .cal-header {
- background: var(--md-sys-color-surface-container-high);
- padding: 16px 24px;
- }
+ .calendar-card { background: var(--md-sys-color-surface); padding: 0; border-radius: 28px; width: 320px; overflow: hidden; display: flex; flex-direction: column; }
+ .cal-header { background: var(--md-sys-color-surface-container-high); padding: 16px 24px; }
.cal-header h3 { font-size: 14px; color: var(--md-sys-color-on-surface-variant); font-weight: 500; }
.cal-header h2 { font-size: 32px; color: var(--md-sys-color-on-surface); font-weight: 400; margin-top: 4px; }
-
.cal-grid { padding: 16px 12px 0; }
.cal-nav { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; padding: 0 12px; }
.cal-days { display: grid; grid-template-columns: repeat(7, 1fr); text-align: center; margin-bottom: 8px; }
.cal-day-label { font-size: 12px; color: var(--md-sys-color-on-surface-variant); width: 36px; }
-
.cal-dates { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; margin-bottom: 16px; }
- .cal-date {
- width: 36px; height: 36px; display: flex; align-items: center; justify-content: center;
- border-radius: 50%; font-size: 14px; cursor: pointer; color: var(--md-sys-color-on-surface);
- }
+ .cal-date { width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; border-radius: 50%; font-size: 14px; cursor: pointer; color: var(--md-sys-color-on-surface); }
.cal-date:hover { background: var(--md-sys-color-surface-container-high); }
.cal-date.active { background: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary); }
.cal-date.empty { pointer-events: none; }
/* Settings Modal */
- .modal-overlay {
- position: fixed; inset: 0; z-index: 2000;
- background: rgba(0,0,0,0.6); backdrop-filter: blur(4px);
- display: flex; align-items: center; justify-content: center;
- opacity: 0; visibility: hidden; transition: 0.3s;
- }
+ .modal-overlay { position: fixed; inset: 0; z-index: 2000; background: rgba(0,0,0,0.6); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; opacity: 0; visibility: hidden; transition: 0.3s; }
.modal-overlay.active { opacity: 1; visibility: visible; }
- .settings-card {
- background: var(--md-sys-color-surface);
- width: 90%; max-width: 600px; height: 80vh;
- border-radius: 28px; display: flex; flex-direction: column;
- overflow: hidden; animation: scaleIn 0.3s var(--anim-enter);
- }
- .settings-header {
- padding: 20px 24px; border-bottom: 1px solid var(--md-sys-color-outline-variant);
- display: flex; justify-content: space-between; align-items: center;
- }
+ .settings-card { background: var(--md-sys-color-surface); width: 90%; max-width: 600px; height: 80vh; border-radius: 28px; display: flex; flex-direction: column; overflow: hidden; animation: scaleIn 0.3s var(--anim-enter); }
+ .settings-header { padding: 20px 24px; border-bottom: 1px solid var(--md-sys-color-outline-variant); display: flex; justify-content: space-between; align-items: center; }
.settings-content { flex: 1; overflow-y: auto; padding: 24px; }
.setting-section { margin-bottom: 32px; }
.setting-title { font-size: 14px; font-weight: 500; color: var(--md-sys-color-primary); margin-bottom: 16px; text-transform: uppercase; letter-spacing: 1px; }
@@ -539,92 +401,39 @@
.setting-label h4 { font-size: 16px; font-weight: 400; margin-bottom: 4px; }
.setting-label p { font-size: 14px; color: var(--md-sys-color-on-surface-variant); }
- /* Toggle Switch */
.switch { position: relative; display: inline-block; width: 50px; height: 28px; }
.switch input { opacity: 0; width: 0; height: 0; }
- .slider {
- position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0;
- background-color: var(--md-sys-color-surface-container-high); border-radius: 34px; transition: .4s;
- border: 1px solid var(--md-sys-color-outline);
- }
- .slider:before {
- position: absolute; content: ""; height: 20px; width: 20px; left: 4px; bottom: 3px;
- background-color: var(--md-sys-color-outline); border-radius: 50%; transition: .4s;
- }
+ .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: var(--md-sys-color-surface-container-high); border-radius: 34px; transition: .4s; border: 1px solid var(--md-sys-color-outline); }
+ .slider:before { position: absolute; content: ""; height: 20px; width: 20px; left: 4px; bottom: 3px; background-color: var(--md-sys-color-outline); border-radius: 50%; transition: .4s; }
input:checked + .slider { background-color: var(--md-sys-color-primary); border-color: var(--md-sys-color-primary); }
input:checked + .slider:before { transform: translateX(20px); background-color: var(--md-sys-color-on-primary); }
- /* Accordion */
.faq-item { border-bottom: 1px solid var(--md-sys-color-outline-variant); }
- .faq-question {
- width: 100%; padding: 16px 0; text-align: left; background: none; border: none;
- font-size: 16px; color: var(--md-sys-color-on-surface); cursor: pointer;
- display: flex; justify-content: space-between;
- }
+ .faq-question { width: 100%; padding: 16px 0; text-align: left; background: none; border: none; font-size: 16px; color: var(--md-sys-color-on-surface); cursor: pointer; display: flex; justify-content: space-between; }
.faq-answer { display: none; padding-bottom: 16px; color: var(--md-sys-color-on-surface-variant); line-height: 1.5; }
.faq-item.open .faq-answer { display: block; }
- /* Background Selector */
.bg-selector { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 10px; }
- .bg-option {
- height: 60px; border-radius: 8px; cursor: pointer; border: 2px solid transparent;
- background-size: cover; position: relative; overflow: hidden;
- }
+ .bg-option { height: 60px; border-radius: 8px; cursor: pointer; border: 2px solid transparent; background-size: cover; position: relative; overflow: hidden; }
.bg-option.selected { border-color: var(--md-sys-color-primary); }
- .bg-1 { background-color: #EFE7DE; }
+ .bg-1 { background-color: #ECE5DD; }
.bg-2 { background-color: #d1d7db; }
.bg-3 { background-color: #e5ddd5; }
.bg-4 { background-color: #ffffff; }
- /* Install Sheet - Hidden by default on PC */
.install-sheet { display: none; }
-
- /* Settings Install Card */
- .install-card {
- background: var(--md-sys-color-primary-container);
- color: var(--md-sys-color-on-primary-container);
- padding: 16px; border-radius: 12px; margin-bottom: 32px;
- display: flex; align-items: center; justify-content: space-between;
- }
+ .install-card { background: var(--md-sys-color-primary-container); color: var(--md-sys-color-on-primary-container); padding: 16px; border-radius: 12px; margin-bottom: 32px; display: flex; align-items: center; justify-content: space-between; }
.install-card h4 { margin-bottom: 4px; }
.install-card p { font-size: 12px; }
- .install-card .m3-btn {
- width: auto; height: 36px; padding: 0 16px;
- font-size: 14px; white-space: nowrap; flex-shrink: 0;
- box-shadow: none;
- }
+ .install-card .m3-btn { width: auto; height: 36px; padding: 0 16px; font-size: 14px; white-space: nowrap; flex-shrink: 0; box-shadow: none; }
@media (max-width: 768px) {
- .sidebar {
- width: 100%;
- flex: 1;
- border-right: none;
- /* Mobile-only background gradient */
- background: radial-gradient(circle at 0% 0%, #c1e8fb49 0%, transparent 50%),
- radial-gradient(circle at 100% 100%, #89f8c86b 0%, transparent 50%),
- #fbfdf96c;
- }
-
- /* Dark mode override for sidebar on mobile */
- [data-theme="dark"] .sidebar {
- background: radial-gradient(circle at 0% 0%, #001F29 0%, transparent 50%),
- radial-gradient(circle at 100% 100%, #002114 0%, transparent 50%),
- #111412;
- }
-
+ .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; }
+ [data-theme="dark"] .sidebar { background: radial-gradient(circle at 0% 0%, #001F29 0%, transparent 50%), radial-gradient(circle at 100% 100%, #002114 0%, transparent 50%), #111412; }
.chat-area { position: fixed; inset: 0; z-index: 20; transform: translateX(100%); transition: transform 0.3s; background-color: var(--chat-bg-color); }
.chat-area.active { transform: translateX(0); }
.back-btn { display: flex !important; margin-right: 8px; }
-
- /* Bottom Sheet Install Prompt - Only visible on Mobile */
- .install-sheet {
- display: block; /* Enable it for mobile logic */
- position: fixed; bottom: 0; left: 0; right: 0; z-index: 3000;
- background: var(--md-sys-color-surface);
- border-top-left-radius: 28px; border-top-right-radius: 28px;
- padding: 24px; box-shadow: 0 -4px 20px rgba(0,0,0,0.15);
- transform: translateY(100%); transition: transform 0.3s cubic-bezier(0.2, 0, 0, 1);
- }
+ .install-sheet { 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); }
.install-sheet.visible { transform: translateY(0); }
.sheet-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
.sheet-content { text-align: center; }
@@ -632,48 +441,13 @@
}
.back-btn { display: none; margin-right: 8px; }
- /* UTILITY MODALS STYLING FIX */
- .modal-card {
- background-color: var(--md-sys-color-surface);
- padding: 24px;
- border-radius: 28px;
- width: 90%;
- max-width: 320px;
- transform: scale(0.9); transition: 0.3s cubic-bezier(0.2, 0, 0, 1);
- animation: scaleIn 0.3s var(--anim-enter);
- }
+ .modal-card { background-color: var(--md-sys-color-surface); padding: 24px; border-radius: 28px; width: 90%; max-width: 320px; transform: scale(0.9); transition: 0.3s cubic-bezier(0.2, 0, 0, 1); animation: scaleIn 0.3s var(--anim-enter); }
.modal-overlay.active .modal-card { transform: scale(1); }
-
- .modal-card.utility {
- width: 320px;
- padding: 24px;
- border-radius: 28px;
- }
- .modal-actions {
- display: flex;
- justify-content: flex-end;
- gap: 8px;
- margin-top: 24px;
- }
- .modal-actions .m3-btn {
- width: auto;
- min-width: 80px;
- height: 40px;
- padding: 0 16px;
- }
-
- /* Empty State Centering */
- .empty-state {
- display: flex; flex-direction: column; align-items: center; justify-content: center;
- height: 100%; width: 100%; color: var(--md-sys-color-outline);
- text-align: center; animation: fadeIn 0.5s;
- }
-
- /* Skeleton */
- .skeleton {
- background: linear-gradient(90deg, var(--md-sys-color-surface-container-high) 25%, var(--md-sys-color-surface-container) 50%, var(--md-sys-color-surface-container-high) 75%);
- background-size: 200% 100%; animation: shimmer 1.5s infinite linear; border-radius: 4px;
- }
+ .modal-card.utility { width: 320px; padding: 24px; border-radius: 28px; }
+ .modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 24px; }
+ .modal-actions .m3-btn { width: auto; min-width: 80px; height: 40px; padding: 0 16px; }
+ .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; width: 100%; color: var(--md-sys-color-outline); text-align: center; animation: fadeIn 0.5s; }
+ .skeleton { background: linear-gradient(90deg, var(--md-sys-color-surface-container-high) 25%, var(--md-sys-color-surface-container) 50%, var(--md-sys-color-surface-container-high) 75%); background-size: 200% 100%; animation: shimmer 1.5s infinite linear; border-radius: 4px; }
@keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
.skel-contact { display: flex; align-items: center; padding: 12px 16px; gap: 16px; width: 100%; }
.skel-avatar { width: 48px; height: 48px; border-radius: 50%; }
@@ -683,16 +457,79 @@
.skel-left { align-self: flex-start; border-bottom-left-radius: 0; }
.skel-right { align-self: flex-end; border-bottom-right-radius: 0; width: 50%; }
+ /* --- SETTINGS SIDEBAR --- */
+ .settings-sidebar {
+ position: fixed;
+ top: 0; left: 0; bottom: 0;
+ width: 400px;
+ background: var(--md-sys-color-surface);
+ z-index: 2000;
+ display: flex;
+ flex-direction: column;
+ border-right: 1px solid var(--md-sys-color-outline-variant);
+ box-shadow: var(--elevation-2);
+ transform: translateX(-100%);
+ transition: transform 0.4s cubic-bezier(0.2, 0, 0, 1);
+ }
+
+ .settings-sidebar.active {
+ transform: translateX(0);
+ }
+
+ /* Mobile full-width override */
+ @media (max-width: 768px) {
+ .settings-sidebar {
+ width: 100%;
+ border-right: none;
+ }
+ }
+
+ @media (max-width: 768px) {
+ /* Pin all utility modals to the bottom */
+ .modal-card.utility {
+ position: absolute;
+ bottom: 0;
+ width: 100% !important;
+ max-width: 100%;
+ border-radius: 28px 28px 0 0;
+ padding: 32px 24px 24px;
+ transform: translateY(100%) scale(1);
+ animation: none; /* Override the desktop scaleIn */
+ transition: transform 0.4s cubic-bezier(0.2, 0, 0, 1);
+ }
+
+ /* Add the M3 drag handle indicator */
+ .modal-card.utility::before {
+ content: '';
+ position: absolute;
+ top: 12px;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 32px;
+ height: 4px;
+ border-radius: 2px;
+ background: var(--md-sys-color-outline-variant);
+ }
+
+ .modal-overlay.active .modal-card.utility {
+ transform: translateY(0) scale(1);
+ }
+ }
+
</style>
</head>
<body>
+ <div id="offlineIndicator" class="offline-banner hidden">
+ <span class="material-symbols-rounded" style="font-size: 16px;">cloud_off</span> You are offline
+ </div>
+
<!-- AUTH SCREEN -->
<div id="auth-screen">
<div class="auth-card">
<div style="text-align: center;" class="anim-up delay-1">
<div class="brand-icon">
- <img src="icon.svg" alt="App Logo">
+ <img src="/assets/icon.svg" alt="App Logo">
</div>
<h1 style="font-family:'Google Sans'; font-size:28px; margin:8px 0; font-weight:400;">Welcome Back</h1>
<p style="color:var(--md-sys-color-on-surface-variant); font-size:14px;">Sign in to your chat log</p>
@@ -729,36 +566,30 @@
</div>
</div>
- <!-- LOCK SCREEN (PIN / BIO / LOCKOUT) -->
+ <!-- LOCK SCREEN -->
<div id="lock-screen">
- <!-- LOCKOUT UI -->
<div id="lockoutUI" class="hidden lockout-container">
- <div class="lockout-icon-bg">
- <span class="material-symbols-rounded lockout-icon" id="lockoutIcon">lock_clock</span>
- </div>
+ <div class="lockout-icon-bg"><span class="material-symbols-rounded lockout-icon" id="lockoutIcon">lock_clock</span></div>
<h2 class="lockout-title" id="lockoutTitle">Locked</h2>
<p class="lockout-msg" id="lockoutMsg">Too many attempts.</p>
<div class="lockout-timer" id="lockoutTimer">00:30</div>
<p class="lockout-sub" id="lockoutSub">Try again later</p>
- <div id="permanentLockActions" class="hidden" style="margin-top:24px; display:flex; gap:12px;">
+ <div id="permanentLockActions" class="hidden lockout-actions">
<button class="m3-btn filled" onclick="showForgotPin()" style="background:var(--md-sys-color-error); width:auto; padding:0 32px;">Forgot PIN</button>
</div>
</div>
- <!-- HEADER (Hidden during lockout) -->
<div class="lock-header" id="lockHeader">
<span class="material-symbols-rounded lock-icon" id="lockIcon">lock</span>
<div class="lock-title" id="lockTitle">Enter PIN</div>
<div class="lock-sub" id="lockSub">Protected by App Lock</div>
</div>
- <!-- PIN UI -->
<div id="pinUI">
<div class="pin-dots" id="pinDots">
<div class="pin-dot"></div><div class="pin-dot"></div><div class="pin-dot"></div>
<div class="pin-dot"></div><div class="pin-dot"></div><div class="pin-dot"></div>
</div>
-
<div class="keypad">
<button class="key-btn ripple-surface" onclick="pressKey(1)">1</button>
<button class="key-btn ripple-surface" onclick="pressKey(2)">2</button>
@@ -775,7 +606,6 @@
</div>
</div>
- <!-- BIO ONLY UI -->
<div id="bioUI" class="hidden" style="flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center;">
<div class="ripple-surface" onclick="triggerBiometricAuth()" style="width:96px; height:96px; border-radius:50%; background:var(--md-sys-color-secondary-container); color:var(--md-sys-color-on-secondary-container); display:flex; align-items:center; justify-content:center; cursor:pointer;">
<span class="material-symbols-rounded" style="font-size:48px;">fingerprint</span>
@@ -783,9 +613,7 @@
<p style="margin-top:16px; color:var(--md-sys-color-on-surface-variant);">Tap to Unlock</p>
</div>
- <div style="text-align: center;">
- <button class="forgot-pin-btn" onclick="showForgotPin()">Forgot PIN / Use Password</button>
- </div>
+ <div style="text-align: center;"><button class="forgot-pin-btn" onclick="showForgotPin()">Forgot PIN / Use Password</button></div>
</div>
<!-- MAIN APP -->
@@ -793,17 +621,9 @@
<aside class="sidebar" id="sidebar">
<div class="sidebar-header">
<div style="display: flex; align-items: center; gap: 12px; padding: 12px 0;">
- <!-- Title -->
- <h2 style="font-family: 'Google Sans', sans-serif; font-size: 20px; font-weight: 500; color: var(--md-sys-color-on-surface); margin: 0;">
- Chat Log
- </h2>
-
- <!-- Version (Tighter Gap) -->
- <span style="font-family: 'Google Sans', sans-serif; font-size: 12px; font-weight: 500; color: var(--md-sys-color-on-surface-variant); background-color: var(--md-sys-color-surface-container-high); padding: 2px 8px; border-radius: 100px; display: inline-flex; align-items: center;">
- v3.10.5
- </span>
+ <h2 style="font-family: 'Google Sans', sans-serif; font-size: 20px; font-weight: 500; color: var(--md-sys-color-on-surface); margin: 0;">Chat Log</h2>
+ <span style="font-family: 'Google Sans', sans-serif; font-size: 12px; font-weight: 500; color: var(--md-sys-color-on-surface-variant); background-color: var(--md-sys-color-surface-container-high); padding: 2px 8px; border-radius: 100px; display: inline-flex; align-items: center;">v4.0</span>
</div>
-
<div style="display:flex; gap: 8px;">
<button class="icon-btn ripple-surface" onclick="refreshApp()"><span class="material-symbols-rounded">refresh</span></button>
<button class="icon-btn ripple-surface" onclick="openSettings()" title="Settings"><span class="material-symbols-rounded">settings</span></button>
@@ -845,7 +665,16 @@
<button class="icon-btn" style="width:30px; height:30px;" onclick="toggleChatSearch()"><span class="material-symbols-rounded">close</span></button>
</div>
+ <!-- Cinematic Loading UI -->
+ <div class="sync-spinner-wrapper" id="syncSpinnerWrap">
+ <div class="sync-chip">
+ <div class="spinner"></div>
+ <span>Syncing messages...</span>
+ </div>
+ </div>
+
<div class="chat-bg-pattern"></div>
+
<div class="chat-content" id="messagesContainer">
<div class="empty-state">
<span class="material-symbols-rounded" style="font-size: 64px; opacity: 0.2;">chat</span>
@@ -853,102 +682,80 @@
</div>
</div>
- <!-- Scroll to Bottom FAB - Now triggers SMOOTH scroll manually -->
<button class="scroll-fab" id="scrollFab" onclick="scrollToBottom('smooth')">
<span class="material-symbols-rounded">arrow_downward</span>
+ <span id="newMsgBadge" class="new-msg-badge hidden"></span>
</button>
</main>
</div>
- <!-- SETTINGS MODAL -->
- <div id="settingsModal" class="modal-overlay">
- <div class="settings-card">
- <div class="settings-header">
- <h2 style="font-family:'Google Sans'">Settings</h2>
- <button class="icon-btn" onclick="closeModal('settingsModal')"><span class="material-symbols-rounded">close</span></button>
+ <!-- MODALS SECTION -->
+ <!-- SETTINGS SIDEBAR -->
+ <div id="settingsSidebar" class="settings-sidebar">
+ <div class="settings-header" style="height: 64px; padding: 0 16px; justify-content: flex-start; gap: 24px; background: var(--md-sys-color-surface-container-high);">
+ <button class="icon-btn ripple-surface" onclick="closeSettingsSidebar()">
+ <span class="material-symbols-rounded">arrow_back</span>
+ </button>
+ <h2 style="font-family:'Lexend'; font-size: 20px; font-weight: 500; margin: 0;">Settings</h2>
+ </div>
+
+ <div class="settings-content">
+ <div id="installAppSection" class="install-card hidden">
+ <div><h4>Install App</h4><p>Get the app for better experience</p></div>
+ <button class="m3-btn filled" onclick="installPWA()">Install</button>
</div>
- <div class="settings-content">
-
- <!-- Install App -->
- <div id="installAppSection" class="install-card hidden">
- <div>
- <h4>Install App</h4>
- <p>Get the app for better experience</p>
- </div>
- <button class="m3-btn filled" onclick="installPWA()">Install</button>
- </div>
-
- <!-- Security (Only visible on Mobile App) -->
- <div class="setting-section hidden" id="mobileSecuritySection">
- <div class="setting-title">App Security</div>
- <div class="setting-row">
- <div class="setting-label"><h4>PIN Lock</h4><p>Require PIN on startup</p></div>
- <label class="switch"><input type="checkbox" id="pinLockToggle" onchange="togglePinSetting()"><span class="slider"></span></label>
- </div>
- <div class="setting-row">
- <div class="setting-label"><h4>Fingerprint Lock</h4><p>Use biometrics</p></div>
- <label class="switch"><input type="checkbox" id="bioLockToggle" onchange="toggleBioSetting()"><span class="slider"></span></label>
- </div>
- </div>
-
- <!-- Profile -->
- <div class="setting-section">
- <div class="setting-title">Profile</div>
- <div class="m3-field"><input type="text" id="profileName" class="m3-input" placeholder=" " onchange="saveProfile()"><label class="m3-label">Your Name</label></div>
- <div class="m3-field"><input type="text" id="profilePhone" class="m3-input" placeholder=" " onchange="saveProfile()"><label class="m3-label">Phone Number</label></div>
+
+ <div class="setting-section hidden" id="mobileSecuritySection">
+ <div class="setting-title">App Security</div>
+ <div class="setting-row"><div class="setting-label"><h4>PIN Lock</h4><p>Require PIN on startup</p></div><label class="switch"><input type="checkbox" id="pinLockToggle" onchange="togglePinSetting()"><span class="slider"></span></label></div>
+ <div class="setting-row"><div class="setting-label"><h4>Fingerprint Lock</h4><p>Use biometrics</p></div><label class="switch"><input type="checkbox" id="bioLockToggle" onchange="toggleBioSetting()"><span class="slider"></span></label></div>
+ </div>
+
+ <div class="setting-section">
+ <div class="setting-title">Profile</div>
+ <div class="m3-field"><input type="text" id="profileName" class="m3-input" placeholder=" " onchange="saveProfile()"><label class="m3-label">Your Name</label></div>
+ <div class="m3-field"><input type="text" id="profilePhone" class="m3-input" placeholder=" " onchange="saveProfile()"><label class="m3-label">Phone Number</label></div>
+ </div>
+
+ <div class="setting-section">
+ <div class="setting-title">Appearance</div>
+ <div class="setting-row"><div class="setting-label"><h4>Dark Mode</h4><p>Easier on the eyes</p></div><label class="switch"><input type="checkbox" id="darkModeToggle" onchange="toggleTheme()"><span class="slider"></span></label></div>
+ <div class="setting-row"><div class="setting-label"><h4>Text Size</h4><p>Adjust message font size</p></div><input type="range" min="12" max="20" value="15" oninput="changeFontSize(this.value)"></div>
+ <div class="setting-label" style="margin-bottom:8px"><h4>Chat Wallpaper</h4></div>
+ <div class="bg-selector">
+ <div class="bg-option bg-1 selected" onclick="changeBg(1)"></div>
+ <div class="bg-option bg-2" onclick="changeBg(2)"></div>
+ <div class="bg-option bg-3" onclick="changeBg(3)"></div>
+ <div class="bg-option bg-4" onclick="changeBg(4)"></div>
</div>
-
- <!-- Appearance -->
- <div class="setting-section">
- <div class="setting-title">Appearance</div>
- <div class="setting-row">
- <div class="setting-label"><h4>Dark Mode</h4><p>Easier on the eyes</p></div>
- <label class="switch"><input type="checkbox" id="darkModeToggle" onchange="toggleTheme()"><span class="slider"></span></label>
- </div>
- <div class="setting-row">
- <div class="setting-label"><h4>Text Size</h4><p>Adjust message font size</p></div>
- <input type="range" min="12" max="20" value="15" oninput="changeFontSize(this.value)">
- </div>
-
- <div class="setting-label" style="margin-bottom:8px"><h4>Chat Wallpaper</h4></div>
- <div class="bg-selector">
- <div class="bg-option bg-1 selected" onclick="changeBg(1)"></div>
- <div class="bg-option bg-2" onclick="changeBg(2)"></div>
- <div class="bg-option bg-3" onclick="changeBg(3)"></div>
- <div class="bg-option bg-4" onclick="changeBg(4)"></div>
+ </div>
+
+ <div class="setting-section">
+ <div class="setting-title">Storage and Data</div>
+ <div class="setting-row">
+ <div class="setting-label">
+ <h4>Local Storage Used</h4>
+ <p id="localStorageSizeDisplay">Calculating...</p>
</div>
</div>
-
- <!-- Help & Legal -->
- <div class="setting-section">
- <div class="setting-title">Help & Support</div>
- <div id="faqContainer">
- <!-- FAQs injected via JS -->
- </div>
- <div style="margin-top:24px; display:flex; gap:12px;">
- <button class="m3-btn filled"
- style="background:var(--md-sys-color-secondary-container); color:var(--md-sys-color-on-secondary-container); cursor: pointer;"
- onclick="window.open('https://amit.is-a.dev/logger#faq', '_blank')">
- View FAQ
- </button>
- </div>
- <div style="margin-top:24px; display:flex; gap:12px;">
- <button class="m3-btn filled" style="background:var(--md-sys-color-secondary-container); color:var(--md-sys-color-on-secondary-container)" onclick="window.open('mailto:mail@amit.is-a.dev')">Email Support</button>
+ <div class="setting-row">
+ <div class="setting-label">
+ <h4>Clear Cache</h4>
+ <p>Delete all messages, cached chats, and media records locally.</p>
</div>
+ <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>
</div>
-
- <div class="setting-section">
- <div class="setting-title">Legal</div>
- <p style="font-size:12px; color:var(--md-sys-color-on-surface-variant); line-height:1.5;">
- <strong>MIT License</strong><br>
- <strong>Security:</strong> Logs are stored strictly in your private Firebase. No external access.
- <br>Check out this project on <a href="https://amit.is-a.dev/logger">amit.is-a.dev</a> or <a href="https://github.com/notamitgamer/WhatsApp-Logger-Self-Hosted-">Github</a>
- <br>Developed by <a href="https://github.com/notamitgamer">@notamitgamer</a>
- </p>
+ </div>
+
+ <div class="setting-section">
+ <div class="setting-title">Help & Support</div>
+ <div id="faqContainer"></div>
+ <div style="margin-top:24px; display:flex; gap:12px;">
+ <button class="m3-btn filled" style="background:var(--md-sys-color-secondary-container); color:var(--md-sys-color-on-secondary-container); cursor: pointer;" onclick="window.open('https://amit.is-a.dev/logger#faq', '_blank')">View FAQ</button>
</div>
-
- <button class="m3-btn filled" style="background:var(--md-sys-color-error);" onclick="showLogoutModal()">Log Out</button>
</div>
+
+ <button class="m3-btn filled" style="background:var(--md-sys-color-error); margin-top: 16px;" onclick="showLogoutModal()">Log Out</button>
</div>
</div>
@@ -964,134 +771,107 @@
</div>
</div>
- <!-- FORGOT PIN MODAL -->
<div id="forgotPinModal" class="modal-overlay">
<div class="modal-card utility">
<h3 class="modal-title">Reset App Lock</h3>
<p style="margin-top:8px; color:var(--md-sys-color-on-surface-variant); font-size:14px;">Enter your server credentials to reset local security.</p>
-
- <div class="m3-field" style="margin-top:16px;">
- <input type="text" id="resetUsername" class="m3-input" placeholder=" "><label class="m3-label">Username</label>
- </div>
- <div class="m3-field">
- <input type="password" id="resetPassword" class="m3-input" placeholder=" ">
- <label class="m3-label">Password</label>
- <button class="eye-toggle" onclick="togglePasswordVisibility('resetPassword', 'resetPassIcon')">
- <span class="material-symbols-rounded" id="resetPassIcon">visibility</span>
- </button>
- </div>
-
+ <div class="m3-field" style="margin-top:16px;"><input type="text" id="resetUsername" class="m3-input" placeholder=" "><label class="m3-label">Username</label></div>
+ <div class="m3-field"><input type="password" id="resetPassword" class="m3-input" placeholder=" "><label class="m3-label">Password</label><button class="eye-toggle" onclick="togglePasswordVisibility('resetPassword', 'resetPassIcon')"><span class="material-symbols-rounded" id="resetPassIcon">visibility</span></button></div>
<p id="resetError" style="color:var(--md-sys-color-error); font-size:12px; margin-bottom:12px; display:none;">Invalid credentials</p>
-
- <div class="modal-actions">
- <button class="m3-btn text" onclick="closeModal('forgotPinModal')">Cancel</button>
- <button class="m3-btn filled" id="resetBtn" onclick="attemptPinReset()">
- <span class="btn-text">Reset</span>
- <div class="spinner"></div>
- </button>
- </div>
+ <div class="modal-actions"><button class="m3-btn text" onclick="closeModal('forgotPinModal')">Cancel</button><button class="m3-btn filled" id="resetBtn" onclick="attemptPinReset()"><span class="btn-text">Reset</span><div class="spinner"></div></button></div>
</div>
</div>
- <!-- RENAME MODAL -->
<div id="renameModal" class="modal-overlay">
<div class="modal-card utility">
<h3 class="modal-title">Rename Chat</h3>
<div class="m3-field" style="margin-top:16px;"><input type="text" id="renameInput" class="m3-input"><label class="m3-label">New Name</label></div>
- <div class="modal-actions">
- <button class="m3-btn text" onclick="closeModal('renameModal')">Cancel</button>
- <button class="m3-btn filled" onclick="saveRename()">Save</button>
- </div>
+ <div class="modal-actions"><button class="m3-btn text" onclick="closeModal('renameModal')">Cancel</button><button class="m3-btn filled" onclick="saveRename()">Save</button></div>
</div>
</div>
- <!-- NOT FOUND MODAL -->
+ <!-- HARD REFRESH MODAL -->
+ <div id="refreshModal" class="modal-overlay">
+ <div class="modal-card utility">
+ <h3 class="modal-title">System Sync</h3>
+
+ <div id="refreshAuthStep">
+ <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>
+ <div class="m3-field" style="margin-top:16px;">
+ <input type="text" id="refreshUsername" class="m3-input" placeholder=" "><label class="m3-label">Username</label>
+ </div>
+ <div class="m3-field">
+ <input type="password" id="refreshPassword" class="m3-input" placeholder=" ">
+ <label class="m3-label">Password</label>
+ <button class="eye-toggle" onclick="togglePasswordVisibility('refreshPassword', 'refreshPassIcon')"><span class="material-symbols-rounded" id="refreshPassIcon">visibility</span></button>
+ </div>
+ <p id="refreshError" style="color:var(--md-sys-color-error); font-size:12px; margin-bottom:12px; display:none;">Invalid credentials</p>
+ <div class="modal-actions">
+ <button class="m3-btn text" onclick="closeModal('refreshModal')">Cancel</button>
+ <button class="m3-btn filled" id="refreshBtn" onclick="startHardRefresh()">
+ <span class="btn-text">Sync</span><div class="spinner"></div>
+ </button>
+ </div>
+ </div>
+
+ <div id="refreshProgressStep" class="hidden" style="text-align:center; padding: 16px 0;">
+ <p id="refreshStatusText" style="font-size:14px; font-weight:500; margin-bottom:16px; color:var(--md-sys-color-on-surface);">Starting sync...</p>
+ <div style="width:100%; height:8px; background:var(--md-sys-color-surface-container-highest); border-radius:4px; overflow:hidden;">
+ <div id="refreshProgressBar" style="width:0%; height:100%; background:var(--md-sys-color-primary); transition:width 0.3s ease;"></div>
+ </div>
+ <p id="refreshPercentText" style="font-size:12px; color:var(--md-sys-color-on-surface-variant); margin-top:8px;">0%</p>
+ </div>
+ </div>
+ </div>
+
<div id="notFoundModal" class="modal-overlay">
<div class="modal-card utility">
<h3 class="modal-title">No Messages Found</h3>
<p style="margin:16px 0; color:var(--md-sys-color-on-surface-variant);">There are no messages from this date onwards.</p>
- <div class="modal-actions">
- <button class="m3-btn filled" onclick="closeModal('notFoundModal')">OK</button>
- </div>
+ <div class="modal-actions"><button class="m3-btn filled" onclick="closeModal('notFoundModal')">OK</button></div>
</div>
</div>
- <!-- PIN SETUP MODAL -->
<div id="pinSetupModal" class="modal-overlay">
<div class="modal-card utility" style="text-align:center;">
<h3 class="modal-title" id="pinSetupTitle">Set 6-Digit PIN</h3>
- <div class="pin-dots" id="setupDots" style="margin: 24px 0;">
- <div class="pin-dot"></div><div class="pin-dot"></div><div class="pin-dot"></div>
- <div class="pin-dot"></div><div class="pin-dot"></div><div class="pin-dot"></div>
- </div>
+ <div class="pin-dots" id="setupDots" style="margin: 24px 0;"><div class="pin-dot"></div><div class="pin-dot"></div><div class="pin-dot"></div><div class="pin-dot"></div><div class="pin-dot"></div><div class="pin-dot"></div></div>
<p style="color:var(--md-sys-color-on-surface-variant); font-size:12px;" id="pinSetupMsg">Use the number pad below</p>
-
<div class="keypad" style="transform: scale(0.9); margin-bottom:0;">
- <button class="key-btn ripple-surface" onclick="pressSetupKey(1)">1</button>
- <button class="key-btn ripple-surface" onclick="pressSetupKey(2)">2</button>
- <button class="key-btn ripple-surface" onclick="pressSetupKey(3)">3</button>
- <button class="key-btn ripple-surface" onclick="pressSetupKey(4)">4</button>
- <button class="key-btn ripple-surface" onclick="pressSetupKey(5)">5</button>
- <button class="key-btn ripple-surface" onclick="pressSetupKey(6)">6</button>
- <button class="key-btn ripple-surface" onclick="pressSetupKey(7)">7</button>
- <button class="key-btn ripple-surface" onclick="pressSetupKey(8)">8</button>
- <button class="key-btn ripple-surface" onclick="pressSetupKey(9)">9</button>
- <button class="key-btn ripple-surface" style="visibility:hidden"> </button>
- <button class="key-btn ripple-surface" onclick="pressSetupKey(0)">0</button>
- <button class="key-btn ripple-surface" onclick="pressSetupKey(-1)"><span class="material-symbols-rounded key-backspace">backspace</span></button>
- </div>
- <div class="modal-actions" style="justify-content:center; margin-top:8px;">
- <button class="m3-btn text" onclick="cancelPinSetup()">Cancel</button>
+ <button class="key-btn ripple-surface" onclick="pressSetupKey(1)">1</button><button class="key-btn ripple-surface" onclick="pressSetupKey(2)">2</button><button class="key-btn ripple-surface" onclick="pressSetupKey(3)">3</button>
+ <button class="key-btn ripple-surface" onclick="pressSetupKey(4)">4</button><button class="key-btn ripple-surface" onclick="pressSetupKey(5)">5</button><button class="key-btn ripple-surface" onclick="pressSetupKey(6)">6</button>
+ <button class="key-btn ripple-surface" onclick="pressSetupKey(7)">7</button><button class="key-btn ripple-surface" onclick="pressSetupKey(8)">8</button><button class="key-btn ripple-surface" onclick="pressSetupKey(9)">9</button>
+ <button class="key-btn ripple-surface" style="visibility:hidden"> </button><button class="key-btn ripple-surface" onclick="pressSetupKey(0)">0</button><button class="key-btn ripple-surface" onclick="pressSetupKey(-1)"><span class="material-symbols-rounded key-backspace">backspace</span></button>
</div>
+ <div class="modal-actions" style="justify-content:center; margin-top:8px;"><button class="m3-btn text" onclick="cancelPinSetup()">Cancel</button></div>
</div>
</div>
- <!-- CALENDAR MODAL -->
<div id="calendarModal" class="modal-overlay">
<div class="calendar-card animate-enter">
- <div class="cal-header">
- <h3>Select date</h3>
- <h2 id="calSelectedDateStr">Tue, Oct 24</h2>
- </div>
+ <div class="cal-header"><h3>Select date</h3><h2 id="calSelectedDateStr">Tue, Oct 24</h2></div>
<div class="cal-grid">
<div class="cal-nav">
<span id="calMonthYear" style="font-weight:500">October 2023</span>
- <div style="display:flex; gap:12px;">
- <button class="icon-btn" onclick="changeCalMonth(-1)"><span class="material-symbols-rounded">chevron_left</span></button>
- <button class="icon-btn" onclick="changeCalMonth(1)"><span class="material-symbols-rounded">chevron_right</span></button>
- </div>
- </div>
- <div class="cal-days">
- <span class="cal-day-label">S</span><span class="cal-day-label">M</span><span class="cal-day-label">T</span>
- <span class="cal-day-label">W</span><span class="cal-day-label">T</span><span class="cal-day-label">F</span><span class="cal-day-label">S</span>
+ <div style="display:flex; gap:12px;"><button class="icon-btn" onclick="changeCalMonth(-1)"><span class="material-symbols-rounded">chevron_left</span></button><button class="icon-btn" onclick="changeCalMonth(1)"><span class="material-symbols-rounded">chevron_right</span></button></div>
</div>
+ <div class="cal-days"><span class="cal-day-label">S</span><span class="cal-day-label">M</span><span class="cal-day-label">T</span><span class="cal-day-label">W</span><span class="cal-day-label">T</span><span class="cal-day-label">F</span><span class="cal-day-label">S</span></div>
<div class="cal-dates" id="calDatesGrid"></div>
</div>
- <div class="modal-actions" style="padding: 16px 24px;">
- <button class="m3-btn text" onclick="closeModal('calendarModal')">Cancel</button>
- <button class="m3-btn filled" onclick="jumpToDate()" style="width:auto; padding:0 24px; height:40px;">OK</button>
- </div>
+ <div class="modal-actions" style="padding: 16px 24px;"><button class="m3-btn text" onclick="closeModal('calendarModal')">Cancel</button><button class="m3-btn filled" onclick="jumpToDate()" style="width:auto; padding:0 24px; height:40px;">OK</button></div>
</div>
</div>
- <!-- PWA INSTALL SHEET (Mobile Only) -->
<div id="installSheet" class="install-sheet">
- <div class="sheet-header">
- <h3 style="font-family:'Google Sans'; margin: 0;">Install App</h3>
- <button class="icon-btn" onclick="closeInstallSheet()"><span class="material-symbols-rounded">close</span></button>
- </div>
+ <div class="sheet-header"><h3 style="font-family:'Google Sans'; margin: 0;">Install App</h3><button class="icon-btn" onclick="closeInstallSheet()"><span class="material-symbols-rounded">close</span></button></div>
<div class="sheet-content">
- <div style="width:64px; height:64px; margin:0 auto 16px; display:flex; align-items:center; justify-content:center;">
- <img src="icon.svg" alt="App Logo" style="width:100%; height:100%; object-fit: contain;">
- </div>
+ <div style="width:64px; height:64px; margin:0 auto 16px; display:flex; align-items:center; justify-content:center;"><img src="/assets/icon.svg" alt="App Logo" style="width:100%; height:100%; object-fit: contain;"></div>
<p style="color:var(--md-sys-color-on-surface-variant); margin-bottom:24px;">Install WhatsApp Logger App for a better experience, full screen view, and enhanced security.</p>
- <div class="sheet-actions">
- <button class="m3-btn filled" onclick="installPWA()">Install App</button>
- <button class="m3-btn text" onclick="closeInstallSheet()">Continue in Browser</button>
- </div>
+ <div class="sheet-actions"><button class="m3-btn filled" onclick="installPWA()">Install App</button><button class="m3-btn text" onclick="closeInstallSheet()">Continue in Browser</button></div>
</div>
</div>
+ <!-- MAIN APP SCRIPT -->
<script>
const RENDER_BACKEND_URL = "";
const firebaseConfig = {
@@ -1106,45 +886,118 @@
firebase.initializeApp(firebaseConfig);
const db = firebase.firestore();
let activeChatId = null;
+ let isCurrentChatGroup = false;
let deferredPrompt;
let currentMessages = [];
+ // --- INDEXED DB CACHING ---
+ const DB_NAME = 'WPLoggerDB';
+ const DB_VERSION = 2; // Incremented for chat list cache
+ let dbInstance = null;
+ let isIdbSupported = true;
+
+ function initIndexedDB() {
+ return new Promise((resolve) => {
+ if (!window.indexedDB) {
+ isIdbSupported = false;
+ resolve();
+ return;
+ }
+ const req = indexedDB.open(DB_NAME, DB_VERSION);
+ req.onupgradeneeded = (e) => {
+ const database = e.target.result;
+ if (!database.objectStoreNames.contains('messages')) {
+ const store = database.createObjectStore('messages', { keyPath: 'id' });
+ store.createIndex('chatId', 'chatId', { unique: false });
+ store.createIndex('timestamp', 'timestamp', { unique: false });
+ }
+ if (!database.objectStoreNames.contains('chats')) {
+ database.createObjectStore('chats', { keyPath: 'id' });
+ }
+ };
+ req.onsuccess = (e) => { dbInstance = e.target.result; resolve(); };
+ req.onerror = (e) => {
+ console.warn('IDB init error', e);
+ isIdbSupported = false;
+ resolve();
+ };
+ });
+ }
+
+ async function getCachedMessages(chatId) {
+ if(!isIdbSupported || !dbInstance) return [];
+ return new Promise((resolve) => {
+ try {
+ const tx = dbInstance.transaction('messages', 'readonly');
+ const store = tx.objectStore('messages');
+ const index = store.index('chatId');
+ const req = index.getAll(chatId);
+ req.onsuccess = () => resolve(req.result || []);
+ req.onerror = () => resolve([]);
+ } catch(e) { resolve([]); }
+ });
+ }
+
+ async function saveCachedMessages(messages) {
+ if(!isIdbSupported || !dbInstance) return;
+ return new Promise((resolve) => {
+ try {
+ const tx = dbInstance.transaction('messages', 'readwrite');
+ const store = tx.objectStore('messages');
+ messages.forEach(m => store.put(m));
+ tx.oncomplete = () => resolve();
+ tx.onerror = () => resolve();
+ } catch(e) { resolve(); }
+ });
+ }
+
+ async function getCachedChats() {
+ if(!isIdbSupported || !dbInstance) return [];
+ return new Promise((resolve) => {
+ try {
+ const tx = dbInstance.transaction('chats', 'readonly');
+ const store = tx.objectStore('chats');
+ const req = store.getAll();
+ req.onsuccess = () => resolve(req.result || []);
+ req.onerror = () => resolve([]);
+ } catch(e) { resolve([]); }
+ });
+ }
+
+ async function saveCachedChats(chats) {
+ if(!isIdbSupported || !dbInstance) return;
+ return new Promise((resolve) => {
+ try {
+ const tx = dbInstance.transaction('chats', 'readwrite');
+ const store = tx.objectStore('chats');
+ chats.forEach(c => store.put(c));
+ tx.oncomplete = () => resolve();
+ tx.onerror = () => resolve();
+ } catch(e) { resolve(); }
+ });
+ }
+
// --- SECURITY VARS ---
- let enteredPin = "";
- let setupPin = "";
- let setupStep = 0;
- let isLocked = false;
- // Lockout Variables
+ let enteredPin = ""; let setupPin = ""; let setupStep = 0; let isLocked = false;
let failAttempts = parseInt(localStorage.getItem('wp_fail_attempts') || '0');
let lockUntil = parseInt(localStorage.getItem('wp_lock_until') || '0');
let lockoutTimerInterval;
-
- // --- CALENDAR VARS ---
- let calCurrentDate = new Date();
- let calSelectedDate = new Date();
+ let calCurrentDate = new Date(); let calSelectedDate = new Date();
- // --- HELPERS ---
const arrayBufferToBase64 = (buffer) => {
- let binary = '';
- const bytes = new Uint8Array(buffer);
- const len = bytes.byteLength;
+ let binary = ''; const bytes = new Uint8Array(buffer); const len = bytes.byteLength;
for (let i = 0; i < len; i++) binary += String.fromCharCode(bytes[i]);
return window.btoa(binary);
};
const base64ToArrayBuffer = (base64) => {
- const binary_string = window.atob(base64);
- const len = binary_string.length;
- const bytes = new Uint8Array(len);
+ const binary_string = window.atob(base64); const len = binary_string.length; const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) bytes[i] = binary_string.charCodeAt(i);
return bytes.buffer;
};
function formatDividerDate(ts) {
- const date = new Date(ts * 1000);
- const today = new Date();
- const yesterday = new Date();
+ const date = new Date(ts * 1000); const today = new Date(); const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
-
if (date.toDateString() === today.toDateString()) return "Today";
if (date.toDateString() === yesterday.toDateString()) return "Yesterday";
return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
@@ -1152,18 +1005,23 @@
// --- PWA INSTALLATION ---
if ('serviceWorker' in navigator && (window.location.protocol === 'https:' || window.location.protocol === 'http:')) {
- navigator.serviceWorker.register('sw-wpChat.js').catch(err => {
+ navigator.serviceWorker.register('/assets/sw-wpChat.js').catch(err => {
console.warn('SW Register Fail:', err);
});
+ // Reconnect Sync trigger
+ window.addEventListener('online', () => {
+ if (navigator.serviceWorker.controller) navigator.serviceWorker.controller.postMessage('sync_on_reconnect');
+ });
}
+
window.addEventListener('beforeinstallprompt', (e) => {
- e.preventDefault();
- deferredPrompt = e;
+ e.preventDefault(); deferredPrompt = e;
document.getElementById('installAppSection').classList.remove('hidden');
if (window.innerWidth <= 768) {
setTimeout(() => document.getElementById('installSheet').classList.add('visible'), 2000);
}
});
+
async function installPWA() {
if (!deferredPrompt) return;
deferredPrompt.prompt();
@@ -1175,18 +1033,28 @@
deferredPrompt = null;
}
- function closeInstallSheet() {
- document.getElementById('installSheet').classList.remove('visible');
- }
+ function closeInstallSheet() { document.getElementById('installSheet').classList.remove('visible'); }
- // --- AUTH & BOOT SEQUENCE ---
+ // --- BOOT SEQUENCE ---
function isMobileApp() {
const isStandalone = (window.matchMedia('(display-mode: standalone)').matches) || (window.navigator.standalone) || document.referrer.includes('android-app://');
- const isMobile = window.innerWidth <= 768;
- return isStandalone && isMobile;
+ return isStandalone && window.innerWidth <= 768;
}
- window.onload = () => {
+ window.onload = async () => {
+ await initIndexedDB();
+
+ if (!navigator.onLine) {
+ document.getElementById('offlineIndicator').classList.remove('hidden');
+ }
+ window.addEventListener('online', () => {
+ document.getElementById('offlineIndicator').classList.add('hidden');
+ loadChats();
+ });
+ window.addEventListener('offline', () => {
+ document.getElementById('offlineIndicator').classList.remove('hidden');
+ });
+
if (localStorage.getItem('theme') === 'dark') {
document.body.setAttribute('data-theme', 'dark');
document.getElementById('darkModeToggle').checked = true;
@@ -1199,12 +1067,10 @@
if(isMobileApp()) {
document.getElementById('mobileSecuritySection').classList.remove('hidden');
-
const hasPin = localStorage.getItem('wp_app_pin');
const useBio = localStorage.getItem('wp_app_bio') === 'true';
document.getElementById('pinLockToggle').checked = !!hasPin;
document.getElementById('bioLockToggle').checked = useBio;
-
const authValid = localStorage.getItem('wp_auth_expiry') > Date.now();
if (authValid && (hasPin || useBio)) {
@@ -1227,152 +1093,93 @@
}
}
- // Scroll Listener for FAB
document.getElementById('messagesContainer').addEventListener('scroll', function() {
const fab = document.getElementById('scrollFab');
- const st = this.scrollTop;
- const sh = this.scrollHeight;
- const ch = this.clientHeight;
-
- // Show if we are more than 500px from the bottom
- if (sh - st - ch > 500) {
+ if (this.scrollHeight - this.scrollTop - this.clientHeight > 500) {
fab.classList.add('visible');
} else {
fab.classList.remove('visible');
+ document.getElementById('newMsgBadge').classList.add('hidden');
}
});
- // Fix for mobile keyboard covering inputs
- const inputs = document.querySelectorAll('input[type="text"], input[type="password"]');
- inputs.forEach(input => {
+ document.querySelectorAll('input[type="text"], input[type="password"]').forEach(input => {
input.addEventListener('focus', () => {
if (window.innerWidth <= 768) {
- setTimeout(() => {
- input.scrollIntoView({ behavior: 'smooth', block: 'center' });
- }, 300);
+ setTimeout(() => input.scrollIntoView({ behavior: 'smooth', block: 'center' }), 300);
}
});
});
};
- // --- LOCK SCREEN & LOCKOUT LOGIC ---
+ // --- LOCK SCREEN LOGIC ---
function initLockScreen(hasPin, useBio) {
const lockScreen = document.getElementById('lock-screen');
- lockScreen.classList.add('active');
- isLocked = true;
- enteredPin = "";
+ lockScreen.classList.add('active'); isLocked = true; enteredPin = "";
updatePinDots(0);
-
- // Check Lockout State First
if (checkLockoutState()) return;
if(hasPin) {
- document.getElementById('pinUI').classList.remove('hidden');
- document.getElementById('bioUI').classList.add('hidden');
- document.getElementById('lockTitle').innerText = "Enter PIN";
- document.getElementById('lockIcon').innerText = "lock";
+ document.getElementById('pinUI').classList.remove('hidden'); document.getElementById('bioUI').classList.add('hidden');
+ document.getElementById('lockTitle').innerText = "Enter PIN"; document.getElementById('lockIcon').innerText = "lock";
} else if (useBio && !hasPin) {
- document.getElementById('pinUI').classList.add('hidden');
- document.getElementById('bioUI').classList.remove('hidden');
- document.getElementById('lockTitle').innerText = "Scan Fingerprint";
- document.getElementById('lockIcon').innerText = "fingerprint";
+ document.getElementById('pinUI').classList.add('hidden'); document.getElementById('bioUI').classList.remove('hidden');
+ document.getElementById('lockTitle').innerText = "Scan Fingerprint"; document.getElementById('lockIcon').innerText = "fingerprint";
triggerBiometricAuth();
}
}
function checkLockoutState() {
const now = Date.now();
-
- // Permanent Lock
- if (failAttempts >= 5) {
- renderPermanentLock();
- return true;
- }
-
- // Temporary Timer Lock
- if (lockUntil > now) {
- startLockoutTimer();
- return true;
- }
-
- // Reset if timer expired
+ if (failAttempts >= 5) { renderPermanentLock(); return true; }
+ if (lockUntil > now) { startLockoutTimer(); return true; }
if (lockUntil > 0 && lockUntil <= now) {
- lockUntil = 0;
- localStorage.setItem('wp_lock_until', '0');
- // Don't reset failAttempts to 0 here to keep escalating.
- // But we need to hide lockout UI
+ lockUntil = 0; localStorage.setItem('wp_lock_until', '0');
document.getElementById('lockoutUI').classList.add('hidden');
document.getElementById('lockHeader').classList.remove('hidden');
document.getElementById('lock-screen').classList.remove('locked-mode');
}
-
return false;
}
function handleFailedAttempt() {
- failAttempts++;
- localStorage.setItem('wp_fail_attempts', failAttempts);
-
+ failAttempts++; localStorage.setItem('wp_fail_attempts', failAttempts);
if (failAttempts === 3 || failAttempts === 4) {
- lockUntil = Date.now() + (30 * 1000); // 30 seconds
- localStorage.setItem('wp_lock_until', lockUntil);
+ lockUntil = Date.now() + (30 * 1000); localStorage.setItem('wp_lock_until', lockUntil);
startLockoutTimer();
} else if (failAttempts >= 5) {
renderPermanentLock();
} else {
- // Shake animation for attempt 1 and 2
- document.getElementById('pinDots').classList.add('shake');
- updatePinDots(6, true);
- setTimeout(() => {
- document.getElementById('pinDots').classList.remove('shake');
- enteredPin = "";
- updatePinDots(0);
- }, 400);
+ document.getElementById('pinDots').classList.add('shake'); updatePinDots(6, true);
+ setTimeout(() => { document.getElementById('pinDots').classList.remove('shake'); enteredPin = ""; updatePinDots(0); }, 400);
}
}
function startLockoutTimer() {
- const ui = document.getElementById('lockoutUI');
- const screen = document.getElementById('lock-screen');
- const timerDisplay = document.getElementById('lockoutTimer');
-
- ui.classList.remove('hidden');
- document.getElementById('lockHeader').classList.add('hidden');
- screen.classList.add('locked-mode'); // Hides PIN/BIO inputs
-
- document.getElementById('lockoutTitle').innerText = "Locked";
- document.getElementById('lockoutMsg').innerText = `Too many attempts (${failAttempts})`;
- document.getElementById('lockoutSub').innerText = "Try again later";
- document.getElementById('permanentLockActions').classList.add('hidden');
+ const ui = document.getElementById('lockoutUI'); const screen = document.getElementById('lock-screen'); const timerDisplay = document.getElementById('lockoutTimer');
+ ui.classList.remove('hidden'); document.getElementById('lockHeader').classList.add('hidden'); screen.classList.add('locked-mode');
+ document.getElementById('lockoutTitle').innerText = "Locked"; document.getElementById('lockoutMsg').innerText = `Too many attempts (${failAttempts})`;
+ document.getElementById('lockoutSub').innerText = "Try again later"; document.getElementById('permanentLockActions').classList.add('hidden');
document.getElementById('lockoutTimer').classList.remove('hidden');
-
if(lockoutTimerInterval) clearInterval(lockoutTimerInterval);
lockoutTimerInterval = setInterval(() => {
const left = Math.ceil((lockUntil - Date.now()) / 1000);
if (left <= 0) {
- clearInterval(lockoutTimerInterval);
- timerDisplay.innerText = "00:00";
- // Reset View
+ clearInterval(lockoutTimerInterval); timerDisplay.innerText = "00:00";
initLockScreen(localStorage.getItem('wp_app_pin'), localStorage.getItem('wp_app_bio') === 'true');
- document.getElementById('lockoutUI').classList.add('hidden');
- document.getElementById('lockHeader').classList.remove('hidden');
+ document.getElementById('lockoutUI').classList.add('hidden'); document.getElementById('lockHeader').classList.remove('hidden');
screen.classList.remove('locked-mode');
} else {
- const secs = left < 10 ? `0${left}` : left;
- timerDisplay.innerText = `00:${secs}`;
+ timerDisplay.innerText = `00:${left < 10 ? '0'+left : left}`;
}
}, 1000);
}
function renderPermanentLock() {
- const ui = document.getElementById('lockoutUI');
- const screen = document.getElementById('lock-screen');
-
- ui.classList.remove('hidden');
+ document.getElementById('lockoutUI').classList.remove('hidden');
document.getElementById('lockHeader').classList.add('hidden');
- screen.classList.add('locked-mode');
-
+ document.getElementById('lock-screen').classList.add('locked-mode');
document.getElementById('lockoutIcon').innerText = "gpp_bad";
document.getElementById('lockoutTitle').innerText = "Security Lockout";
document.getElementById('lockoutMsg').innerText = "Maximum attempts exceeded. App is locked.";
@@ -1382,93 +1189,42 @@
}
function pressKey(num) {
- if(num === -1) {
- enteredPin = enteredPin.slice(0, -1);
- } else if (enteredPin.length < 6) {
- enteredPin += num;
- }
+ if(num === -1) enteredPin = enteredPin.slice(0, -1);
+ else if (enteredPin.length < 6) enteredPin += num;
updatePinDots(enteredPin.length);
-
- if(enteredPin.length === 6) {
- verifyPin();
- }
+ if(enteredPin.length === 6) verifyPin();
}
function updatePinDots(count, isError=false) {
- const dots = document.querySelectorAll('#pinDots .pin-dot');
- dots.forEach((d, i) => {
- if(isError) {
- d.classList.add('error');
- setTimeout(() => d.classList.remove('error'), 400);
- } else {
- d.classList.remove('error');
- if(i < count) d.classList.add('filled');
- else d.classList.remove('filled');
- }
+ document.querySelectorAll('#pinDots .pin-dot').forEach((d, i) => {
+ if(isError) { d.classList.add('error'); setTimeout(() => d.classList.remove('error'), 400); }
+ else { d.classList.remove('error'); if(i < count) d.classList.add('filled'); else d.classList.remove('filled'); }
});
}
function verifyPin() {
- // Re-check lockout before verifying
if(checkLockoutState()) return;
-
const storedPin = localStorage.getItem('wp_app_pin');
if(storedPin && enteredPin === storedPin) {
- // Success - Reset Fail Counters
- failAttempts = 0;
- localStorage.setItem('wp_fail_attempts', '0');
- localStorage.setItem('wp_lock_until', '0');
-
- const useBio = localStorage.getItem('wp_app_bio') === 'true';
- if (useBio) {
- document.getElementById('pinUI').classList.add('hidden');
- document.getElementById('bioUI').classList.remove('hidden');
- document.getElementById('lockTitle').innerText = "Verify Identity";
- document.getElementById('lockIcon').innerText = "fingerprint";
+ failAttempts = 0; localStorage.setItem('wp_fail_attempts', '0'); localStorage.setItem('wp_lock_until', '0');
+ if (localStorage.getItem('wp_app_bio') === 'true') {
+ document.getElementById('pinUI').classList.add('hidden'); document.getElementById('bioUI').classList.remove('hidden');
+ document.getElementById('lockTitle').innerText = "Verify Identity"; document.getElementById('lockIcon').innerText = "fingerprint";
triggerBiometricAuth();
- } else {
- unlockApp();
- }
- } else {
- handleFailedAttempt();
- }
+ } else unlockApp();
+ } else handleFailedAttempt();
}
async function triggerBiometricAuth() {
if(checkLockoutState()) return;
-
- if(document.getElementById('bioUI').classList.contains('hidden') === false) {
- document.getElementById('lockTitle').innerText = "Verify Identity";
- }
+ if(!document.getElementById('bioUI').classList.contains('hidden')) document.getElementById('lockTitle').innerText = "Verify Identity";
try {
const storedId = localStorage.getItem('wp_bio_id');
- const options = {
- publicKey: {
- challenge: new Uint8Array([1, 2, 3, 4]),
- timeout: 60000,
- userVerification: "required"
- }
- };
- if (storedId) {
- options.publicKey.allowCredentials = [{
- type: 'public-key',
- id: base64ToArrayBuffer(storedId),
- transports: ['internal']
- }];
- }
+ const options = { publicKey: { challenge: new Uint8Array([1, 2, 3, 4]), timeout: 60000, userVerification: "required" } };
+ if (storedId) options.publicKey.allowCredentials = [{ type: 'public-key', id: base64ToArrayBuffer(storedId), transports: ['internal'] }];
const credential = await navigator.credentials.get(options);
- if(credential) {
- // Success Bio - Reset Counters
- failAttempts = 0;
- localStorage.setItem('wp_fail_attempts', '0');
- localStorage.setItem('wp_lock_until', '0');
- unlockApp();
- }
- } catch (e) {
- console.warn("Bio failed", e);
- // Note: We generally don't lock on Bio failures here as OS handles retries.
- // But if user cancels and goes to PIN, PIN failures will trigger lock.
- }
+ if(credential) { failAttempts = 0; localStorage.setItem('wp_fail_attempts', '0'); localStorage.setItem('wp_lock_until', '0'); unlockApp(); }
+ } catch (e) { console.warn("Bio failed", e); }
}
function unlockApp() {
@@ -1480,205 +1236,128 @@
// --- SECURITY SETTINGS ---
function togglePinSetting() {
- const chk = document.getElementById('pinLockToggle');
- if(chk.checked) openPinSetup();
- else localStorage.removeItem('wp_app_pin');
+ if(document.getElementById('pinLockToggle').checked) openPinSetup(); else localStorage.removeItem('wp_app_pin');
}
function toggleBioSetting() {
- const chk = document.getElementById('bioLockToggle');
- if(chk.checked) registerBiometric();
- else {
- localStorage.removeItem('wp_app_bio');
- localStorage.removeItem('wp_bio_id');
- }
+ if(document.getElementById('bioLockToggle').checked) registerBiometric();
+ else { localStorage.removeItem('wp_app_bio'); localStorage.removeItem('wp_bio_id'); }
}
function openPinSetup() {
document.getElementById('pinSetupModal').classList.add('active');
- setupStep = 1;
- setupPin = "";
- enteredPin = "";
- updateSetupDots(0);
+ setupStep = 1; setupPin = ""; enteredPin = ""; updateSetupDots(0);
document.getElementById('pinSetupTitle').innerText = "Set 6-Digit PIN";
document.getElementById('pinSetupMsg').innerText = "Enter a new PIN";
}
function updateSetupDots(count) {
- const dots = document.querySelectorAll('#setupDots .pin-dot');
- dots.forEach((d, i) => i < count ? d.classList.add('filled') : d.classList.remove('filled'));
+ document.querySelectorAll('#setupDots .pin-dot').forEach((d, i) => i < count ? d.classList.add('filled') : d.classList.remove('filled'));
}
function pressSetupKey(num) {
- if(num === -1) {
- enteredPin = enteredPin.slice(0, -1);
- } else if (enteredPin.length < 6) {
- enteredPin += num;
- }
+ if(num === -1) enteredPin = enteredPin.slice(0, -1);
+ else if (enteredPin.length < 6) enteredPin += num;
updateSetupDots(enteredPin.length);
if(enteredPin.length === 6) {
if(setupStep === 1) {
- setupPin = enteredPin;
- enteredPin = "";
- setupStep = 2;
- setTimeout(() => {
- updateSetupDots(0);
- document.getElementById('pinSetupTitle').innerText = "Confirm PIN";
- document.getElementById('pinSetupMsg').innerText = "Re-enter to confirm";
- }, 200);
+ setupPin = enteredPin; enteredPin = ""; setupStep = 2;
+ setTimeout(() => { updateSetupDots(0); document.getElementById('pinSetupTitle').innerText = "Confirm PIN"; document.getElementById('pinSetupMsg').innerText = "Re-enter to confirm"; }, 200);
} else if (setupStep === 2) {
if(enteredPin === setupPin) {
localStorage.setItem('wp_app_pin', setupPin);
- closeModal('pinSetupModal');
- enteredPin = "";
+ closeModal('pinSetupModal'); enteredPin = "";
} else {
- const dots = document.getElementById('setupDots');
- dots.classList.add('shake');
- setTimeout(() => {
- dots.classList.remove('shake');
- enteredPin = "";
- setupPin = "";
- setupStep = 1;
- updateSetupDots(0);
- document.getElementById('pinSetupTitle').innerText = "Mismatch. Try Again";
- document.getElementById('pinSetupMsg').innerText = "Enter a new PIN";
- }, 500);
+ const dots = document.getElementById('setupDots'); dots.classList.add('shake');
+ setTimeout(() => { dots.classList.remove('shake'); enteredPin = ""; setupPin = ""; setupStep = 1; updateSetupDots(0); document.getElementById('pinSetupTitle').innerText = "Mismatch. Try Again"; document.getElementById('pinSetupMsg').innerText = "Enter a new PIN"; }, 500);
}
}
}
}
- function cancelPinSetup() {
- document.getElementById('pinLockToggle').checked = false;
- closeModal('pinSetupModal');
- }
+ function cancelPinSetup() { document.getElementById('pinLockToggle').checked = false; closeModal('pinSetupModal'); }
async function registerBiometric() {
try {
- const iconPath = new URL('icon.svg', window.location.href).toString();
+ const iconPath = new URL('/assets/icon.svg', window.location.href).toString();
const credential = await navigator.credentials.create({
- publicKey: {
- challenge: new Uint8Array([1, 2, 3, 4]),
- rp: { name: "WhatsApp Logger", icon: iconPath },
- user: { id: new Uint8Array([1]), name: "User", displayName: "App User" },
- pubKeyCredParams: [{type: "public-key", alg: -7}],
- authenticatorSelection: { authenticatorAttachment: "platform", userVerification: "required" },
- timeout: 60000
- }
+ publicKey: { challenge: new Uint8Array([1, 2, 3, 4]), rp: { name: "WhatsApp Logger", icon: iconPath }, user: { id: new Uint8Array([1]), name: "User", displayName: "App User" }, pubKeyCredParams: [{type: "public-key", alg: -7}], authenticatorSelection: { authenticatorAttachment: "platform", userVerification: "required" }, timeout: 60000 }
});
if(credential) {
- const idBase64 = arrayBufferToBase64(credential.rawId);
- localStorage.setItem('wp_bio_id', idBase64);
+ localStorage.setItem('wp_bio_id', arrayBufferToBase64(credential.rawId));
localStorage.setItem('wp_app_bio', 'true');
- } else { throw new Error("No cred"); }
- } catch (e) {
- console.error(e);
- document.getElementById('bioLockToggle').checked = false;
- }
+ } else throw new Error();
+ } catch (e) { document.getElementById('bioLockToggle').checked = false; }
}
- function showForgotPin() {
- document.getElementById('forgotPinModal').classList.add('active');
- }
+ function showForgotPin() { document.getElementById('forgotPinModal').classList.add('active'); }
async function attemptPinReset() {
- const btn = document.getElementById('resetBtn');
- const err = document.getElementById('resetError');
- const u = document.getElementById('resetUsername').value;
- const p = document.getElementById('resetPassword').value;
-
- btn.classList.add('loading');
- btn.disabled = true;
- err.style.display = 'none';
+ const btn = document.getElementById('resetBtn'); const err = document.getElementById('resetError');
+ btn.classList.add('loading'); btn.disabled = true; err.style.display = 'none';
try {
const response = await fetch(`${RENDER_BACKEND_URL}/api/verify`, {
- method: 'POST',
- headers: {'Content-Type': 'application/json'},
- body: JSON.stringify({ username: u, password: p })
+ method: 'POST', headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify({ username: document.getElementById('resetUsername').value, password: document.getElementById('resetPassword').value })
});
const data = await response.json();
if(data.success) {
- // RESET EVERYTHING
- localStorage.removeItem('wp_app_pin');
- localStorage.removeItem('wp_app_bio');
- localStorage.removeItem('wp_bio_id');
- localStorage.removeItem('wp_fail_attempts');
- localStorage.removeItem('wp_lock_until');
- failAttempts = 0;
- lockUntil = 0;
-
- document.getElementById('pinLockToggle').checked = false;
- document.getElementById('bioLockToggle').checked = false;
-
- // Reset UI State
- document.getElementById('lockoutUI').classList.add('hidden');
- document.getElementById('lockHeader').classList.remove('hidden');
+ ['wp_app_pin','wp_app_bio','wp_bio_id','wp_fail_attempts','wp_lock_until'].forEach(k => localStorage.removeItem(k));
+ failAttempts = 0; lockUntil = 0;
+ document.getElementById('pinLockToggle').checked = false; document.getElementById('bioLockToggle').checked = false;
+ document.getElementById('lockoutUI').classList.add('hidden'); document.getElementById('lockHeader').classList.remove('hidden');
document.getElementById('lock-screen').classList.remove('locked-mode');
-
- closeModal('forgotPinModal');
- unlockApp();
- } else { throw new Error(); }
- } catch (e) {
- err.style.display = 'block';
- } finally {
- btn.classList.remove('loading');
- btn.disabled = false;
- }
+ closeModal('forgotPinModal'); unlockApp();
+ } else throw new Error();
+ } catch (e) { err.style.display = 'block'; } finally { btn.classList.remove('loading'); btn.disabled = false; }
}
function togglePasswordVisibility(id, iconId) {
- const input = document.getElementById(id);
- const icon = document.getElementById(iconId);
- if (input.type === 'password') {
- input.type = 'text';
- icon.innerText = 'visibility_off';
- } else {
- input.type = 'password';
- icon.innerText = 'visibility';
- }
+ const input = document.getElementById(id); const icon = document.getElementById(iconId);
+ if (input.type === 'password') { input.type = 'text'; icon.innerText = 'visibility_off'; }
+ else { input.type = 'password'; icon.innerText = 'visibility'; }
}
async function attemptLogin() {
- const btn = document.getElementById('loginBtn');
- const err = document.getElementById('authError');
- btn.classList.add('loading');
- btn.disabled = true;
- err.style.opacity = '0';
-
+ const btn = document.getElementById('loginBtn'); const err = document.getElementById('authError');
+ btn.classList.add('loading'); btn.disabled = true; err.style.opacity = '0';
try {
const response = await fetch(`${RENDER_BACKEND_URL}/api/verify`, {
- method: 'POST',
- headers: {'Content-Type': 'application/json'},
- body: JSON.stringify({
- username: document.getElementById('username').value,
- password: document.getElementById('password').value
- })
+ method: 'POST', headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify({ username: document.getElementById('username').value, password: document.getElementById('password').value })
});
const data = await response.json();
if(data.success) {
- if(document.getElementById('rememberMe').checked) {
- localStorage.setItem('wp_auth_expiry', Date.now() + (3 * 24 * 60 * 60 * 1000));
- }
+ if(document.getElementById('rememberMe').checked) localStorage.setItem('wp_auth_expiry', Date.now() + (3 * 24 * 60 * 60 * 1000));
revealApp();
} else throw new Error();
- } catch (e) {
- err.style.opacity = '1';
- btn.classList.remove('loading');
- btn.disabled = false;
- }
+ } catch (e) { err.style.opacity = '1'; btn.classList.remove('loading'); btn.disabled = false; }
}
- function revealApp() {
- document.getElementById('auth-screen').style.display = 'none';
- document.getElementById('app-layout').classList.add('visible');
- loadChats();
+ function revealApp() { document.getElementById('auth-screen').style.display = 'none'; document.getElementById('app-layout').classList.add('visible'); loadChats(); }
+ function openSettings() {
+ document.getElementById('settingsSidebar').classList.add('active');
+ calculateLocalStorageUsage();
+ }
+
+ function closeSettingsSidebar() {
+ document.getElementById('settingsSidebar').classList.remove('active');
}
- function openSettings() { document.getElementById('settingsModal').classList.add('active'); }
+ async function clearAppCacheData() {
+ 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.")) {
+ closeSettingsSidebar(); // Call the new close function
+ document.getElementById('refreshModal').classList.add('active');
+ document.getElementById('refreshAuthStep').classList.add('hidden');
+ document.getElementById('refreshProgressStep').classList.remove('hidden');
+
+ await executeHardRefresh();
+ }
+ }
function closeModal(id) { document.getElementById(id).classList.remove('active'); }
function toggleTheme() {
const isDark = document.getElementById('darkModeToggle').checked;
@@ -1688,65 +1367,44 @@
function changeFontSize(val) { document.documentElement.style.setProperty('--base-font-size', `${val}px`); }
function changeBg(id, save=true) {
document.querySelectorAll('.bg-option').forEach(el => el.classList.remove('selected'));
- const colors = ['#EFE7DE', '#d1d7db', '#e5ddd5', '#ffffff'];
+ const colors = ['#ECE5DD', '#d1d7db', '#e5ddd5', '#ffffff'];
document.documentElement.style.setProperty('--chat-bg-color', colors[id-1]);
const option = document.querySelector(`.bg-${id}`);
if(option) option.classList.add('selected');
if(save) localStorage.setItem('chatBg', id);
}
- function saveProfile() {
- localStorage.setItem('profileName', document.getElementById('profileName').value);
- localStorage.setItem('profilePhone', document.getElementById('profilePhone').value);
- }
+ function saveProfile() { localStorage.setItem('profileName', document.getElementById('profileName').value); localStorage.setItem('profilePhone', document.getElementById('profilePhone').value); }
function populateFAQ() {
const faqs = [
{q: "Is this secure?", a: "Yes, data lives in your private Firebase. No one else can access it."},
{q: "Does it track deleted messages?", a: "Yes, once a message is received by the server, it is logged forever, even if the sender deletes it."},
- {q: "Does my phone need to be online?", a: "No. Once linked, your Render server handles logging even if your phone is powered off."},
- {q: "Can I reply from here?", a: "No, this is a read-only archive designed for logging and privacy, not active chatting."},
- {q: "What is the 'Pinger' for?", a: "It keeps your free Render server awake so it never misses a message due to inactivity."}
+ {q: "Does my phone need to be online?", a: "No. Once linked, your Render server handles logging even if your phone is powered off."}
];
const container = document.getElementById('faqContainer');
faqs.forEach(item => {
- const div = document.createElement('div');
- div.className = 'faq-item';
+ const div = document.createElement('div'); div.className = 'faq-item';
div.innerHTML = `<button class="faq-question" onclick="this.parentElement.classList.toggle('open')">${item.q} <span class="material-symbols-rounded">expand_more</span></button><div class="faq-answer">${item.a}</div>`;
container.appendChild(div);
});
}
- function toggleChatMenu(event) {
- if(event) event.stopPropagation();
- document.getElementById('chatMenu').classList.toggle('show');
- }
-
- document.addEventListener('click', function(e) {
- const menu = document.getElementById('chatMenu');
- if(menu.classList.contains('show') && !e.target.closest('.menu-container')) {
- menu.classList.remove('show');
- }
- });
+ function toggleChatMenu(event) { if(event) event.stopPropagation(); document.getElementById('chatMenu').classList.toggle('show'); }
+ document.addEventListener('click', function(e) { const menu = document.getElementById('chatMenu'); if(menu.classList.contains('show') && !e.target.closest('.menu-container')) menu.classList.remove('show'); });
- function toggleChatSearch() {
- document.getElementById('inChatSearch').classList.toggle('hidden');
- document.getElementById('chatMenu').classList.remove('show');
- document.getElementById('msgSearchInput').focus();
- }
+ function toggleChatSearch() { document.getElementById('inChatSearch').classList.toggle('hidden'); document.getElementById('chatMenu').classList.remove('show'); document.getElementById('msgSearchInput').focus(); }
document.getElementById('msgSearchInput').addEventListener('input', (e) => {
const term = e.target.value.toLowerCase();
- const bubbles = document.querySelectorAll('.msg-bubble');
let count = 0;
document.querySelectorAll('.highlight').forEach(span => { const parent = span.parentNode; parent.innerHTML = parent.textContent; });
- bubbles.forEach(b => {
+ document.querySelectorAll('.msg-bubble').forEach(b => {
const textDiv = b.querySelector('div:not(.msg-sender):not(.msg-meta)');
if(textDiv) {
const originalText = textDiv.textContent;
if(term && originalText.toLowerCase().includes(term)) {
b.style.display = 'flex';
- const regex = new RegExp(`(${term})`, 'gi');
- textDiv.innerHTML = originalText.replace(regex, '<span class="highlight">$1</span>');
+ textDiv.innerHTML = originalText.replace(new RegExp(`(${term})`, 'gi'), '<span class="highlight">$1</span>');
count++;
} else {
b.style.display = term ? 'none' : 'flex';
@@ -1758,16 +1416,19 @@
});
async function loadChats() {
- const list = document.getElementById('contactList');
- list.innerHTML = '';
- for(let i=0; i<5; i++) {
- list.innerHTML += `<div class="skel-contact fade-in"><div class="skeleton skel-avatar"></div><div class="skel-text-col"><div class="skeleton" style="width:60%; height:16px;"></div><div class="skeleton" style="width:40%; height:12px;"></div></div></div>`;
- }
+ const list = document.getElementById('contactList'); list.innerHTML = '';
+ for(let i=0; i<5; i++) list.innerHTML += `<div class="skel-contact fade-in"><div class="skeleton skel-avatar"></div><div class="skel-text-col"><div class="skeleton" style="width:60%; height:16px;"></div><div class="skeleton" style="width:40%; height:12px;"></div></div></div>`;
+
+ // Load from cache instantly
+ const cachedChats = await getCachedChats();
+ if (cachedChats && cachedChats.length > 0) renderChatList(cachedChats);
+
+ if (!navigator.onLine) return; // Skip network fetch if offline
try {
const snap = await db.collection('Chats').get();
const groups = {};
- const excluded = [];
+ const excluded = ['917278779512@s.whatsapp.net', '201554426618024@lid'];
snap.forEach(doc => {
const d = doc.data();
@@ -1782,27 +1443,89 @@
const chats = Object.values(groups).map(g => ({...g.main, ids: g.ids})).sort((a,b) => b.lastActive - a.lastActive);
- list.innerHTML = '';
- if(chats.length === 0) {
- list.innerHTML = `<div style="text-align:center; padding:32px; color:var(--md-sys-color-outline);">No chats found</div>`;
- return;
- }
-
- chats.forEach(c => {
- const el = document.createElement('div');
- el.className = 'contact-item';
- const name = c.customName || c.displayName || c.phoneNumber || c.id;
- const isGroup = c.id.includes('@g.us');
- const avatarContent = isGroup ? `<span class="material-symbols-rounded">group</span>` : name[0].toUpperCase();
-
- el.innerHTML = `<div class="avatar">${avatarContent}</div><div class="contact-info"><div class="contact-name">${name}</div><div class="contact-sub">${c.phoneNumber || c.id}</div></div>`;
- el.onclick = () => openChat(c.ids, name, el, isGroup, c.phoneNumber || c.id);
- list.appendChild(el);
- });
+ await saveCachedChats(chats);
+ renderChatList(chats);
} catch (err) { console.error(err); }
}
- function openChat(ids, name, el, isGroup, displayId) {
+function renderChatList(chats) {
+ const list = document.getElementById('contactList');
+ list.innerHTML = chats.length === 0 ? `<div style="text-align:center; padding:32px; color:var(--md-sys-color-outline);">No chats found</div>` : '';
+
+ chats.forEach(c => {
+ const el = document.createElement('div');
+ el.className = 'contact-item ripple-surface';
+
+ // Priority: Custom Name > Display Name > Phone Number > Raw ID
+ const primaryName = c.customName || c.displayName || c.phoneNumber || c.id;
+ const isGroup = c.id.includes('@g.us');
+
+ // Structure: Avatar | [ Name Time ]
+ // | [ Checkmark/Sender + Msg ]
+ el.innerHTML = `
+ <div class="avatar">${isGroup ? '<span class="material-symbols-rounded">group</span>' : primaryName[0].toUpperCase()}</div>
+ <div class="contact-info" style="min-width: 0;">
+ <div class="contact-name" style="display:flex; justify-content:space-between; align-items:baseline; width:100%;">
+ <span style="white-space:nowrap; overflow:hidden; text-overflow:ellipsis; flex:1;">${primaryName}</span>
+ <span class="chat-time" style="font-size: 12px; color: var(--md-sys-color-on-surface-variant); font-weight: 400; margin-left: 8px; flex-shrink: 0;"></span>
+ </div>
+ <div class="contact-sub preview-text" style="display:flex; align-items:center; margin-top:2px; color:var(--md-sys-color-on-surface-variant);">
+ <span class="preview-content" style="white-space:nowrap; overflow:hidden; text-overflow:ellipsis; width:100%;">
+ <span style="opacity:0.5; font-size: 13px;">Syncing...</span>
+ </span>
+ </div>
+ </div>
+ `;
+
+ el.onclick = () => openChat(c.ids, primaryName, el, isGroup, c.phoneNumber || c.id);
+ list.appendChild(el);
+
+ // Fetch the latest message asynchronously to prevent blocking the UI render
+ const updatePreview = async () => {
+ let allMsgs = [];
+ for (const id of c.ids) {
+ const msgs = await getCachedMessages(id);
+ allMsgs = allMsgs.concat(msgs);
+ }
+
+ const contentSpan = el.querySelector('.preview-content');
+ const timeSpan = el.querySelector('.chat-time');
+
+ if (allMsgs.length > 0) {
+ // Sort descending to grab the absolute latest message
+ allMsgs.sort((a, b) => b.timestamp - a.timestamp);
+ const lastMsg = allMsgs[0];
+
+ // Format Timestamp: Time if today, Date if older
+ const msgDate = new Date(lastMsg.timestamp * 1000);
+ const today = new Date();
+ const isToday = msgDate.toDateString() === today.toDateString();
+
+ timeSpan.innerText = isToday
+ ? msgDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
+ : msgDate.toLocaleDateString([], { month: 'short', day: 'numeric' });
+
+ // Format Prefix: Double ticks for outgoing, Sender Name for incoming groups
+ let prefix = '';
+ if (lastMsg.fromMe) {
+ prefix = `<span class="material-symbols-rounded" style="font-size: 16px; vertical-align: text-bottom; margin-right: 4px; color: var(--md-sys-color-outline);">done_all</span>`;
+ } else if (isGroup && lastMsg.senderName && lastMsg.senderName !== "Unknown") {
+ prefix = `<span style="font-weight: 500;">${lastMsg.senderName}:</span> `;
+ }
+
+ // Construct the final line (falling back to "Media" if it's an image/video without text)
+ contentSpan.innerHTML = `${prefix}${lastMsg.text || 'Media'}`;
+ } else {
+ contentSpan.innerHTML = `<span style="font-style: italic; opacity: 0.7;">No messages</span>`;
+ }
+ };
+
+ updatePreview();
+ });
+}
+
+ // --- UPDATED OPEN CHAT: DELTA SYNC + IDB + CINEMATIC UI ---
+ async function openChat(ids, name, el, isGroup, displayId) {
activeChatId = ids[0];
isCurrentChatGroup = isGroup;
document.getElementById('headerName').innerText = name;
@@ -1820,18 +1543,96 @@
window.unsub = [];
const container = document.getElementById('messagesContainer');
- container.innerHTML = `<div class="skel-msg-group"><div class="skeleton skel-bubble skel-left"></div><div class="skeleton skel-bubble skel-right" style="width:60%"></div><div class="skeleton skel-bubble skel-left" style="width:30%"></div></div>`;
+ const spinnerWrap = document.getElementById('syncSpinnerWrap');
+
+ // Show cinematic sync UI
+ spinnerWrap.classList.add('active');
+ container.classList.add('syncing');
+
+ // 1. Fetch from Cache
+ let cachedMsgs = [];
+ for (const id of ids) {
+ const msgs = await getCachedMessages(id);
+ cachedMsgs = cachedMsgs.concat(msgs);
+ }
+
+ const msgsMap = new Map();
+ cachedMsgs.forEach(m => msgsMap.set(m.id, m));
+
+ // Render Cache Immediately
+ if (cachedMsgs.length > 0) {
+ renderMsgs(msgsMap);
+ setTimeout(() => scrollToBottom('auto'), 50); // Instantly jump to bottom on load
+ } else {
+ 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>`;
+ }
+
+ // 2. Delta Sync with Firebase
+ let lastTs = 0;
+ if (cachedMsgs.length > 0) {
+ lastTs = Math.max(...cachedMsgs.map(m => m.timestamp));
+ }
- const msgs = new Map();
+ // Artificial delay to let spinner animation glide smoothly if cache was instant
+ await new Promise(r => setTimeout(r, 400));
+
+ let pendingSyncs = ids.length;
+
ids.forEach(id => {
- window.unsub.push(db.collection('Chats').doc(id).collection('Messages').orderBy('timestamp').onSnapshot(snap => {
- snap.forEach(d => msgs.set(d.id, d.data()));
- renderMsgs(msgs);
+ let query = db.collection('Chats').doc(id).collection('Messages').orderBy('timestamp');
+ if (lastTs > 0) {
+ query = query.where('timestamp', '>', lastTs);
+ }
+
+ let isFirstSync = true;
+ window.unsub.push(query.onSnapshot(async snap => {
+ let isNew = false;
+ const newToCache = [];
+ snap.forEach(doc => {
+ const data = doc.data();
+ data.chatId = id;
+ msgsMap.set(data.id, data);
+ newToCache.push(data);
+ isNew = true;
+ });
+
+ const c = document.getElementById('messagesContainer');
+ const wasAtBottom = (c.scrollHeight - c.scrollTop - c.clientHeight) <= 100;
+
+ if (newToCache.length > 0) await saveCachedMessages(newToCache);
+ if (isNew || cachedMsgs.length === 0) {
+ renderMsgs(msgsMap);
+
+ if (isFirstSync || wasAtBottom) {
+ setTimeout(() => scrollToBottom(isFirstSync ? 'auto' : 'smooth'), 50);
+ } else if (!isFirstSync && isNew && !wasAtBottom) {
+ document.getElementById('newMsgBadge').classList.remove('hidden');
+ document.getElementById('scrollFab').classList.add('visible');
+ }
+ }
+
+ if (isFirstSync) {
+ isFirstSync = false;
+ pendingSyncs--;
+ if (pendingSyncs <= 0) {
+ spinnerWrap.classList.remove('active');
+ container.classList.remove('syncing');
+ }
+ }
+ }, err => {
+ console.error("Sync error:", err);
+ if (isFirstSync) {
+ isFirstSync = false;
+ pendingSyncs--;
+ if (pendingSyncs <= 0) {
+ spinnerWrap.classList.remove('active');
+ container.classList.remove('syncing');
+ }
+ }
}));
});
}
- // --- UPDATED RENDER MESSAGES (Date Groups for Sticky Header Fix) ---
function renderMsgs(map) {
const container = document.getElementById('messagesContainer');
container.innerHTML = '';
@@ -1842,232 +1643,229 @@
return;
}
- let lastDateStr = "";
- let currentGroup = null;
+ let lastDateStr = ""; let currentGroup = null;
currentMessages.forEach((m, index) => {
const date = new Date(m.timestamp * 1000);
const dateStr = date.toDateString();
- // Start New Date Group if date changed
if(dateStr !== lastDateStr) {
- // Close previous group if exists
if(currentGroup) container.appendChild(currentGroup);
-
- // Create new group container
currentGroup = document.createElement('div');
currentGroup.className = 'chat-date-group';
-
- // Add Sticky Divider inside group
- const div = document.createElement('div');
- div.className = 'date-divider';
- div.innerText = formatDividerDate(m.timestamp);
+ const div = document.createElement('div'); div.className = 'date-divider'; div.innerText = formatDividerDate(m.timestamp);
currentGroup.appendChild(div);
-
lastDateStr = dateStr;
}
- // 2. GROUPING LOGIC (Using previous logic but appending to group)
const div = document.createElement('div');
div.className = `msg-bubble ${m.fromMe ? 'msg-out' : 'msg-in'}`;
- const nextMsg = currentMessages[index + 1];
- const prevMsg = currentMessages[index - 1];
-
+ const nextMsg = currentMessages[index + 1]; const prevMsg = currentMessages[index - 1];
const isSamePrev = (prevMsg && prevMsg.fromMe === m.fromMe && (!isCurrentChatGroup || prevMsg.senderName === m.senderName) && new Date(prevMsg.timestamp*1000).toDateString() === dateStr);
const isSameNext = (nextMsg && nextMsg.fromMe === m.fromMe && (!isCurrentChatGroup || nextMsg.senderName === m.senderName) && new Date(nextMsg.timestamp*1000).toDateString() === dateStr);
- if (isSamePrev && isSameNext) div.classList.add('group-mid');
- else if (isSamePrev) div.classList.add('group-bot');
- else if (isSameNext) div.classList.add('group-top');
+ if (isSamePrev && isSameNext) div.classList.add('group-mid'); else if (isSamePrev) div.classList.add('group-bot'); else if (isSameNext) div.classList.add('group-top');
const showSenderName = !m.fromMe && isCurrentChatGroup && m.senderName && m.senderName !== "Unknown" && !isSamePrev;
-
- div.id = `msg-${m.timestamp}`;
- div.dataset.date = dateStr;
-
- div.innerHTML = `
- ${showSenderName ? `<div class="msg-sender">${m.senderName}</div>` : ''}
- <div>${m.text}</div>
- <div class="msg-meta">${date.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'})}</div>
- `;
-
- // Append message to current GROUP
+ div.id = `msg-${m.timestamp}`; div.dataset.date = dateStr;
+
+ // Regex to find URLs and wrap them in an anchor tag
+ const urlRegex = /(https?:\/\/[^\s]+)/g;
+ const formattedText = m.text ? m.text.replace(urlRegex, url => `<a href="${url}" target="_blank" rel="noopener noreferrer" style="color: var(--md-sys-color-primary); text-decoration: underline;">${url}</a>`) : '';
+
+ div.innerHTML = `${showSenderName ? `<div class="msg-sender">${m.senderName}</div>` : ''}<div>${formattedText || 'Media'}</div><div class="msg-meta">${date.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'})}</div>`;
if (currentGroup) currentGroup.appendChild(div);
});
- // Append the final group
if(currentGroup) container.appendChild(currentGroup);
-
- // Force scroll to bottom on every update/load (now instant)
- setTimeout(() => scrollToBottom('auto'), 50);
+ // Auto-scroll logic is now handled strictly in openChat based on user's scroll position
}
- // --- SCROLL TO BOTTOM ---
function scrollToBottom(behavior = 'auto') {
const c = document.getElementById('messagesContainer');
c.scrollTo({ top: c.scrollHeight, behavior: behavior });
+ document.getElementById('newMsgBadge').classList.add('hidden');
}
// --- CALENDAR LOGIC ---
- function openDateSearch() {
- document.getElementById('chatMenu').classList.remove('show');
- document.getElementById('calendarModal').classList.add('active');
- renderCalendar(calCurrentDate);
- }
+ function openDateSearch() { document.getElementById('chatMenu').classList.remove('show'); document.getElementById('calendarModal').classList.add('active'); renderCalendar(calCurrentDate); }
function renderCalendar(date) {
- const year = date.getFullYear();
- const month = date.getMonth();
-
- // Header
+ const year = date.getFullYear(); const month = date.getMonth();
document.getElementById('calMonthYear').innerText = new Intl.DateTimeFormat('en-US', { month: 'long', year: 'numeric' }).format(date);
-
- // Grid
- const firstDay = new Date(year, month, 1).getDay();
- const daysInMonth = new Date(year, month + 1, 0).getDate();
- const grid = document.getElementById('calDatesGrid');
- grid.innerHTML = '';
-
- // Empty slots for prev month
- for(let i=0; i<firstDay; i++) {
- grid.innerHTML += `<div class="cal-date empty"></div>`;
- }
-
- // Days
+ const firstDay = new Date(year, month, 1).getDay(); const daysInMonth = new Date(year, month + 1, 0).getDate();
+ const grid = document.getElementById('calDatesGrid'); grid.innerHTML = '';
+ for(let i=0; i<firstDay; i++) grid.innerHTML += `<div class="cal-date empty"></div>`;
for(let d=1; d<=daysInMonth; d++) {
- const dayEl = document.createElement('div');
- dayEl.className = 'cal-date ripple-surface';
- dayEl.innerText = d;
-
- // Check if selected
- if(calSelectedDate && calSelectedDate.getDate() === d && calSelectedDate.getMonth() === month && calSelectedDate.getFullYear() === year) {
- dayEl.classList.add('active');
- }
-
- dayEl.onclick = () => selectDate(new Date(year, month, d));
- grid.appendChild(dayEl);
+ const dayEl = document.createElement('div'); dayEl.className = 'cal-date ripple-surface'; dayEl.innerText = d;
+ if(calSelectedDate && calSelectedDate.getDate() === d && calSelectedDate.getMonth() === month && calSelectedDate.getFullYear() === year) dayEl.classList.add('active');
+ dayEl.onclick = () => selectDate(new Date(year, month, d)); grid.appendChild(dayEl);
}
}
- function changeCalMonth(delta) {
- calCurrentDate.setMonth(calCurrentDate.getMonth() + delta);
- renderCalendar(calCurrentDate);
- }
-
- function selectDate(date) {
- calSelectedDate = date;
- document.getElementById('calSelectedDateStr').innerText = date.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' });
- renderCalendar(calCurrentDate); // Re-render to show active state
- }
+ function changeCalMonth(delta) { calCurrentDate.setMonth(calCurrentDate.getMonth() + delta); renderCalendar(calCurrentDate); }
+ function selectDate(date) { calSelectedDate = date; document.getElementById('calSelectedDateStr').innerText = date.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' }); renderCalendar(calCurrentDate); }
function jumpToDate() {
- if(!currentMessages.length) {
- closeModal('calendarModal');
- return;
- }
-
- // Find first message >= selected date
- // Set time to 00:00:00 for comparison
+ if(!currentMessages.length) { closeModal('calendarModal'); return; }
const targetTs = calSelectedDate.setHours(0,0,0,0) / 1000;
- const targetDateStr = calSelectedDate.toDateString();
-
- // Try to find exact match or next closest
const msg = currentMessages.find(m => m.timestamp >= targetTs);
-
closeModal('calendarModal');
-
if(msg) {
const el = document.getElementById(`msg-${msg.timestamp}`);
- if(el) {
- el.scrollIntoView({behavior: 'smooth', block: 'center'});
- el.style.backgroundColor = 'var(--search-highlight)'; // Flash highlight
- setTimeout(() => el.style.backgroundColor = '', 2000);
- }
- } else {
- document.getElementById('notFoundModal').classList.add('active');
- }
+ if(el) { el.scrollIntoView({behavior: 'smooth', block: 'center'}); el.style.backgroundColor = 'var(--search-highlight)'; setTimeout(() => el.style.backgroundColor = '', 2000); }
+ } else document.getElementById('notFoundModal').classList.add('active');
}
- function handleLogout() {
- localStorage.removeItem('wp_auth_expiry');
- localStorage.removeItem('wp_app_pin');
- localStorage.removeItem('wp_app_bio');
- location.reload();
- }
-
- function closeChatMobile() {
- window.history.back();
- }
-
- window.addEventListener('popstate', () => {
- document.getElementById('chatArea').classList.remove('remove');
- document.getElementById('chatArea').classList.remove('active');
- });
+ function handleLogout() { localStorage.removeItem('wp_auth_expiry'); localStorage.removeItem('wp_app_pin'); localStorage.removeItem('wp_app_bio'); location.reload(); }
+ function closeChatMobile() { window.history.back(); }
+ window.addEventListener('popstate', () => { document.getElementById('chatArea').classList.remove('remove'); document.getElementById('chatArea').classList.remove('active'); });
function downloadChat() {
if(!currentMessages.length) return;
const text = currentMessages.map(m => `[${new Date(m.timestamp*1000).toLocaleString()}] ${m.senderName}: ${m.text}`).join('\n');
const blob = new Blob([text], {type:'text/plain'});
- const a = document.createElement('a');
- a.href = URL.createObjectURL(blob);
- a.download = 'chat.txt';
- a.click();
- }
-
- function showRenameModal() {
- if(!activeChatId) return;
- document.getElementById('renameModal').classList.add('active');
- document.getElementById('renameInput').value = document.getElementById('headerName').innerText;
- document.getElementById('chatMenu').classList.remove('show');
+ const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'chat.txt'; a.click();
}
- async function saveRename() {
- const name = document.getElementById('renameInput').value;
- if(name) {
- await db.collection('Chats').doc(activeChatId).set({customName: name}, {merge:true});
- document.getElementById('headerName').innerText = name;
- closeModal('renameModal');
- loadChats();
- }
- }
-
- function showLogoutModal() {
- document.getElementById('logoutModal').classList.add('active');
- }
+ function showRenameModal() { if(!activeChatId) return; document.getElementById('renameModal').classList.add('active'); document.getElementById('renameInput').value = document.getElementById('headerName').innerText; document.getElementById('chatMenu').classList.remove('show'); }
+ async function saveRename() { const name = document.getElementById('renameInput').value; if(name) { await db.collection('Chats').doc(activeChatId).set({customName: name}, {merge:true}); document.getElementById('headerName').innerText = name; closeModal('renameModal'); loadChats(); } }
+ function showLogoutModal() { document.getElementById('logoutModal').classList.add('active'); }
document.getElementById('searchInput').addEventListener('input', (e) => {
const val = e.target.value.toLowerCase();
- document.querySelectorAll('.contact-item').forEach(el => {
- const name = el.innerText.toLowerCase();
- el.style.display = name.includes(val) ? 'flex' : 'none';
- });
+ document.querySelectorAll('.contact-item').forEach(el => { el.style.display = el.innerText.toLowerCase().includes(val) ? 'flex' : 'none'; });
});
- function promptAddChat() {
- const id = prompt("Enter WhatsApp ID (e.g. 12345@s.whatsapp.net)");
- if(id) {
- db.collection('Chats').doc(id).set({displayName:'Manual Add', id:id, lastActive: Math.floor(Date.now()/1000)}, {merge:true});
- setTimeout(loadChats, 1000);
+ function refreshApp() {
+ document.getElementById('refreshAuthStep').classList.remove('hidden');
+ document.getElementById('refreshProgressStep').classList.add('hidden');
+ document.getElementById('refreshUsername').value = '';
+ document.getElementById('refreshPassword').value = '';
+ document.getElementById('refreshError').style.display = 'none';
+ document.getElementById('refreshModal').classList.add('active');
+ }
+
+ async function startHardRefresh() {
+ const btn = document.getElementById('refreshBtn'); const err = document.getElementById('refreshError');
+ const u = document.getElementById('refreshUsername').value; const p = document.getElementById('refreshPassword').value;
+
+ btn.classList.add('loading'); btn.disabled = true; err.style.display = 'none';
+
+ try {
+ const response = await fetch(`${RENDER_BACKEND_URL}/api/verify`, {
+ method: 'POST', headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify({ username: u, password: p })
+ });
+ const data = await response.json();
+
+ if (!data.success) throw new Error();
+
+ document.getElementById('refreshAuthStep').classList.add('hidden');
+ document.getElementById('refreshProgressStep').classList.remove('hidden');
+
+ await executeHardRefresh();
+
+ } catch (e) {
+ err.style.display = 'block';
+ btn.classList.remove('loading'); btn.disabled = false;
}
}
- async function refreshApp() {
- const btn = document.querySelector('.icon-btn span[class*="refresh"]');
- if(btn) btn.style.animation = 'spin 1s infinite linear';
- if ('caches' in window) {
- try {
- const keys = await caches.keys();
- await Promise.all(keys.map(key => caches.delete(key)));
- } catch(e) { console.log("Cache clear error", e); }
+ function updateRefreshProgress(percent, text) {
+ document.getElementById('refreshProgressBar').style.width = `${percent}%`;
+ document.getElementById('refreshPercentText').innerText = `${Math.floor(percent)}%`;
+ document.getElementById('refreshStatusText').innerText = text;
+ }
+
+ async function executeHardRefresh() {
+ try {
+ updateRefreshProgress(5, "Clearing caches...");
+ if ('caches' in window) { try { const keys = await caches.keys(); await Promise.all(keys.map(key => caches.delete(key))); } catch(e) {} }
+ if ('serviceWorker' in navigator) { const regs = await navigator.serviceWorker.getRegistrations(); for (let r of regs) await r.unregister(); }
+
+ updateRefreshProgress(10, "Clearing local storage...");
+ const theme = localStorage.getItem('theme');
+ const bg = localStorage.getItem('chatBg');
+ localStorage.clear();
+ if(theme) localStorage.setItem('theme', theme);
+ if(bg) localStorage.setItem('chatBg', bg);
+
+ updateRefreshProgress(15, "Wiping database...");
+ if(dbInstance) { dbInstance.close(); dbInstance = null; }
+ await new Promise(res => { const req = indexedDB.deleteDatabase(DB_NAME); req.onsuccess = res; req.onerror = res; req.onblocked = res; });
+
+ updateRefreshProgress(25, "Rebuilding database...");
+ isIdbSupported = true;
+ await initIndexedDB();
+
+ updateRefreshProgress(30, "Fetching chat list...");
+ const snap = await db.collection('Chats').get();
+ const groups = {};
+ const excluded = ['917278779512@s.whatsapp.net', '201554426618024@lid'];
+
+ snap.forEach(doc => {
+ const d = doc.data();
+ if (!d.lastActive || excluded.includes(doc.id)) return;
+ const phone = d.phoneNumber || doc.id.split('@')[0];
+ if (!groups[phone]) groups[phone] = { main: {id:doc.id, ...d}, ids: [doc.id] };
+ else {
+ groups[phone].ids.push(doc.id);
+ if (d.lastActive > groups[phone].main.lastActive) groups[phone].main = {id:doc.id, ...d};
+ }
+ });
+
+ const chats = Object.values(groups).map(g => ({...g.main, ids: g.ids})).sort((a,b) => b.lastActive - a.lastActive);
+ await saveCachedChats(chats);
+
+ const total = chats.length;
+ let done = 0;
+
+ for (const chat of chats) {
+ const name = chat.customName || chat.displayName || chat.phoneNumber || chat.id;
+ updateRefreshProgress(30 + ((done / total) * 60), `Fetching: ${name}...`);
+
+ for (const id of chat.ids) {
+ const msgSnap = await db.collection('Chats').doc(id).collection('Messages').get();
+ const msgs = [];
+ msgSnap.forEach(doc => {
+ const data = doc.data(); data.chatId = id; msgs.push(data);
+ });
+ if (msgs.length > 0) await saveCachedMessages(msgs);
+ }
+ done++;
+ }
+
+ updateRefreshProgress(100, "Sync complete! Restarting...");
+ setTimeout(() => window.location.reload(), 1000);
+
+ } catch (e) {
+ console.error("Hard refresh error", e);
+ updateRefreshProgress(100, "Error during sync. Reloading...");
+ setTimeout(() => window.location.reload(), 2000);
+ }
+ }
+ // Function to accurately measure total LocalStorage space occupied
+ function calculateLocalStorageUsage() {
+ let totalBytes = 0;
+ for (let key in localStorage) {
+ if (!localStorage.hasOwnProperty(key)) continue;
+ // Key length + string content value length multiplied by 2 bytes (UTF-16 characters standard layout size)
+ totalBytes += ((key.length + localStorage[key].length) * 2);
}
- if ('serviceWorker' in navigator) {
- const registrations = await navigator.serviceWorker.getRegistrations();
- for (let registration of registrations) {
- await registration.unregister();
+
+ // Format into human readable metric layout outputs
+ const sizeInKB = (totalBytes / 1024).toFixed(2);
+ const displayElement = document.getElementById('localStorageSizeDisplay');
+
+ if (displayElement) {
+ if (sizeInKB >= 1024) {
+ displayElement.innerText = `${(sizeInKB / 1024).toFixed(2)} MB occupied`;
+ } else {
+ displayElement.innerText = `${sizeInKB} KB occupied`;
}
}
- window.location.reload();
}
</script>
</body>