6 Commits

Author SHA1 Message Date
4d9346219d General Overhaul of the Frontend + Functionallity for Room Searching
All checks were successful
Build Tauri App (Linux + Windows exe) / build (push) Successful in 11m34s
2025-08-31 23:49:04 +02:00
34cdff241b windows installer will hopefully work for this?
All checks were successful
Build Tauri App (Linux + Windows exe) / build (push) Successful in 11m47s
2025-08-29 02:06:36 +02:00
120bddba62 trying the windows config to bundle webview2loader.dll automatically. im not sure if this works
All checks were successful
Build Tauri App (Linux + Windows exe) / build (push) Successful in 12m26s
2025-08-29 01:30:39 +02:00
ff05ffbb5e Merge branch 'main' of https://rattatwinko.servecounterstrike.com/gitea/rattatwinko/bytechat-desktop
All checks were successful
Build Tauri App (Linux + Windows exe) / build (push) Successful in 11m6s
2025-08-28 22:21:09 +02:00
04068b3e96 Issue 1: Completely Fixed Now. 2025-08-28 22:19:45 +02:00
526e5a2f63 fixed the CI pipeline again 2025-08-27 21:11:40 +02:00
15 changed files with 1795 additions and 222 deletions

29
src-tauri/Cargo.lock generated
View File

@@ -346,6 +346,7 @@ dependencies = [
"tauri",
"tauri-build",
"tauri-plugin-opener",
"tauri-plugin-store",
]
[[package]]
@@ -3725,6 +3726,22 @@ dependencies = [
"zbus",
]
[[package]]
name = "tauri-plugin-store"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d85dd80d60a76ee2c2fdce09e9ef30877b239c2a6bb76e6d7d03708aa5f13a19"
dependencies = [
"dunce",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.16",
"tokio",
"tracing",
]
[[package]]
name = "tauri-runtime"
version = "2.8.0"
@@ -3944,9 +3961,21 @@ dependencies = [
"pin-project-lite",
"slab",
"socket2",
"tokio-macros",
"windows-sys 0.59.0",
]
[[package]]
name = "tokio-macros"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]]
name = "tokio-util"
version = "0.7.16"

View File

@@ -22,4 +22,5 @@ tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tauri-plugin-store = "2"

View File

@@ -2,9 +2,12 @@
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"windows": [
"main"
],
"permissions": [
"core:default",
"opener:default"
"opener:default",
"store:default"
]
}
}

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

View File

@@ -1,6 +1,7 @@
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_store::Builder::new().build())
.plugin(tauri_plugin_opener::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");

View File

@@ -11,7 +11,6 @@ fn disable_dmabuf_if_true() {
}
}
}
}
fn main() {

View File

@@ -13,7 +13,6 @@
"title": "ByteChat",
"width": 600,
"height": 700,
"devtools": false,
"zoomHotkeysEnabled": false
}
],
@@ -27,6 +26,20 @@
"icon": [
"icons/icon.ico",
"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

Binary file not shown.

BIN
src/assets/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

View File

@@ -6,19 +6,18 @@
<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">
<link rel="icon" type="image/png" href="./assets/favicon.ico">
<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>
<script src="./frontend.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
<div class="logo"
style="font-size: clamp(1.25rem, 4vw, 1.5rem); display: flex; align-items: center;">
<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>
</div>
<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 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>
<!-- Welcome Screen -->
<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-input-container" style="flex-direction: column; gap: 0.75rem; width: 100%;">
<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 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>
<!-- 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 class="status-text" id="statusText" style="font-size: clamp(0.8rem, 3vw, 0.9rem);">
<div class="loading-message">
@@ -57,52 +63,278 @@
</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>
<!-- 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 -->
<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 id="chatScreen" style="display: none;" class="chat-screen">
<!-- Room info moved to top and fixed position -->
<div class="room-info" id="roomInfo" style="
display: none;
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 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>
<!-- wrapper for scroll -->
<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>
<!-- 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="Send a Message…"
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 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>
<!-- 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>
</html>

View File

@@ -50,9 +50,62 @@ const MAX_MESSAGE_LENGTH = 4000;
const MAX_ROOM_ID_LENGTH = 32;
const MAX_MESSAGES = 512;
// In-memory storage for keys (Tauri-compatible)
// Enhanced keyStorage with better error handling and forced saves
const keyStorage = {
// Store RSA keypair as JWK for persistence
async initTauriStore() {
if (!isRunningInTauri) return null;
try {
// Try modern Tauri store API first
if (window.__TAURI_PLUGIN_STORE__) {
const { Store } = window.__TAURI_PLUGIN_STORE__;
return await Store.load('bytechat-keys.json');
}
// Try legacy API
else if (window.__TAURI__ && window.__TAURI__.store) {
const { Store } = window.__TAURI__.store;
return await Store.load('bytechat-keys.json');
}
// Try direct access to store
else if (window.__TAURI__ && window.__TAURI__.invoke) {
// Use invoke API directly
return {
get: async (key) => {
try {
return await window.__TAURI__.invoke('plugin:store|get', { key });
} catch (e) {
console.warn('Store get failed:', e);
return null;
}
},
set: async (key, value) => {
try {
await window.__TAURI__.invoke('plugin:store|set', { key, value });
return true;
} catch (e) {
console.warn('Store set failed:', e);
return false;
}
},
delete: async (key) => {
try {
await window.__TAURI__.invoke('plugin:store|delete', { key });
return true;
} catch (e) {
console.warn('Store delete failed:', e);
return false;
}
}
};
}
} catch (e) {
console.warn('Tauri store initialization failed:', e);
}
return null;
},
// Store RSA keypair as JWK for persistence
async storeKeyPair(keyPair) {
try {
const publicJWK = await window.crypto.subtle.exportKey('jwk', keyPair.publicKey);
@@ -64,71 +117,89 @@ const keyStorage = {
timestamp: Date.now()
};
if (isRunningInTauri && tauriStore) {
if (isRunningInTauri) {
try {
await tauriStore.set('rsa_keypair', keyData);
await tauriStore.save();
console.log('RSA keypair stored in Tauri store');
const store = await this.initTauriStore();
if (store) {
await store.set('rsa_keypair', keyData);
console.log('RSA keypair stored in Tauri store');
return true;
}
} catch (storeError) {
console.warn('Failed to use Tauri store, falling back to memory:', storeError);
this._memoryKeys = keyData;
}
} else {
// Fallback to memory storage
this._memoryKeys = keyData;
console.log('RSA keypair stored in memory (no persistent storage)');
}
// Fallback to memory storage
this._memoryKeys = keyData;
console.log('RSA keypair stored in memory (no persistent storage)');
return false;
} catch (error) {
console.error('Failed to store keypair:', error);
throw error;
}
},
// Fixed loadKeyPair function with correct extractable settings
async loadKeyPair() {
try {
let keyData;
if (isRunningInTauri && tauriStore) {
try {
keyData = await tauriStore.get('rsa_keypair');
} catch (storeError) {
console.warn('Failed to read from Tauri store:', storeError);
keyData = this._memoryKeys;
try {
let keyData;
if (isRunningInTauri) {
try {
const store = await this.initTauriStore();
if (store) {
keyData = await store.get('rsa_keypair');
if (keyData) {
console.log('RSA keypair loaded from Tauri store');
}
}
} else {
keyData = this._memoryKeys;
} catch (storeError) {
console.warn('Failed to read from Tauri store:', storeError);
}
if (!keyData || !keyData.publicKey || !keyData.privateKey) {
console.log('No stored keypair found');
return null;
}
// Fallback to memory if Tauri store failed
if (!keyData) {
keyData = this._memoryKeys;
if (keyData) {
console.log('RSA keypair loaded from memory');
}
// Import keys from JWK
const publicKey = await window.crypto.subtle.importKey(
'jwk',
keyData.publicKey,
{ name: 'RSA-OAEP', hash: 'SHA-256' },
false,
['encrypt']
);
const privateKey = await window.crypto.subtle.importKey(
'jwk',
keyData.privateKey,
{ name: 'RSA-OAEP', hash: 'SHA-256' },
false,
['decrypt']
);
console.log('RSA keypair loaded from storage');
return { publicKey, privateKey };
} catch (error) {
console.error('Failed to load keypair:', error);
}
if (!keyData || !keyData.publicKey || !keyData.privateKey) {
console.log('No stored keypair found');
return null;
}
// Import keys from JWK - CRITICAL: Make public key extractable for export
const publicKey = await window.crypto.subtle.importKey(
'jwk',
keyData.publicKey,
{ name: 'RSA-OAEP', hash: 'SHA-256' },
true, // Make extractable so we can export it later
['encrypt']
);
const privateKey = await window.crypto.subtle.importKey(
'jwk',
keyData.privateKey,
{ name: 'RSA-OAEP', hash: 'SHA-256' },
false, // Private key doesn't need to be extractable for our use case
['decrypt']
);
return { publicKey, privateKey };
} catch (error) {
console.error('Failed to load keypair:', error);
return null;
}
},
// Also fix the generateKeyPair function to ensure consistent extractable settings
async storeSessionKey(roomId, sessionKey) {
try {
const keyData = await window.crypto.subtle.exportKey('raw', sessionKey);
@@ -140,23 +211,39 @@ const keyStorage = {
timestamp: Date.now()
};
if (isRunningInTauri && tauriStore) {
let stored = false;
if (isRunningInTauri) {
try {
await tauriStore.set(`session_key_${roomId}`, sessionData);
await tauriStore.save();
console.log('Session key stored for room:', roomId);
const store = await this.initTauriStore();
if (store) {
await store.set(`session_key_${roomId}`, sessionData);
console.log('✅ Session key stored in Tauri store for room:', roomId);
// Verify storage worked
const verification = await store.get(`session_key_${roomId}`);
if (verification) {
stored = true;
} else {
throw new Error('Session key storage verification failed');
}
}
} catch (storeError) {
console.warn('Failed to store session key in Tauri store:', storeError);
this._sessionKeys = this._sessionKeys || {};
this._sessionKeys[roomId] = sessionData;
console.warn('Failed to store session key in Tauri store:', storeError);
}
} else {
}
if (!stored) {
// Fallback to memory
this._sessionKeys = this._sessionKeys || {};
this._sessionKeys[roomId] = sessionData;
console.log('Session key stored in memory for room:', roomId);
console.log('📝 Session key stored in memory for room:', roomId);
}
return stored;
} catch (error) {
console.error('Failed to store session key:', error);
console.error('❌ Critical error storing session key:', error);
throw error;
}
},
@@ -164,21 +251,31 @@ const keyStorage = {
try {
let sessionData;
if (isRunningInTauri && tauriStore) {
if (isRunningInTauri) {
try {
sessionData = await tauriStore.get(`session_key_${roomId}`);
const store = await this.initTauriStore();
if (store) {
sessionData = await store.get(`session_key_${roomId}`);
if (sessionData) {
console.log('✅ Session key loaded from Tauri store for room:', roomId);
}
}
} catch (storeError) {
console.warn('Failed to load session key from Tauri store:', storeError);
this._sessionKeys = this._sessionKeys || {};
sessionData = this._sessionKeys[roomId];
console.warn('⚠️ Failed to load session key from Tauri store:', storeError);
}
} else {
}
// Fallback to memory if Tauri store failed
if (!sessionData) {
this._sessionKeys = this._sessionKeys || {};
sessionData = this._sessionKeys[roomId];
if (sessionData) {
console.log('📝 Session key loaded from memory for room:', roomId);
}
}
if (!sessionData || !sessionData.key) {
console.log('No stored session key found for room:', roomId);
console.log('No stored session key found for room:', roomId);
return null;
}
@@ -187,23 +284,130 @@ const keyStorage = {
'raw',
keyData,
{ name: 'AES-GCM' },
false,
true, // Make extractable so we can re-store if needed
['encrypt', 'decrypt']
);
console.log('Session key loaded for room:', roomId);
console.log('Session key successfully imported for room:', roomId);
return sessionKey;
} catch (error) {
console.error('Failed to load session key:', error);
console.error('Failed to load session key for room', roomId, ':', error);
return null;
}
},
async clearSessionKey(roomId) {
try {
if (isRunningInTauri) {
try {
const store = await this.initTauriStore();
if (store) {
await store.delete(`session_key_${roomId}`);
console.log('🗑️ Session key cleared from Tauri store for room:', roomId);
}
} catch (storeError) {
console.warn('Failed to clear from Tauri store:', storeError);
}
}
if (this._sessionKeys && this._sessionKeys[roomId]) {
delete this._sessionKeys[roomId];
console.log('🗑️ Session key cleared from memory for room:', roomId);
}
} catch (error) {
console.error('❌ Failed to clear session key for room', roomId, ':', error);
}
},
async forceSave() {
// Modern Tauri stores auto-save, this is mainly for compatibility
return true;
},
_memoryKeys: null,
_sessionKeys: {}
};
async function generateKeyPair() {
try {
console.log('=== Starting key generation process ===');
updateStatus('Loading or generating encryption keys...', false);
// Try to load existing keypair first
console.log('Attempting to load stored keypair...');
const storedKeyPair = await keyStorage.loadKeyPair();
if (storedKeyPair) {
keyPair = storedKeyPair;
console.log("✅ Using stored RSA keypair");
} else {
console.log("🔧 Generating new RSA keypair...");
keyPair = await window.crypto.subtle.generateKey(
{
name: "RSA-OAEP",
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-256"
},
true, // Make extractable for storage AND export
["encrypt", "decrypt"]
);
console.log("✅ New RSA keypair generated");
// Store the new keypair
console.log("💾 Storing new keypair...");
await keyStorage.storeKeyPair(keyPair);
console.log("✅ Keypair stored successfully");
}
// Validate the keypair
if (!keyPair || !keyPair.publicKey || !keyPair.privateKey) {
throw new Error('Invalid keypair generated/loaded');
}
// Test that public key is extractable
try {
await window.crypto.subtle.exportKey("spki", keyPair.publicKey);
console.log("✅ Public key export test passed");
} catch (exportError) {
console.error("❌ Public key is not extractable:", exportError);
throw new Error('Public key must be extractable for protocol to work');
}
keysReady = true;
updateStatus("Ready to join a room", false);
console.log("✅ Key pair ready - can now join rooms");
console.log('=== Key generation process complete ===');
} catch (error) {
console.error("❌ Failed to generate/load key pair:", error);
updateStatus("Error: Failed to set up encryption keys", true);
// Try to generate a fallback keypair
try {
console.log("🔄 Attempting fallback key generation...");
keyPair = await window.crypto.subtle.generateKey(
{
name: "RSA-OAEP",
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-256"
},
true, // Must be extractable for export
["encrypt", "decrypt"]
);
keysReady = true;
updateStatus("Ready to join a room (temporary keys)", false);
console.log("✅ Fallback keypair generated successfully");
} catch (fallbackError) {
console.error("❌ Fallback key generation also failed:", fallbackError);
throw new SecureChatError("Key generation failed completely", "KEY_GEN_ERROR");
}
}
}
// Security utilities
const SecurityUtils = {
sanitizeInput: function(input) {
@@ -307,94 +511,37 @@ function clearSensitiveData() {
}
}
async function generateKeyPair() {
try {
console.log('=== Starting key generation process ===');
updateStatus('Loading or generating encryption keys...', false);
// Try to load existing keypair first
console.log('Attempting to load stored keypair...');
const storedKeyPair = await keyStorage.loadKeyPair();
if (storedKeyPair) {
keyPair = storedKeyPair;
console.log("✅ Using stored RSA keypair");
} else {
console.log("🔧 Generating new RSA keypair...");
keyPair = await window.crypto.subtle.generateKey(
{
name: "RSA-OAEP",
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-256"
},
true, // Make extractable for storage
["encrypt", "decrypt"]
);
console.log("✅ New RSA keypair generated");
// Store the new keypair
console.log("💾 Storing new keypair...");
await keyStorage.storeKeyPair(keyPair);
console.log("✅ Keypair stored successfully");
}
// Validate the keypair
if (!keyPair || !keyPair.publicKey || !keyPair.privateKey) {
throw new Error('Invalid keypair generated/loaded');
}
keysReady = true;
updateStatus("Ready to join a room", false);
console.log("✅ Key pair ready - can now join rooms");
console.log('=== Key generation process complete ===');
} catch (error) {
console.error("❌ Failed to generate/load key pair:", error);
updateStatus("Error: Failed to set up encryption keys", true);
// Try to generate a fallback keypair without storage
try {
console.log("🔄 Attempting fallback key generation...");
keyPair = await window.crypto.subtle.generateKey(
{
name: "RSA-OAEP",
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-256"
},
false, // Not extractable for fallback
["encrypt", "decrypt"]
);
keysReady = true;
updateStatus("Ready to join a room (temporary keys)", false);
console.log("✅ Fallback keypair generated successfully");
} catch (fallbackError) {
console.error("❌ Fallback key generation also failed:", fallbackError);
throw new SecureChatError("Key generation failed completely", "KEY_GEN_ERROR");
}
}
}
// Enhanced generateSessionKey with guaranteed storage
async function generateSessionKey() {
try {
console.log("🔧 Generating new session key...");
sessionKey = await window.crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true,
true, // Make extractable for storage
["encrypt", "decrypt"]
);
console.log("Session key generated");
// Store session key for this room
if (!sessionKey) {
throw new Error('Session key generation returned null/undefined');
}
console.log("✅ Session key generated");
// CRITICAL: Immediately store the session key
if (currentRoom) {
console.log("💾 Storing newly generated session key...");
await keyStorage.storeSessionKey(currentRoom, sessionKey);
await keyStorage.forceSave();
console.log("✅ Session key stored and saved successfully");
} else {
console.warn("⚠️ No current room - session key not stored");
}
return sessionKey;
} catch (error) {
console.error("Failed to generate session key:", error);
console.error("Failed to generate session key:", error);
throw new SecureChatError("Session key generation failed", "SESSION_KEY_ERROR");
}
}
@@ -487,20 +634,22 @@ async function encryptSessionKey(sessionKey, publicKey) {
}
}
// Enhanced decryptSessionKey function
async function decryptSessionKey(encryptedKey) {
try {
console.log("Decrypting session key...");
console.log("🔓 Decrypting session key...");
const decryptedSessionKey = await unwrapAesKeyWithRsa(keyPair.privateKey, encryptedKey);
// Store the decrypted session key
if (currentRoom) {
await keyStorage.storeSessionKey(currentRoom, decryptedSessionKey);
if (!decryptedSessionKey) {
throw new Error('Failed to unwrap session key - got null/undefined result');
}
console.log("✅ Session key decrypted successfully");
return decryptedSessionKey;
} catch (error) {
console.error("Failed to decrypt session key:", error);
console.error("Failed to decrypt session key:", error);
throw new SecureChatError("Session key decryption failed", "SESSION_KEY_DECRYPT_ERROR");
}
}
@@ -615,6 +764,7 @@ function setupSocketListeners() {
}
});
// Enhanced room_joined handler with better key loading
socket.on('room_joined', async (data) => {
try {
if (data.error) {
@@ -632,11 +782,16 @@ function setupSocketListeners() {
currentDisplayName = data.display_name;
roomUsers = data.user_keys || {};
// Try to load existing session key for this room
// ENHANCED: Try to load existing session key for this room FIRST
console.log("🔍 Checking for existing session key for room:", currentRoom);
const storedSessionKey = await keyStorage.loadSessionKey(currentRoom);
if (storedSessionKey && !sessionKey) {
if (storedSessionKey) {
sessionKey = storedSessionKey;
console.log("Loaded stored session key for room:", currentRoom);
console.log("✅ Using stored session key for room:", currentRoom);
} else {
console.log("❌ No stored session key found for room:", currentRoom);
sessionKey = null; // Explicitly clear
}
// Switch to chat screen
@@ -652,25 +807,49 @@ function setupSocketListeners() {
// Handle session key based on user status
if (data.is_first_user) {
console.log("First user - generating session key");
console.log("👑 First user - generating new session key");
await generateSessionKey();
// Key is stored in generateSessionKey()
updateStatus("You're the first user! Session key generated. Others will receive it when they join.", false);
updateInputState();
} else if (sessionKey) {
console.log("Using existing/stored session key");
console.log("Using existing/stored session key");
updateStatus("Session key available. You can chat.", false);
updateInputState();
} else {
console.log("Requesting session key from existing users");
console.log("❓ Need session key - requesting from existing users");
updateStatus("Requesting session key from other users...", false);
// Explicitly request session key
// Explicitly request session key with retry logic
const publicKeyString = await exportPublicKey(keyPair.publicKey);
socket.emit('request_session_key', {
room_id: currentRoom,
public_key: publicKeyString
});
// Set up retry mechanism for session key request
let retryCount = 0;
const maxRetries = 3;
const retryInterval = setInterval(() => {
if (sessionKey) {
clearInterval(retryInterval);
return;
}
retryCount++;
if (retryCount <= maxRetries) {
console.log(`🔄 Retrying session key request (${retryCount}/${maxRetries})`);
socket.emit('request_session_key', {
room_id: currentRoom,
public_key: publicKeyString
});
} else {
clearInterval(retryInterval);
console.log("❌ Max session key request retries reached");
updateStatus("Failed to obtain session key. Try leaving and rejoining the room.", true);
}
}, 3000);
updateInputState();
}
@@ -683,10 +862,10 @@ function setupSocketListeners() {
}
}
console.log("[room_joined] sessionKey:", !!sessionKey, "isFirstUser:", data.is_first_user);
console.log(`🏠 Room joined successfully. SessionKey: ${!!sessionKey}, FirstUser: ${data.is_first_user}`);
} catch (error) {
console.error("Error processing room_joined:", error);
console.error("Error processing room_joined:", error);
updateStatus("Error joining room", true);
updateInputState();
}
@@ -751,34 +930,61 @@ function setupSocketListeners() {
}
});
// This is the critical event that was likely not firing properly
// Enhanced session_key_received handler with better storage
socket.on('session_key_received', async (data) => {
try {
console.log("session_key_received event fired:", data);
console.log("🔑 session_key_received event fired for room:", currentRoom);
if (!data || !data.encrypted_key) {
console.log("Invalid session key data received");
console.log("Invalid session key data received");
return;
}
if (!sessionKey) {
try {
console.log("Attempting to decrypt received session key");
sessionKey = await decryptSessionKey(data.encrypted_key);
console.log("🔓 Decrypting received session key...");
const decryptedKey = await decryptSessionKey(data.encrypted_key);
console.log("Session key decrypted successfully!");
if (!decryptedKey) {
throw new Error('Decryption returned null/undefined key');
}
// Set the session key
sessionKey = decryptedKey;
// CRITICAL: Explicitly store the session key immediately
console.log("💾 Storing decrypted session key...");
await keyStorage.storeSessionKey(currentRoom, sessionKey);
// Modern Tauri store auto-saves
await keyStorage.forceSave();
console.log("✅ Session key decrypted, stored, and saved successfully!");
updateStatus("Session key received! You can now chat securely.", false);
updateInputState();
addSystemMessage("🔑 Session key received - you can now chat!");
// Verify storage by attempting to reload
if (isRunningInTauri) {
console.log("🔍 Verifying session key storage...");
const verificationKey = await keyStorage.loadSessionKey(currentRoom);
if (verificationKey) {
console.log("✅ Session key storage verification successful");
} else {
console.error("❌ Session key storage verification failed");
updateStatus("Warning: Session key may not be saved", true);
}
}
} catch (error) {
console.error("Failed to decrypt received session key:", error);
console.error("Failed to process received session key:", error);
updateStatus("Failed to decrypt session key - please try rejoining", true);
}
} else {
console.log("Session key already exists, ignoring duplicate");
console.log(" Session key already exists, ignoring duplicate");
}
} catch (error) {
console.error("Failed to process session_key_received:", error);
console.error("❌ Critical error in session_key_received:", error);
updateStatus("Error: Failed to process session key", true);
}
});
@@ -948,8 +1154,8 @@ async function displayMessage(messageData) {
hour: '2-digit',
minute: '2-digit'
});
const displayName = messageData.display_name || `User-${messageData.sender_id?.substring(0, 8) || 'Unknown'}`;
// if shit breaks for uuid display fix this
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 messageGroup = document.createElement('div');
@@ -1182,6 +1388,26 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
// Add periodic session key backup for Tauri
if (isRunningInTauri) {
// Save session keys periodically in case of unexpected shutdown
setInterval(async () => {
if (sessionKey && currentRoom) {
try {
await keyStorage.storeSessionKey(currentRoom, sessionKey);
console.log("🔄 Periodic session key backup completed");
} catch (error) {
console.warn("⚠️ Periodic session key backup failed:", error);
}
}
}, 30000); // Every 30 seconds
// Note: Modern Tauri store auto-saves on app close
window.addEventListener('beforeunload', async () => {
console.log("🔄 App closing - Tauri store will auto-save");
});
}
// Periodic connection health check
setInterval(() => {
if (socket && isConnected) {
@@ -1217,7 +1443,7 @@ window.SecureChat = {
hasKeyPair: !!keyPair,
keysReady,
isConnected,
isTauri
isTauri: isRunningInTauri
})
};
@@ -1247,6 +1473,67 @@ if (isRunningInTauri) {
return !!loaded;
}
return false;
},
// Test session key persistence
testSessionKeyPersistence: async () => {
if (!sessionKey || !currentRoom) {
console.log("❌ No session key or room to test");
return false;
}
console.log("🧪 Testing session key persistence...");
try {
// Store current session key
await keyStorage.storeSessionKey(currentRoom, sessionKey);
await keyStorage.forceSave();
// Try to load it back
const loaded = await keyStorage.loadSessionKey(currentRoom);
if (loaded) {
console.log("✅ Session key persistence test: SUCCESS");
return true;
} else {
console.log("❌ Session key persistence test: FAILED");
return false;
}
} catch (error) {
console.error("❌ Session key persistence test error:", error);
return false;
}
},
// Force session key request
forceRequestSessionKey: async () => {
if (!currentRoom || !keyPair) {
console.log("❌ Cannot request session key - missing room or keypair");
return;
}
console.log("🔄 Forcing session key request...");
const publicKeyString = await exportPublicKey(keyPair.publicKey);
socket.emit('request_session_key', {
room_id: currentRoom,
public_key: publicKeyString
});
},
// Manual session key storage
forceStoreSessionKey: async () => {
if (!sessionKey || !currentRoom) {
console.log("❌ No session key or room to store");
return false;
}
try {
await keyStorage.storeSessionKey(currentRoom, sessionKey);
console.log("✅ Manual session key storage completed");
return true;
} catch (error) {
console.error("❌ Manual session key storage failed:", error);
return false;
}
}
};
}

737
src/rooms.js Normal file
View 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
View 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;

View File

@@ -10,6 +10,11 @@ html, body {
overflow: hidden; /* prevents double scrollbars */
}
@font-face {
font-family: SevenSegment;
src: url(assets/7Segment.ttf);
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
background: linear-gradient(135deg, #0a0a0a 0%, #1a1a1a 100%);
@@ -19,6 +24,7 @@ body {
}
/* Main container */
/*
.chat-container {
flex: 1;
display: flex;
@@ -26,6 +32,7 @@ body {
height: 100vh;
max-width: 100vw;
}
*/
/* Header */
.chat-header {
@@ -57,7 +64,6 @@ body {
font-size: 0.85rem;
}
/* Welcome screen */
.welcome-screen {
flex: 1;
display: flex;
@@ -66,8 +72,11 @@ body {
align-items: center;
padding: 2rem;
text-align: center;
min-height: 100vh; /* Ensure it takes full viewport height */
box-sizing: border-box; /* Include padding in height calculation */
}
.welcome-title {
font-size: 3rem;
font-weight: 700;