diff --git a/src-tauri/src/commands/chat.rs b/src-tauri/src/commands/chat.rs index 126e0ac..607b1ba 100644 --- a/src-tauri/src/commands/chat.rs +++ b/src-tauri/src/commands/chat.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use futures_util::StreamExt; +use surrealdb::types::{RecordId, RecordIdKey}; use surrealdb::Notification; use tauri::{AppHandle, Emitter, State}; use uuid::Uuid; @@ -50,6 +51,46 @@ fn validate_message_body(body: &str) -> Result<(), String> { Ok(()) } +fn record_key_string(id: &RecordId) -> String { + match &id.key { + RecordIdKey::String(value) => value.clone(), + RecordIdKey::Number(value) => value.to_string(), + RecordIdKey::Uuid(value) => value.to_string(), + other => format!("{other:?}"), + } +} + +fn user_record_key(user_id: &str) -> String { + let user_id = user_id.trim(); + if let Some((table, key)) = user_id.split_once(':') { + if table == "user" { + return format!("user:{key}"); + } + } + + format!("user:{user_id}") +} + +fn user_id_key(user_id: &str) -> String { + let user_id = user_id.trim(); + if let Some((table, key)) = user_id.split_once(':') { + if table == "user" { + return key.to_string(); + } + } + + user_id.to_string() +} + +fn direct_room_key(current_user: &RecordId, target_user_id: &str) -> String { + let mut participants = [ + user_record_key(&record_key_string(current_user)), + user_record_key(target_user_id), + ]; + participants.sort(); + participants.join("|") +} + async fn current_user(state: &State<'_, AppState>) -> Result { let mut result: Vec = state .db @@ -129,6 +170,26 @@ async fn hydrate_direct_rooms( Ok(()) } +fn dedupe_direct_rooms(rooms: Vec) -> Vec { + let mut seen_direct_users = HashMap::new(); + let mut deduped = Vec::with_capacity(rooms.len()); + + for room in rooms { + if room.kind == "direct" { + if let Some(other_user) = &room.other_user { + let key = user_record_key(&record_key_string(&other_user.id)); + if seen_direct_users.insert(key, ()).is_some() { + continue; + } + } + } + + deduped.push(room); + } + + deduped +} + /// Create a new chat room and add the creator as owner. #[tauri::command] pub async fn create_room( @@ -197,7 +258,7 @@ pub async fn get_rooms(state: State<'_, AppState>) -> Result, String> .map_err(into_err)?; hydrate_direct_rooms(&state, &mut result).await?; - Ok(result) + Ok(dedupe_direct_rooms(result)) } /// Add a user to a room. Room owners can invite others. @@ -232,16 +293,15 @@ pub async fn get_or_create_direct_room( user_id: String, ) -> Result { let me = current_user(&state).await?; - let me_key = - serde_json::to_string(&me.id).map_err(|e| into_err(AppError::Auth(e.to_string())))?; - let target_key = serde_json::json!({ - "table": "user", - "key": { "String": user_id.clone() } - }) - .to_string(); - let mut participants = [me_key, target_key]; - participants.sort(); - let direct_key = participants.join("|"); + let target_user_id = user_id_key(&user_id); + let current_user_key = record_key_string(&me.id); + if user_record_key(¤t_user_key) == user_record_key(&target_user_id) { + return Err( + AppError::Auth("cannot start a direct message with yourself".into()).to_string(), + ); + } + + let direct_key = direct_room_key(&me.id, &target_user_id); let mut existing: Vec = state .db @@ -257,6 +317,27 @@ pub async fn get_or_create_direct_room( return Ok(room); } + let mut existing_by_members: Vec = state + .db + .query( + "SELECT * FROM room + WHERE kind = 'direct' + AND id IN (SELECT VALUE room FROM room_member WHERE user = $auth) + AND id IN (SELECT VALUE room FROM room_member WHERE user = type::record('user', $user_id)) + ORDER BY updated DESC, created DESC + LIMIT 1", + ) + .bind(("user_id", target_user_id.clone())) + .await + .map_err(into_err)? + .take(0) + .map_err(into_err)?; + + if let Some(mut room) = existing_by_members.pop() { + hydrate_direct_rooms(&state, std::slice::from_mut(&mut room)).await?; + return Ok(room); + } + let mut created: Vec = state .db .query( @@ -285,7 +366,7 @@ pub async fn get_or_create_direct_room( CREATE room_member SET room = $room, user = type::record('user', $user_id), role = 'member', joined = time::now(), muted = false;", ) .bind(("room", room.id.clone())) - .bind(("user_id", user_id)) + .bind(("user_id", target_user_id)) .await .map_err(into_err)?; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 1809919..cd6f887 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,332 +1,444 @@ -{#if view === 'loading'} - - -{:else if view === 'auth'} - { authMode = authMode === 'signin' ? 'signup' : 'signin'; err = ''; }} - /> - +{#if view === "loading"} + +{:else if view === "auth"} + { + authMode = authMode === "signin" ? "signup" : "signin"; + err = ""; + }} + /> {:else} -
- - -
- {#if contextMenu} - contextMenu = null} - /> - {/if} +
+ + +
+ {#if contextMenu} + (contextMenu = null)} + /> + {/if} {/if} diff --git a/surreal/connect-database.sh b/surreal/connect-database.sh new file mode 100755 index 0000000..832c673 --- /dev/null +++ b/surreal/connect-database.sh @@ -0,0 +1,2 @@ +#!/usr/bin/bash +surreal sql -e http://127.0.0.1:8000 --namespace dev --database oxyde --user root --pass root