commit 6edcb981a5ecace890c02c294523729604fd4b0e
parent 7d4f0e6910ccb09d309ac2cf1469ec68fc4caf15
Author: AMIT DUTTA <amitdutta4255@gmail.com>
Date: Thu, 19 Feb 2026 15:58:07 +0530
v3.10.4
Diffstat:
| M | index.html | | | 1213 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------- |
1 file changed, 1111 insertions(+), 102 deletions(-)
diff --git a/index.html b/index.html
@@ -6,7 +6,7 @@
<title>WhatsApp Logger</title>
<!-- PWA Manifest -->
- <link rel="manifest" href="manifest.json">
+ <link rel="manifest" href="manifest-wpChat.json">
<meta name="theme-color" content="#006C4C">
<!-- SVG Favicon -->
@@ -46,6 +46,8 @@
--md-sys-color-outline-variant: #BFC9C2;
--md-sys-color-error: #BA1A1A;
+ --md-sys-color-error-container: #FFDAD6;
+ --md-sys-color-on-error-container: #410002;
/* WhatsApp Specifics */
--chat-bubble-me: #E2FFEC;
@@ -85,6 +87,8 @@
--md-sys-color-outline-variant: #424945;
--md-sys-color-error: #FFB4AB;
+ --md-sys-color-error-container: #93000A;
+ --md-sys-color-on-error-container: #FFDAD6;
--chat-bubble-me: #005C4B;
--chat-bubble-them: #202C33;
@@ -116,47 +120,173 @@
@keyframes scaleIn { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } }
@keyframes slideUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
.animate-enter { animation: scaleIn 0.3s cubic-bezier(0.2, 0.0, 0, 1.0); }
-
- /* --- AUTH SCREEN --- */
+ .shake { animation: shake 0.4s cubic-bezier(.36,.07,.19,.97) both; }
+ @keyframes shake { 10%, 90% { transform: translate3d(-1px, 0, 0); } 20%, 80% { transform: translate3d(2px, 0, 0); } 30%, 50%, 70% { transform: translate3d(-4px, 0, 0); } 40%, 60% { transform: translate3d(4px, 0, 0); } }
+
+ /* --- M3 STAGGERED ENTRANCE --- */
+ @keyframes fadeInUp {
+ from { opacity: 0; transform: translateY(30px); }
+ to { opacity: 1; transform: translateY(0); }
+ }
+ .anim-up { opacity: 0; animation: fadeInUp 0.6s cubic-bezier(0.2, 0, 0, 1) forwards; }
+ .delay-1 { animation-delay: 0.1s; }
+ .delay-2 { animation-delay: 0.2s; }
+ .delay-3 { animation-delay: 0.3s; }
+ .delay-4 { animation-delay: 0.4s; }
+ .delay-5 { animation-delay: 0.5s; }
+
+ /* --- AUTH SCREEN (UPDATED to M3 Classic) --- */
#auth-screen {
position: fixed; inset: 0; z-index: 999;
- background: radial-gradient(circle at 50% 10%, var(--md-sys-color-surface-container) 0%, var(--md-sys-color-surface) 60%);
+ /* Static blue-green background */
+ background: radial-gradient(circle at 0% 0%, #C1E8FB 0%, transparent 50%),
+ radial-gradient(circle at 100% 100%, #89F8C7 0%, transparent 50%),
+ #FBFDF9;
display: flex; flex-direction: column; align-items: center; justify-content: center;
- padding: 24px; height: 100dvh; padding-bottom: 15vh;
- /* Initially handled by inline style to prevent flash */
+ padding: 24px; height: 100dvh;
transition: opacity 0.3s;
}
- .auth-card { width: 100%; max-width: 400px; text-align: center; }
- .brand-icon { width: 96px; height: 96px; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 24px; }
- .brand-icon img { width: 100%; height: 100%; filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.2)); }
+ /* Dark Mode Override for Auth Background */
+ [data-theme="dark"] #auth-screen {
+ background: radial-gradient(circle at 0% 0%, #001F29 0%, transparent 50%),
+ radial-gradient(circle at 100% 100%, #002114 0%, transparent 50%),
+ #111412;
+ }
+
+ /* Updated M3 Auth Card (Classic Style) */
+ .auth-card {
+ background: rgba(251, 253, 249, 0.7); /* Translucent */
+ backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
+ border-radius: 28px;
+ padding: 32px 24px;
+ width: 90%; max-width: 380px;
+ box-shadow: var(--elevation-2);
+ display: flex; flex-direction: column; gap: 20px;
+ }
+
+ [data-theme="dark"] .auth-card {
+ background: rgba(30, 33, 31, 0.7);
+ }
+
+ .brand-icon {
+ width: 64px; height: 64px;
+ display: flex; align-items: center; justify-content: center;
+ margin: 0 auto 8px;
+ }
+ .brand-icon img { width: 100%; height: 100%; object-fit: contain; }
+
+ .auth-header { text-align: center; }
+ .auth-header h1 {
+ font-family: 'Google Sans', sans-serif;
+ font-size: 28px;
+ font-weight: 400;
+ color: var(--md-sys-color-on-surface);
+ margin: 8px 0;
+ }
+ .auth-header p {
+ font-family: 'Roboto', sans-serif;
+ font-size: 14px;
+ color: var(--md-sys-color-on-surface-variant);
+ margin: 0;
+ }
+
+ /* --- LOCK SCREEN (PIN/BIO) --- */
+ #lock-screen {
+ position: fixed; inset: 0; z-index: 2000;
+ background: var(--md-sys-color-surface);
+ display: flex; flex-direction: column; align-items: center; justify-content: space-between;
+ padding: 48px 24px; opacity: 0; visibility: hidden; transition: opacity 0.3s;
+ }
+ #lock-screen.active { opacity: 1; visibility: visible; }
+
+ .lock-header { text-align: center; margin-top: 4vh; }
+ .lock-icon { width: 64px; height: 64px; margin-bottom: 16px; color: var(--md-sys-color-primary); }
+ .lock-title { font-family: 'Google Sans'; font-size: 24px; margin-bottom: 8px; }
+ .lock-sub { color: var(--md-sys-color-on-surface-variant); font-size: 14px; }
+
+ /* PIN Dots */
+ .pin-dots { display: flex; gap: 16px; margin: 32px 0; justify-content: center; }
+ .pin-dot {
+ width: 14px; height: 14px; border-radius: 50%;
+ border: 1px solid var(--md-sys-color-outline);
+ transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+ }
+ .pin-dot.filled { background: var(--md-sys-color-on-surface); border-color: var(--md-sys-color-on-surface); transform: scale(1.1); }
+ .pin-dot.error { border-color: var(--md-sys-color-error); background: var(--md-sys-color-error); }
+
+ /* Native Keypad */
+ .keypad { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px 32px; margin-bottom: 2vh; }
+ .key-btn {
+ width: 72px; height: 72px; border-radius: 50%; border: none; background: transparent;
+ font-size: 28px; font-weight: 400; color: var(--md-sys-color-on-surface);
+ display: flex; align-items: center; justify-content: center; cursor: pointer;
+ transition: background 0.2s; font-family: 'Google Sans', sans-serif;
+ }
+ .key-btn:active { background: var(--md-sys-color-surface-container-high); }
+ .key-backspace { font-size: 24px; color: var(--md-sys-color-on-surface-variant); }
+
+ .forgot-pin-btn {
+ margin-top: 16px; color: var(--md-sys-color-primary); background: none; border: none;
+ font-weight: 500; font-size: 14px; padding: 8px 16px; border-radius: 20px; cursor: pointer;
+ }
- .auth-card h1 { font-family: 'Google Sans', sans-serif; }
+ /* --- LOCKOUT UI --- */
+ .lockout-container {
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
+ text-align: center; width: 100%; animation: fadeIn 0.3s; flex: 1;
+ }
+ .lockout-icon-bg {
+ width: 80px; height: 80px; border-radius: 50%;
+ background: var(--md-sys-color-error-container);
+ color: var(--md-sys-color-on-error-container);
+ display: flex; align-items: center; justify-content: center;
+ margin-bottom: 24px;
+ }
+ .lockout-icon { font-size: 40px; }
+ .lockout-title { font-family: 'Google Sans'; font-size: 28px; margin-bottom: 8px; color: var(--md-sys-color-on-surface); }
+ .lockout-msg { color: var(--md-sys-color-on-surface-variant); margin-bottom: 32px; font-size: 16px; }
+ .lockout-timer {
+ font-family: 'Google Sans'; font-size: 56px; font-weight: 400;
+ color: var(--md-sys-color-error); font-feature-settings: "tnum";
+ margin-bottom: 8px;
+ }
+ .lockout-sub { margin-top: 16px; color: var(--md-sys-color-on-surface-variant); font-size: 14px; }
+
+ /* Utility to hide PIN/BIO when locked */
+ .locked-mode #pinUI,
+ .locked-mode #bioUI,
+ .locked-mode .forgot-pin-btn {
+ display: none !important;
+ }
/* --- UI ELEMENTS --- */
.m3-field {
- position: relative; margin-bottom: 16px;
- background: var(--md-sys-color-surface-container-high);
+ 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: 0.3s;
+ 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);
- background: var(--md-sys-color-surface-container);
}
.m3-input {
- width: 100%; padding: 24px 16px 8px 16px;
+ 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;
+ 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: 6px; font-size: 12px; color: var(--md-sys-color-primary);
+ top: 8px; font-size: 12px; color: var(--md-sys-color-primary);
}
.eye-toggle {
position: absolute; right: 12px; top: 50%; transform: translateY(-50%);
@@ -166,19 +296,21 @@
.eye-toggle:hover { background: rgba(0,0,0,0.05); color: var(--md-sys-color-on-surface); }
.m3-btn {
- width: 100%; height: 56px; border-radius: 28px;
+ width: 100%; height: 48px; border-radius: 24px;
border: none; outline: none; cursor: pointer;
- font-family: 'Google Sans', sans-serif; font-weight: 500; font-size: 16px;
+ 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: var(--elevation-1);
+ box-shadow: none;
+ }
+ .m3-btn.filled:hover {
+ box-shadow: var(--elevation-1);
}
- .m3-btn.filled:hover { box-shadow: var(--elevation-2); opacity: 0.9; }
- .m3-btn:disabled { opacity: 0.7; cursor: not-allowed; }
+ .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 {
@@ -248,7 +380,7 @@
}
.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 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 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");
+ 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);
@@ -258,7 +390,14 @@
.chat-content {
flex: 1; overflow-y: auto; padding: 16px; z-index: 1;
display: flex; flex-direction: column; gap: 4px;
+ scroll-behavior: auto; /* Removed smooth scrolling for initial load */
+ scroll-behavior: smooth;
}
+ /* Override specifically to remove default smooth behavior that causes load animation */
+ .chat-content {
+ scroll-behavior: auto !important;
+ }
+
.msg-bubble {
max-width: 80%; padding: 8px 12px; position: relative;
font-size: var(--base-font-size); line-height: 1.4;
@@ -274,9 +413,34 @@
align-self: flex-end; background: var(--chat-bubble-me);
border-radius: 16px 0 16px 16px; color: var(--md-sys-color-on-surface);
}
+
+ /* Message Grouping - WhatsApp Style */
+ .msg-out.group-top { border-bottom-right-radius: 4px; margin-bottom: 2px; }
+ .msg-out.group-mid { border-top-right-radius: 4px; border-bottom-right-radius: 4px; margin-bottom: 2px; }
+ .msg-out.group-bot { border-top-right-radius: 4px; margin-top: 0; }
+
+ .msg-in.group-top { border-bottom-left-radius: 4px; margin-bottom: 2px; }
+ .msg-in.group-mid { border-top-left-radius: 4px; border-bottom-left-radius: 4px; margin-bottom: 2px; }
+ .msg-in.group-bot { border-top-left-radius: 4px; margin-top: 0; }
+
.msg-sender { font-size: 12px; font-weight: 700; color: var(--md-sys-color-primary); margin-bottom: 2px; }
.msg-meta { font-size: 10px; opacity: 0.7; align-self: flex-end; margin-top: 2px; }
+ /* DATE DIVIDERS */
+ .chat-date-group {
+ display: flex; flex-direction: column; gap: 4px;
+ position: relative; /* Scope for sticky */
+ }
+ .date-divider {
+ align-self: center; margin: 16px 0;
+ background: var(--md-sys-color-surface-container-high);
+ color: var(--md-sys-color-on-surface-variant);
+ font-size: 12px; font-weight: 500;
+ padding: 6px 12px; border-radius: 12px;
+ box-shadow: 0 1px 2px rgba(0,0,0,0.1);
+ position: sticky; top: 8px; z-index: 2;
+ }
+
.highlight { background-color: var(--search-highlight); color: black; border-radius: 2px; padding: 0 2px; }
/* Search In Chat */
@@ -308,6 +472,47 @@
font-family: inherit; font-size: 14px; cursor: pointer; text-align: left;
}
.menu-item:hover { background: rgba(128,128,128,0.1); }
+
+ /* Scroll To Bottom FAB */
+ .scroll-fab {
+ position: fixed; bottom: 24px; right: 24px;
+ width: 40px; height: 40px; border-radius: 50%;
+ background: var(--md-sys-color-surface-container-high);
+ color: var(--md-sys-color-on-surface-variant);
+ border: 1px solid var(--md-sys-color-outline-variant);
+ box-shadow: 0 2px 6px rgba(0,0,0,0.15);
+ display: flex; align-items: center; justify-content: center;
+ cursor: pointer; z-index: 20; opacity: 0; transform: translateY(20px); pointer-events: none;
+ transition: 0.3s;
+ }
+ .scroll-fab.visible { opacity: 1; transform: translateY(0); pointer-events: auto; }
+
+ /* Calendar Modal */
+ .calendar-card {
+ background: var(--md-sys-color-surface);
+ padding: 0; border-radius: 28px; width: 320px;
+ overflow: hidden; display: flex; flex-direction: column;
+ }
+ .cal-header {
+ background: var(--md-sys-color-surface-container-high);
+ padding: 16px 24px;
+ }
+ .cal-header h3 { font-size: 14px; color: var(--md-sys-color-on-surface-variant); font-weight: 500; }
+ .cal-header h2 { font-size: 32px; color: var(--md-sys-color-on-surface); font-weight: 400; margin-top: 4px; }
+
+ .cal-grid { padding: 16px 12px 0; }
+ .cal-nav { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; padding: 0 12px; }
+ .cal-days { display: grid; grid-template-columns: repeat(7, 1fr); text-align: center; margin-bottom: 8px; }
+ .cal-day-label { font-size: 12px; color: var(--md-sys-color-on-surface-variant); width: 36px; }
+
+ .cal-dates { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; margin-bottom: 16px; }
+ .cal-date {
+ width: 36px; height: 36px; display: flex; align-items: center; justify-content: center;
+ border-radius: 50%; font-size: 14px; cursor: pointer; color: var(--md-sys-color-on-surface);
+ }
+ .cal-date:hover { background: var(--md-sys-color-surface-container-high); }
+ .cal-date.active { background: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary); }
+ .cal-date.empty { pointer-events: none; }
/* Settings Modal */
.modal-overlay {
@@ -390,7 +595,23 @@
}
@media (max-width: 768px) {
- .sidebar { width: 100%; flex: 1; border-right: none; }
+ .sidebar {
+ width: 100%;
+ flex: 1;
+ border-right: none;
+ /* Mobile-only background gradient */
+ background: radial-gradient(circle at 0% 0%, #c1e8fb49 0%, transparent 50%),
+ radial-gradient(circle at 100% 100%, #89f8c86b 0%, transparent 50%),
+ #fbfdf96c;
+ }
+
+ /* Dark mode override for sidebar on mobile */
+ [data-theme="dark"] .sidebar {
+ background: radial-gradient(circle at 0% 0%, #001F29 0%, transparent 50%),
+ radial-gradient(circle at 100% 100%, #002114 0%, transparent 50%),
+ #111412;
+ }
+
.chat-area { position: fixed; inset: 0; z-index: 20; transform: translateX(100%); transition: transform 0.3s; background-color: var(--chat-bg-color); }
.chat-area.active { transform: translateX(0); }
.back-btn { display: flex !important; margin-right: 8px; }
@@ -469,28 +690,101 @@
<!-- AUTH SCREEN -->
<div id="auth-screen">
<div class="auth-card">
- <div class="brand-icon animate-enter">
- <img src="icon.svg" alt="App Logo" style="width: 100%; height: 100%;">
+ <div style="text-align: center;" class="anim-up delay-1">
+ <div class="brand-icon">
+ <img src="icon.svg" alt="App Logo">
+ </div>
+ <h1 style="font-family:'Google Sans'; font-size:28px; margin:8px 0; font-weight:400;">Welcome Back</h1>
+ <p style="color:var(--md-sys-color-on-surface-variant); font-size:14px;">Sign in to your chat log</p>
+ </div>
+
+ <div class="m3-field anim-up delay-2">
+ <input type="text" id="username" class="m3-input" placeholder=" ">
+ <label class="m3-label">Username</label>
</div>
- <h1 style="font-family:'Google Sans'; margin-bottom: 8px;" class="animate-enter">Welcome Back</h1>
- <p style="color:var(--md-sys-color-on-surface-variant); margin-bottom: 32px;" class="animate-enter">Enter your credentials</p>
- <div class="m3-field animate-enter"><input type="text" id="username" class="m3-input" placeholder=" "><label class="m3-label">Username</label></div>
- <div class="m3-field animate-enter">
+
+ <div class="m3-field anim-up delay-3">
<input type="password" id="password" class="m3-input" placeholder=" ">
<label class="m3-label">Password</label>
- <button class="eye-toggle" onclick="togglePasswordVisibility()">
+ <button class="eye-toggle" onclick="togglePasswordVisibility('password', 'passIcon')">
<span class="material-symbols-rounded" id="passIcon">visibility</span>
</button>
</div>
- <div style="display:flex; align-items:center; gap:12px; margin: 16px 0 24px;" class="animate-enter">
- <input type="checkbox" id="rememberMe" style="width:18px; height:18px;"><label for="rememberMe">Remember me</label>
+
+ <div class="anim-up delay-4" style="display:flex; align-items:center; padding-left:4px;">
+ <div style="display:flex; align-items:center; gap:8px;">
+ <input type="checkbox" id="rememberMe" style="width:18px; height:18px; accent-color: var(--md-sys-color-primary);">
+ <label for="rememberMe" style="font-size: 14px; color: var(--md-sys-color-on-surface);">Remember me</label>
+ </div>
</div>
- <button class="m3-btn filled animate-enter" id="loginBtn" onclick="attemptLogin()">
- <span class="btn-text">Access Dashboard</span>
- <span class="material-symbols-rounded btn-icon" style="font-size:20px;">arrow_forward</span>
- <div class="spinner"></div>
- </button>
- <p id="authError" style="color: var(--md-sys-color-error); margin-top: 16px; opacity: 0;">Invalid Credentials</p>
+
+ <div class="anim-up delay-5" style="display: flex; flex-direction: column; gap: 12px; margin-top: 8px;">
+ <button class="m3-btn filled" id="loginBtn" onclick="attemptLogin()">
+ <span class="btn-text">Sign In</span>
+ <span class="material-symbols-rounded btn-icon" style="font-size:20px;">arrow_forward</span>
+ <div class="spinner"></div>
+ </button>
+ <p id="authError" style="color: var(--md-sys-color-error); font-size: 14px; opacity: 0; text-align: center;">Invalid Credentials</p>
+ </div>
+ </div>
+ </div>
+
+ <!-- LOCK SCREEN (PIN / BIO / LOCKOUT) -->
+ <div id="lock-screen">
+ <!-- LOCKOUT UI -->
+ <div id="lockoutUI" class="hidden lockout-container">
+ <div class="lockout-icon-bg">
+ <span class="material-symbols-rounded lockout-icon" id="lockoutIcon">lock_clock</span>
+ </div>
+ <h2 class="lockout-title" id="lockoutTitle">Locked</h2>
+ <p class="lockout-msg" id="lockoutMsg">Too many attempts.</p>
+ <div class="lockout-timer" id="lockoutTimer">00:30</div>
+ <p class="lockout-sub" id="lockoutSub">Try again later</p>
+ <div id="permanentLockActions" class="hidden" style="margin-top:24px; display:flex; gap:12px;">
+ <button class="m3-btn filled" onclick="showForgotPin()" style="background:var(--md-sys-color-error); width:auto; padding:0 32px;">Forgot PIN</button>
+ </div>
+ </div>
+
+ <!-- HEADER (Hidden during lockout) -->
+ <div class="lock-header" id="lockHeader">
+ <span class="material-symbols-rounded lock-icon" id="lockIcon">lock</span>
+ <div class="lock-title" id="lockTitle">Enter PIN</div>
+ <div class="lock-sub" id="lockSub">Protected by App Lock</div>
+ </div>
+
+ <!-- PIN UI -->
+ <div id="pinUI">
+ <div class="pin-dots" id="pinDots">
+ <div class="pin-dot"></div><div class="pin-dot"></div><div class="pin-dot"></div>
+ <div class="pin-dot"></div><div class="pin-dot"></div><div class="pin-dot"></div>
+ </div>
+
+ <div class="keypad">
+ <button class="key-btn ripple-surface" onclick="pressKey(1)">1</button>
+ <button class="key-btn ripple-surface" onclick="pressKey(2)">2</button>
+ <button class="key-btn ripple-surface" onclick="pressKey(3)">3</button>
+ <button class="key-btn ripple-surface" onclick="pressKey(4)">4</button>
+ <button class="key-btn ripple-surface" onclick="pressKey(5)">5</button>
+ <button class="key-btn ripple-surface" onclick="pressKey(6)">6</button>
+ <button class="key-btn ripple-surface" onclick="pressKey(7)">7</button>
+ <button class="key-btn ripple-surface" onclick="pressKey(8)">8</button>
+ <button class="key-btn ripple-surface" onclick="pressKey(9)">9</button>
+ <button class="key-btn ripple-surface" style="visibility:hidden"> </button>
+ <button class="key-btn ripple-surface" onclick="pressKey(0)">0</button>
+ <button class="key-btn ripple-surface" onclick="pressKey(-1)"><span class="material-symbols-rounded key-backspace">backspace</span></button>
+ </div>
+ </div>
+
+ <!-- BIO ONLY UI -->
+ <div id="bioUI" class="hidden" style="flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center;">
+ <div class="ripple-surface" onclick="triggerBiometricAuth()" style="width:96px; height:96px; border-radius:50%; background:var(--md-sys-color-secondary-container); color:var(--md-sys-color-on-secondary-container); display:flex; align-items:center; justify-content:center; cursor:pointer;">
+ <span class="material-symbols-rounded" style="font-size:48px;">fingerprint</span>
+ </div>
+ <p style="margin-top:16px; color:var(--md-sys-color-on-surface-variant);">Tap to Unlock</p>
+ </div>
+
+ <div style="text-align: center;">
+ <button class="forgot-pin-btn" onclick="showForgotPin()">Forgot PIN / Use Password</button>
</div>
</div>
@@ -498,11 +792,20 @@
<div id="app-layout">
<aside class="sidebar" id="sidebar">
<div class="sidebar-header">
- <div style="display:flex; align-items:center; gap:12px;">
- <h2 style="font-family:'Google Sans'; font-size: 20px;">Chat log</h2>
+ <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.4
+ </span>
</div>
+
<div style="display:flex; gap: 8px;">
- <button class="icon-btn ripple-surface" onclick="promptAddChat()"><span class="material-symbols-rounded">add_circle</span></button>
+ <button class="icon-btn ripple-surface" onclick="refreshApp()"><span class="material-symbols-rounded">refresh</span></button>
<button class="icon-btn ripple-surface" onclick="openSettings()" title="Settings"><span class="material-symbols-rounded">settings</span></button>
</div>
</div>
@@ -528,6 +831,7 @@
<button class="icon-btn ripple-surface" onclick="toggleChatMenu(event)"><span class="material-symbols-rounded">more_vert</span></button>
<div class="dropdown-menu" id="chatMenu">
<button class="menu-item" onclick="toggleChatSearch()"><span class="material-symbols-rounded">search</span>Search in Chat</button>
+ <button class="menu-item" onclick="openDateSearch()"><span class="material-symbols-rounded">calendar_month</span>Search by Date</button>
<button class="menu-item" onclick="showRenameModal()"><span class="material-symbols-rounded">edit</span>Rename</button>
<button class="menu-item" onclick="downloadChat()"><span class="material-symbols-rounded">download</span>Download</button>
</div>
@@ -548,6 +852,11 @@
<p>Select a conversation</p>
</div>
</div>
+
+ <!-- Scroll to Bottom FAB - Now triggers SMOOTH scroll manually -->
+ <button class="scroll-fab" id="scrollFab" onclick="scrollToBottom('smooth')">
+ <span class="material-symbols-rounded">arrow_downward</span>
+ </button>
</main>
</div>
@@ -568,6 +877,19 @@
</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">
@@ -641,7 +963,37 @@
</div>
</div>
</div>
+
+ <!-- FORGOT PIN MODAL -->
+ <div id="forgotPinModal" class="modal-overlay">
+ <div class="modal-card utility">
+ <h3 class="modal-title">Reset App Lock</h3>
+ <p style="margin-top:8px; color:var(--md-sys-color-on-surface-variant); font-size:14px;">Enter your server credentials to reset local security.</p>
+
+ <div class="m3-field" style="margin-top:16px;">
+ <input type="text" id="resetUsername" class="m3-input" placeholder=" "><label class="m3-label">Username</label>
+ </div>
+ <div class="m3-field">
+ <input type="password" id="resetPassword" class="m3-input" placeholder=" ">
+ <label class="m3-label">Password</label>
+ <button class="eye-toggle" onclick="togglePasswordVisibility('resetPassword', 'resetPassIcon')">
+ <span class="material-symbols-rounded" id="resetPassIcon">visibility</span>
+ </button>
+ </div>
+
+ <p id="resetError" style="color:var(--md-sys-color-error); font-size:12px; margin-bottom:12px; display:none;">Invalid credentials</p>
+
+ <div class="modal-actions">
+ <button class="m3-btn text" onclick="closeModal('forgotPinModal')">Cancel</button>
+ <button class="m3-btn filled" id="resetBtn" onclick="attemptPinReset()">
+ <span class="btn-text">Reset</span>
+ <div class="spinner"></div>
+ </button>
+ </div>
+ </div>
+ </div>
+ <!-- RENAME MODAL -->
<div id="renameModal" class="modal-overlay">
<div class="modal-card utility">
<h3 class="modal-title">Rename Chat</h3>
@@ -652,6 +1004,75 @@
</div>
</div>
</div>
+
+ <!-- NOT FOUND MODAL -->
+ <div id="notFoundModal" class="modal-overlay">
+ <div class="modal-card utility">
+ <h3 class="modal-title">No Messages Found</h3>
+ <p style="margin:16px 0; color:var(--md-sys-color-on-surface-variant);">There are no messages from this date onwards.</p>
+ <div class="modal-actions">
+ <button class="m3-btn filled" onclick="closeModal('notFoundModal')">OK</button>
+ </div>
+ </div>
+ </div>
+
+ <!-- PIN SETUP MODAL -->
+ <div id="pinSetupModal" class="modal-overlay">
+ <div class="modal-card utility" style="text-align:center;">
+ <h3 class="modal-title" id="pinSetupTitle">Set 6-Digit PIN</h3>
+ <div class="pin-dots" id="setupDots" style="margin: 24px 0;">
+ <div class="pin-dot"></div><div class="pin-dot"></div><div class="pin-dot"></div>
+ <div class="pin-dot"></div><div class="pin-dot"></div><div class="pin-dot"></div>
+ </div>
+ <p style="color:var(--md-sys-color-on-surface-variant); font-size:12px;" id="pinSetupMsg">Use the number pad below</p>
+
+ <div class="keypad" style="transform: scale(0.9); margin-bottom:0;">
+ <button class="key-btn ripple-surface" onclick="pressSetupKey(1)">1</button>
+ <button class="key-btn ripple-surface" onclick="pressSetupKey(2)">2</button>
+ <button class="key-btn ripple-surface" onclick="pressSetupKey(3)">3</button>
+ <button class="key-btn ripple-surface" onclick="pressSetupKey(4)">4</button>
+ <button class="key-btn ripple-surface" onclick="pressSetupKey(5)">5</button>
+ <button class="key-btn ripple-surface" onclick="pressSetupKey(6)">6</button>
+ <button class="key-btn ripple-surface" onclick="pressSetupKey(7)">7</button>
+ <button class="key-btn ripple-surface" onclick="pressSetupKey(8)">8</button>
+ <button class="key-btn ripple-surface" onclick="pressSetupKey(9)">9</button>
+ <button class="key-btn ripple-surface" style="visibility:hidden"> </button>
+ <button class="key-btn ripple-surface" onclick="pressSetupKey(0)">0</button>
+ <button class="key-btn ripple-surface" onclick="pressSetupKey(-1)"><span class="material-symbols-rounded key-backspace">backspace</span></button>
+ </div>
+ <div class="modal-actions" style="justify-content:center; margin-top:8px;">
+ <button class="m3-btn text" onclick="cancelPinSetup()">Cancel</button>
+ </div>
+ </div>
+ </div>
+
+ <!-- CALENDAR MODAL -->
+ <div id="calendarModal" class="modal-overlay">
+ <div class="calendar-card animate-enter">
+ <div class="cal-header">
+ <h3>Select date</h3>
+ <h2 id="calSelectedDateStr">Tue, Oct 24</h2>
+ </div>
+ <div class="cal-grid">
+ <div class="cal-nav">
+ <span id="calMonthYear" style="font-weight:500">October 2023</span>
+ <div style="display:flex; gap:12px;">
+ <button class="icon-btn" onclick="changeCalMonth(-1)"><span class="material-symbols-rounded">chevron_left</span></button>
+ <button class="icon-btn" onclick="changeCalMonth(1)"><span class="material-symbols-rounded">chevron_right</span></button>
+ </div>
+ </div>
+ <div class="cal-days">
+ <span class="cal-day-label">S</span><span class="cal-day-label">M</span><span class="cal-day-label">T</span>
+ <span class="cal-day-label">W</span><span class="cal-day-label">T</span><span class="cal-day-label">F</span><span class="cal-day-label">S</span>
+ </div>
+ <div class="cal-dates" id="calDatesGrid"></div>
+ </div>
+ <div class="modal-actions" style="padding: 16px 24px;">
+ <button class="m3-btn text" onclick="closeModal('calendarModal')">Cancel</button>
+ <button class="m3-btn filled" onclick="jumpToDate()" style="width:auto; padding:0 24px; height:40px;">OK</button>
+ </div>
+ </div>
+ </div>
<!-- PWA INSTALL SHEET (Mobile Only) -->
<div id="installSheet" class="install-sheet">
@@ -663,7 +1084,7 @@
<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>
- <p style="color:var(--md-sys-color-on-surface-variant); margin-bottom:24px;">Install WhatsApp Logger App for a better experience, and full screen view.</p>
+ <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>
@@ -687,20 +1108,58 @@
let activeChatId = null;
let deferredPrompt;
let currentMessages = [];
+
+ // --- SECURITY VARS ---
+ let enteredPin = "";
+ let setupPin = "";
+ let setupStep = 0;
+ let isLocked = false;
+ // Lockout Variables
+ let failAttempts = parseInt(localStorage.getItem('wp_fail_attempts') || '0');
+ let lockUntil = parseInt(localStorage.getItem('wp_lock_until') || '0');
+ let lockoutTimerInterval;
+
+ // --- CALENDAR VARS ---
+ let calCurrentDate = new Date();
+ let calSelectedDate = new Date();
+
+ // --- HELPERS ---
+ const arrayBufferToBase64 = (buffer) => {
+ let binary = '';
+ const bytes = new Uint8Array(buffer);
+ const len = bytes.byteLength;
+ for (let i = 0; i < len; i++) binary += String.fromCharCode(bytes[i]);
+ return window.btoa(binary);
+ };
+ const base64ToArrayBuffer = (base64) => {
+ const binary_string = window.atob(base64);
+ const len = binary_string.length;
+ const bytes = new Uint8Array(len);
+ for (let i = 0; i < len; i++) bytes[i] = binary_string.charCodeAt(i);
+ return bytes.buffer;
+ };
+
+ function formatDividerDate(ts) {
+ const date = new Date(ts * 1000);
+ const today = new Date();
+ const yesterday = new Date();
+ yesterday.setDate(yesterday.getDate() - 1);
+
+ if (date.toDateString() === today.toDateString()) return "Today";
+ if (date.toDateString() === yesterday.toDateString()) return "Yesterday";
+ return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
+ }
// --- PWA INSTALLATION ---
if ('serviceWorker' in navigator && (window.location.protocol === 'https:' || window.location.protocol === 'http:')) {
- navigator.serviceWorker.register('sw.js').catch(err => {
+ navigator.serviceWorker.register('sw-wpChat.js').catch(err => {
console.warn('SW Register Fail:', err);
});
}
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
- // Also show settings button for manual install
document.getElementById('installAppSection').classList.remove('hidden');
-
- // Show bottom sheet on mobile, simple banner on desktop if preferred
if (window.innerWidth <= 768) {
setTimeout(() => document.getElementById('installSheet').classList.add('visible'), 2000);
}
@@ -720,35 +1179,460 @@
document.getElementById('installSheet').classList.remove('visible');
}
- // --- AUTH & INIT ---
- // Immediate check to prevent flash
- if(localStorage.getItem('wp_auth_expiry') > Date.now()) {
- document.write('<style>#auth-screen{display:none} #app-layout{opacity:1; visibility:visible}</style>');
- window.addEventListener('DOMContentLoaded', loadChats);
- } else {
- document.write('<style>#auth-screen{opacity:1; visibility:visible}</style>');
+ // --- AUTH & BOOT SEQUENCE ---
+ function isMobileApp() {
+ const isStandalone = (window.matchMedia('(display-mode: standalone)').matches) || (window.navigator.standalone) || document.referrer.includes('android-app://');
+ const isMobile = window.innerWidth <= 768;
+ return isStandalone && isMobile;
}
window.onload = () => {
- // Load theme
if (localStorage.getItem('theme') === 'dark') {
document.body.setAttribute('data-theme', 'dark');
document.getElementById('darkModeToggle').checked = true;
}
- // Load Profile
document.getElementById('profileName').value = localStorage.getItem('profileName') || '';
document.getElementById('profilePhone').value = localStorage.getItem('profilePhone') || '';
-
- // Load BG
const bg = localStorage.getItem('chatBg') || 1;
changeBg(bg, false);
-
populateFAQ();
+
+ if(isMobileApp()) {
+ document.getElementById('mobileSecuritySection').classList.remove('hidden');
+
+ const hasPin = localStorage.getItem('wp_app_pin');
+ const useBio = localStorage.getItem('wp_app_bio') === 'true';
+ document.getElementById('pinLockToggle').checked = !!hasPin;
+ document.getElementById('bioLockToggle').checked = useBio;
+
+ const authValid = localStorage.getItem('wp_auth_expiry') > Date.now();
+
+ if (authValid && (hasPin || useBio)) {
+ document.getElementById('auth-screen').style.display = 'none';
+ initLockScreen(hasPin, useBio);
+ } else if (authValid) {
+ document.getElementById('auth-screen').style.display = 'none';
+ document.getElementById('app-layout').classList.add('visible');
+ loadChats();
+ } else {
+ document.getElementById('auth-screen').style.opacity = '1';
+ }
+ } else {
+ if(localStorage.getItem('wp_auth_expiry') > Date.now()) {
+ document.getElementById('auth-screen').style.display = 'none';
+ document.getElementById('app-layout').classList.add('visible');
+ loadChats();
+ } else {
+ document.getElementById('auth-screen').style.opacity = '1';
+ }
+ }
+
+ // Scroll Listener for FAB
+ document.getElementById('messagesContainer').addEventListener('scroll', function() {
+ const fab = document.getElementById('scrollFab');
+ const st = this.scrollTop;
+ const sh = this.scrollHeight;
+ const ch = this.clientHeight;
+
+ // Show if we are more than 500px from the bottom
+ if (sh - st - ch > 500) {
+ fab.classList.add('visible');
+ } else {
+ fab.classList.remove('visible');
+ }
+ });
+
+ // Fix for mobile keyboard covering inputs
+ const inputs = document.querySelectorAll('input[type="text"], input[type="password"]');
+ inputs.forEach(input => {
+ input.addEventListener('focus', () => {
+ if (window.innerWidth <= 768) {
+ setTimeout(() => {
+ input.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ }, 300);
+ }
+ });
+ });
};
- function togglePasswordVisibility() {
- const input = document.getElementById('password');
- const icon = document.getElementById('passIcon');
+ // --- LOCK SCREEN & LOCKOUT LOGIC ---
+ function initLockScreen(hasPin, useBio) {
+ const lockScreen = document.getElementById('lock-screen');
+ lockScreen.classList.add('active');
+ isLocked = true;
+ enteredPin = "";
+ updatePinDots(0);
+
+ // Check Lockout State First
+ if (checkLockoutState()) return;
+
+ if(hasPin) {
+ document.getElementById('pinUI').classList.remove('hidden');
+ document.getElementById('bioUI').classList.add('hidden');
+ document.getElementById('lockTitle').innerText = "Enter PIN";
+ document.getElementById('lockIcon').innerText = "lock";
+ } else if (useBio && !hasPin) {
+ document.getElementById('pinUI').classList.add('hidden');
+ document.getElementById('bioUI').classList.remove('hidden');
+ document.getElementById('lockTitle').innerText = "Scan Fingerprint";
+ document.getElementById('lockIcon').innerText = "fingerprint";
+ triggerBiometricAuth();
+ }
+ }
+
+ function checkLockoutState() {
+ const now = Date.now();
+
+ // Permanent Lock
+ if (failAttempts >= 5) {
+ renderPermanentLock();
+ return true;
+ }
+
+ // Temporary Timer Lock
+ if (lockUntil > now) {
+ startLockoutTimer();
+ return true;
+ }
+
+ // Reset if timer expired
+ if (lockUntil > 0 && lockUntil <= now) {
+ lockUntil = 0;
+ localStorage.setItem('wp_lock_until', '0');
+ // Don't reset failAttempts to 0 here to keep escalating.
+ // But we need to hide lockout UI
+ document.getElementById('lockoutUI').classList.add('hidden');
+ document.getElementById('lockHeader').classList.remove('hidden');
+ document.getElementById('lock-screen').classList.remove('locked-mode');
+ }
+
+ return false;
+ }
+
+ function handleFailedAttempt() {
+ failAttempts++;
+ localStorage.setItem('wp_fail_attempts', failAttempts);
+
+ if (failAttempts === 3 || failAttempts === 4) {
+ lockUntil = Date.now() + (30 * 1000); // 30 seconds
+ localStorage.setItem('wp_lock_until', lockUntil);
+ startLockoutTimer();
+ } else if (failAttempts >= 5) {
+ renderPermanentLock();
+ } else {
+ // Shake animation for attempt 1 and 2
+ document.getElementById('pinDots').classList.add('shake');
+ updatePinDots(6, true);
+ setTimeout(() => {
+ document.getElementById('pinDots').classList.remove('shake');
+ enteredPin = "";
+ updatePinDots(0);
+ }, 400);
+ }
+ }
+
+ function startLockoutTimer() {
+ const ui = document.getElementById('lockoutUI');
+ const screen = document.getElementById('lock-screen');
+ const timerDisplay = document.getElementById('lockoutTimer');
+
+ ui.classList.remove('hidden');
+ document.getElementById('lockHeader').classList.add('hidden');
+ screen.classList.add('locked-mode'); // Hides PIN/BIO inputs
+
+ document.getElementById('lockoutTitle').innerText = "Locked";
+ document.getElementById('lockoutMsg').innerText = `Too many attempts (${failAttempts})`;
+ document.getElementById('lockoutSub').innerText = "Try again later";
+ document.getElementById('permanentLockActions').classList.add('hidden');
+ document.getElementById('lockoutTimer').classList.remove('hidden');
+
+ if(lockoutTimerInterval) clearInterval(lockoutTimerInterval);
+
+ lockoutTimerInterval = setInterval(() => {
+ const left = Math.ceil((lockUntil - Date.now()) / 1000);
+ if (left <= 0) {
+ clearInterval(lockoutTimerInterval);
+ timerDisplay.innerText = "00:00";
+ // Reset View
+ initLockScreen(localStorage.getItem('wp_app_pin'), localStorage.getItem('wp_app_bio') === 'true');
+ document.getElementById('lockoutUI').classList.add('hidden');
+ document.getElementById('lockHeader').classList.remove('hidden');
+ screen.classList.remove('locked-mode');
+ } else {
+ const secs = left < 10 ? `0${left}` : left;
+ timerDisplay.innerText = `00:${secs}`;
+ }
+ }, 1000);
+ }
+
+ function renderPermanentLock() {
+ const ui = document.getElementById('lockoutUI');
+ const screen = document.getElementById('lock-screen');
+
+ ui.classList.remove('hidden');
+ document.getElementById('lockHeader').classList.add('hidden');
+ screen.classList.add('locked-mode');
+
+ document.getElementById('lockoutIcon').innerText = "gpp_bad";
+ document.getElementById('lockoutTitle').innerText = "Security Lockout";
+ document.getElementById('lockoutMsg').innerText = "Maximum attempts exceeded. App is locked.";
+ document.getElementById('lockoutSub').innerText = "Use Forgot Password to reset.";
+ document.getElementById('lockoutTimer').classList.add('hidden');
+ document.getElementById('permanentLockActions').classList.remove('hidden');
+ }
+
+ function pressKey(num) {
+ if(num === -1) {
+ enteredPin = enteredPin.slice(0, -1);
+ } else if (enteredPin.length < 6) {
+ enteredPin += num;
+ }
+ updatePinDots(enteredPin.length);
+
+ if(enteredPin.length === 6) {
+ verifyPin();
+ }
+ }
+
+ function updatePinDots(count, isError=false) {
+ const dots = document.querySelectorAll('#pinDots .pin-dot');
+ dots.forEach((d, i) => {
+ if(isError) {
+ d.classList.add('error');
+ setTimeout(() => d.classList.remove('error'), 400);
+ } else {
+ d.classList.remove('error');
+ if(i < count) d.classList.add('filled');
+ else d.classList.remove('filled');
+ }
+ });
+ }
+
+ function verifyPin() {
+ // Re-check lockout before verifying
+ if(checkLockoutState()) return;
+
+ const storedPin = localStorage.getItem('wp_app_pin');
+ if(storedPin && enteredPin === storedPin) {
+ // Success - Reset Fail Counters
+ failAttempts = 0;
+ localStorage.setItem('wp_fail_attempts', '0');
+ localStorage.setItem('wp_lock_until', '0');
+
+ const useBio = localStorage.getItem('wp_app_bio') === 'true';
+ if (useBio) {
+ document.getElementById('pinUI').classList.add('hidden');
+ document.getElementById('bioUI').classList.remove('hidden');
+ document.getElementById('lockTitle').innerText = "Verify Identity";
+ document.getElementById('lockIcon').innerText = "fingerprint";
+ triggerBiometricAuth();
+ } else {
+ unlockApp();
+ }
+ } else {
+ handleFailedAttempt();
+ }
+ }
+
+ async function triggerBiometricAuth() {
+ if(checkLockoutState()) return;
+
+ if(document.getElementById('bioUI').classList.contains('hidden') === false) {
+ document.getElementById('lockTitle').innerText = "Verify Identity";
+ }
+ try {
+ const storedId = localStorage.getItem('wp_bio_id');
+ const options = {
+ publicKey: {
+ challenge: new Uint8Array([1, 2, 3, 4]),
+ timeout: 60000,
+ userVerification: "required"
+ }
+ };
+ if (storedId) {
+ options.publicKey.allowCredentials = [{
+ type: 'public-key',
+ id: base64ToArrayBuffer(storedId),
+ transports: ['internal']
+ }];
+ }
+ const credential = await navigator.credentials.get(options);
+ if(credential) {
+ // Success Bio - Reset Counters
+ failAttempts = 0;
+ localStorage.setItem('wp_fail_attempts', '0');
+ localStorage.setItem('wp_lock_until', '0');
+ unlockApp();
+ }
+ } catch (e) {
+ console.warn("Bio failed", e);
+ // Note: We generally don't lock on Bio failures here as OS handles retries.
+ // But if user cancels and goes to PIN, PIN failures will trigger lock.
+ }
+ }
+
+ function unlockApp() {
+ isLocked = false;
+ document.getElementById('lock-screen').classList.remove('active');
+ document.getElementById('app-layout').classList.add('visible');
+ loadChats();
+ }
+
+ // --- SECURITY SETTINGS ---
+ function togglePinSetting() {
+ const chk = document.getElementById('pinLockToggle');
+ if(chk.checked) openPinSetup();
+ else localStorage.removeItem('wp_app_pin');
+ }
+
+ function toggleBioSetting() {
+ const chk = document.getElementById('bioLockToggle');
+ if(chk.checked) registerBiometric();
+ else {
+ localStorage.removeItem('wp_app_bio');
+ localStorage.removeItem('wp_bio_id');
+ }
+ }
+
+ function openPinSetup() {
+ document.getElementById('pinSetupModal').classList.add('active');
+ setupStep = 1;
+ setupPin = "";
+ enteredPin = "";
+ updateSetupDots(0);
+ document.getElementById('pinSetupTitle').innerText = "Set 6-Digit PIN";
+ document.getElementById('pinSetupMsg').innerText = "Enter a new PIN";
+ }
+
+ function updateSetupDots(count) {
+ const dots = document.querySelectorAll('#setupDots .pin-dot');
+ dots.forEach((d, i) => i < count ? d.classList.add('filled') : d.classList.remove('filled'));
+ }
+
+ function pressSetupKey(num) {
+ if(num === -1) {
+ enteredPin = enteredPin.slice(0, -1);
+ } else if (enteredPin.length < 6) {
+ enteredPin += num;
+ }
+ updateSetupDots(enteredPin.length);
+
+ if(enteredPin.length === 6) {
+ if(setupStep === 1) {
+ setupPin = enteredPin;
+ enteredPin = "";
+ setupStep = 2;
+ setTimeout(() => {
+ updateSetupDots(0);
+ document.getElementById('pinSetupTitle').innerText = "Confirm PIN";
+ document.getElementById('pinSetupMsg').innerText = "Re-enter to confirm";
+ }, 200);
+ } else if (setupStep === 2) {
+ if(enteredPin === setupPin) {
+ localStorage.setItem('wp_app_pin', setupPin);
+ closeModal('pinSetupModal');
+ enteredPin = "";
+ } else {
+ const dots = document.getElementById('setupDots');
+ dots.classList.add('shake');
+ setTimeout(() => {
+ dots.classList.remove('shake');
+ enteredPin = "";
+ setupPin = "";
+ setupStep = 1;
+ updateSetupDots(0);
+ document.getElementById('pinSetupTitle').innerText = "Mismatch. Try Again";
+ document.getElementById('pinSetupMsg').innerText = "Enter a new PIN";
+ }, 500);
+ }
+ }
+ }
+ }
+
+ function cancelPinSetup() {
+ document.getElementById('pinLockToggle').checked = false;
+ closeModal('pinSetupModal');
+ }
+
+ async function registerBiometric() {
+ try {
+ const iconPath = new URL('icon.svg', window.location.href).toString();
+ const credential = await navigator.credentials.create({
+ publicKey: {
+ challenge: new Uint8Array([1, 2, 3, 4]),
+ rp: { name: "WhatsApp Logger", icon: iconPath },
+ user: { id: new Uint8Array([1]), name: "User", displayName: "App User" },
+ pubKeyCredParams: [{type: "public-key", alg: -7}],
+ authenticatorSelection: { authenticatorAttachment: "platform", userVerification: "required" },
+ timeout: 60000
+ }
+ });
+
+ if(credential) {
+ const idBase64 = arrayBufferToBase64(credential.rawId);
+ localStorage.setItem('wp_bio_id', idBase64);
+ localStorage.setItem('wp_app_bio', 'true');
+ } else { throw new Error("No cred"); }
+ } catch (e) {
+ console.error(e);
+ document.getElementById('bioLockToggle').checked = false;
+ }
+ }
+
+ function showForgotPin() {
+ document.getElementById('forgotPinModal').classList.add('active');
+ }
+
+ async function attemptPinReset() {
+ const btn = document.getElementById('resetBtn');
+ const err = document.getElementById('resetError');
+ const u = document.getElementById('resetUsername').value;
+ const p = document.getElementById('resetPassword').value;
+
+ btn.classList.add('loading');
+ btn.disabled = true;
+ err.style.display = 'none';
+
+ try {
+ const response = await fetch(`${RENDER_BACKEND_URL}/api/verify`, {
+ method: 'POST',
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify({ username: u, password: p })
+ });
+ const data = await response.json();
+
+ if(data.success) {
+ // RESET EVERYTHING
+ localStorage.removeItem('wp_app_pin');
+ localStorage.removeItem('wp_app_bio');
+ localStorage.removeItem('wp_bio_id');
+ localStorage.removeItem('wp_fail_attempts');
+ localStorage.removeItem('wp_lock_until');
+ failAttempts = 0;
+ lockUntil = 0;
+
+ document.getElementById('pinLockToggle').checked = false;
+ document.getElementById('bioLockToggle').checked = false;
+
+ // Reset UI State
+ document.getElementById('lockoutUI').classList.add('hidden');
+ document.getElementById('lockHeader').classList.remove('hidden');
+ document.getElementById('lock-screen').classList.remove('locked-mode');
+
+ closeModal('forgotPinModal');
+ unlockApp();
+ } else { throw new Error(); }
+ } catch (e) {
+ err.style.display = 'block';
+ } finally {
+ btn.classList.remove('loading');
+ btn.disabled = false;
+ }
+ }
+
+ function togglePasswordVisibility(id, iconId) {
+ const input = document.getElementById(id);
+ const icon = document.getElementById(iconId);
if (input.type === 'password') {
input.type = 'text';
icon.innerText = 'visibility_off';
@@ -761,7 +1645,6 @@
async function attemptLogin() {
const btn = document.getElementById('loginBtn');
const err = document.getElementById('authError');
-
btn.classList.add('loading');
btn.disabled = true;
err.style.opacity = '0';
@@ -795,27 +1678,18 @@
loadChats();
}
- // --- SETTINGS LOGIC ---
- function openSettings() {
- document.getElementById('settingsModal').classList.add('active');
- }
- function closeModal(id) {
- document.getElementById(id).classList.remove('active');
- }
+ function openSettings() { document.getElementById('settingsModal').classList.add('active'); }
+ function closeModal(id) { document.getElementById(id).classList.remove('active'); }
function toggleTheme() {
const isDark = document.getElementById('darkModeToggle').checked;
document.body.setAttribute('data-theme', isDark ? 'dark' : 'light');
localStorage.setItem('theme', isDark ? 'dark' : 'light');
}
- function changeFontSize(val) {
- document.documentElement.style.setProperty('--base-font-size', `${val}px`);
- }
+ function 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'];
- // Apply to variable so it works everywhere
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);
@@ -841,7 +1715,6 @@
});
}
- // --- CHAT FUNCTIONS ---
function toggleChatMenu(event) {
if(event) event.stopPropagation();
document.getElementById('chatMenu').classList.toggle('show');
@@ -864,12 +1737,7 @@
const term = e.target.value.toLowerCase();
const bubbles = document.querySelectorAll('.msg-bubble');
let count = 0;
-
- // Unwrap highlights
- document.querySelectorAll('.highlight').forEach(span => {
- const parent = span.parentNode;
- parent.innerHTML = parent.textContent;
- });
+ document.querySelectorAll('.highlight').forEach(span => { const parent = span.parentNode; parent.innerHTML = parent.textContent; });
bubbles.forEach(b => {
const textDiv = b.querySelector('div:not(.msg-sender):not(.msg-meta)');
@@ -890,7 +1758,6 @@
});
async function loadChats() {
- // Skeleton
const list = document.getElementById('contactList');
list.innerHTML = '';
for(let i=0; i<5; i++) {
@@ -932,9 +1799,7 @@
el.onclick = () => openChat(c.ids, name, el, isGroup, c.phoneNumber || c.id);
list.appendChild(el);
});
- } catch (err) {
- console.error(err);
- }
+ } catch (err) { console.error(err); }
}
function openChat(ids, name, el, isGroup, displayId) {
@@ -942,15 +1807,10 @@
isCurrentChatGroup = isGroup;
document.getElementById('headerName').innerText = name;
document.getElementById('chatHeaderContent').style.opacity = '1';
-
- // Update Header ID with phone/group ID
document.getElementById('headerId').innerText = displayId;
+ document.getElementById('headerAvatar').innerHTML = isGroup ? `<span class="material-symbols-rounded">group</span>` : name[0].toUpperCase();
- const avatarEl = document.getElementById('headerAvatar');
- avatarEl.innerHTML = isGroup ? `<span class="material-symbols-rounded">group</span>` : name[0].toUpperCase();
-
- const isMobile = window.matchMedia('(max-width: 768px)').matches;
- if (isMobile) {
+ if (window.matchMedia('(max-width: 768px)').matches) {
window.history.pushState({chatOpen: true}, "");
document.getElementById('sidebar').classList.add('inactive');
document.getElementById('chatArea').classList.add('active');
@@ -963,7 +1823,6 @@
container.innerHTML = `<div class="skel-msg-group"><div class="skeleton skel-bubble skel-left"></div><div class="skeleton skel-bubble skel-right" style="width:60%"></div><div class="skeleton skel-bubble skel-left" style="width:30%"></div></div>`;
const msgs = new Map();
-
ids.forEach(id => {
window.unsub.push(db.collection('Chats').doc(id).collection('Messages').orderBy('timestamp').onSnapshot(snap => {
snap.forEach(d => msgs.set(d.id, d.data()));
@@ -972,6 +1831,7 @@
});
}
+ // --- UPDATED RENDER MESSAGES (Date Groups for Sticky Header Fix) ---
function renderMsgs(map) {
const container = document.getElementById('messagesContainer');
container.innerHTML = '';
@@ -982,23 +1842,157 @@
return;
}
- currentMessages.forEach(m => {
+ let lastDateStr = "";
+ let currentGroup = null;
+
+ currentMessages.forEach((m, index) => {
+ const date = new Date(m.timestamp * 1000);
+ const dateStr = date.toDateString();
+
+ // Start New Date Group if date changed
+ if(dateStr !== lastDateStr) {
+ // Close previous group if exists
+ if(currentGroup) container.appendChild(currentGroup);
+
+ // Create new group container
+ currentGroup = document.createElement('div');
+ currentGroup.className = 'chat-date-group';
+
+ // Add Sticky Divider inside group
+ const div = document.createElement('div');
+ div.className = 'date-divider';
+ div.innerText = formatDividerDate(m.timestamp);
+ currentGroup.appendChild(div);
+
+ lastDateStr = dateStr;
+ }
+
+ // 2. GROUPING LOGIC (Using previous logic but appending to group)
const div = document.createElement('div');
div.className = `msg-bubble ${m.fromMe ? 'msg-out' : 'msg-in'}`;
- const showSenderName = !m.fromMe && isCurrentChatGroup && m.senderName && m.senderName !== "Unknown";
+ const nextMsg = currentMessages[index + 1];
+ const prevMsg = currentMessages[index - 1];
+
+ const isSamePrev = (prevMsg && prevMsg.fromMe === m.fromMe && (!isCurrentChatGroup || prevMsg.senderName === m.senderName) && new Date(prevMsg.timestamp*1000).toDateString() === dateStr);
+ const isSameNext = (nextMsg && nextMsg.fromMe === m.fromMe && (!isCurrentChatGroup || nextMsg.senderName === m.senderName) && new Date(nextMsg.timestamp*1000).toDateString() === dateStr);
+
+ if (isSamePrev && isSameNext) div.classList.add('group-mid');
+ else if (isSamePrev) div.classList.add('group-bot');
+ else if (isSameNext) div.classList.add('group-top');
+
+ const showSenderName = !m.fromMe && isCurrentChatGroup && m.senderName && m.senderName !== "Unknown" && !isSamePrev;
+
+ div.id = `msg-${m.timestamp}`;
+ div.dataset.date = dateStr;
+
div.innerHTML = `
${showSenderName ? `<div class="msg-sender">${m.senderName}</div>` : ''}
<div>${m.text}</div>
- <div class="msg-meta">${new Date(m.timestamp*1000).toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'})}</div>
+ <div class="msg-meta">${date.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'})}</div>
`;
- container.appendChild(div);
+
+ // Append message to current GROUP
+ if (currentGroup) currentGroup.appendChild(div);
});
- container.scrollTop = container.scrollHeight;
+
+ // Append the final group
+ if(currentGroup) container.appendChild(currentGroup);
+
+ // Force scroll to bottom on every update/load (now instant)
+ setTimeout(() => scrollToBottom('auto'), 50);
+ }
+
+ // --- SCROLL TO BOTTOM ---
+ function scrollToBottom(behavior = 'auto') {
+ const c = document.getElementById('messagesContainer');
+ c.scrollTo({ top: c.scrollHeight, behavior: behavior });
+ }
+
+ // --- CALENDAR LOGIC ---
+ function openDateSearch() {
+ document.getElementById('chatMenu').classList.remove('show');
+ document.getElementById('calendarModal').classList.add('active');
+ renderCalendar(calCurrentDate);
+ }
+
+ function renderCalendar(date) {
+ const year = date.getFullYear();
+ const month = date.getMonth();
+
+ // Header
+ document.getElementById('calMonthYear').innerText = new Intl.DateTimeFormat('en-US', { month: 'long', year: 'numeric' }).format(date);
+
+ // Grid
+ const firstDay = new Date(year, month, 1).getDay();
+ const daysInMonth = new Date(year, month + 1, 0).getDate();
+ const grid = document.getElementById('calDatesGrid');
+ grid.innerHTML = '';
+
+ // Empty slots for prev month
+ for(let i=0; i<firstDay; i++) {
+ grid.innerHTML += `<div class="cal-date empty"></div>`;
+ }
+
+ // Days
+ for(let d=1; d<=daysInMonth; d++) {
+ const dayEl = document.createElement('div');
+ dayEl.className = 'cal-date ripple-surface';
+ dayEl.innerText = d;
+
+ // Check if selected
+ if(calSelectedDate && calSelectedDate.getDate() === d && calSelectedDate.getMonth() === month && calSelectedDate.getFullYear() === year) {
+ dayEl.classList.add('active');
+ }
+
+ dayEl.onclick = () => selectDate(new Date(year, month, d));
+ grid.appendChild(dayEl);
+ }
+ }
+
+ function changeCalMonth(delta) {
+ calCurrentDate.setMonth(calCurrentDate.getMonth() + delta);
+ renderCalendar(calCurrentDate);
+ }
+
+ function selectDate(date) {
+ calSelectedDate = date;
+ document.getElementById('calSelectedDateStr').innerText = date.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' });
+ renderCalendar(calCurrentDate); // Re-render to show active state
+ }
+
+ function jumpToDate() {
+ if(!currentMessages.length) {
+ closeModal('calendarModal');
+ return;
+ }
+
+ // Find first message >= selected date
+ // Set time to 00:00:00 for comparison
+ const targetTs = calSelectedDate.setHours(0,0,0,0) / 1000;
+ const targetDateStr = calSelectedDate.toDateString();
+
+ // Try to find exact match or next closest
+ const msg = currentMessages.find(m => m.timestamp >= targetTs);
+
+ closeModal('calendarModal');
+
+ if(msg) {
+ const el = document.getElementById(`msg-${msg.timestamp}`);
+ if(el) {
+ el.scrollIntoView({behavior: 'smooth', block: 'center'});
+ el.style.backgroundColor = 'var(--search-highlight)'; // Flash highlight
+ setTimeout(() => el.style.backgroundColor = '', 2000);
+ }
+ } else {
+ document.getElementById('notFoundModal').classList.add('active');
+ }
}
function handleLogout() {
localStorage.removeItem('wp_auth_expiry');
+ localStorage.removeItem('wp_app_pin');
+ localStorage.removeItem('wp_app_bio');
location.reload();
}
@@ -1011,7 +2005,6 @@
document.getElementById('chatArea').classList.remove('active');
});
- // --- DOWNLOAD ---
function downloadChat() {
if(!currentMessages.length) return;
const text = currentMessages.map(m => `[${new Date(m.timestamp*1000).toLocaleString()}] ${m.senderName}: ${m.text}`).join('\n');
@@ -1022,7 +2015,6 @@
a.click();
}
- // --- RENAME CHAT ---
function showRenameModal() {
if(!activeChatId) return;
document.getElementById('renameModal').classList.add('active');
@@ -1044,7 +2036,6 @@
document.getElementById('logoutModal').classList.add('active');
}
- // Contact search
document.getElementById('searchInput').addEventListener('input', (e) => {
const val = e.target.value.toLowerCase();
document.querySelectorAll('.contact-item').forEach(el => {
@@ -1060,6 +2051,24 @@
setTimeout(loadChats, 1000);
}
}
+
+ async function refreshApp() {
+ const btn = document.querySelector('.icon-btn span[class*="refresh"]');
+ if(btn) btn.style.animation = 'spin 1s infinite linear';
+ if ('caches' in window) {
+ try {
+ const keys = await caches.keys();
+ await Promise.all(keys.map(key => caches.delete(key)));
+ } catch(e) { console.log("Cache clear error", e); }
+ }
+ if ('serviceWorker' in navigator) {
+ const registrations = await navigator.serviceWorker.getRegistrations();
+ for (let registration of registrations) {
+ await registration.unregister();
+ }
+ }
+ window.location.reload();
+ }
</script>
</body>
</html>