Compare commits
3 Commits
issue1fix
...
frontendch
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d9346219d | |||
| 34cdff241b | |||
| 120bddba62 |
Binary file not shown.
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 208 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 2.1 MiB |
@@ -26,6 +26,20 @@
|
|||||||
"icon": [
|
"icon": [
|
||||||
"icons/icon.ico",
|
"icons/icon.ico",
|
||||||
"icons/icon.png"
|
"icons/icon.png"
|
||||||
]
|
],
|
||||||
|
"windows": {
|
||||||
|
"allowDowngrades": true,
|
||||||
|
"certificateThumbprint": null,
|
||||||
|
"digestAlgorithm": null,
|
||||||
|
"nsis": null,
|
||||||
|
"signCommand": null,
|
||||||
|
"timestampUrl": null,
|
||||||
|
"tsp": false,
|
||||||
|
"webviewInstallMode": {
|
||||||
|
"silent": true,
|
||||||
|
"type": "offlineInstaller"
|
||||||
|
},
|
||||||
|
"wix": null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
src/assets/7Segment.ttf
Normal file
BIN
src/assets/7Segment.ttf
Normal file
Binary file not shown.
BIN
src/assets/icon.ico
Normal file
BIN
src/assets/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 208 KiB |
326
src/index.html
326
src/index.html
@@ -6,19 +6,18 @@
|
|||||||
<title>ByteChat</title>
|
<title>ByteChat</title>
|
||||||
<meta http-equiv="X-Content-Type-Options" content="nosniff">
|
<meta http-equiv="X-Content-Type-Options" content="nosniff">
|
||||||
<meta http-equiv="X-Frame-Options" content="DENY">
|
<meta http-equiv="X-Frame-Options" content="DENY">
|
||||||
<link rel="stylesheet" href="./stylesheet.css">
|
<link rel="stylesheet" href="stylesheet.css">
|
||||||
<link rel="icon" type="image/png" href="./assets/favicon.ico">
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/socket.io-client@4.7.2/dist/socket.io.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/socket.io-client@4.7.2/dist/socket.io.min.js"></script>
|
||||||
<script src="./main.js"></script>
|
|
||||||
<script src="./frontend.js"></script>
|
|
||||||
</head>
|
</head>
|
||||||
<body style="overflow-y: auto;">
|
<body style="overflow-y: auto;">
|
||||||
<div class="chat-container">
|
<div class="chat-container">
|
||||||
<div class="chat-header" style="padding: 0.75rem 1rem;">
|
<div class="chat-header" style="padding: 0.75rem 1rem;">
|
||||||
<div class="header-content" style="flex-wrap: wrap; gap: 0.5rem;">
|
<div class="header-content" style="flex-wrap: wrap; gap: 0.5rem;">
|
||||||
<div class="logo" style="font-size: clamp(1.25rem, 4vw, 1.5rem);">
|
<div class="logo"
|
||||||
<a href="/" style="text-decoration: none; font-weight: inherit; font-style: inherit; color: inherit;">
|
style="font-size: clamp(1.25rem, 4vw, 1.5rem); display: flex; align-items: center;">
|
||||||
ByteChat
|
<a href="/" style="text-decoration: none; font-weight: inherit; font-style: inherit; color: inherit; font-family: SevenSegment; display: flex; align-items: center;">
|
||||||
|
<img src="assets/icon.ico" style="width:4vh; height: 5vh; margin-right: 0.5rem;">
|
||||||
|
- Bytechat
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="encryption-badge" id="encryptionBadge" style="font-size: clamp(0.75rem, 3vw, 0.85rem); padding: 0.4rem 0.8rem;">
|
<div class="encryption-badge" id="encryptionBadge" style="font-size: clamp(0.75rem, 3vw, 0.85rem); padding: 0.4rem 0.8rem;">
|
||||||
@@ -26,8 +25,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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;">
|
<!-- Welcome Screen -->
|
||||||
<h1 class="welcome-title" style="font-size: clamp(1.75rem, 8vw, 3rem); margin-bottom: 0.75rem;">ByteChat</h1>
|
<div id="welcomeScreen" class="welcome-screen" style="padding: 1rem;">
|
||||||
|
<h1 class="welcome-title" style="font-size: clamp(1.75rem, 8vw, 3rem); margin-bottom: 0.75rem; font-family: SevenSegment;">ByteChat</h1>
|
||||||
<div class="room-section" style="width: 100%; max-width: 500px;">
|
<div class="room-section" style="width: 100%; max-width: 500px;">
|
||||||
<div class="room-input-container" style="flex-direction: column; gap: 0.75rem; width: 100%;">
|
<div class="room-input-container" style="flex-direction: column; gap: 0.75rem; width: 100%;">
|
||||||
<input type="text"
|
<input type="text"
|
||||||
@@ -49,6 +49,12 @@
|
|||||||
<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" 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>
|
<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>
|
||||||
|
<!-- Browse Public Rooms Button -->
|
||||||
|
<div style="display: flex; justify-content: center; width: 100%;">
|
||||||
|
<button class="btn btn-secondary" id="browsePublicRoomsBtn" style="font-size: clamp(0.85rem, 3.5vw, 1rem); padding: 0.6rem 1.5rem;">
|
||||||
|
Browse Public Rooms
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-text" id="statusText" style="font-size: clamp(0.8rem, 3vw, 0.9rem);">
|
<div class="status-text" id="statusText" style="font-size: clamp(0.8rem, 3vw, 0.9rem);">
|
||||||
<div class="loading-message">
|
<div class="loading-message">
|
||||||
@@ -57,52 +63,278 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Laptop Image and Desktop App Section -->
|
||||||
|
<!--<div style="margin-top: 2rem; display: flex; flex-direction: column; align-items: center; width: 100%;">
|
||||||
|
<img class="laptopimg"
|
||||||
|
src="{{ url_for('static', filename='laptop.png')}}"
|
||||||
|
alt="ByteChat Desktop Application"
|
||||||
|
style="width: clamp(200px, 50vw, 400px); height: auto; border-radius: 12px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); transition: transform 0.3s ease, box-shadow 0.3s ease; margin-bottom: 1.5rem; display: block;">
|
||||||
|
-->
|
||||||
|
<!-- Desktop App Section -->
|
||||||
|
<!--
|
||||||
|
<div style="text-align: center; max-width: 600px; padding: 0 1rem;">
|
||||||
|
<div style="margin-top: 2rem; padding: 1rem; border: 1px solid #333; border-radius: 8px; background: rgba(0, 0, 0, 0.2);">
|
||||||
|
<h3 style="font-size: clamp(1rem, 4vw, 1.25rem); margin-bottom: 0.75rem; color: #ffffff;">Release Information</h3>
|
||||||
|
<div style="font-size: clamp(0.8rem, 3vw, 0.95rem); color: #888; line-height: 1.5;">
|
||||||
|
<div id="rss-feed" style="padding: 1rem; color: #ccc;">
|
||||||
|
<p>Loading release notes...</p>
|
||||||
|
</div>
|
||||||
|
<div id="show-legacy-container" style="display: none; text-align: center; margin-top: 1rem;">
|
||||||
|
<button id="show-legacy-btn" class="btn" style="font-size: 0.8rem; padding: 0.4rem 0.8rem;">
|
||||||
|
Show Legacy Releases
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Public Rooms Browser - Fixed structure -->
|
||||||
|
<div id="publicRoomsBrowser" style="
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
z-index: 2000;
|
||||||
|
padding: 1rem;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
overflow: auto;
|
||||||
|
">
|
||||||
|
<div id="browserContent" style="
|
||||||
|
max-width: 95%;
|
||||||
|
max-height: 95%;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #333;
|
||||||
|
min-height: 80vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
">
|
||||||
|
<!-- Header -->
|
||||||
|
<div style="
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
">
|
||||||
|
<div>
|
||||||
|
<h2 style="
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
font-size: clamp(1.25rem, 5vw, 1.75rem);
|
||||||
|
color: #00ff88;
|
||||||
|
">Public Rooms</h2>
|
||||||
|
<p style="
|
||||||
|
margin: 0;
|
||||||
|
color: #888;
|
||||||
|
font-size: clamp(0.85rem, 3.5vw, 1rem);
|
||||||
|
" id="roomsStats">Browse and join active public rooms</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="btn btn-secondary"
|
||||||
|
id="closePublicRoomsBrowserBtn"
|
||||||
|
style="
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
"
|
||||||
|
>Close</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Controls -->
|
||||||
|
<div style="
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
">
|
||||||
|
<select id="roomsSortSelect" style="
|
||||||
|
background: #333;
|
||||||
|
color: #ffffff;
|
||||||
|
border: 1px solid #555;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
min-width: 120px;
|
||||||
|
">
|
||||||
|
<option value="activity">Most Active</option>
|
||||||
|
<option value="created">Newest</option>
|
||||||
|
<option value="users">Most Users</option>
|
||||||
|
<option value="messages">Most Messages</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="minUsersFilter"
|
||||||
|
placeholder="Min users"
|
||||||
|
min="0"
|
||||||
|
max="1000"
|
||||||
|
style="
|
||||||
|
background: #333;
|
||||||
|
color: #ffffff;
|
||||||
|
border: 1px solid #555;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
width: 150px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn"
|
||||||
|
id="refreshPublicRoomsBtn"
|
||||||
|
style="
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
"
|
||||||
|
>Refresh</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading State -->
|
||||||
|
<div id="roomsLoading" style="
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 3rem;
|
||||||
|
flex: 1;
|
||||||
|
">
|
||||||
|
<div style="
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
color: #888;
|
||||||
|
">
|
||||||
|
<div class="loading-spinner" style="
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid #333;
|
||||||
|
border-top: 2px solid #00ff88;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
"></div>
|
||||||
|
<span>Loading public rooms...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rooms List -->
|
||||||
|
<div id="roomsList" style="
|
||||||
|
flex: 1;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
display: none;
|
||||||
|
">
|
||||||
|
<!-- Rooms will be populated here -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div id="roomsEmpty" style="
|
||||||
|
display: none;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 3rem;
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
">
|
||||||
|
<div>
|
||||||
|
<h3 style="margin: 0 0 0.5rem 0; color: #ffffff;">No Public Rooms Found</h3>
|
||||||
|
<p style="margin: 0; color: #888;">Check back later or create your own room</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Chat Screen -->
|
<!-- Chat Screen -->
|
||||||
<div id="chatScreen" style="display: none;" class="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;">
|
<!-- Room info moved to top and fixed position -->
|
||||||
<div class="room-details" style="min-width: auto; flex: 1;">
|
<div class="room-info" id="roomInfo" style="
|
||||||
<span style="font-size: clamp(0.75rem, 3vw, 0.85rem);">Room: <strong id="currentRoomId"></strong></span>
|
display: none;
|
||||||
<span id="messageCounter" style="font-size: clamp(0.75rem, 3vw, 0.85rem);">Messages: 0/256</span>
|
padding: 0.6rem 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
background: rgba(20, 20, 20, 0.98);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
||||||
|
">
|
||||||
|
<div class="room-details" style="min-width: auto; flex: 1; display: flex; flex-wrap: wrap; gap: 1rem; justify-content: space-between;">
|
||||||
|
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
|
||||||
|
<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 style="display: flex; gap: 1rem; flex-wrap: wrap;">
|
||||||
|
<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>
|
||||||
</div>
|
</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>
|
<!-- wrapper for scroll -->
|
||||||
<span id="encryptionStatus" style="font-size: clamp(0.75rem, 3vw, 0.85rem);">🔐 Encrypted</span>
|
<div class="messages-wrapper" id="messagesWrapper" style="padding: 0.75rem; padding-top: 0; 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>
|
|
||||||
|
|
||||||
<!-- wrapper for scroll -->
|
<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="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="input-container" style="gap: 0.4rem; max-width: 100%;">
|
||||||
<div class="messages-container" id="messagesContainer" role="log" aria-live="polite" aria-label="Chat messages">
|
<label for="messageInput" class="visually-hidden">Type your message</label>
|
||||||
<!-- Messages will be inserted here -->
|
<textarea
|
||||||
</div>
|
class="message-input"
|
||||||
</div>
|
id="messageInput"
|
||||||
</div>
|
placeholder="Message ByteChat…"
|
||||||
|
rows="1"
|
||||||
<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);">
|
disabled
|
||||||
<div class="input-container" style="gap: 0.4rem; max-width: 100%;">
|
maxlength="4000"
|
||||||
<label for="messageInput" class="visually-hidden">Type your message</label>
|
aria-label="Type your message"
|
||||||
<textarea
|
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;"
|
||||||
class="message-input"
|
autocomplete="off"
|
||||||
id="messageInput"
|
autocapitalize="sentences"
|
||||||
placeholder="Send a Message…"
|
spellcheck="true"
|
||||||
rows="1"
|
></textarea>
|
||||||
disabled
|
<button class="send-button" id="sendButton" disabled aria-label="Send message" style="width: 40px; height: 40px; flex-shrink: 0; -webkit-tap-highlight-color: transparent;">
|
||||||
maxlength="4000"
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||||
aria-label="Type your message"
|
<path d="m22 2-7 20-4-9-9-4 20-7z"/>
|
||||||
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;"
|
</svg>
|
||||||
autocomplete="off"
|
</button>
|
||||||
autocapitalize="sentences"
|
</div>
|
||||||
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Load scripts at the bottom for better initialization -->
|
||||||
|
<script src="main.js" defer></script>
|
||||||
|
<script src="frontend.js" defer></script>
|
||||||
|
<script src="rooms.js"></script>
|
||||||
|
<!--<script src="rss.js" async></script>-->
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -1154,8 +1154,8 @@ async function displayMessage(messageData) {
|
|||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit'
|
minute: '2-digit'
|
||||||
});
|
});
|
||||||
|
// if shit breaks for uuid display fix this
|
||||||
const displayName = messageData.display_name || `User-${messageData.sender_id?.substring(0, 8) || 'Unknown'}`;
|
const displayName = messageData.display_name || `<p style="font-family:SevenSegment;">User-${messageData.sender_id?.substring(0, 8) + '</p>' || 'Unknown'}`;
|
||||||
const avatarText = displayName.substring(0, 2).toUpperCase();
|
const avatarText = displayName.substring(0, 2).toUpperCase();
|
||||||
|
|
||||||
const messageGroup = document.createElement('div');
|
const messageGroup = document.createElement('div');
|
||||||
|
|||||||
737
src/rooms.js
Normal file
737
src/rooms.js
Normal file
@@ -0,0 +1,737 @@
|
|||||||
|
let publicRoomsData = [];
|
||||||
|
let roomsRefreshInterval = null;
|
||||||
|
let currentSortBy = 'activity';
|
||||||
|
let currentMinUsers = 0;
|
||||||
|
let isSubscribedToRooms = false;
|
||||||
|
let isRoomsBrowserOpen = false;
|
||||||
|
let retryAttempts = 0;
|
||||||
|
const MAX_RETRY_ATTEMPTS = 3;
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
const ROOMS_CONFIG = {
|
||||||
|
refreshInterval: 30000, // 30 seconds
|
||||||
|
maxRetries: 3,
|
||||||
|
retryDelay: 2000, // 2 seconds
|
||||||
|
socketTimeout: 25000, // 25 seconds to wait for socket
|
||||||
|
fallbackToHttp: true,
|
||||||
|
autoRefresh: true
|
||||||
|
};
|
||||||
|
|
||||||
|
// Show the public rooms browser
|
||||||
|
function showPublicRoomsBrowser() {
|
||||||
|
console.log('Opening public rooms browser');
|
||||||
|
const browserElement = document.getElementById('publicRoomsBrowser');
|
||||||
|
if (!browserElement) {
|
||||||
|
console.error('Public rooms browser element not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
browserElement.style.display = 'flex';
|
||||||
|
isRoomsBrowserOpen = true;
|
||||||
|
retryAttempts = 0;
|
||||||
|
|
||||||
|
// Reset scroll position
|
||||||
|
const browserContent = document.getElementById('browserContent');
|
||||||
|
if (browserContent) {
|
||||||
|
browserContent.scrollTop = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load rooms and subscribe to updates
|
||||||
|
loadPublicRoomsWebSocket();
|
||||||
|
subscribeToPublicRooms();
|
||||||
|
|
||||||
|
// Set up auto-refresh if enabled
|
||||||
|
if (ROOMS_CONFIG.autoRefresh && !roomsRefreshInterval) {
|
||||||
|
roomsRefreshInterval = setInterval(() => {
|
||||||
|
if (isRoomsBrowserOpen) {
|
||||||
|
console.log('Auto-refreshing public rooms');
|
||||||
|
loadPublicRoomsWebSocket();
|
||||||
|
}
|
||||||
|
}, ROOMS_CONFIG.refreshInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the public rooms browser
|
||||||
|
function closePublicRoomsBrowser() {
|
||||||
|
console.log('Closing public rooms browser');
|
||||||
|
const browserElement = document.getElementById('publicRoomsBrowser');
|
||||||
|
if (browserElement) {
|
||||||
|
browserElement.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
isRoomsBrowserOpen = false;
|
||||||
|
unsubscribeFromPublicRooms();
|
||||||
|
|
||||||
|
// Clear auto-refresh
|
||||||
|
if (roomsRefreshInterval) {
|
||||||
|
clearInterval(roomsRefreshInterval);
|
||||||
|
roomsRefreshInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to live public rooms updates
|
||||||
|
function subscribeToPublicRooms() {
|
||||||
|
if (isSocketAvailable() && !isSubscribedToRooms) {
|
||||||
|
try {
|
||||||
|
socket.emit('subscribe_public_rooms');
|
||||||
|
isSubscribedToRooms = true;
|
||||||
|
console.log('Subscribed to public rooms updates');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error subscribing to public rooms:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsubscribe from public rooms updates
|
||||||
|
function unsubscribeFromPublicRooms() {
|
||||||
|
if (isSocketAvailable() && isSubscribedToRooms) {
|
||||||
|
try {
|
||||||
|
socket.emit('unsubscribe_public_rooms');
|
||||||
|
isSubscribedToRooms = false;
|
||||||
|
console.log('Unsubscribed from public rooms updates');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error unsubscribing from public rooms:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if socket is available and connected
|
||||||
|
function isSocketAvailable() {
|
||||||
|
return typeof socket !== 'undefined' &&
|
||||||
|
socket !== null &&
|
||||||
|
socket.connected;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load public rooms via WebSocket with fallback
|
||||||
|
function loadPublicRoomsWebSocket() {
|
||||||
|
if (!isRoomsBrowserOpen) {
|
||||||
|
console.log('Rooms browser not open, skipping load');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
showLoadingState();
|
||||||
|
updateFiltersFromUI();
|
||||||
|
|
||||||
|
if (isSocketAvailable()) {
|
||||||
|
console.log('Loading rooms via WebSocket');
|
||||||
|
socket.emit('request_public_rooms', {
|
||||||
|
sort: currentSortBy,
|
||||||
|
min_users: currentMinUsers,
|
||||||
|
limit: 50
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set a timeout for WebSocket response
|
||||||
|
setTimeout(() => {
|
||||||
|
if (document.getElementById('roomsLoading')?.style.display !== 'none') {
|
||||||
|
console.warn('WebSocket timeout, falling back to HTTP');
|
||||||
|
loadPublicRoomsHTTP();
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
} else {
|
||||||
|
console.log('WebSocket not available, using HTTP fallback');
|
||||||
|
loadPublicRoomsHTTP();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error requesting public rooms via WebSocket:', error);
|
||||||
|
loadPublicRoomsHTTP();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update filters from UI elements
|
||||||
|
function updateFiltersFromUI() {
|
||||||
|
const sortSelect = document.getElementById('roomsSortSelect');
|
||||||
|
const minUsersFilter = document.getElementById('minUsersFilter');
|
||||||
|
|
||||||
|
currentSortBy = sortSelect?.value || 'activity';
|
||||||
|
currentMinUsers = parseInt(minUsersFilter?.value || '0') || 0;
|
||||||
|
|
||||||
|
console.log(`Updated filters: sort=${currentSortBy}, minUsers=${currentMinUsers}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced HTTP fallback with retry logic
|
||||||
|
async function loadPublicRoomsHTTP() {
|
||||||
|
if (!isRoomsBrowserOpen) {
|
||||||
|
console.log('Rooms browser not open, skipping HTTP load');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
showLoadingState();
|
||||||
|
updateFiltersFromUI();
|
||||||
|
|
||||||
|
console.log(`Loading rooms via HTTP (attempt ${retryAttempts + 1}/${MAX_RETRY_ATTEMPTS})`);
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/rooms/public?sort=${encodeURIComponent(currentSortBy)}&min_users=${currentMinUsers}&limit=50`,
|
||||||
|
{ signal: controller.signal }
|
||||||
|
);
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!data || !Array.isArray(data.rooms)) {
|
||||||
|
throw new Error('Invalid response format');
|
||||||
|
}
|
||||||
|
|
||||||
|
publicRoomsData = data.rooms;
|
||||||
|
console.log(`Loaded ${publicRoomsData.length} rooms via HTTP`);
|
||||||
|
|
||||||
|
// Load stats separately
|
||||||
|
await loadRoomStats();
|
||||||
|
|
||||||
|
displayRooms();
|
||||||
|
retryAttempts = 0; // Reset on success
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading public rooms via HTTP:', error);
|
||||||
|
|
||||||
|
if (error.name === 'AbortError') {
|
||||||
|
console.error('Request timed out');
|
||||||
|
}
|
||||||
|
|
||||||
|
retryAttempts++;
|
||||||
|
|
||||||
|
if (retryAttempts < MAX_RETRY_ATTEMPTS) {
|
||||||
|
console.log(`Retrying in ${ROOMS_CONFIG.retryDelay}ms...`);
|
||||||
|
setTimeout(() => {
|
||||||
|
if (isRoomsBrowserOpen) {
|
||||||
|
loadPublicRoomsHTTP();
|
||||||
|
}
|
||||||
|
}, ROOMS_CONFIG.retryDelay);
|
||||||
|
} else {
|
||||||
|
showErrorState();
|
||||||
|
retryAttempts = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load room statistics
|
||||||
|
async function loadRoomStats() {
|
||||||
|
try {
|
||||||
|
const statsResponse = await fetch('/api/rooms/stats');
|
||||||
|
if (statsResponse.ok) {
|
||||||
|
const stats = await statsResponse.json();
|
||||||
|
updateStatsDisplay(stats);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to load room stats:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh public rooms with user feedback
|
||||||
|
function refreshPublicRooms() {
|
||||||
|
console.log('Manual refresh requested');
|
||||||
|
retryAttempts = 0; // Reset retry counter on manual refresh
|
||||||
|
|
||||||
|
// Show brief loading indicator
|
||||||
|
const refreshBtn = document.getElementById('refreshPublicRoomsBtn');
|
||||||
|
const originalText = refreshBtn?.textContent;
|
||||||
|
|
||||||
|
if (refreshBtn) {
|
||||||
|
refreshBtn.textContent = 'Refreshing...';
|
||||||
|
refreshBtn.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPublicRoomsWebSocket();
|
||||||
|
|
||||||
|
// Reset button after delay
|
||||||
|
setTimeout(() => {
|
||||||
|
if (refreshBtn) {
|
||||||
|
refreshBtn.textContent = originalText || 'Refresh';
|
||||||
|
refreshBtn.disabled = false;
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update stats display with enhanced formatting
|
||||||
|
function updateStatsDisplay(stats) {
|
||||||
|
const statsElement = document.getElementById('roomsStats');
|
||||||
|
if (!statsElement || !stats) return;
|
||||||
|
|
||||||
|
const publicRooms = stats.public_rooms || 0;
|
||||||
|
const totalUsers = stats.total_users || 0;
|
||||||
|
const lastUpdated = new Date().toLocaleTimeString();
|
||||||
|
|
||||||
|
statsElement.innerHTML = `
|
||||||
|
<span>${publicRooms} public room${publicRooms !== 1 ? 's' : ''}</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>${totalUsers} user${totalUsers !== 1 ? 's' : ''} online</span>
|
||||||
|
<span style="color: #666; font-size: 0.85em;">• Updated ${lastUpdated}</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced loading state
|
||||||
|
function showLoadingState() {
|
||||||
|
const loadingElement = document.getElementById('roomsLoading');
|
||||||
|
const listElement = document.getElementById('roomsList');
|
||||||
|
const emptyElement = document.getElementById('roomsEmpty');
|
||||||
|
|
||||||
|
if (loadingElement) {
|
||||||
|
loadingElement.style.display = 'flex';
|
||||||
|
loadingElement.innerHTML = `
|
||||||
|
<div style="display: flex; align-items: center; gap: 1rem; color: #888;">
|
||||||
|
<div class="loading-spinner" style="
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid #333;
|
||||||
|
border-top: 2px solid #00ff88;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
"></div>
|
||||||
|
<span>Loading public rooms...</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
if (listElement) listElement.style.display = 'none';
|
||||||
|
if (emptyElement) emptyElement.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced error state with retry options
|
||||||
|
function showErrorState() {
|
||||||
|
const loadingElement = document.getElementById('roomsLoading');
|
||||||
|
const listElement = document.getElementById('roomsList');
|
||||||
|
const emptyElement = document.getElementById('roomsEmpty');
|
||||||
|
|
||||||
|
if (loadingElement) loadingElement.style.display = 'none';
|
||||||
|
if (listElement) listElement.style.display = 'none';
|
||||||
|
|
||||||
|
if (emptyElement) {
|
||||||
|
emptyElement.style.display = 'flex';
|
||||||
|
emptyElement.innerHTML = `
|
||||||
|
<div style="text-align: center; max-width: 400px;">
|
||||||
|
<div style="font-size: 3rem; margin-bottom: 1rem;">⚠️</div>
|
||||||
|
<h3 style="margin: 0 0 0.5rem 0; color: #ffffff;">Failed to Load Rooms</h3>
|
||||||
|
<p style="margin: 0 0 1.5rem 0; color: #888; line-height: 1.5;">
|
||||||
|
Unable to connect to the server. This could be due to network issues or server maintenance.
|
||||||
|
</p>
|
||||||
|
<div style="display: flex; gap: 0.75rem; justify-content: center; flex-wrap: wrap;">
|
||||||
|
<button class="btn" onclick="refreshPublicRooms()">Try Again</button>
|
||||||
|
<button class="btn btn-secondary" onclick="loadPublicRoomsHTTP()">Force HTTP</button>
|
||||||
|
</div>
|
||||||
|
<p style="margin-top: 1rem; color: #666; font-size: 0.85em;">
|
||||||
|
Attempted ${retryAttempts}/${MAX_RETRY_ATTEMPTS} retries
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced room display with better error handling
|
||||||
|
function displayRooms() {
|
||||||
|
const roomsList = document.getElementById('roomsList');
|
||||||
|
const roomsLoading = document.getElementById('roomsLoading');
|
||||||
|
const roomsEmpty = document.getElementById('roomsEmpty');
|
||||||
|
|
||||||
|
if (roomsLoading) roomsLoading.style.display = 'none';
|
||||||
|
|
||||||
|
if (!Array.isArray(publicRoomsData) || publicRoomsData.length === 0) {
|
||||||
|
if (roomsEmpty) {
|
||||||
|
roomsEmpty.style.display = 'flex';
|
||||||
|
roomsEmpty.innerHTML = `
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<div style="font-size: 2.5rem; margin-bottom: 1rem;">🏠</div>
|
||||||
|
<h3 style="margin: 0 0 0.5rem 0; color: #ffffff;">No Public Rooms Found</h3>
|
||||||
|
<p style="margin: 0 0 1rem 0; color: #888;">
|
||||||
|
${currentMinUsers > 0 ? `Try reducing the minimum users filter (currently ${currentMinUsers})` : 'Check back later or create your own room'}
|
||||||
|
</p>
|
||||||
|
<button class="btn btn-secondary" onclick="refreshPublicRooms()">Refresh</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
if (roomsList) roomsList.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roomsEmpty) roomsEmpty.style.display = 'none';
|
||||||
|
if (roomsList) {
|
||||||
|
roomsList.style.display = 'block';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const roomsHtml = publicRoomsData.map(room => createRoomCard(room)).join('');
|
||||||
|
roomsList.innerHTML = roomsHtml;
|
||||||
|
console.log(`Displayed ${publicRoomsData.length} rooms`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error rendering rooms:', error);
|
||||||
|
showErrorState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced room card with better data handling
|
||||||
|
function createRoomCard(room) {
|
||||||
|
if (!room || typeof room !== 'object') {
|
||||||
|
console.warn('Invalid room data:', room);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const roomId = sanitizeText(room.room_id || 'unknown');
|
||||||
|
const title = sanitizeText(room.title || room.room_id || 'Unnamed Room');
|
||||||
|
const description = sanitizeText(room.description || 'No description available');
|
||||||
|
const userCount = Math.max(0, parseInt(room.user_count) || 0);
|
||||||
|
const messageCount = Math.max(0, parseInt(room.message_count) || 0);
|
||||||
|
const minutesAgo = Math.max(0, parseInt(room.minutes_since_activity) || 0);
|
||||||
|
|
||||||
|
const activityClass = getActivityClass(minutesAgo);
|
||||||
|
const timeAgo = formatTimeAgo(minutesAgo);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="room-card"
|
||||||
|
onclick="joinPublicRoom('${escapeHtml(roomId)}')"
|
||||||
|
style="
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
background: #2a2a2a;
|
||||||
|
"
|
||||||
|
onmouseover="this.style.background='#333'; this.style.borderColor='#555';"
|
||||||
|
onmouseout="this.style.background='#2a2a2a'; this.style.borderColor='#333';">
|
||||||
|
|
||||||
|
<div class="room-title" style="
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ffffff;
|
||||||
|
">
|
||||||
|
${title}
|
||||||
|
<span class="activity-indicator ${activityClass}" style="
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="room-description" style="
|
||||||
|
color: #b0b0b0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
">${description}</div>
|
||||||
|
|
||||||
|
<div class="room-stats" style="
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #888;
|
||||||
|
">
|
||||||
|
<div class="room-stat" style="display: flex; align-items: center; gap: 0.25rem;">
|
||||||
|
<span>👥</span>
|
||||||
|
<span>${userCount} user${userCount !== 1 ? 's' : ''}</span>
|
||||||
|
</div>
|
||||||
|
<div class="room-stat" style="display: flex; align-items: center; gap: 0.25rem;">
|
||||||
|
<span>💬</span>
|
||||||
|
<span>${messageCount} message${messageCount !== 1 ? 's' : ''}</span>
|
||||||
|
</div>
|
||||||
|
<div class="room-stat" style="display: flex; align-items: center; gap: 0.25rem;">
|
||||||
|
<span>🕐</span>
|
||||||
|
<span>Active ${timeAgo}</span>
|
||||||
|
</div>
|
||||||
|
<div class="room-stat" style="display: flex; align-items: center; gap: 0.25rem;">
|
||||||
|
<span>🆔</span>
|
||||||
|
<span style="font-family: monospace; font-size: 0.8em;">${roomId}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced activity classification
|
||||||
|
function getActivityClass(minutesAgo) {
|
||||||
|
if (minutesAgo <= 5) return 'activity-active';
|
||||||
|
if (minutesAgo <= 30) return 'activity-recent';
|
||||||
|
if (minutesAgo <= 120) return 'activity-moderate';
|
||||||
|
return 'activity-old';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced time formatting
|
||||||
|
function formatTimeAgo(minutes) {
|
||||||
|
if (minutes < 1) return 'just now';
|
||||||
|
if (minutes < 60) return `${Math.floor(minutes)}m ago`;
|
||||||
|
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
if (hours < 24) return `${hours}h ago`;
|
||||||
|
|
||||||
|
const days = Math.floor(hours / 24);
|
||||||
|
if (days < 7) return `${days}d ago`;
|
||||||
|
|
||||||
|
const weeks = Math.floor(days / 7);
|
||||||
|
return `${weeks}w ago`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced text sanitization
|
||||||
|
function sanitizeText(text) {
|
||||||
|
if (typeof text !== 'string') return '';
|
||||||
|
return text.trim().substring(0, 200); // Limit length and trim
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced HTML escaping
|
||||||
|
function escapeHtml(text) {
|
||||||
|
if (!text) return '';
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = String(text);
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced room joining with validation
|
||||||
|
function joinPublicRoom(roomId) {
|
||||||
|
if (!roomId || typeof roomId !== 'string') {
|
||||||
|
console.error('Invalid room ID provided:', roomId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate room ID format
|
||||||
|
const roomIdPattern = /^[a-zA-Z0-9\-_]{1,32}$/;
|
||||||
|
if (!roomIdPattern.test(roomId)) {
|
||||||
|
console.error('Invalid room ID format:', roomId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Joining public room: ${roomId}`);
|
||||||
|
|
||||||
|
// Close the browser
|
||||||
|
closePublicRoomsBrowser();
|
||||||
|
|
||||||
|
// Fill in the room input
|
||||||
|
const roomInput = document.getElementById('roomInput');
|
||||||
|
if (roomInput) {
|
||||||
|
roomInput.value = roomId;
|
||||||
|
roomInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear password field for public rooms
|
||||||
|
const roomPasswordInput = document.getElementById('roomPasswordInput');
|
||||||
|
if (roomPasswordInput) {
|
||||||
|
roomPasswordInput.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger join room functionality with delay to ensure UI updates
|
||||||
|
setTimeout(() => {
|
||||||
|
const joinButton = document.getElementById('joinRoomBtn');
|
||||||
|
if (joinButton) {
|
||||||
|
joinButton.click();
|
||||||
|
} else {
|
||||||
|
console.error('Join button not found');
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced WebSocket handlers
|
||||||
|
function setupPublicRoomsWebSocketHandlers() {
|
||||||
|
if (!isSocketAvailable()) {
|
||||||
|
console.log('Socket not available, WebSocket handlers not attached');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle public rooms data response
|
||||||
|
socket.on('public_rooms_data', function(data) {
|
||||||
|
console.log('Received public rooms data via WebSocket:', data);
|
||||||
|
|
||||||
|
if (data && Array.isArray(data.rooms)) {
|
||||||
|
publicRoomsData = data.rooms;
|
||||||
|
} else {
|
||||||
|
console.warn('Invalid public rooms data format');
|
||||||
|
publicRoomsData = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data && data.stats) {
|
||||||
|
updateStatsDisplay(data.stats);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRoomsBrowserOpen) {
|
||||||
|
displayRooms();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle live updates
|
||||||
|
socket.on('public_rooms_updated', function(data) {
|
||||||
|
console.log('Received live public rooms update:', data);
|
||||||
|
|
||||||
|
if (isRoomsBrowserOpen && isSubscribedToRooms) {
|
||||||
|
if (data && Array.isArray(data.rooms)) {
|
||||||
|
publicRoomsData = data.rooms;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data && data.stats) {
|
||||||
|
updateStatsDisplay(data.stats);
|
||||||
|
}
|
||||||
|
|
||||||
|
displayRooms();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle WebSocket errors
|
||||||
|
socket.on('public_rooms_error', function(data) {
|
||||||
|
console.error('Public rooms WebSocket error:', data);
|
||||||
|
if (isRoomsBrowserOpen) {
|
||||||
|
showErrorState();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle connection events
|
||||||
|
socket.on('connect', function() {
|
||||||
|
console.log('Socket connected, resubscribing if browser is open');
|
||||||
|
if (isRoomsBrowserOpen && !isSubscribedToRooms) {
|
||||||
|
subscribeToPublicRooms();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('disconnect', function() {
|
||||||
|
console.log('Socket disconnected');
|
||||||
|
isSubscribedToRooms = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Enhanced public rooms WebSocket handlers attached');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced socket initialization
|
||||||
|
function waitForSocketAndSetupHandlers(retryCount = 0) {
|
||||||
|
if (isSocketAvailable()) {
|
||||||
|
setupPublicRoomsWebSocketHandlers();
|
||||||
|
console.log('WebSocket handlers setup successfully');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retryCount > 100) { // 100 * 250ms = 25 seconds max wait
|
||||||
|
console.warn('Socket failed to initialize after 25 seconds, using HTTP-only mode');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retryCount % 20 === 0) { // Log every 5 seconds
|
||||||
|
console.log(`Waiting for socket... (attempt ${retryCount + 1})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => waitForSocketAndSetupHandlers(retryCount + 1), 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced event listeners with better error handling
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
console.log('Setting up public rooms browser event listeners');
|
||||||
|
|
||||||
|
// Browse button (this was missing!)
|
||||||
|
const browseButton = document.getElementById('browsePublicRoomsBtn');
|
||||||
|
if (browseButton) {
|
||||||
|
browseButton.addEventListener('click', showPublicRoomsBrowser);
|
||||||
|
console.log('Browse public rooms button listener attached');
|
||||||
|
} else {
|
||||||
|
console.warn('Browse public rooms button not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close button
|
||||||
|
const closeButton = document.getElementById('closePublicRoomsBrowserBtn');
|
||||||
|
if (closeButton) {
|
||||||
|
closeButton.addEventListener('click', closePublicRoomsBrowser);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort selector
|
||||||
|
const sortSelect = document.getElementById('roomsSortSelect');
|
||||||
|
if (sortSelect) {
|
||||||
|
sortSelect.addEventListener('change', refreshPublicRooms);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Min users filter
|
||||||
|
const minUsersFilter = document.getElementById('minUsersFilter');
|
||||||
|
if (minUsersFilter) {
|
||||||
|
minUsersFilter.addEventListener('change', refreshPublicRooms);
|
||||||
|
minUsersFilter.addEventListener('input', debounce(refreshPublicRooms, 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh button
|
||||||
|
const refreshButton = document.getElementById('refreshPublicRoomsBtn');
|
||||||
|
if (refreshButton) {
|
||||||
|
refreshButton.addEventListener('click', refreshPublicRooms);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backdrop click handler
|
||||||
|
const browserElement = document.getElementById('publicRoomsBrowser');
|
||||||
|
if (browserElement) {
|
||||||
|
browserElement.addEventListener('click', function(e) {
|
||||||
|
if (e.target === this) {
|
||||||
|
closePublicRoomsBrowser();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard shortcuts
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (!isRoomsBrowserOpen) return;
|
||||||
|
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
closePublicRoomsBrowser();
|
||||||
|
} else if (e.key === 'F5' || (e.ctrlKey && e.key === 'r')) {
|
||||||
|
e.preventDefault();
|
||||||
|
refreshPublicRooms();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start WebSocket setup
|
||||||
|
waitForSocketAndSetupHandlers();
|
||||||
|
|
||||||
|
console.log('Public rooms browser setup complete');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Utility: Debounce function
|
||||||
|
function debounce(func, wait) {
|
||||||
|
let timeout;
|
||||||
|
return function executedFunction(...args) {
|
||||||
|
const later = () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
func.apply(this, args);
|
||||||
|
};
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(later, wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup on page unload
|
||||||
|
window.addEventListener('beforeunload', function() {
|
||||||
|
if (isSubscribedToRooms) {
|
||||||
|
unsubscribeFromPublicRooms();
|
||||||
|
}
|
||||||
|
if (roomsRefreshInterval) {
|
||||||
|
clearInterval(roomsRefreshInterval);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add CSS for activity indicators if not already present
|
||||||
|
if (!document.getElementById('roomsActivityStyles')) {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.id = 'roomsActivityStyles';
|
||||||
|
style.textContent = `
|
||||||
|
.activity-active {
|
||||||
|
background-color: #00ff88 !important;
|
||||||
|
box-shadow: 0 0 6px rgba(0, 255, 136, 0.6);
|
||||||
|
}
|
||||||
|
.activity-recent {
|
||||||
|
background-color: #ffaa00 !important;
|
||||||
|
}
|
||||||
|
.activity-moderate {
|
||||||
|
background-color: #888 !important;
|
||||||
|
}
|
||||||
|
.activity-old {
|
||||||
|
background-color: #444 !important;
|
||||||
|
}
|
||||||
|
.room-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
262
src/rss.js
Normal file
262
src/rss.js
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
async function loadRSS() {
|
||||||
|
const container = document.getElementById("rss-feed");
|
||||||
|
const showLegacyContainer = document.getElementById("show-legacy-container");
|
||||||
|
const showLegacyBtn = document.getElementById("show-legacy-btn");
|
||||||
|
|
||||||
|
if (!container) {
|
||||||
|
console.error("RSS feed container not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
container.innerHTML = "<p>Loading release notes...</p>";
|
||||||
|
|
||||||
|
const rssUrl = "https://rattatwinko.servecounterstrike.com/gitea/rattatwinko/bytechat-desktop/releases.rss";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try direct fetch first
|
||||||
|
let response;
|
||||||
|
let xmlText;
|
||||||
|
|
||||||
|
try {
|
||||||
|
response = await fetch(rssUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/rss+xml, application/xml, text/xml, */*',
|
||||||
|
'Cache-Control': 'no-cache'
|
||||||
|
},
|
||||||
|
mode: 'cors'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
xmlText = await response.text();
|
||||||
|
} else {
|
||||||
|
throw new Error(`HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
} catch (corsError) {
|
||||||
|
console.warn("Direct fetch failed, trying proxy...", corsError);
|
||||||
|
|
||||||
|
// Try CORS proxy
|
||||||
|
const proxyUrl = `https://api.allorigins.win/get?url=${encodeURIComponent(rssUrl)}`;
|
||||||
|
const proxyResponse = await fetch(proxyUrl);
|
||||||
|
|
||||||
|
if (!proxyResponse.ok) {
|
||||||
|
throw new Error(`Proxy failed: ${proxyResponse.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const proxyData = await proxyResponse.json();
|
||||||
|
if (!proxyData.contents) {
|
||||||
|
throw new Error("No content from proxy");
|
||||||
|
}
|
||||||
|
|
||||||
|
xmlText = proxyData.contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the XML
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const xml = parser.parseFromString(xmlText, "application/xml");
|
||||||
|
|
||||||
|
// Check for parsing errors
|
||||||
|
const parserError = xml.querySelector("parsererror");
|
||||||
|
if (parserError) {
|
||||||
|
throw new Error("XML parsing failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = parseRSSItems(xml);
|
||||||
|
displayRSSItems(items, container, showLegacyContainer, showLegacyBtn);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to load RSS:", err);
|
||||||
|
|
||||||
|
// Show error with retry and direct link
|
||||||
|
container.innerHTML = `
|
||||||
|
<div style="padding: 1rem; background: rgba(255, 100, 100, 0.1); border: 1px solid rgba(255, 100, 100, 0.3); border-radius: 6px; text-align: center;">
|
||||||
|
<p style="margin: 0 0 0.5rem 0;"><strong>Unable to load release notes</strong></p>
|
||||||
|
<p style="font-size: 0.9em; margin: 0.5rem 0; color: #ccc;">
|
||||||
|
Network issue detected.
|
||||||
|
</p>
|
||||||
|
<div style="display: flex; gap: 0.5rem; justify-content: center; flex-wrap: wrap; margin-top: 1rem;">
|
||||||
|
<button onclick="loadRSS()" style="padding: 0.4rem 0.8rem; background: #00ff88; color: black; border: none; border-radius: 4px; cursor: pointer; font-weight: 500;">
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
<a href="${rssUrl}" target="_blank" rel="noopener" style="padding: 0.4rem 0.8rem; background: #333; color: #00ff88; text-decoration: none; border-radius: 4px; display: inline-block;">
|
||||||
|
View Direct
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseRSSItems(xml) {
|
||||||
|
console.log("Parsing RSS XML...");
|
||||||
|
|
||||||
|
const items = Array.from(xml.querySelectorAll("item")).map(item => {
|
||||||
|
const title = item.querySelector("title")?.textContent?.trim() || "Untitled Release";
|
||||||
|
const link = item.querySelector("link")?.textContent?.trim() || "#";
|
||||||
|
const pubDate = item.querySelector("pubDate")?.textContent?.trim() || "";
|
||||||
|
const author = item.querySelector("author")?.textContent?.trim() || "Unknown";
|
||||||
|
const description = item.querySelector("description")?.textContent?.trim() || "";
|
||||||
|
|
||||||
|
// Extract content from CDATA if available
|
||||||
|
const contentEncoded = item.querySelector("content\\:encoded");
|
||||||
|
let content = "";
|
||||||
|
if (contentEncoded) {
|
||||||
|
content = contentEncoded.textContent.trim();
|
||||||
|
// Remove HTML tags for a clean preview
|
||||||
|
content = content.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
|
||||||
|
// Limit to first 150 characters
|
||||||
|
if (content.length > 150) {
|
||||||
|
content = content.substring(0, 150) + "...";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Parsed item:", { title, pubDate, author });
|
||||||
|
return { title, link, pubDate, author, description, content };
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Found ${items.length} releases`);
|
||||||
|
|
||||||
|
// Sort by date (newest first)
|
||||||
|
return items.sort((a, b) => {
|
||||||
|
if (!a.pubDate || !b.pubDate) return 0;
|
||||||
|
return new Date(b.pubDate) - new Date(a.pubDate);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayRSSItems(items, container, showLegacyContainer, showLegacyBtn) {
|
||||||
|
if (!items.length) {
|
||||||
|
container.innerHTML = "<p style='text-align: center; color: #888;'>No releases found.</p>";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Displaying items:", items);
|
||||||
|
|
||||||
|
const visibleItems = 2;
|
||||||
|
const hasMoreItems = items.length > visibleItems;
|
||||||
|
|
||||||
|
let html = "<div class='release-list' style='display: flex; flex-direction: column; gap: 1rem;'>";
|
||||||
|
|
||||||
|
items.forEach((item, index) => {
|
||||||
|
const isHidden = index >= visibleItems;
|
||||||
|
|
||||||
|
// Format the date nicely
|
||||||
|
let formattedDate = "";
|
||||||
|
if (item.pubDate) {
|
||||||
|
try {
|
||||||
|
const date = new Date(item.pubDate);
|
||||||
|
formattedDate = date.toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
formattedDate = item.pubDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="release-item ${isHidden ? 'legacy-release' : ''}"
|
||||||
|
style="
|
||||||
|
${isHidden ? 'display: none;' : ''}
|
||||||
|
padding: 1rem;
|
||||||
|
background: linear-gradient(135deg, rgba(0, 255, 136, 0.05) 0%, rgba(0, 255, 136, 0.02) 100%);
|
||||||
|
border: 1px solid rgba(0, 255, 136, 0.2);
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid #00ff88;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
"
|
||||||
|
onmouseover="this.style.background='linear-gradient(135deg, rgba(0, 255, 136, 0.08) 0%, rgba(0, 255, 136, 0.04) 100%)'; this.style.transform='translateY(-1px)'"
|
||||||
|
onmouseout="this.style.background='linear-gradient(135deg, rgba(0, 255, 136, 0.05) 0%, rgba(0, 255, 136, 0.02) 100%)'; this.style.transform='translateY(0px)'">
|
||||||
|
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.5rem; flex-wrap: wrap; gap: 0.5rem;">
|
||||||
|
<h4 style="margin: 0; color: #00ff88; font-size: 1.1rem; font-weight: 600;">
|
||||||
|
<a href="${item.link}"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
style="color: inherit; text-decoration: none;"
|
||||||
|
onmouseover="this.style.textDecoration='underline'"
|
||||||
|
onmouseout="this.style.textDecoration='none'">
|
||||||
|
${item.title}
|
||||||
|
</a>
|
||||||
|
</h4>
|
||||||
|
${formattedDate ? `
|
||||||
|
<span style="color: #888; font-size: 0.85rem; white-space: nowrap;">
|
||||||
|
${formattedDate}
|
||||||
|
</span>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${item.author !== 'Unknown' ? `
|
||||||
|
<div style="color: #666; font-size: 0.8rem; margin-bottom: 0.5rem;">
|
||||||
|
by ${item.author}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
${item.content ? `
|
||||||
|
<div style="color: #ccc; font-size: 0.9rem; line-height: 1.4; margin-top: 0.5rem;">
|
||||||
|
${item.content}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += "</div>";
|
||||||
|
container.innerHTML = html;
|
||||||
|
|
||||||
|
// Setup legacy releases toggle
|
||||||
|
if (hasMoreItems && showLegacyContainer && showLegacyBtn) {
|
||||||
|
showLegacyContainer.style.display = 'block';
|
||||||
|
|
||||||
|
// Remove existing listeners by cloning
|
||||||
|
const newBtn = showLegacyBtn.cloneNode(true);
|
||||||
|
showLegacyBtn.parentNode.replaceChild(newBtn, showLegacyBtn);
|
||||||
|
|
||||||
|
let showingLegacy = false;
|
||||||
|
newBtn.addEventListener('click', function() {
|
||||||
|
showingLegacy = !showingLegacy;
|
||||||
|
const legacyItems = container.querySelectorAll('.legacy-release');
|
||||||
|
|
||||||
|
legacyItems.forEach(item => {
|
||||||
|
if (showingLegacy) {
|
||||||
|
item.style.display = 'block';
|
||||||
|
item.style.animation = 'fadeIn 0.3s ease-in';
|
||||||
|
} else {
|
||||||
|
item.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
newBtn.textContent = showingLegacy ?
|
||||||
|
'Hide Legacy Releases' : `Show ${items.length - visibleItems} More Releases`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update button text to show count
|
||||||
|
newBtn.textContent = `Show ${items.length - visibleItems} More Releases`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("RSS items displayed successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add some CSS for animations
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(-10px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
|
||||||
|
// Initialize when DOM is ready
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', loadRSS);
|
||||||
|
} else {
|
||||||
|
// DOM is already loaded
|
||||||
|
loadRSS();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make loadRSS available globally for retry button
|
||||||
|
window.loadRSS = loadRSS;
|
||||||
@@ -10,6 +10,11 @@ html, body {
|
|||||||
overflow: hidden; /* prevents double scrollbars */
|
overflow: hidden; /* prevents double scrollbars */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: SevenSegment;
|
||||||
|
src: url(assets/7Segment.ttf);
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||||
background: linear-gradient(135deg, #0a0a0a 0%, #1a1a1a 100%);
|
background: linear-gradient(135deg, #0a0a0a 0%, #1a1a1a 100%);
|
||||||
@@ -19,6 +24,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Main container */
|
/* Main container */
|
||||||
|
/*
|
||||||
.chat-container {
|
.chat-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -26,6 +32,7 @@ body {
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
/* Header */
|
/* Header */
|
||||||
.chat-header {
|
.chat-header {
|
||||||
@@ -57,7 +64,6 @@ body {
|
|||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Welcome screen */
|
|
||||||
.welcome-screen {
|
.welcome-screen {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -66,8 +72,11 @@ body {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
min-height: 100vh; /* Ensure it takes full viewport height */
|
||||||
|
box-sizing: border-box; /* Include padding in height calculation */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.welcome-title {
|
.welcome-title {
|
||||||
font-size: 3rem;
|
font-size: 3rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
|||||||
Reference in New Issue
Block a user