Some changes codex made that I want to test how they work

This commit is contained in:
2026-04-18 20:42:58 -04:00
parent effaf64bcf
commit 80a217fc5b
17 changed files with 1613 additions and 115 deletions

2
src-tauri/Cargo.lock generated
View File

@@ -3824,7 +3824,7 @@ dependencies = [
[[package]]
name = "oxyde"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"futures-util",
"serde",

View File

@@ -1,11 +1,18 @@
use tauri::{AppHandle, Emitter, State};
use uuid::Uuid;
use std::collections::HashMap;
use futures_util::StreamExt;
use surrealdb::Notification;
use tauri::{AppHandle, Emitter, State};
use uuid::Uuid;
use crate::db::AppState;
use crate::error::{into_err, AppError};
use crate::models::{Message, Room};
use crate::models::{Message, MessageReaction, MessageReactionSummary, Room, User};
const DEFAULT_PAGE_SIZE: i64 = 50;
const MAX_PAGE_SIZE: i64 = 100;
const MAX_MESSAGE_LEN: usize = 4000;
const MAX_ROOM_NAME_LEN: usize = 80;
/// Wrapper emitted to the frontend for each LIVE query notification.
/// Includes the action type so the frontend can distinguish create/update/delete.
@@ -15,92 +22,379 @@ struct LiveMessageEvent<'a> {
data: &'a Message,
}
/// Create a new chat room.
fn validate_room_name(name: &str) -> Result<(), String> {
let trimmed = name.trim();
if trimmed.is_empty() {
return Err(AppError::Auth("room name is required".into()).to_string());
}
if trimmed.chars().count() > MAX_ROOM_NAME_LEN {
return Err(AppError::Auth(format!(
"room name must be {MAX_ROOM_NAME_LEN} characters or less"
))
.to_string());
}
Ok(())
}
fn validate_message_body(body: &str) -> Result<(), String> {
let trimmed = body.trim();
if trimmed.is_empty() {
return Err(AppError::Auth("message cannot be empty".into()).to_string());
}
if trimmed.chars().count() > MAX_MESSAGE_LEN {
return Err(AppError::Auth(format!(
"message must be {MAX_MESSAGE_LEN} characters or less"
))
.to_string());
}
Ok(())
}
async fn current_user(state: &State<'_, AppState>) -> Result<User, String> {
let mut result: Vec<User> = state
.db
.query("SELECT * FROM $auth")
.await
.map_err(into_err)?
.take(0)
.map_err(into_err)?;
result
.pop()
.ok_or_else(|| into_err(AppError::Auth("not authenticated".into())))
}
async fn hydrate_reactions(
state: &State<'_, AppState>,
user: &User,
messages: &mut [Message],
) -> Result<(), String> {
for message in messages {
let reactions: Vec<MessageReaction> = state
.db
.query("SELECT * FROM message_reaction WHERE message = $message")
.bind(("message", message.id.clone()))
.await
.map_err(into_err)?
.take(0)
.map_err(into_err)?;
let mut grouped: HashMap<String, MessageReactionSummary> = HashMap::new();
for reaction in reactions {
let entry = grouped
.entry(reaction.emoji.clone())
.or_insert(MessageReactionSummary {
emoji: reaction.emoji,
count: 0,
reacted_by_me: false,
});
entry.count += 1;
if reaction.user == user.id {
entry.reacted_by_me = true;
}
}
let mut summaries: Vec<MessageReactionSummary> = grouped.into_values().collect();
summaries.sort_by(|a, b| a.emoji.cmp(&b.emoji));
message.reactions = Some(summaries);
}
Ok(())
}
async fn hydrate_direct_rooms(
state: &State<'_, AppState>,
rooms: &mut [Room],
) -> Result<(), String> {
for room in rooms.iter_mut().filter(|room| room.kind == "direct") {
let mut users: Vec<User> = state
.db
.query(
"SELECT * FROM user
WHERE id IN (
SELECT VALUE user FROM room_member
WHERE room = $room AND user != $auth
)
LIMIT 1",
)
.bind(("room", room.id.clone()))
.await
.map_err(into_err)?
.take(0)
.map_err(into_err)?;
room.other_user = users.pop();
}
Ok(())
}
/// Create a new chat room and add the creator as owner.
#[tauri::command]
pub async fn create_room(
state: State<'_, AppState>,
name: String,
kind: Option<String>,
) -> Result<Room, String> {
validate_room_name(&name)?;
let room_kind = kind.unwrap_or_else(|| "public".to_string());
if !matches!(room_kind.as_str(), "public" | "private") {
return Err(AppError::Auth("room kind must be public or private".into()).to_string());
}
let mut result: Vec<Room> = state
.db
.query("CREATE room SET name = $name, created = time::now()")
.bind(("name", name))
.query(
"CREATE room SET
name = $name,
kind = $kind,
created_by = $auth,
created = time::now(),
updated = time::now()",
)
.bind(("name", name.trim().to_string()))
.bind(("kind", room_kind))
.await
.map_err(into_err)?
.take(0)
.map_err(into_err)?;
result.pop().ok_or_else(|| into_err(AppError::NotFound("room after create".into())))
let room = result
.pop()
.ok_or_else(|| into_err(AppError::NotFound("room after create".into())))?;
state
.db
.query(
"CREATE room_member SET
room = $room,
user = $auth,
role = 'owner',
joined = time::now(),
last_read_at = time::now(),
muted = false",
)
.bind(("room", room.id.clone()))
.await
.map_err(into_err)?;
Ok(room)
}
/// Fetch all rooms.
/// Fetch public rooms and rooms the current user belongs to.
#[tauri::command]
pub async fn get_rooms(state: State<'_, AppState>) -> Result<Vec<Room>, String> {
let result: Vec<Room> = state
let mut result: Vec<Room> = state
.db
.query("SELECT * FROM room ORDER BY created DESC")
.query(
"SELECT * FROM room
WHERE kind = 'public' OR id IN (SELECT VALUE room FROM room_member WHERE user = $auth)
ORDER BY updated DESC, created DESC",
)
.await
.map_err(into_err)?
.take(0)
.map_err(into_err)?;
hydrate_direct_rooms(&state, &mut result).await?;
Ok(result)
}
/// Add a user to a room. Room owners can invite others.
#[tauri::command]
pub async fn invite_to_room(
state: State<'_, AppState>,
room_id: String,
user_id: String,
) -> Result<(), String> {
state
.db
.query(
"CREATE room_member SET
room = type::record('room', $room_id),
user = type::record('user', $user_id),
role = 'member',
joined = time::now(),
muted = false",
)
.bind(("room_id", room_id))
.bind(("user_id", user_id))
.await
.map_err(into_err)?;
Ok(())
}
/// Return an existing direct room for two users or create it.
#[tauri::command]
pub async fn get_or_create_direct_room(
state: State<'_, AppState>,
user_id: String,
) -> Result<Room, String> {
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 mut existing: Vec<Room> = state
.db
.query("SELECT * FROM room WHERE kind = 'direct' AND direct_key = $direct_key LIMIT 1")
.bind(("direct_key", direct_key.clone()))
.await
.map_err(into_err)?
.take(0)
.map_err(into_err)?;
if let Some(mut room) = existing.pop() {
hydrate_direct_rooms(&state, std::slice::from_mut(&mut room)).await?;
return Ok(room);
}
let mut created: Vec<Room> = state
.db
.query(
"CREATE room SET
name = NONE,
kind = 'direct',
direct_key = $direct_key,
created_by = $auth,
created = time::now(),
updated = time::now()",
)
.bind(("direct_key", direct_key))
.await
.map_err(into_err)?
.take(0)
.map_err(into_err)?;
let room = created
.pop()
.ok_or_else(|| into_err(AppError::NotFound("direct room after create".into())))?;
state
.db
.query(
"CREATE room_member SET room = $room, user = $auth, role = 'owner', joined = time::now(), last_read_at = time::now(), muted = false;
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))
.await
.map_err(into_err)?;
let mut room = room;
hydrate_direct_rooms(&state, std::slice::from_mut(&mut room)).await?;
Ok(room)
}
/// Send a message to a room.
#[tauri::command]
pub async fn send_message(
state: State<'_, AppState>,
room_id: String,
body: String,
reply_to: Option<String>,
) -> Result<Message, String> {
let mut result: Vec<Message> = state
.db
.query(
"CREATE message SET
validate_message_body(&body)?;
let query = if reply_to.is_some() {
"CREATE message SET
room = type::record('room', $room_id),
author = $auth,
author_username = $auth.username,
body = $body,
created = time::now()",
)
reply_to = type::record('message', $reply_to),
deleted = false,
created = time::now();
UPDATE type::record('room', $room_id) SET updated = time::now();"
} else {
"CREATE message SET
room = type::record('room', $room_id),
author = $auth,
author_username = $auth.username,
body = $body,
deleted = false,
created = time::now();
UPDATE type::record('room', $room_id) SET updated = time::now();"
};
let mut response = state
.db
.query(query)
.bind(("room_id", room_id))
.bind(("body", body))
.bind(("body", body.trim().to_string()));
if let Some(reply_to) = reply_to {
response = response.bind(("reply_to", reply_to));
}
let mut result: Vec<Message> = response
.await
.map_err(into_err)?
.take(0)
.map_err(into_err)?;
result.pop().ok_or_else(|| into_err(AppError::NotFound("message after create".into())))
result
.pop()
.ok_or_else(|| into_err(AppError::NotFound("message after create".into())))
}
/// Fetch all messages in a room, oldest first.
/// Fetch a bounded page of messages in a room, oldest first.
#[tauri::command]
pub async fn get_messages(
state: State<'_, AppState>,
room_id: String,
before: Option<String>,
limit: Option<i64>,
) -> Result<Vec<Message>, String> {
let result: Vec<Message> = state
let limit = limit.unwrap_or(DEFAULT_PAGE_SIZE).clamp(1, MAX_PAGE_SIZE);
let query = if before.is_some() {
"SELECT * FROM message
WHERE room = type::record('room', $room_id) AND created < <datetime>$before
ORDER BY created DESC
LIMIT $limit"
} else {
"SELECT * FROM message
WHERE room = type::record('room', $room_id)
ORDER BY created DESC
LIMIT $limit"
};
let mut response = state
.db
.query("SELECT * FROM message WHERE room = type::record('room', $room_id) ORDER BY created ASC")
.query(query)
.bind(("room_id", room_id))
.bind(("limit", limit));
if let Some(before) = before {
response = response.bind(("before", before));
}
let mut result: Vec<Message> = response
.await
.map_err(into_err)?
.take(0)
.map_err(into_err)?;
result.reverse();
let user = current_user(&state).await?;
hydrate_reactions(&state, &user, &mut result).await?;
Ok(result)
}
/// Delete a message by its ID string (e.g. "message:abc123").
/// Soft-delete a message by its ID string.
#[tauri::command]
pub async fn delete_message(
state: State<'_, AppState>,
message_id: String,
) -> Result<(), String> {
pub async fn delete_message(state: State<'_, AppState>, message_id: String) -> Result<(), String> {
state
.db
.query("DELETE type::record($id) WHERE author = $auth")
.query("UPDATE type::record($id) SET deleted = true, body = '', updated = time::now() WHERE author = $auth")
.bind(("id", message_id))
.await
.map_err(into_err)?;
@@ -108,6 +402,85 @@ pub async fn delete_message(
Ok(())
}
/// Edit the current user's message.
#[tauri::command]
pub async fn edit_message(
state: State<'_, AppState>,
message_id: String,
body: String,
) -> Result<Message, String> {
validate_message_body(&body)?;
let mut result: Vec<Message> = state
.db
.query("UPDATE type::record($id) SET body = $body, updated = time::now() WHERE author = $auth RETURN AFTER")
.bind(("id", message_id))
.bind(("body", body.trim().to_string()))
.await
.map_err(into_err)?
.take(0)
.map_err(into_err)?;
result
.pop()
.ok_or_else(|| into_err(AppError::NotFound("message".into())))
}
/// Toggle one emoji reaction for the current user.
#[tauri::command]
pub async fn toggle_reaction(
state: State<'_, AppState>,
message_id: String,
emoji: String,
) -> Result<(), String> {
let emoji = emoji.trim();
if emoji.is_empty() || emoji.chars().count() > 16 {
return Err(AppError::Auth("invalid reaction".into()).to_string());
}
let existing: Vec<MessageReaction> = state
.db
.query("SELECT * FROM message_reaction WHERE message = type::record($message_id) AND user = $auth AND emoji = $emoji")
.bind(("message_id", message_id.clone()))
.bind(("emoji", emoji.to_string()))
.await
.map_err(into_err)?
.take(0)
.map_err(into_err)?;
if existing.is_empty() {
state
.db
.query("CREATE message_reaction SET message = type::record($message_id), user = $auth, emoji = $emoji, created = time::now()")
.bind(("message_id", message_id))
.bind(("emoji", emoji.to_string()))
.await
.map_err(into_err)?;
} else {
state
.db
.query("DELETE message_reaction WHERE message = type::record($message_id) AND user = $auth AND emoji = $emoji")
.bind(("message_id", message_id))
.bind(("emoji", emoji.to_string()))
.await
.map_err(into_err)?;
}
Ok(())
}
/// Mark the room read for the current user.
#[tauri::command]
pub async fn mark_room_read(state: State<'_, AppState>, room_id: String) -> Result<(), String> {
state
.db
.query("UPDATE room_member SET last_read_at = time::now() WHERE room = type::record('room', $room_id) AND user = $auth")
.bind(("room_id", room_id))
.await
.map_err(into_err)?;
Ok(())
}
/// Start a LIVE query for new messages in a room.
/// Spawns a background tokio task that emits "chat:message" Tauri events.
///
@@ -133,10 +506,13 @@ pub async fn subscribe_room(
let handle = tokio::spawn(async move {
while let Some(Ok(notification)) = stream.next().await {
let _ = app_handle.emit("chat:message", &LiveMessageEvent {
action: format!("{:?}", notification.action),
data: &notification.data,
});
let _ = app_handle.emit(
"chat:message",
&LiveMessageEvent {
action: format!("{:?}", notification.action),
data: &notification.data,
},
);
}
});
@@ -148,10 +524,7 @@ pub async fn subscribe_room(
/// Stop a LIVE query subscription.
/// Aborts the background task — dropping the stream closes the LIVE query.
#[tauri::command]
pub async fn unsubscribe_room(
state: State<'_, AppState>,
sub_id: String,
) -> Result<(), String> {
pub async fn unsubscribe_room(state: State<'_, AppState>, sub_id: String) -> Result<(), String> {
let uuid = sub_id
.parse::<Uuid>()
.map_err(|e| into_err(AppError::Subscription(e.to_string())))?;

View File

@@ -7,6 +7,58 @@ use crate::models::{Contact, User};
const SESSION_STORE: &str = "session.json";
const TOKEN_KEY: &str = "token";
const MIN_PASSWORD_LEN: usize = 8;
const MAX_USERNAME_LEN: usize = 32;
const MAX_EMAIL_LEN: usize = 254;
fn validate_email(email: &str) -> Result<(), String> {
let email = email.trim();
if email.is_empty() || email.len() > MAX_EMAIL_LEN || !email.contains('@') {
return Err(AppError::Auth("enter a valid email address".into()).to_string());
}
Ok(())
}
fn validate_password(password: &str) -> Result<(), String> {
if password.chars().count() < MIN_PASSWORD_LEN {
return Err(AppError::Auth(format!(
"password must be at least {MIN_PASSWORD_LEN} characters"
))
.to_string());
}
Ok(())
}
fn validate_username(username: &str) -> Result<(), String> {
let username = username.trim();
if username.is_empty() || username.chars().count() > MAX_USERNAME_LEN {
return Err(
AppError::Auth(format!("username must be 1-{MAX_USERNAME_LEN} characters")).to_string(),
);
}
if !username
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
{
return Err(
AppError::Auth("username can use letters, numbers, _, -, and .".into()).to_string(),
);
}
Ok(())
}
fn validate_avatar(avatar: &Option<String>) -> Result<(), String> {
if let Some(avatar) = avatar {
let avatar = avatar.trim();
if !avatar.is_empty() && !(avatar.starts_with("https://") || avatar.starts_with("http://"))
{
return Err(
AppError::Auth("avatar must be a valid http or https URL".into()).to_string(),
);
}
}
Ok(())
}
/// Create a new user account via SurrealDB Record Auth SIGNUP.
/// Returns the created User record. Persists the JWT token to disk.
@@ -18,13 +70,17 @@ pub async fn signup(
username: String,
password: String,
) -> Result<User, String> {
validate_email(&email)?;
validate_username(&username)?;
validate_password(&password)?;
let credentials = surrealdb::opt::auth::Record {
access: SURREAL_ACCESS.to_string(),
namespace: SURREAL_NS.to_string(),
database: SURREAL_DB.to_string(),
params: serde_json::json!({
"email": email,
"username": username,
"email": email.trim(),
"username": username.trim(),
"password": password,
}),
};
@@ -41,7 +97,9 @@ pub async fn signup(
.take(0)
.map_err(into_err)?;
result.pop().ok_or_else(|| into_err(AppError::Auth("signup succeeded but $auth not set".into())))
result
.pop()
.ok_or_else(|| into_err(AppError::Auth("signup succeeded but $auth not set".into())))
}
/// Authenticate an existing user via SurrealDB Record Auth SIGNIN.
@@ -53,16 +111,25 @@ pub async fn signin(
email: String,
password: String,
) -> Result<String, String> {
validate_email(&email)?;
validate_password(&password)?;
let credentials = surrealdb::opt::auth::Record {
access: SURREAL_ACCESS.to_string(),
namespace: SURREAL_NS.to_string(),
database: SURREAL_DB.to_string(),
params: serde_json::json!({
"email": email,
"email": email.trim(),
"password": password,
}),
};
let token_str = state.db.signin(credentials).await.map_err(into_err)?.access.into_insecure_token();
let token_str = state
.db
.signin(credentials)
.await
.map_err(into_err)?
.access
.into_insecure_token();
*state.token.lock().unwrap() = Some(token_str.clone());
save_token(&app_handle, &token_str)?;
Ok(token_str)
@@ -70,10 +137,7 @@ pub async fn signin(
/// Clear the current session. Invalidates the token in state and removes it from disk.
#[tauri::command]
pub async fn signout(
state: State<'_, AppState>,
app_handle: AppHandle,
) -> Result<(), String> {
pub async fn signout(state: State<'_, AppState>, app_handle: AppHandle) -> Result<(), String> {
state.db.invalidate().await.map_err(into_err)?;
*state.token.lock().unwrap() = None;
clear_token(&app_handle)?;
@@ -89,11 +153,14 @@ pub async fn restore_session(
state: State<'_, AppState>,
app_handle: AppHandle,
) -> Result<User, String> {
let token_str = load_token(&app_handle)?.ok_or_else(|| {
AppError::Auth("no saved session".into()).to_string()
})?;
let token_str = load_token(&app_handle)?
.ok_or_else(|| AppError::Auth("no saved session".into()).to_string())?;
match state.db.authenticate(surrealdb::opt::auth::Token::from(token_str.clone())).await {
match state
.db
.authenticate(surrealdb::opt::auth::Token::from(token_str.clone()))
.await
{
Ok(_) => {
*state.token.lock().unwrap() = Some(token_str);
@@ -105,7 +172,9 @@ pub async fn restore_session(
.take(0)
.map_err(into_err)?;
result.pop().ok_or_else(|| into_err(AppError::Auth("session restored but $auth not set".into())))
result.pop().ok_or_else(|| {
into_err(AppError::Auth("session restored but $auth not set".into()))
})
}
Err(_) => {
let _ = clear_token(&app_handle);
@@ -126,7 +195,9 @@ pub async fn get_me(state: State<'_, AppState>) -> Result<User, String> {
.take(0)
.map_err(into_err)?;
result.pop().ok_or_else(|| into_err(AppError::Auth("not authenticated".into())))
result
.pop()
.ok_or_else(|| into_err(AppError::Auth("not authenticated".into())))
}
/// Update mutable profile fields. Only provided fields are changed.
@@ -136,6 +207,11 @@ pub async fn update_profile(
username: Option<String>,
avatar: Option<String>,
) -> Result<User, String> {
if let Some(username) = &username {
validate_username(username)?;
}
validate_avatar(&avatar)?;
let mut result: Vec<User> = state
.db
.query(
@@ -144,14 +220,46 @@ pub async fn update_profile(
avatar = $avatar ?? avatar
RETURN AFTER",
)
.bind(("username", username))
.bind(("avatar", avatar))
.bind(("username", username.map(|s| s.trim().to_string())))
.bind((
"avatar",
avatar
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty()),
))
.await
.map_err(into_err)?
.take(0)
.map_err(into_err)?;
result.pop().ok_or_else(|| into_err(AppError::NotFound("user".into())))
result
.pop()
.ok_or_else(|| into_err(AppError::NotFound("user".into())))
}
/// Search users by username. Returns safe profile fields only.
#[tauri::command]
pub async fn search_users(state: State<'_, AppState>, query: String) -> Result<Vec<User>, String> {
let query = query.trim();
if query.chars().count() < 2 {
return Ok(Vec::new());
}
let result: Vec<User> = state
.db
.query(
"SELECT id, username, email, avatar, created FROM user
WHERE id != $auth AND string::lowercase(username) CONTAINS string::lowercase($query)
ORDER BY username
LIMIT 10",
)
.bind(("query", query.to_string()))
.await
.map_err(into_err)?
.take(0)
.map_err(into_err)?;
Ok(result)
}
/// Return the contacts list for the current user.
@@ -159,7 +267,11 @@ pub async fn update_profile(
pub async fn get_contacts(state: State<'_, AppState>) -> Result<Vec<User>, String> {
let result: Vec<User> = state
.db
.query("SELECT target.* FROM contact WHERE owner = $auth")
.query(
"SELECT * FROM user
WHERE id IN (SELECT VALUE target FROM contact WHERE owner = $auth)
ORDER BY username",
)
.await
.map_err(into_err)?
.take(0)
@@ -170,10 +282,7 @@ pub async fn get_contacts(state: State<'_, AppState>) -> Result<Vec<User>, Strin
/// Add a user to the current user's contact list.
#[tauri::command]
pub async fn add_contact(
state: State<'_, AppState>,
user_id: String,
) -> Result<Contact, String> {
pub async fn add_contact(state: State<'_, AppState>, user_id: String) -> Result<Contact, String> {
let mut result: Vec<Contact> = state
.db
.query("CREATE contact SET owner = $auth, target = type::record('user', $uid)")
@@ -183,7 +292,9 @@ pub async fn add_contact(
.take(0)
.map_err(into_err)?;
result.pop().ok_or_else(|| into_err(AppError::NotFound("contact after create".into())))
result
.pop()
.ok_or_else(|| into_err(AppError::NotFound("contact after create".into())))
}
// ── Private helpers ───────────────────────────────────────────────────────────
@@ -196,7 +307,9 @@ fn save_token(app: &AppHandle, token: &str) -> Result<(), String> {
fn load_token(app: &AppHandle) -> Result<Option<String>, String> {
let store = app.store(SESSION_STORE).map_err(|e| e.to_string())?;
Ok(store.get(TOKEN_KEY).and_then(|v| v.as_str().map(String::from)))
Ok(store
.get(TOKEN_KEY)
.and_then(|v| v.as_str().map(String::from)))
}
fn clear_token(app: &AppHandle) -> Result<(), String> {

View File

@@ -18,9 +18,13 @@ pub fn run() {
.setup(|app| {
let app_handle = app.handle().clone();
tauri::async_runtime::block_on(async move {
let surreal = init_db(SURREAL_URL.as_str(), SURREAL_NS.as_str(), SURREAL_DB.as_str())
.await
.expect("Failed to connect to SurrealDB");
let surreal = init_db(
SURREAL_URL.as_str(),
SURREAL_NS.as_str(),
SURREAL_DB.as_str(),
)
.await
.expect("Failed to connect to SurrealDB");
let state = AppState {
db: Arc::new(surreal),
@@ -39,13 +43,19 @@ pub fn run() {
commands::user::get_me,
commands::user::restore_session,
commands::user::update_profile,
commands::user::search_users,
commands::user::get_contacts,
commands::user::add_contact,
commands::chat::create_room,
commands::chat::get_rooms,
commands::chat::invite_to_room,
commands::chat::get_or_create_direct_room,
commands::chat::send_message,
commands::chat::get_messages,
commands::chat::delete_message,
commands::chat::edit_message,
commands::chat::toggle_reaction,
commands::chat::mark_room_read,
commands::chat::subscribe_room,
commands::chat::unsubscribe_room,
])

View File

@@ -6,7 +6,7 @@ use surrealdb_types::SurrealValue;
pub struct User {
pub id: RecordId,
pub username: String,
pub email: String,
pub email: Option<String>,
pub avatar: Option<String>,
pub created: Datetime,
}
@@ -14,8 +14,27 @@ pub struct User {
#[derive(Debug, Clone, Serialize, Deserialize, SurrealValue)]
pub struct Room {
pub id: RecordId,
pub name: String,
pub name: Option<String>,
pub kind: String,
pub created_by: Option<RecordId>,
pub direct_key: Option<String>,
pub created: Datetime,
pub updated: Option<Datetime>,
pub last_message: Option<Message>,
pub unread_count: Option<i64>,
pub other_user: Option<User>,
}
#[derive(Debug, Clone, Serialize, Deserialize, SurrealValue)]
#[allow(dead_code)]
pub struct RoomMember {
pub id: RecordId,
pub room: RecordId,
pub user: RecordId,
pub role: String,
pub joined: Datetime,
pub last_read_at: Option<Datetime>,
pub muted: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, SurrealValue)]
@@ -26,6 +45,26 @@ pub struct Message {
pub author_username: Option<String>,
pub body: String,
pub created: Datetime,
pub updated: Option<Datetime>,
pub deleted: Option<bool>,
pub reply_to: Option<RecordId>,
pub reactions: Option<Vec<MessageReactionSummary>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, SurrealValue)]
pub struct MessageReaction {
pub id: RecordId,
pub message: RecordId,
pub user: RecordId,
pub emoji: String,
pub created: Datetime,
}
#[derive(Debug, Clone, Serialize, Deserialize, SurrealValue)]
pub struct MessageReactionSummary {
pub emoji: String,
pub count: i64,
pub reacted_by_me: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, SurrealValue)]
@@ -45,7 +84,10 @@ mod tests {
fn _assert_serialize<T: Serialize + for<'de> Deserialize<'de>>() {}
_assert_serialize::<User>();
_assert_serialize::<Room>();
_assert_serialize::<RoomMember>();
_assert_serialize::<Message>();
_assert_serialize::<MessageReaction>();
_assert_serialize::<MessageReactionSummary>();
_assert_serialize::<Contact>();
}
}