Message / index.html
Hectozar's picture
Update index.html
b497bfa verified
Raw History Blame Contribute Delete
59.1 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Liquid Chat - Professional Edition</title>
<style>
/* ==========================================================================
CSS VARIABLES & RESET
========================================================================== */
:root {
--primary-bg: #f4f7f6;
--glass-bg: rgba(255, 255, 255, 0.65);
--glass-border: rgba(255, 255, 255, 0.8);
--glass-shadow: 0 12px 40px 0 rgba(31, 38, 135, 0.1);
--apple-blue: #007aff;
--apple-blue-hover: #005ce6;
--apple-red: #ff3b30;
--apple-red-hover: #d63028;
--text-dark: #1d1d1f;
--text-muted: #86868b;
--sidebar-width: 300px;
--gold-gradient: linear-gradient(135deg, #FFD700 0%, #FFA500 50%, #FF8C00 100%);
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: var(--font-family);
}
body {
background: linear-gradient(135deg, #e0c3fc 0%, #8ec5fc 100%);
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
color: var(--text-dark);
overflow: hidden;
position: relative;
}
/* ==========================================================================
LOADING SCREEN
========================================================================== */
#loading-screen {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: #ffffff;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 9999;
transition: opacity 0.8s ease-in-out, visibility 0.8s;
}
.spinner {
width: 60px;
height: 60px;
border: 4px solid rgba(0, 122, 255, 0.2);
border-top-color: var(--apple-blue);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 20px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-text {
font-size: 18px;
font-weight: 500;
color: var(--text-muted);
letter-spacing: 1px;
}
/* ==========================================================================
MAIN LAYOUT & PANELS
========================================================================== */
.glass-panel {
background: var(--glass-bg);
backdrop-filter: blur(30px);
-webkit-backdrop-filter: blur(30px);
border-radius: 24px;
border: 1px solid var(--glass-border);
box-shadow: var(--glass-shadow);
width: 96vw;
height: 94vh;
display: flex;
overflow: hidden;
position: relative;
}
#auth-screen {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
text-align: center;
background: rgba(255,255,255,0.3);
z-index: 100;
position: absolute;
top: 0;
left: 0;
transition: opacity 0.5s ease;
}
.auth-box {
background: rgba(255, 255, 255, 0.8);
padding: 50px 40px;
border-radius: 24px;
border: 1px solid var(--glass-border);
width: 420px;
box-shadow: 0 15px 35px rgba(0,0,0,0.1);
backdrop-filter: blur(20px);
}
.auth-box h2 {
margin-bottom: 30px;
font-size: 28px;
font-weight: 600;
color: var(--text-dark);
}
#main-app {
display: none;
width: 100%;
height: 100%;
flex-direction: row;
}
/* ==========================================================================
SIDEBAR
========================================================================== */
.sidebar {
width: var(--sidebar-width);
border-right: 1px solid rgba(0, 0, 0, 0.08);
background: rgba(255, 255, 255, 0.4);
display: flex;
flex-direction: column;
padding: 24px;
box-sizing: border-box;
}
.sidebar-header {
font-size: 22px;
font-weight: 700;
margin-bottom: 24px;
display: flex;
justify-content: space-between;
align-items: center;
color: var(--text-dark);
}
.user-profile {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 24px;
padding: 12px;
background: rgba(255, 255, 255, 0.6);
border-radius: 16px;
border: 1px solid rgba(255,255,255,0.8);
box-shadow: 0 4px 10px rgba(0,0,0,0.03);
}
.user-profile .avatar {
width: 40px;
height: 40px;
background: var(--apple-blue);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 16px;
}
.gold-name {
background: var(--gold-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: 800 !important;
text-shadow: 0px 2px 4px rgba(255, 215, 0, 0.3);
}
/* Changelog */
.changelog-container {
background: rgba(255, 255, 255, 0.5);
padding: 16px;
border-radius: 16px;
margin-bottom: 24px;
font-size: 13px;
border: 1px solid rgba(255,255,255,0.7);
max-height: 200px;
overflow-y: auto;
color: var(--text-muted);
}
.changelog-container h4 {
margin-bottom: 8px;
color: var(--text-dark);
font-size: 14px;
}
#changelog-content {
outline: none;
white-space: pre-wrap;
line-height: 1.5;
}
#changelog-content[contenteditable="true"] {
background: rgba(255, 255, 255, 0.8);
padding: 8px;
border-radius: 8px;
border: 1px solid var(--apple-blue);
color: var(--text-dark);
}
/* Content Area */
.content-area {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
background: rgba(255,255,255,0.2);
}
/* ==========================================================================
FORMS & BUTTONS
========================================================================== */
.setup-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
padding: 40px;
text-align: center;
}
.setup-box {
width: 100%;
max-width: 420px;
background: rgba(255, 255, 255, 0.7);
padding: 40px;
border-radius: 24px;
border: 1px solid var(--glass-border);
box-shadow: 0 10px 30px rgba(0,0,0,0.05);
}
.setup-box h2 {
margin-bottom: 25px;
font-size: 26px;
color: var(--text-dark);
}
input {
width: 100%;
padding: 16px 20px;
margin-bottom: 16px;
border-radius: 14px;
border: 1px solid rgba(0, 0, 0, 0.1);
background: rgba(255, 255, 255, 0.9);
font-size: 16px;
outline: none;
transition: all 0.3s ease;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.02);
}
input:focus {
border-color: var(--apple-blue);
box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.2);
background: #ffffff;
}
button {
width: 100%;
padding: 16px;
border-radius: 14px;
border: none;
background: var(--apple-blue);
color: white;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 4px 15px rgba(0, 122, 255, 0.2);
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
}
button:hover {
background: var(--apple-blue-hover);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 122, 255, 0.3);
}
button:active {
transform: scale(0.98);
}
button.secondary {
background: rgba(255, 255, 255, 0.8);
color: var(--text-dark);
border: 1px solid rgba(0,0,0,0.1);
box-shadow: 0 2px 5px rgba(0,0,0,0.02);
}
button.secondary:hover {
background: #ffffff;
border-color: rgba(0,0,0,0.2);
}
button.danger {
background: var(--apple-red);
box-shadow: 0 4px 15px rgba(255, 59, 48, 0.2);
}
button.danger:hover {
background: var(--apple-red-hover);
}
/* SVGs in buttons */
.btn-icon {
width: 20px;
height: 20px;
fill: currentColor;
}
/* ==========================================================================
RECENT ROOMS
========================================================================== */
.recent-list {
margin-top: 10px;
display: flex;
flex-direction: column;
gap: 10px;
width: 100%;
overflow-y: auto;
flex: 1;
}
.recent-btn {
background: rgba(255, 255, 255, 0.6);
color: var(--text-dark);
text-align: left;
padding: 14px 16px;
display: flex;
justify-content: space-between;
align-items: center;
border-radius: 12px;
box-shadow: none;
border: 1px solid rgba(255,255,255,0.5);
font-weight: 500;
}
.recent-btn:hover {
background: rgba(255, 255, 255, 0.9);
}
.recent-delete {
background: rgba(255, 59, 48, 0.1);
color: var(--apple-red);
padding: 6px 12px;
border-radius: 8px;
display: none;
font-size: 12px;
cursor: pointer;
font-weight: 600;
}
.recent-btn:hover .recent-delete {
display: flex;
align-items: center;
gap: 4px;
}
.recent-delete:hover {
background: rgba(255, 59, 48, 0.2);
}
/* ==========================================================================
CHAT SCREEN
========================================================================== */
#chat-screen {
display: none;
flex-direction: column;
height: 100%;
width: 100%;
}
.chat-header {
padding: 16px 24px;
border-bottom: 1px solid rgba(0,0,0,0.05);
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.4);
justify-content: space-between;
backdrop-filter: blur(10px);
}
.header-left {
display: flex;
align-items: center;
gap: 16px;
}
.header-title {
font-weight: 600;
font-size: 20px;
color: var(--text-dark);
}
.icon-btn {
background: rgba(255,255,255,0.5);
border-radius: 50%;
width: 44px;
height: 44px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
border: 1px solid rgba(0,0,0,0.05);
transition: all 0.2s;
padding: 0;
box-shadow: 0 2px 5px rgba(0,0,0,0.02);
color: var(--text-dark);
}
.icon-btn:hover {
background: #ffffff;
box-shadow: 0 4px 10px rgba(0,0,0,0.05);
}
.icon-svg {
width: 22px;
height: 22px;
fill: currentColor;
}
.messages-container {
flex: 1;
padding: 24px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 16px;
scroll-behavior: smooth;
}
/* ==========================================================================
CHAT BUBBLES & INTERACTIONS
========================================================================== */
.msg-wrapper {
display: flex;
flex-direction: column;
max-width: 75%;
position: relative;
margin-bottom: 8px;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.msg-wrapper.received { align-self: flex-start; }
.msg-wrapper.sent { align-self: flex-end; align-items: flex-end; }
.bubble {
padding: 14px 20px;
border-radius: 20px;
font-size: 15px;
line-height: 1.5;
box-shadow: 0 4px 15px rgba(0,0,0,0.04);
word-wrap: break-word;
position: relative;
}
.msg-wrapper.received .bubble {
background: #ffffff;
border-bottom-left-radius: 4px;
border: 1px solid rgba(0,0,0,0.05);
}
.msg-wrapper.sent .bubble {
background: var(--apple-blue);
color: #ffffff;
border-bottom-right-radius: 4px;
}
.msg-sender {
font-size: 13px;
font-weight: 600;
margin-bottom: 4px;
opacity: 0.8;
color: var(--text-dark);
}
.msg-time {
font-size: 11px;
margin-top: 6px;
opacity: 0.6;
text-align: right;
}
.msg-wrapper.sent .msg-time {
color: rgba(255,255,255,0.8);
}
/* Hover Actions Menu */
.msg-actions {
position: absolute;
top: -20px;
background: #ffffff;
border-radius: 20px;
border: 1px solid rgba(0,0,0,0.1);
display: none;
gap: 4px;
padding: 4px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
z-index: 10;
white-space: nowrap;
}
.msg-wrapper.received .msg-actions { right: -10px; }
.msg-wrapper.sent .msg-actions { left: -10px; }
.msg-wrapper:hover .msg-actions {
display: flex;
animation: popIn 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
@keyframes popIn {
from { transform: scale(0.8); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
.action-item {
cursor: pointer;
padding: 6px 10px;
border-radius: 12px;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--text-muted);
background: transparent;
border: none;
}
.action-item:hover {
background: rgba(0,0,0,0.05);
color: var(--text-dark);
}
.action-item.delete:hover {
background: rgba(255,59,48,0.1);
color: var(--apple-red);
}
.action-icon {
width: 16px;
height: 16px;
fill: currentColor;
}
/* Reactions Bar */
.reactions-bar {
display: flex;
gap: 6px;
margin-top: 6px;
flex-wrap: wrap;
}
.reaction-badge {
background: rgba(0,0,0,0.05);
border: 1px solid rgba(0,0,0,0.05);
border-radius: 12px;
padding: 4px 8px;
font-size: 12px;
display: flex;
align-items: center;
gap: 4px;
color: var(--text-dark);
}
.msg-wrapper.sent .reaction-badge {
background: rgba(255,255,255,0.2);
border-color: rgba(255,255,255,0.3);
color: white;
}
/* Reply Preview */
.reply-preview {
font-size: 13px;
background: rgba(0,0,0,0.04);
padding: 8px 12px;
border-radius: 10px;
margin-bottom: 8px;
border-left: 4px solid var(--apple-blue);
color: var(--text-muted);
}
.msg-wrapper.sent .reply-preview {
background: rgba(255,255,255,0.15);
border-left-color: #ffffff;
color: rgba(255,255,255,0.9);
}
/* Voting Message */
.vote-msg {
align-self: center;
background: #ffffff;
border: 1px solid rgba(255, 59, 48, 0.3);
padding: 20px;
border-radius: 16px;
text-align: center;
width: 80%;
max-width: 450px;
box-shadow: 0 10px 25px rgba(255,59,48,0.1);
margin: 20px 0;
}
.vote-title {
color: var(--apple-red);
font-weight: 700;
font-size: 16px;
margin-bottom: 12px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.vote-buttons {
display: flex;
gap: 12px;
margin-top: 16px;
}
/* ==========================================================================
INPUT AREA
========================================================================== */
.input-wrapper {
background: rgba(255, 255, 255, 0.6);
border-top: 1px solid rgba(0,0,0,0.08);
display: flex;
flex-direction: column;
backdrop-filter: blur(20px);
}
.active-reply-bar {
display: none;
background: #ffffff;
padding: 12px 24px;
font-size: 14px;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid rgba(0,0,0,0.05);
color: var(--text-muted);
}
.cancel-reply {
cursor: pointer;
color: var(--apple-red);
padding: 4px;
}
.input-area {
padding: 16px 24px;
display: flex;
gap: 12px;
align-items: center;
}
.input-area input {
margin-bottom: 0;
border-radius: 25px;
height: 52px;
flex: 1;
padding: 0 20px;
background: #ffffff;
border: 1px solid rgba(0,0,0,0.1);
}
.send-btn {
width: 52px;
height: 52px;
border-radius: 50%;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
}
/* ==========================================================================
MODALS & MENUS
========================================================================== */
.modal-overlay {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.3);
backdrop-filter: blur(8px);
display: none;
justify-content: center;
align-items: center;
z-index: 1000;
opacity: 0;
transition: opacity 0.3s ease;
}
.modal-overlay.active {
display: flex;
opacity: 1;
}
.modal-box {
background: #ffffff;
padding: 32px;
border-radius: 24px;
width: 90%;
max-width: 400px;
text-align: center;
box-shadow: 0 20px 50px rgba(0,0,0,0.2);
transform: scale(0.95);
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.modal-overlay.active .modal-box {
transform: scale(1);
}
.modal-title {
font-size: 22px;
font-weight: 700;
margin-bottom: 12px;
color: var(--text-dark);
}
.modal-text {
font-size: 16px;
color: var(--text-muted);
margin-bottom: 24px;
line-height: 1.5;
}
.modal-buttons {
display: flex;
gap: 12px;
}
/* Dropdown Menus */
.dropdown-menu {
position: absolute;
background: #ffffff;
border: 1px solid rgba(0,0,0,0.1);
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
display: none;
flex-direction: column;
overflow: hidden;
z-index: 50;
min-width: 200px;
}
.dropdown-menu.active {
display: flex;
animation: fadeIn 0.2s ease;
}
#settings-menu {
top: 70px;
right: 24px;
}
#attach-menu {
bottom: 80px;
left: 24px;
}
.dropdown-item {
padding: 16px 20px;
font-size: 15px;
cursor: pointer;
border-bottom: 1px solid rgba(0,0,0,0.05);
display: flex;
align-items: center;
gap: 12px;
color: var(--text-dark);
transition: background 0.2s;
}
.dropdown-item:last-child {
border-bottom: none;
}
.dropdown-item:hover {
background: rgba(0,0,0,0.03);
}
.dropdown-item.danger {
color: var(--apple-red);
font-weight: 600;
}
.dropdown-icon {
width: 18px;
height: 18px;
fill: currentColor;
}
.attached-media {
max-width: 100%;
border-radius: 12px;
margin-top: 8px;
border: 1px solid rgba(0,0,0,0.1);
}
/* ==========================================================================
SCROLLBARS & CONFETTI
========================================================================== */
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.2); border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,0.3); }
.confetti {
position: absolute;
width: 12px;
height: 12px;
background: #f00;
animation: fall 3s linear forwards;
z-index: 9999;
pointer-events: none;
border-radius: 2px;
}
@keyframes fall {
0% { transform: translateY(-10vh) rotate(0deg); opacity: 1; }
100% { transform: translateY(110vh) rotate(720deg); opacity: 0; }
}
</style>
</head>
<body>
<!-- LOADING SCREEN -->
<div id="loading-screen">
<div class="spinner"></div>
<div class="loading-text">Loading Liquid Chat...</div>
</div>
<!-- MAIN APP CONTAINER -->
<div class="glass-panel">
<!-- AUTHENTICATION SCREEN -->
<div id="auth-screen">
<div class="auth-box">
<h2>Welcome to Liquid</h2>
<input type="text" id="authName" placeholder="Username" autocomplete="off">
<input type="password" id="authPass" placeholder="Password" autocomplete="off">
<button onclick="login()">
<svg class="btn-icon" viewBox="0 0 24 24"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
Log In / Sign Up
</button>
</div>
</div>
<!-- INSIDE THE APP -->
<div id="main-app">
<!-- SIDEBAR -->
<div class="sidebar">
<div class="sidebar-header">
<span>Liquid</span>
</div>
<div class="user-profile">
<div class="avatar" id="nav-avatar">U</div>
<div style="display:flex; flex-direction:column;">
<span id="nav-name" style="font-size:16px; font-weight:600;">User</span>
<span id="nav-role" style="font-size:12px; color:var(--text-muted);">Member</span>
</div>
</div>
<div class="changelog-container">
<h4>Changelog (Ctrl+Shift+^)</h4>
<div id="changelog-content" contenteditable="false">v3.0 - Professional Update
- Added secure Room Passwords
- Implemented Loading Screen
- Upgraded UI to professional SVGs
- Added Developer authentication
- Improved layout & animations</div>
</div>
<h4 style="margin-top: 16px; font-size: 14px; color: var(--text-muted);">Saved Rooms</h4>
<div class="recent-list" id="sidebar-recents"></div>
<div style="margin-top: 24px;">
<button class="secondary" onclick="logout()">
<svg class="btn-icon" viewBox="0 0 24 24"><path d="M17 7l-1.41 1.41L18.17 11H8v2h10.17l-2.58 2.58L17 17l5-5zM4 5h8V3H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h8v-2H4V5z"/></svg>
Log Out
</button>
</div>
</div>
<!-- CONTENT AREA -->
<div class="content-area">
<!-- SETUP ROOM SCREEN -->
<div id="setup-screen" class="setup-container">
<div class="setup-box">
<h2>Join a Room</h2>
<input type="text" id="roomNameInput" placeholder="Room Name" autocomplete="off">
<input type="password" id="roomPassInput" placeholder="Room Password" autocomplete="off">
<button onclick="joinChat()">
<svg class="btn-icon" viewBox="0 0 24 24"><path d="M20 6h-8l-2-2H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm0 12H4V8h16v10z"/></svg>
Enter Room
</button>
</div>
</div>
<!-- CHAT SCREEN -->
<div id="chat-screen">
<!-- Chat Header -->
<div class="chat-header">
<div class="header-left">
<button class="icon-btn" onclick="goBack()" aria-label="Go Back">
<svg class="icon-svg" viewBox="0 0 24 24"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
</button>
<div>
<div class="header-title" id="chat-title">Room</div>
<div style="font-size: 13px; color: var(--text-muted);" id="chat-subtitle">Secure Connection</div>
</div>
</div>
<button class="icon-btn" onclick="toggleMenu('settings-menu')" aria-label="Settings">
<svg class="icon-svg" viewBox="0 0 24 24"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.73 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .43-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.49-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg>
</button>
</div>
<!-- Settings Dropdown -->
<div class="dropdown-menu" id="settings-menu">
<div class="dropdown-item" onclick="showPeople()">
<svg class="dropdown-icon" viewBox="0 0 24 24"><path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/></svg>
View Members
</div>
<div class="dropdown-item danger" onclick="initiateReset()">
<svg class="dropdown-icon" viewBox="0 0 24 24"><path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></svg>
Reset Chat
</div>
</div>
<!-- Chat Messages Container -->
<div class="messages-container" id="chat-box"></div>
<!-- Input Wrapper -->
<div class="input-wrapper">
<!-- Reply Indicator Bar -->
<div class="active-reply-bar" id="reply-bar">
<span>Replying to <strong id="reply-to-name" style="color:var(--text-dark);">User</strong>: <i id="reply-to-text">Text</i></span>
<div class="cancel-reply" onclick="cancelReply()">
<svg class="icon-svg" viewBox="0 0 24 24"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</div>
</div>
<div class="input-area">
<button class="icon-btn" onclick="toggleMenu('attach-menu')">
<svg class="icon-svg" viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
</button>
<!-- Attachments Dropdown -->
<div class="dropdown-menu" id="attach-menu">
<div class="dropdown-item" onclick="attachImage()">
<svg class="dropdown-icon" viewBox="0 0 24 24"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/></svg>
Attach Image
</div>
<div class="dropdown-item" onclick="attachLink()">
<svg class="dropdown-icon" viewBox="0 0 24 24"><path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"/></svg>
Attach Link
</div>
</div>
<input type="text" id="messageInput" placeholder="Type a message..." onkeypress="handleKeyPress(event)" autocomplete="off">
<button class="send-btn" onclick="sendMsg()">
<svg class="icon-svg" style="fill:white; width:24px; height:24px; transform:translateX(2px);" viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- CUSTOM MODAL OVERLAY -->
<div class="modal-overlay" id="custom-modal">
<div class="modal-box">
<div class="modal-title" id="modal-title">System Alert</div>
<div class="modal-text" id="modal-text">Message goes here.</div>
<div class="modal-buttons" id="modal-buttons"></div>
</div>
</div>
<!-- JAVASCRIPT LOGIC -->
<script>
/**
* State Variables
*/
let myName = "";
let isDev = false;
let myRoom = "";
let savedRooms = [];
let currentReplyTo = null;
let pollInterval;
let attachedImgData = null;
let attachedLinkData = null;
/**
* Mock Backend Database Class
* Handles secure storage, room passwords, messages, and reactions.
*/
class BackendManager {
static getDB() {
return JSON.parse(localStorage.getItem('liquid_pro_db') || '{}');
}
static saveDB(db) {
localStorage.setItem('liquid_pro_db', JSON.stringify(db));
}
static registerRoom(roomName, password) {
let db = this.getDB();
if (!db[roomName]) {
db[roomName] = {
password: password,
messages: [],
users: [],
resetVote: null
};
this.saveDB(db);
return true;
} else {
return db[roomName].password === password;
}
}
static sendMessage(room, sender, senderIsDev, text, replyData, imgData, linkData, isVoteMsg = false) {
let db = this.getDB();
if (!db[room]) return [];
// Track unique users for vote counts
if (!db[room].users.includes(sender) && sender !== 'SYSTEM') {
db[room].users.push(sender);
}
let msgObj = {
id: 'msg_' + Date.now().toString() + Math.floor(Math.random()*1000),
sender: sender,
isDev: senderIsDev,
text: text,
replyData: replyData,
imgData: imgData,
linkData: linkData,
isVoteMsg: isVoteMsg,
time: new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}),
timestamp: Date.now(),
reactions: []
};
db[room].messages.push(msgObj);
this.saveDB(db);
return db[room].messages;
}
static getMessages(room) {
let db = this.getDB();
return db[room] ? db[room].messages : [];
}
static deleteMessage(room, id) {
let db = this.getDB();
if(db[room]) {
db[room].messages = db[room].messages.filter(m => m.id !== id);
this.saveDB(db);
}
}
static addReaction(room, id, actionName) {
let db = this.getDB();
if (!db[room]) return;
let msg = db[room].messages.find(m => m.id === id);
if(msg) {
// Store text representations of reactions
let badge = actionName === 'like' ? '👍 Like' : '😂 Haha';
msg.reactions.push(badge);
this.saveDB(db);
}
}
static initResetVote(room, sender) {
let db = this.getDB();
if(!db[room]) return;
let totalUsers = db[room].users.length || 1;
db[room].resetVote = { initiator: sender, agrees: [sender], totalNeeded: totalUsers };
this.saveDB(db);
this.sendMessage(room, 'SYSTEM', false, `${sender} has requested to reset this chat history.`, null, null, null, true);
}
static voteReset(room, user, agree) {
let db = this.getDB();
if(!db[room] || !db[room].resetVote) return false;
if(agree && !db[room].resetVote.agrees.includes(user)) {
db[room].resetVote.agrees.push(user);
}
this.saveDB(db);
// Execute reset if threshold met
if(db[room].resetVote.agrees.length >= db[room].resetVote.totalNeeded) {
db[room].messages = [];
db[room].resetVote = null;
this.saveDB(db);
return true;
}
return false;
}
static getPeople(room) {
let db = this.getDB();
return db[room] ? db[room].users : [];
}
}
/**
* Initialization & Loading Screen
*/
window.onload = function() {
// Simulate smooth loading
setTimeout(() => {
const loader = document.getElementById('loading-screen');
loader.style.opacity = '0';
setTimeout(() => loader.style.display = 'none', 800);
}, 1200);
// Load saved data
const storedName = localStorage.getItem('liquid_name');
const storedDevStatus = localStorage.getItem('liquid_isDev') === 'true';
const cl = localStorage.getItem('liquid_changelog');
if (cl) {
document.getElementById('changelog-content').innerText = cl;
}
if (storedName) {
myName = storedName;
isDev = storedDevStatus;
showMainApp();
}
// Global click listener to close dropdowns
document.addEventListener('click', (e) => {
if(!e.target.closest('.dropdown-menu') && !e.target.closest('.icon-btn')) {
document.querySelectorAll('.dropdown-menu').forEach(menu => menu.classList.remove('active'));
}
});
// Changelog Edit Listener (Ctrl + Shift + ^)
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.shiftKey && (e.key === '^' || e.key === '6')) {
const el = document.getElementById('changelog-content');
const isEditable = el.getAttribute('contenteditable') === 'true';
el.setAttribute('contenteditable', !isEditable);
if (isEditable) {
localStorage.setItem('liquid_changelog', el.innerText);
showModal("Saved", "Changelog successfully updated.", [{text: "Confirm", class: "secondary"}]);
} else {
el.focus();
}
}
});
};
/**
* Authentication Logic
*/
function login() {
const nameInput = document.getElementById('authName').value.trim();
const passInput = document.getElementById('authPass').value.trim();
if (!nameInput) {
showModal("Validation Error", "Username is required to enter Liquid Chat.", [{text: "Understood", class: "secondary"}]);
return;
}
myName = nameInput;
// Check for Developer credentials
if (myName.toLowerCase() === 'milo' && passInput === '123456') {
isDev = true;
myName = "Milo"; // Enforce casing
} else {
isDev = false;
}
localStorage.setItem('liquid_name', myName);
localStorage.setItem('liquid_isDev', isDev);
showMainApp();
}
function logout() {
localStorage.removeItem('liquid_name');
localStorage.removeItem('liquid_isDev');
location.reload();
}
function showMainApp() {
document.getElementById('auth-screen').style.opacity = '0';
setTimeout(() => {
document.getElementById('auth-screen').style.display = 'none';
document.getElementById('main-app').style.display = 'flex';
const navName = document.getElementById('nav-name');
navName.innerText = myName;
document.getElementById('nav-avatar').innerText = myName.charAt(0).toUpperCase();
if(isDev) {
navName.classList.add('gold-name');
document.getElementById('nav-role').innerText = "Developer";
document.getElementById('nav-role').classList.add('gold-name');
}
loadSavedRooms();
}, 400);
}
/**
* Custom Modal System
*/
function showModal(title, text, buttons) {
document.getElementById('modal-title').innerText = title;
document.getElementById('modal-text').innerText = text;
const btnContainer = document.getElementById('modal-buttons');
btnContainer.innerHTML = '';
buttons.forEach(btn => {
const b = document.createElement('button');
b.innerText = btn.text;
if(btn.class) b.className = btn.class;
b.onclick = () => {
document.getElementById('custom-modal').classList.remove('active');
if(btn.action) btn.action();
};
btnContainer.appendChild(b);
});
document.getElementById('custom-modal').classList.add('active');
}
/**
* Room Management
*/
function loadSavedRooms() {
const storedRooms = localStorage.getItem('liquid_rooms');
if (storedRooms) {
savedRooms = JSON.parse(storedRooms);
}
renderSidebarRecents();
}
function renderSidebarRecents() {
const list = document.getElementById('sidebar-recents');
list.innerHTML = '';
if(savedRooms.length === 0) {
list.innerHTML = '<div style="font-size:13px; color:var(--text-muted);">No saved rooms.</div>';
return;
}
savedRooms.forEach(roomObj => {
const div = document.createElement('div');
div.className = "recent-btn";
// SVG lock icon for secure rooms
const lockIcon = `<svg style="width:14px; height:14px; margin-right:8px; fill:var(--text-muted);" viewBox="0 0 24 24"><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zM9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9V6zm9 14H6V10h12v10zm-6-3c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"/></svg>`;
div.innerHTML = `
<div onclick="quickJoin('${roomObj.name}', '${roomObj.password}')" style="cursor:pointer; flex:1; display:flex; align-items:center;">
${lockIcon} ${roomObj.name}
</div>
<div class="recent-delete" onclick="promptDeleteRoom('${roomObj.name}')">
<svg class="icon-svg" style="width:14px; height:14px;" viewBox="0 0 24 24"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
</div>
`;
list.appendChild(div);
});
}
function promptDeleteRoom(roomName) {
showModal("Remove Room", `Are you sure you want to remove ${roomName} from your saved list?`, [
{ text: "Cancel", class: "secondary" },
{ text: "Remove", class: "danger", action: () => {
savedRooms = savedRooms.filter(r => r.name !== roomName);
localStorage.setItem('liquid_rooms', JSON.stringify(savedRooms));
renderSidebarRecents();
}}
]);
}
function quickJoin(name, pass) {
document.getElementById('roomNameInput').value = name;
document.getElementById('roomPassInput').value = pass;
joinChat();
}
function joinChat() {
const nameInput = document.getElementById('roomNameInput').value.trim();
const passInput = document.getElementById('roomPassInput').value.trim();
if (!nameInput || !passInput) {
return showModal("Access Denied", "Both Room Name and Room Password are required.", [{text: "Understood", class: "secondary"}]);
}
const isAuthorized = BackendManager.registerRoom(nameInput, passInput);
if (!isAuthorized) {
return showModal("Access Denied", "Incorrect password for this room.", [{text: "Retry", class: "danger"}]);
}
myRoom = nameInput;
// Save to recents if not exists
if (!savedRooms.find(r => r.name === myRoom)) {
savedRooms.unshift({ name: myRoom, password: passInput });
localStorage.setItem('liquid_rooms', JSON.stringify(savedRooms));
renderSidebarRecents();
}
// Transition UI
document.getElementById('setup-screen').style.display = 'none';
document.getElementById('chat-screen').style.display = 'flex';
document.getElementById('chat-title').innerText = myRoom;
// Clear inputs and chat box
document.getElementById('roomNameInput').value = '';
document.getElementById('roomPassInput').value = '';
document.getElementById('chat-box').innerHTML = '';
// Start Polling
pollServer();
pollInterval = setInterval(pollServer, 2000);
}
function goBack() {
clearInterval(pollInterval);
document.getElementById('chat-screen').style.display = 'none';
document.getElementById('setup-screen').style.display = 'flex';
lastDataString = ""; // Reset cache
}
/**
* UI Toggles & Attachments
*/
function toggleMenu(menuId) {
const el = document.getElementById(menuId);
const isAct = el.classList.contains('active');
document.querySelectorAll('.dropdown-menu').forEach(m => m.classList.remove('active'));
if(!isAct) {
// Small timeout prevents immediate close from document click listener
setTimeout(() => el.classList.add('active'), 10);
}
}
function attachImage() {
const url = prompt("Enter an Image URL:");
if(url) {
attachedImgData = url;
document.getElementById('messageInput').focus();
}
}
function attachLink() {
const url = prompt("Enter a Website Link:");
if(url) {
attachedLinkData = url;
document.getElementById('messageInput').focus();
}
}
/**
* Core Features: People & Reset
*/
function showPeople() {
const people = BackendManager.getPeople(myRoom);
showModal("Room Members", people.length > 0 ? people.join(", ") : "Just you.", [{text: "Close", class: "secondary"}]);
}
function initiateReset() {
BackendManager.initResetVote(myRoom, myName);
pollServer();
}
function handleVote(agree) {
const didReset = BackendManager.voteReset(myRoom, myName, agree);
if(didReset) {
triggerConfetti();
const box = document.getElementById('chat-box');
box.innerHTML = `
<div style="text-align:center; padding:40px; display:flex; flex-direction:column; align-items:center;">
<svg style="width:48px; height:48px; fill:var(--apple-blue); margin-bottom:16px;" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>
<h3 style="color:var(--text-dark);">Chat Cleared</h3>
<p style="color:var(--text-muted); font-size:14px;">All members agreed. Starting fresh.</p>
</div>`;
lastDataString = "";
}
pollServer();
}
function triggerConfetti() {
for(let i=0; i<60; i++) {
let c = document.createElement('div');
c.className = 'confetti';
c.style.left = Math.random() * 100 + 'vw';
c.style.backgroundColor = ['#007aff', '#ff3b30', '#34c759', '#ffcc00'][Math.floor(Math.random()*4)];
c.style.animationDuration = (Math.random() * 2 + 2) + 's';
document.body.appendChild(c);
setTimeout(() => c.remove(), 4000);
}
}
/**
* Replies & Actions
*/
function setReply(id, name, text) {
currentReplyTo = { id: id, name: name, text: text.substring(0,40) + "..." };
document.getElementById('reply-bar').style.display = 'flex';
document.getElementById('reply-to-name').innerText = name;
document.getElementById('reply-to-text').innerText = currentReplyTo.text;
document.getElementById('messageInput').focus();
}
function cancelReply() {
currentReplyTo = null;
document.getElementById('reply-bar').style.display = 'none';
}
function handleReact(id, type) {
BackendManager.addReaction(myRoom, id, type);
pollServer();
}
function handleDeleteMsg(id, timestamp) {
const now = Date.now();
// 10 minute rule: 10 * 60 * 1000 = 600,000 ms
if(now - timestamp > 600000) {
showModal("Time Limit Exceeded", "Messages can only be deleted within 10 minutes of sending.", [{text:"Okay", class:"secondary"}]);
return;
}
BackendManager.deleteMessage(myRoom, id);
pollServer();
}
/**
* Message Rendering & Polling
*/
function handleKeyPress(e) {
if (e.key === 'Enter') sendMsg();
}
function sendMsg() {
const input = document.getElementById('messageInput');
const text = input.value.trim();
if (!text && !attachedImgData && !attachedLinkData) return;
BackendManager.sendMessage(myRoom, myName, isDev, text, currentReplyTo, attachedImgData, attachedLinkData);
input.value = '';
attachedImgData = null;
attachedLinkData = null;
cancelReply();
pollServer();
}
let lastDataString = "";
function pollServer() {
if (!myRoom) return;
const messages = BackendManager.getMessages(myRoom);
const dataStr = JSON.stringify(messages);
if(dataStr === lastDataString) return;
lastDataString = dataStr;
const box = document.getElementById('chat-box');
// Check scroll position before adding new content
const isScrolledToBottom = box.scrollHeight - box.clientHeight <= box.scrollTop + 60;
box.innerHTML = '';
messages.forEach(msg => {
const isMe = (msg.sender === myName);
const wrap = document.createElement('div');
wrap.className = `msg-wrapper ${isMe ? 'sent' : 'received'}`;
// Construct Professional SVG Action Menu
const replySvg = `<svg class="action-icon" viewBox="0 0 24 24"><path d="M10 9V5l-7 7 7 7v-4.1c5 0 8.5 1.6 11 5.1-1-5-4-10-11-11z"/></svg>`;
const likeSvg = `<svg class="action-icon" viewBox="0 0 24 24"><path d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z"/></svg>`;
const hahaSvg = `<svg class="action-icon" viewBox="0 0 24 24"><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm3.5-9c.83 0 1.5-.67 1.5-1.5S16.33 8 15.5 8 14 8.67 14 9.5s.67 1.5 1.5 1.5zm-7 0c.83 0 1.5-.67 1.5-1.5S9.33 8 8.5 8 7 8.67 7 9.5 7.67 11 8.5 11zm3.5 6.5c2.33 0 4.31-1.46 5.11-3.5H6.89c.8 2.04 2.78 3.5 5.11 3.5z"/></svg>`;
const delSvg = `<svg class="action-icon" viewBox="0 0 24 24"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>`;
let actionsHtml = `<div class="msg-actions">
<button class="action-item" onclick="setReply('${msg.id}', '${msg.sender}', '${(msg.text || '').replace(/'/g,"")}')">${replySvg} Reply</button>
<button class="action-item" onclick="handleReact('${msg.id}', 'like')">${likeSvg} Like</button>
<button class="action-item" onclick="handleReact('${msg.id}', 'haha')">${hahaSvg} Haha</button>
${isMe ? `<button class="action-item delete" onclick="handleDeleteMsg('${msg.id}', ${msg.timestamp})">${delSvg} Delete</button>` : ''}
</div>`;
let bubbleHtml = `<div class="bubble">`;
// Render Vote Message Type
if(msg.isVoteMsg) {
const alertSvg = `<svg style="width:24px; height:24px; fill:var(--apple-red);" viewBox="0 0 24 24"><path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></svg>`;
bubbleHtml = `<div class="vote-msg">
<div class="vote-title">${alertSvg} System Alert</div>
<div style="color:var(--text-dark); margin-bottom:16px;">${msg.text}</div>
<div class="vote-buttons">
<button onclick="handleVote(true)">Agree</button>
<button class="secondary" onclick="handleVote(false)">Disagree</button>
</div>
</div>`;
actionsHtml = ''; // No actions permitted on system vote messages
}
// Render Standard Message Type
else {
// Check if sender is developer for gold styling
let senderClass = msg.isDev ? "msg-sender gold-name" : "msg-sender";
if (!isMe) bubbleHtml += `<div class="${senderClass}">${msg.sender}</div>`;
if (msg.replyData) {
bubbleHtml += `<div class="reply-preview">Replying to <strong style="font-weight:600;">${msg.replyData.name}</strong>: ${msg.replyData.text}</div>`;
}
if (msg.imgData) {
bubbleHtml += `<img src="${msg.imgData}" class="attached-media" alt="User Attachment"><br>`;
}
if (msg.linkData) {
bubbleHtml += `<a href="${msg.linkData}" target="_blank" style="color:inherit; text-decoration:underline; font-weight:500;">${msg.linkData}</a><br>`;
}
bubbleHtml += `<div>${msg.text}</div>`;
bubbleHtml += `<div class="msg-time">${msg.time}</div>`;
if(msg.reactions && msg.reactions.length > 0) {
bubbleHtml += `<div class="reactions-bar">`;
msg.reactions.forEach(r => {
bubbleHtml += `<span class="reaction-badge">${r}</span>`;
});
bubbleHtml += `</div>`;
}
bubbleHtml += `</div>`;
}
wrap.innerHTML = actionsHtml + bubbleHtml;
box.appendChild(wrap);
});
// Maintain scroll position logic
if (isScrolledToBottom) {
box.scrollTop = box.scrollHeight;
}
}
</script>
</body>
</html>