This commit is contained in:
2025-08-26 17:56:24 +02:00
commit 913d75efac
17 changed files with 7311 additions and 0 deletions

494
src/index.html Normal file
View File

@@ -0,0 +1,494 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ByteChat</title>
<meta http-equiv="X-Content-Type-Options" content="nosniff">
<meta http-equiv="X-Frame-Options" content="DENY">
<link rel="stylesheet" href="./stylesheet.css">
<script src="https://cdn.jsdelivr.net/npm/socket.io-client@4.7.2/dist/socket.io.min.js"></script>
<script src="./main.js"></script>
</head>
<body style="overflow-y: auto;">
<div class="chat-container">
<div class="chat-header" style="padding: 0.75rem 1rem;">
<div class="header-content" style="flex-wrap: wrap; gap: 0.5rem;">
<div class="logo" style="font-size: clamp(1.25rem, 4vw, 1.5rem);">
<a href="/" style="text-decoration: none; font-weight: inherit; font-style: inherit; color: inherit;">
ByteChat
</a>
</div>
<div class="encryption-badge" id="encryptionBadge" style="font-size: clamp(0.75rem, 3vw, 0.85rem); padding: 0.4rem 0.8rem;">
E2E Encrypted
</div>
</div>
</div>
<div id="welcomeScreen" class="welcome-screen" style="display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 1rem; flex: 1; min-height: 0;">
<h1 class="welcome-title" style="font-size: clamp(1.75rem, 8vw, 3rem); margin-bottom: 0.75rem;">ByteChat</h1>
<div class="room-section" style="width: 100%; max-width: 500px;">
<div class="room-input-container" style="flex-direction: column; gap: 0.75rem; width: 100%;">
<input type="text"
class="room-input"
id="roomInput"
placeholder="Enter room ID"
maxlength="32"
pattern="[a-zA-Z0-9\-_]+"
title="Only letters, numbers, hyphens and underscores allowed"
style="width: 100%; min-width: unset; font-size: clamp(0.9rem, 4vw, 1rem);">
<input type="password"
class="room-input"
id="roomPasswordInput"
placeholder="Optional: Channel Password"
maxlength="128"
autocomplete="new-password"
style="width: 100%; min-width: unset; max-width: none; font-size: clamp(0.9rem, 4vw, 1rem); overflow: hidden; text-overflow: ellipsis;">
<div style="display: flex; gap: 0.75rem; flex-wrap: wrap;">
<button class="btn" id="joinRoomBtn" aria-label="Join existing room" style="flex: 1; min-width: 120px; font-size: clamp(0.85rem, 3.5vw, 1rem); padding: 0.6rem 1rem;">Join Room</button>
<button class="btn btn-secondary" id="createRoomBtn" aria-label="Create new room" style="flex: 1; min-width: 120px; font-size: clamp(0.85rem, 3.5vw, 1rem); padding: 0.6rem 1rem;">Create New Room</button>
</div>
</div>
<div class="status-text" id="statusText" style="font-size: clamp(0.8rem, 3vw, 0.9rem);">
<div class="loading-message">
<div class="loading-spinner"></div>
<span aria-live="polite">Initializing encryption keys...</span>
</div>
</div>
</div>
</div>
<!-- Chat Screen -->
<div id="chatScreen" style="display: none;" class="chat-screen">
<div class="room-info" id="roomInfo" style="display: none; padding: 0.6rem 0.75rem; flex-wrap: wrap; gap: 0.5rem;">
<div class="room-details" style="min-width: auto; flex: 1;">
<span style="font-size: clamp(0.75rem, 3vw, 0.85rem);">Room: <strong id="currentRoomId"></strong></span>
<span id="messageCounter" style="font-size: clamp(0.75rem, 3vw, 0.85rem);">Messages: 0/256</span>
</div>
<div class="room-details" style="min-width: auto; flex: 1;">
<span id="userCounter" style="font-size: clamp(0.75rem, 3vw, 0.85rem);">Users: 0</span>
<span id="encryptionStatus" style="font-size: clamp(0.75rem, 3vw, 0.85rem);">🔐 Encrypted</span>
</div>
</div>
<!-- wrapper for scroll -->
<div class="messages-wrapper" id="messagesWrapper" style="padding: 0.75rem; padding-bottom: 80px; flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch; scroll-behavior: smooth;">
<div class="messages-container" id="messagesContainer" role="log" aria-live="polite" aria-label="Chat messages">
<!-- Messages will be inserted here -->
</div>
</div>
</div>
<div class="input-section" id="inputSection" style="display: none; padding: 0.6rem 0.75rem; position: fixed; bottom: 0; left: 0; right: 0; z-index: 1000; background: rgba(20, 20, 20, 0.98); backdrop-filter: blur(10px); border-top: 1px solid #333; box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.3);">
<div class="input-container" style="gap: 0.4rem; max-width: 100%;">
<label for="messageInput" class="visually-hidden">Type your message</label>
<textarea
class="message-input"
id="messageInput"
placeholder="Message ByteChat…"
rows="1"
disabled
maxlength="4000"
aria-label="Type your message"
style="font-size: clamp(0.9rem, 4vw, 1rem); padding: 0.6rem 0.8rem; border-radius: 10px; resize: none; max-height: 120px; min-height: 44px; font-size: 16px; -webkit-appearance: none;"
autocomplete="off"
autocapitalize="sentences"
spellcheck="true"
></textarea>
<button class="send-button" id="sendButton" disabled aria-label="Send message" style="width: 40px; height: 40px; flex-shrink: 0; -webkit-tap-highlight-color: transparent;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="m22 2-7 20-4-9-9-4 20-7z"/>
</svg>
</button>
</div>
</div>
</div>
<!--<div class="security-indicator" id="securityIndicator" style="display: none; bottom: calc(0.75rem + 80px); right: 15px; padding: 0.4rem 0.8rem; font-size: clamp(0.7rem, 3vw, 0.8rem); z-index: 999;">
🔒 End-to-End Encrypted
</div>-->
<script>
// Mobile keyboard handling
let initialViewportHeight = window.innerHeight;
function adjustForKeyboard() {
const currentHeight = window.visualViewport ? window.visualViewport.height : window.innerHeight;
const diff = initialViewportHeight - currentHeight;
const messagesWrapper = document.getElementById('messagesWrapper');
if (diff > 150) { // Keyboard is likely open
keyboardOpen = true;
if (messagesWrapper) {
messagesWrapper.style.paddingBottom = '140px'; // Even more padding for mobile keyboard
// Reset scroll state and aggressively scroll to bottom
userScrolled = false;
// Multiple scroll attempts with increasing delays
setTimeout(() => {
messagesWrapper.scrollTop = messagesWrapper.scrollHeight + 150;
}, 100);
setTimeout(() => {
messagesWrapper.scrollTop = messagesWrapper.scrollHeight + 150;
}, 250);
setTimeout(() => {
messagesWrapper.scrollTop = messagesWrapper.scrollHeight + 150;
}, 400);
// iOS often needs even more attempts
if (isIOS) {
setTimeout(() => {
messagesWrapper.scrollTop = messagesWrapper.scrollHeight + 200;
}, 600);
setTimeout(() => {
messagesWrapper.scrollTop = messagesWrapper.scrollHeight + 200;
}, 800);
setTimeout(() => {
messagesWrapper.scrollTop = messagesWrapper.scrollHeight + 200;
}, 1000);
}
}
} else {
keyboardOpen = false;
if (messagesWrapper) {
messagesWrapper.style.paddingBottom = '80px';
// Scroll to bottom when keyboard closes too
setTimeout(() => {
if (!userScrolled) {
messagesWrapper.scrollTop = messagesWrapper.scrollHeight + 100;
}
}, 200);
setTimeout(() => {
if (!userScrolled) {
messagesWrapper.scrollTop = messagesWrapper.scrollHeight + 100;
}
}, 400);
}
}
}
// Listen for viewport changes (keyboard open/close)
if (window.visualViewport) {
window.visualViewport.addEventListener('resize', adjustForKeyboard);
} else {
window.addEventListener('resize', adjustForKeyboard);
}
// Mobile-optimized focus handling for textarea
document.addEventListener('DOMContentLoaded', function() {
const messageInput = document.getElementById('messageInput');
if (messageInput) {
messageInput.addEventListener('focus', function() {
// Reset scroll state when focusing input
userScrolled = false;
// Multiple timeout attempts for different mobile browsers
setTimeout(adjustForKeyboard, 200);
setTimeout(adjustForKeyboard, 400);
if (isMobile) {
setTimeout(adjustForKeyboard, 600);
setTimeout(() => {
const messagesWrapper = document.getElementById('messagesWrapper');
if (messagesWrapper) {
messagesWrapper.scrollTop = messagesWrapper.scrollHeight + 200;
}
}, 800);
// Additional scroll attempt for stubborn mobile browsers
setTimeout(() => {
const messagesWrapper = document.getElementById('messagesWrapper');
if (messagesWrapper) {
messagesWrapper.scrollTop = messagesWrapper.scrollHeight + 250;
}
}, 1200);
}
});
messageInput.addEventListener('blur', function() {
setTimeout(adjustForKeyboard, 200);
setTimeout(adjustForKeyboard, 400);
});
// Handle input events for better mobile experience
messageInput.addEventListener('input', function() {
if (isMobile && !userScrolled) {
setTimeout(() => {
scrollToBottom(false);
}, 100);
}
});
}
});
// Mobile-optimized auto-scroll functionality
let isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || window.innerWidth <= 768;
let isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
let keyboardOpen = false;
function scrollToBottom(smooth = true) {
const messagesWrapper = document.getElementById('messagesWrapper');
if (messagesWrapper) {
// On mobile, especially iOS, direct scrollTop assignment is more reliable
if (isMobile) {
// Different scroll behavior based on keyboard state
const extraPadding = keyboardOpen ? 200 : 100;
const targetScroll = messagesWrapper.scrollHeight + extraPadding;
messagesWrapper.scrollTop = targetScroll;
// Multiple aggressive scroll attempts for mobile reliability
setTimeout(() => {
messagesWrapper.scrollTop = messagesWrapper.scrollHeight + extraPadding;
}, 50);
setTimeout(() => {
messagesWrapper.scrollTop = messagesWrapper.scrollHeight + extraPadding;
}, 150);
setTimeout(() => {
messagesWrapper.scrollTop = messagesWrapper.scrollHeight + extraPadding;
}, 300);
// Extra attempts when just viewing (no keyboard)
if (!keyboardOpen) {
setTimeout(() => {
messagesWrapper.scrollTop = messagesWrapper.scrollHeight + 150;
}, 500);
setTimeout(() => {
messagesWrapper.scrollTop = messagesWrapper.scrollHeight + 150;
}, 750);
}
// Final iOS-specific attempt
if (isIOS) {
const finalPadding = keyboardOpen ? 250 : 200;
setTimeout(() => {
messagesWrapper.scrollTop = messagesWrapper.scrollHeight + finalPadding;
}, keyboardOpen ? 500 : 1000);
}
} else {
if (smooth) {
messagesWrapper.scrollTo({
top: messagesWrapper.scrollHeight,
behavior: 'smooth'
});
} else {
messagesWrapper.scrollTop = messagesWrapper.scrollHeight;
}
}
}
}
// Function to check if user is near bottom of messages
function isNearBottom() {
const messagesWrapper = document.getElementById('messagesWrapper');
if (!messagesWrapper) return true;
// Larger threshold for mobile to account for touch scrolling momentum
const threshold = isMobile ? 150 : 100;
return messagesWrapper.scrollHeight - messagesWrapper.scrollTop - messagesWrapper.clientHeight < threshold;
}
// Mobile scroll state management
let lastScrollTop = 0;
let userScrolled = false;
let scrollTimeout;
let touchStartY = 0;
let isScrolling = false;
// Detect user scroll with mobile optimizations
document.addEventListener('DOMContentLoaded', function() {
const messagesWrapper = document.getElementById('messagesWrapper');
if (messagesWrapper) {
// Handle touch events for better mobile detection
messagesWrapper.addEventListener('touchstart', function(e) {
touchStartY = e.touches[0].clientY;
isScrolling = false;
clearTimeout(scrollTimeout);
}, { passive: true });
messagesWrapper.addEventListener('touchmove', function(e) {
if (!isScrolling) {
isScrolling = true;
const touchY = e.touches[0].clientY;
const deltaY = touchStartY - touchY;
// If user is swiping up (scrolling up), mark as user scrolled
if (deltaY < -10 && !isNearBottom()) {
userScrolled = true;
}
}
}, { passive: true });
messagesWrapper.addEventListener('touchend', function() {
// Check scroll position after touch ends with delay for momentum scrolling
setTimeout(() => {
if (isNearBottom()) {
userScrolled = false;
}
isScrolling = false;
}, 300);
}, { passive: true });
// Regular scroll event with debouncing for mobile
messagesWrapper.addEventListener('scroll', function() {
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(() => {
const currentScrollTop = messagesWrapper.scrollTop;
const maxScroll = messagesWrapper.scrollHeight - messagesWrapper.clientHeight;
// More lenient detection for mobile
const upScrollThreshold = isMobile ? 30 : 50;
// If user scrolled up manually, don't auto-scroll for new messages
if (currentScrollTop < lastScrollTop && currentScrollTop < maxScroll - upScrollThreshold) {
userScrolled = true;
}
// If user scrolled to near bottom, resume auto-scrolling
if (isNearBottom()) {
userScrolled = false;
}
lastScrollTop = currentScrollTop;
}, isMobile ? 150 : 50); // Longer debounce for mobile
}, { passive: true });
}
});
// Mobile-optimized message adding function
function addMessageWithAutoScroll(messageElement) {
const messagesContainer = document.getElementById('messagesContainer');
if (messagesContainer && messageElement) {
messagesContainer.appendChild(messageElement);
// Only auto-scroll if user hasn't manually scrolled up
if (!userScrolled) {
const messagesWrapper = document.getElementById('messagesWrapper');
if (messagesWrapper) {
// Immediate first scroll
messagesWrapper.scrollTop = messagesWrapper.scrollHeight + 100;
// Progressive scroll attempts with different padding
setTimeout(() => {
const padding = keyboardOpen ? 200 : 150;
messagesWrapper.scrollTop = messagesWrapper.scrollHeight + padding;
}, 100);
setTimeout(() => {
const padding = keyboardOpen ? 200 : 150;
messagesWrapper.scrollTop = messagesWrapper.scrollHeight + padding;
}, 250);
// Extra attempts when just viewing (no keyboard)
if (!keyboardOpen && isMobile) {
setTimeout(() => {
messagesWrapper.scrollTop = messagesWrapper.scrollHeight + 200;
}, 400);
setTimeout(() => {
messagesWrapper.scrollTop = messagesWrapper.scrollHeight + 250;
}, 600);
// Final attempt for stubborn browsers
setTimeout(() => {
messagesWrapper.scrollTop = messagesWrapper.scrollHeight + 300;
}, 1000);
}
// iOS-specific final attempt
if (isIOS) {
const finalPadding = keyboardOpen ? 250 : 300;
setTimeout(() => {
messagesWrapper.scrollTop = messagesWrapper.scrollHeight + finalPadding;
}, keyboardOpen ? 600 : 1200);
}
}
}
}
}
// Force scroll with mobile optimizations
function forceScrollToBottom() {
userScrolled = false;
const messagesWrapper = document.getElementById('messagesWrapper');
if (messagesWrapper) {
// Immediate aggressive scroll
messagesWrapper.scrollTop = messagesWrapper.scrollHeight + 150;
setTimeout(() => {
messagesWrapper.scrollTop = messagesWrapper.scrollHeight + 150;
}, 100);
setTimeout(() => {
messagesWrapper.scrollTop = messagesWrapper.scrollHeight + 200;
}, 300);
// Extra attempts for iOS
if (isIOS) {
setTimeout(() => {
messagesWrapper.scrollTop = messagesWrapper.scrollHeight + 250;
}, 500);
setTimeout(() => {
messagesWrapper.scrollTop = messagesWrapper.scrollHeight + 250;
}, 700);
}
}
}
// Mobile-optimized mutation observer
document.addEventListener('DOMContentLoaded', function() {
const messagesContainer = document.getElementById('messagesContainer');
if (messagesContainer) {
// Create a MutationObserver to watch for new messages
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
// Check if any added nodes are message elements
const hasNewMessage = Array.from(mutation.addedNodes).some(node =>
node.nodeType === Node.ELEMENT_NODE &&
(node.classList.contains('message-group') || node.querySelector('.message-group'))
);
if (hasNewMessage && !userScrolled) {
const messagesWrapper = document.getElementById('messagesWrapper');
if (messagesWrapper) {
// Immediate scroll
messagesWrapper.scrollTop = messagesWrapper.scrollHeight + 100;
setTimeout(() => {
const padding = keyboardOpen ? 200 : 150;
messagesWrapper.scrollTop = messagesWrapper.scrollHeight + padding;
}, 100);
setTimeout(() => {
const padding = keyboardOpen ? 250 : 200;
messagesWrapper.scrollTop = messagesWrapper.scrollHeight + padding;
}, 300);
// Extra scrolling when just viewing (no keyboard)
if (!keyboardOpen && isMobile) {
setTimeout(() => {
messagesWrapper.scrollTop = messagesWrapper.scrollHeight + 250;
}, 500);
setTimeout(() => {
messagesWrapper.scrollTop = messagesWrapper.scrollHeight + 300;
}, 800);
// Final aggressive attempt for viewing mode
setTimeout(() => {
messagesWrapper.scrollTop = messagesWrapper.scrollHeight + 350;
}, 1200);
}
}
}
}
});
});
// Start observing
observer.observe(messagesContainer, {
childList: true,
subtree: true
});
}
});
// Expose functions globally so your existing JavaScript can use them
window.ByteChat = window.ByteChat || {};
window.ByteChat.scrollToBottom = scrollToBottom;
window.ByteChat.forceScrollToBottom = forceScrollToBottom;
window.ByteChat.addMessageWithAutoScroll = addMessageWithAutoScroll;
</script>
</body>
</html>