diff --git a/backend/src/auth/account.rs b/backend/src/auth/account.rs index 212bbbc..e867344 100644 --- a/backend/src/auth/account.rs +++ b/backend/src/auth/account.rs @@ -63,7 +63,7 @@ pub async fn signup( token_id.use_token(&mut db).await?; println!("phase 5"); - return Ok(Redirect::to("/chat")); + Ok(Redirect::to("/chat")) } #[get("/login")] diff --git a/backend/src/auth/two_factor.rs b/backend/src/auth/two_factor.rs index 038cc6c..3bafd73 100644 --- a/backend/src/auth/two_factor.rs +++ b/backend/src/auth/two_factor.rs @@ -3,7 +3,7 @@ use rocket::{ http::Status, outcome::{Outcome, try_outcome}, request::{self, FromRequest}, - response::status::{self, BadRequest}, + response::status::{self}, serde::json::Json, }; use rocket_db_pools::Connection; @@ -50,7 +50,7 @@ pub async fn confirm_totp( println!("valid"); let totp = totp_gen(mfa.user_id, mfa.secret.as_bytes()).unwrap(); - if !totp.check_current(&format!("{}", form.code)).unwrap() { + if !totp.check_current(&form.code.to_string()).unwrap() { return Err(status::Custom(Status::BadRequest, "Invalid 6-digit code")); } @@ -72,7 +72,7 @@ pub async fn confirm_totp( println!("enabled"); - return Ok(()); + Ok(()) } #[get("/totp.jpg")] diff --git a/backend/src/cdn.rs b/backend/src/cdn.rs index 35ced68..82ada9b 100644 --- a/backend/src/cdn.rs +++ b/backend/src/cdn.rs @@ -50,7 +50,7 @@ pub struct UploadResponse { #[post("/profile//upload", data = "")] pub async fn upload_profile_pic( user_id: usize, - mut file: Form>, + file: Form>, ) -> Result, Status> { const MAX_FILE_SIZE: u64 = 5 * 1024 * 1024; if file.len() > MAX_FILE_SIZE { diff --git a/backend/src/llm.rs b/backend/src/llm.rs index be47d9f..ea79cb4 100644 --- a/backend/src/llm.rs +++ b/backend/src/llm.rs @@ -32,7 +32,7 @@ impl LlmWorker { model: "gpt-oss-20b".into(), // whatever model you run locally messages: vec![Message { role: "user".into(), - content: message.text.clone().into(), + content: message.text.clone(), }], }; diff --git a/backend/src/main.rs b/backend/src/main.rs index 90f9bbf..9c389db 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -2,18 +2,15 @@ #[macro_use] extern crate rocket; -use redis::cmd; use rocket::fs::{FileServer, NamedFile}; use rocket::http::Method; -use rocket::serde::json::Json; use rocket::{Build, Rocket}; use rocket_cors::{AllowedOrigins, CorsOptions}; -use rocket_db_pools::{Connection, Database}; +use rocket_db_pools::Database; use rocket_dyn_templates::Template; use std::env; use std::sync::{Arc, LazyLock}; -use crate::auth::Session; use crate::db::{Postgres, Redis}; pub mod auth; @@ -67,7 +64,6 @@ fn rocket() -> Rocket { "/api", routes![ cdn::upload_profile_pic, - messenger::get_messages, messenger::post_message, messenger::event_stream, user::users, diff --git a/backend/src/messenger/cache.rs b/backend/src/messenger/cache.rs index 68c2d5e..ae0443f 100644 --- a/backend/src/messenger/cache.rs +++ b/backend/src/messenger/cache.rs @@ -51,8 +51,8 @@ pub async fn initialise( ) -> Result<(), Box> { let key = format!("messages:{}", channel_id); - let length: usize = cache.llen(&key).await?; - if length < 100 { + // less than 100 messages in cache? + if cache.llen::<_, i32>(&key).await? < 100 { // Fetch from Postgres let messages = sqlx::query!( "SELECT u.username, u.display_name, u.id, m.content, m.created_at @@ -68,14 +68,12 @@ pub async fn initialise( // Populate cache (in reverse order so oldest is at the end) for msg in messages.into_iter().rev() { - let chat_msg = ChatMsg { + let msg_json = serde_json::to_string(&ChatMsg { display_name: Some(msg.display_name.unwrap_or(msg.username)), user_id: msg.id as usize, text: msg.content, timestamp: (msg.created_at.unwrap().unix_timestamp_nanos() / 1_000_000) as usize, - }; - - let msg_json = serde_json::to_string(&chat_msg)?; + })?; cache.lpush::<_, _, ()>(&key, &msg_json).await?; } diff --git a/backend/src/messenger/messages.rs b/backend/src/messenger/messages.rs index bf0fffb..83f9c66 100644 --- a/backend/src/messenger/messages.rs +++ b/backend/src/messenger/messages.rs @@ -16,6 +16,7 @@ use crate::{ auth::Session, db::{Postgres, Redis}, llm::LlmWorker, + messenger, }; /// ---------- shared broadcaster ---------- @@ -133,15 +134,11 @@ pub async fn post_message( Ok(()) } -#[get("/messages")] pub async fn get_messages( mut db: Connection, mut redis: Connection, - _session: Session, + channel_id: i32, ) -> Json> { - const CHANNEL_ID: i32 = 1; - let channel_id = CHANNEL_ID; - if let Ok(messages) = messenger::cache::get(&mut redis, channel_id).await && !messages.is_empty() { @@ -186,7 +183,7 @@ pub async fn event_stream( chat: &rocket::State>, postgres: Connection, cache: Connection, - ag: Session, + _session: Session, mut shutdown: Shutdown, channel_id: i32, ) -> EventStream![] { @@ -194,7 +191,7 @@ pub async fn event_stream( EventStream! { // Initialize the stream with the last 100 messages - for msg in get_messages(postgres, cache, ag).await.0 { + for msg in get_messages(postgres, cache, channel_id).await.0 { yield Event::json(&msg); } diff --git a/backend/src/user.rs b/backend/src/user.rs index bdb6d91..bb154ce 100644 --- a/backend/src/user.rs +++ b/backend/src/user.rs @@ -1,4 +1,4 @@ -use redis::cmd; +use redis::AsyncCommands; use rocket::serde::json::Json; use rocket_db_pools::Connection; @@ -37,11 +37,7 @@ impl UserCache { redis_conn: &mut Connection, pgsql_conn: &mut Connection, ) -> String { - if let Ok(val) = cmd("GET") - .arg(&[format!("users:{id}")]) - .query_async(&mut **redis_conn) - .await - { + if let Ok(val) = redis_conn.get(format!("users:{id}")).await { return val; } @@ -58,15 +54,8 @@ impl UserCache { } pub async fn insert(id: usize, username: &str, conn: &mut Connection) { - cmd("SET") - .arg(&[ - format!("users:{id}"), - username.to_string(), - "EX".to_string(), - "1800".to_string(), - ]) - .query_async(&mut **conn) + conn.set_ex::<_, _, ()>(format!("users:{id}"), username.to_string(), 1800) .await - .expect("failed to insert key") + .expect("failed to insert key"); } } diff --git a/backend/static/css/index.css b/backend/static/css/index.css index 6aee4c6..390e207 100644 --- a/backend/static/css/index.css +++ b/backend/static/css/index.css @@ -30,12 +30,18 @@ body { justify-content: center; } +.app-container { + display: flex; + width: 100%; + height: 100vh; +} + +/* Chat Container */ .chat-container { display: flex; flex-direction: column; + flex: 1; height: 100dvh; - min-width: 100vw; - margin: 0 0; background: #121212; position: relative; overflow: hidden; @@ -46,12 +52,16 @@ body { padding: 10px; background: #1a1a1a; border-bottom: 1px solid #252525; + display: flex; + align-items: center; + gap: 10px; } .chat-title { display: flex; align-items: center; gap: 12px; + flex: 1; } .status-dot { @@ -773,3 +783,153 @@ body { .checkbox-group a:hover { text-decoration: underline; } + +/* Sidebar Styles */ +.sidebar { + width: 240px; + min-width: 180px; + max-width: 400px; + background: #0f0f0f; + display: flex; + flex-direction: column; + border-right: 1px solid #252525; + position: relative; + transition: + margin-left 0.3s ease, + opacity 0.3s ease; +} + +.sidebar.hidden { + margin-left: -240px; + opacity: 0; + pointer-events: none; +} + +.resize-handle { + position: absolute; + right: 0; + top: 0; + bottom: 0; + width: 4px; + cursor: ew-resize; + background: transparent; + transition: background 0.2s ease; + z-index: 10; +} + +.resize-handle:hover { + background: rgba(106, 90, 205, 0.5); +} + +.resize-handle:active { + background: #6a5acd; +} + +.sidebar-header { + padding: 16px; + border-bottom: 1px solid #252525; + background: #1a1a1a; + color: #ffffff; + font-weight: 600; + font-size: 15px; + letter-spacing: 0.3px; +} + +.channels-list { + flex: 1; + overflow-y: auto; + padding: 10px 8px; +} + +.channel-item { + padding: 10px 12px; + margin: 3px 0; + border-radius: 8px; + color: #b0b0b0; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + gap: 10px; + border: 1px solid transparent; + background: rgba(30, 30, 30, 0.3); +} + +.channel-item:hover { + background: rgba(30, 30, 30, 0.6); + border-color: #2a2a2a; + color: #e0e0e0; + transform: translateX(2px); +} + +.channel-item.active { + background: rgba(106, 90, 205, 0.15); + border-color: rgba(106, 90, 205, 0.3); + color: #ffffff; + box-shadow: 0 2px 8px rgba(106, 90, 205, 0.2); +} + +.channel-icon { + font-size: 18px; + font-weight: 600; + color: #666; + flex-shrink: 0; +} + +.channel-item:hover .channel-icon { + color: #888; +} + +.channel-item.active .channel-icon { + color: #6a5acd; +} + +.channel-name { + font-size: 14px; + font-weight: 500; +} + +/* Scrollbar styling for sidebar */ +.channels-list::-webkit-scrollbar { + width: 6px; +} + +.channels-list::-webkit-scrollbar-track { + background: #0f0f0f; +} + +.channels-list::-webkit-scrollbar-thumb { + background: #252525; + border-radius: 3px; +} + +.channels-list::-webkit-scrollbar-thumb:hover { + background: #333; +} + +.sidebar-toggle { + width: 32px; + height: 32px; + background: rgba(30, 30, 30, 0.6); + border: 1px solid #2a2a2a; + border-radius: 6px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + flex-shrink: 0; + color: #b0b0b0; + font-size: 18px; +} + +.sidebar-toggle:hover { + background: rgba(106, 90, 205, 0.2); + border-color: rgba(106, 90, 205, 0.3); + color: #e0e0e0; + transform: scale(1.05); +} + +.sidebar-toggle:active { + transform: scale(0.95); +} diff --git a/backend/templates/chat.html.tera b/backend/templates/chat.html.tera index f6312bb..6134c37 100644 --- a/backend/templates/chat.html.tera +++ b/backend/templates/chat.html.tera @@ -5,53 +5,83 @@ Discord Clone - Group Chat - -
- - -
-
- -

Wish.com Discord frfr

+
+ + - -
- +
+ +
+ +
+ +

Wish.com Discord frfr

-
-
- Location -
-
+ + +
+ + +
+
+ + -
Live Location
-
- -
-
--> -
- - - -
- - -
-
- -
@@ -69,11 +99,12 @@ } catch (__) {} } - return ''; // use external default escaping + return ''; } }) - const user_id = {{ user_id }}; + let channel_id = 1; + const user_id = 1; // {{ user_id }} var users = {}; // Handle message sending @@ -110,7 +141,7 @@ function sendMessage() { const message = input.value.trim(); if (message) { - fetch("/api/chat", { + fetch(`/api/chat/${channel_id}`, { method: "POST", body: JSON.stringify({ user_id: user_id, @@ -132,6 +163,8 @@ } }); + let messageSource; + async function loadData() { try { const userIds = await fetch("/api/users/") @@ -151,7 +184,11 @@ console.log('Users loaded:', users); - const messageSource = new EventSource("/api/events"); + if (messageSource) { + messageSource.close(); + } + + messageSource = new EventSource(`/api/events/${channel_id}`); messageSource.onopen = () => messagesContainer.innerHTML = ''; messageSource.onmessage = (event) => insertMessage(JSON.parse(event.data)); messageSource.onerror = (error) => { @@ -163,6 +200,51 @@ } } + // Handle channel switching + document.querySelectorAll('.channel-item').forEach(item => { + item.addEventListener('click', function() { + document.querySelectorAll('.channel-item').forEach(i => i.classList.remove('active')); + this.classList.add('active'); + channel_id = parseInt(this.dataset.channelId); + loadData(); + }); + }); + + // Sidebar toggle + const sidebar = document.querySelector('.sidebar'); + const sidebarToggle = document.getElementById('sidebarToggle'); + + sidebarToggle.addEventListener('click', () => { + sidebar.classList.toggle('hidden'); + }); + + // Sidebar resize + const resizeHandle = document.querySelector('.resize-handle'); + let isResizing = false; + + resizeHandle.addEventListener('mousedown', (e) => { + isResizing = true; + document.body.style.cursor = 'ew-resize'; + document.body.style.userSelect = 'none'; + }); + + document.addEventListener('mousemove', (e) => { + if (!isResizing) return; + + const newWidth = e.clientX; + if (newWidth >= 180 && newWidth <= 400) { + sidebar.style.width = newWidth + 'px'; + } + }); + + document.addEventListener('mouseup', () => { + if (isResizing) { + isResizing = false; + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + } + }); + loadData();