diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 30b27bc..fdf8ce2 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -7,6 +7,7 @@ "core:default", "opener:default", "core:event:allow-listen", - "core:event:allow-unlisten" + "core:event:allow-unlisten", + "core:window:allow-close" ] } diff --git a/src-tauri/src/models/app_data.rs b/src-tauri/src/models/app_data.rs index 25d6e24..e37bf96 100644 --- a/src-tauri/src/models/app_data.rs +++ b/src-tauri/src/models/app_data.rs @@ -1,10 +1,11 @@ use serde::{Deserialize, Serialize}; use ts_rs::TS; -use crate::remotes::user::UserProfile; +use crate::remotes::{friends::FriendshipResponseDto, user::UserProfile}; #[derive(Default, Serialize, Deserialize, Clone, Debug, TS)] #[ts(export)] pub struct AppData { pub user: Option, + pub friends: Option>, } diff --git a/src-tauri/src/remotes/friends.rs b/src-tauri/src/remotes/friends.rs new file mode 100644 index 0000000..3386038 --- /dev/null +++ b/src-tauri/src/remotes/friends.rs @@ -0,0 +1,140 @@ +use reqwest::{Client, Error}; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +use crate::{lock_r, services::auth::with_auth, state::FDOLL}; + +#[derive(Default, Serialize, Deserialize, Clone, Debug, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct UserBasicDto { + pub id: String, + pub name: String, + pub username: String, + pub picture: String, +} + +#[derive(Default, Serialize, Deserialize, Clone, Debug, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct FriendshipResponseDto { + pub id: String, + pub friend: UserBasicDto, + pub created_at: String, +} + +#[derive(Default, Serialize, Deserialize, Clone, Debug, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct SendFriendRequestDto { + pub receiver_id: String, +} + +#[derive(Default, Serialize, Deserialize, Clone, Debug, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct FriendRequestResponseDto { + pub id: String, + pub sender: UserBasicDto, + pub receiver: UserBasicDto, + pub status: String, + pub created_at: String, + pub updated_at: String, +} + +pub struct FriendRemote { + pub base_url: String, + pub client: Client, +} + +impl FriendRemote { + pub fn new() -> Self { + let guard = lock_r!(FDOLL); + Self { + base_url: guard + .app_config + .api_base_url + .as_ref() + .expect("App configuration error") + .clone(), + client: guard + .clients + .as_ref() + .expect("App configuration error") + .http_client + .clone(), + } + } + + pub async fn get_friends(&self) -> Result, Error> { + let url = format!("{}/friends", self.base_url); + let resp = with_auth(self.client.get(url)).await.send().await?; + let friends = resp.json().await?; + Ok(friends) + } + + pub async fn search_users(&self, username: Option<&str>) -> Result, Error> { + let mut url = format!("{}/friends/search", self.base_url); + if let Some(u) = username { + url.push_str(&format!("?username={}", u)); + } + let resp = with_auth(self.client.get(&url)).await.send().await?; + let users = resp.json().await?; + Ok(users) + } + + pub async fn send_friend_request( + &self, + request: SendFriendRequestDto, + ) -> Result { + let url = format!("{}/friends/requests", self.base_url); + let resp = with_auth(self.client.post(url)) + .await + .json(&request) + .send() + .await?; + let req_resp = resp.json().await?; + Ok(req_resp) + } + + pub async fn get_received_requests(&self) -> Result, Error> { + let url = format!("{}/friends/requests/received", self.base_url); + let resp = with_auth(self.client.get(url)).await.send().await?; + let requests = resp.json().await?; + Ok(requests) + } + + pub async fn get_sent_requests(&self) -> Result, Error> { + let url = format!("{}/friends/requests/sent", self.base_url); + let resp = with_auth(self.client.get(url)).await.send().await?; + let requests = resp.json().await?; + Ok(requests) + } + + pub async fn accept_friend_request( + &self, + request_id: &str, + ) -> Result { + let url = format!("{}/friends/requests/{}/accept", self.base_url, request_id); + let resp = with_auth(self.client.post(url)).await.send().await?; + let req_resp = resp.json().await?; + Ok(req_resp) + } + + pub async fn deny_friend_request( + &self, + request_id: &str, + ) -> Result { + let url = format!("{}/friends/requests/{}/deny", self.base_url, request_id); + let resp = with_auth(self.client.post(url)).await.send().await?; + let req_resp = resp.json().await?; + Ok(req_resp) + } + + pub async fn unfriend(&self, friend_id: &str) -> Result<(), Error> { + let url = format!("{}/friends/{}", self.base_url, friend_id); + let resp = with_auth(self.client.delete(url)).await.send().await?; + resp.error_for_status()?; + Ok(()) + } +} diff --git a/src-tauri/src/remotes/mod.rs b/src-tauri/src/remotes/mod.rs index 22d12a3..1f94d0c 100644 --- a/src-tauri/src/remotes/mod.rs +++ b/src-tauri/src/remotes/mod.rs @@ -1 +1,2 @@ +pub mod friends; pub mod user; diff --git a/src-tauri/src/remotes/user.rs b/src-tauri/src/remotes/user.rs index 404d668..165b3c8 100644 --- a/src-tauri/src/remotes/user.rs +++ b/src-tauri/src/remotes/user.rs @@ -9,11 +9,22 @@ use crate::{lock_r, services::auth::with_auth, state::FDOLL}; #[ts(export)] pub struct UserProfile { pub id: String, + pub keycloak_sub: String, pub name: String, pub email: String, - pub username: String, + pub username: Option, + pub picture: Option, + pub roles: Vec, pub created_at: String, - pub last_login_at: String, + pub updated_at: String, + pub last_login_at: Option, +} + +#[derive(Default, Serialize, Deserialize, Clone, Debug, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct UpdateUserDto { + // Empty as per API schema } pub struct UserRemote { @@ -47,5 +58,25 @@ impl UserRemote { Ok(user) } - // TODO: Add other endpoints as methods + pub async fn update_user( + &self, + user_id: Option<&str>, + update: UpdateUserDto, + ) -> Result { + let url = format!("{}/users/{}", self.base_url, user_id.unwrap_or("me")); + let resp = with_auth(self.client.put(url)) + .await + .json(&update) + .send() + .await?; + let user = resp.json().await?; + Ok(user) + } + + pub async fn delete_user(&self, user_id: Option<&str>) -> Result<(), Error> { + let url = format!("{}/users/{}", self.base_url, user_id.unwrap_or("me")); + let resp = with_auth(self.client.delete(url)).await.send().await?; + resp.error_for_status()?; + Ok(()) + } } diff --git a/src-tauri/src/services/ws.rs b/src-tauri/src/services/ws.rs index c38d225..35ae443 100644 --- a/src-tauri/src/services/ws.rs +++ b/src-tauri/src/services/ws.rs @@ -1,12 +1,11 @@ use rust_socketio::{ClientBuilder, Payload, RawClient}; use serde_json::json; -use tauri::async_runtime; +use tauri::{async_runtime, Emitter}; use tracing::{error, info}; use crate::{ - lock_r, lock_w, - services::cursor::CursorPosition, - {models::app_config::AppConfig, state::FDOLL}, + get_app_handle, lock_r, lock_w, models::app_config::AppConfig, + services::cursor::CursorPosition, state::FDOLL, }; #[allow(non_camel_case_types)] // pretend to be a const like in js @@ -14,13 +13,54 @@ pub struct WS_EVENT; impl WS_EVENT { pub const CURSOR_REPORT_POSITION: &str = "cursor-report-position"; + pub const FRIEND_REQUEST_RECEIVED: &str = "friend-request-received"; + pub const FRIEND_REQUEST_ACCEPTED: &str = "friend-request-accepted"; + pub const FRIEND_REQUEST_DENIED: &str = "friend-request-denied"; + pub const UNFRIENDED: &str = "unfriended"; } -// Define a callback for handling incoming messages (e.g., 'pong') -fn on_pong(payload: Payload, _socket: RawClient) { +fn on_friend_request_received(payload: Payload, _socket: RawClient) { match payload { - Payload::Text(str) => println!("Received pong: {:?}", str), - Payload::Binary(bin) => println!("Received pong (binary): {:?}", bin), + Payload::Text(str) => { + println!("Received friend request: {:?}", str); + get_app_handle() + .emit(WS_EVENT::FRIEND_REQUEST_RECEIVED, str) + .unwrap(); + } + _ => todo!(), + } +} + +fn on_friend_request_accepted(payload: Payload, _socket: RawClient) { + match payload { + Payload::Text(str) => { + println!("Received friend request accepted: {:?}", str); + get_app_handle() + .emit(WS_EVENT::FRIEND_REQUEST_ACCEPTED, str) + .unwrap(); + } + _ => todo!(), + } +} + +fn on_friend_request_denied(payload: Payload, _socket: RawClient) { + match payload { + Payload::Text(str) => { + println!("Received friend request denied: {:?}", str); + get_app_handle() + .emit(WS_EVENT::FRIEND_REQUEST_DENIED, str) + .unwrap(); + } + _ => todo!(), + } +} + +fn on_unfriended(payload: Payload, _socket: RawClient) { + match payload { + Payload::Text(str) => { + println!("Received unfriended: {:?}", str); + get_app_handle().emit(WS_EVENT::UNFRIENDED, str).unwrap(); + } _ => todo!(), } } @@ -82,7 +122,16 @@ pub async fn build_ws_client(app_config: &AppConfig) -> rust_socketio::client::C let client = async_runtime::spawn_blocking(move || { ClientBuilder::new(api_base_url) .namespace("/") - .on("pong", on_pong) + .on( + WS_EVENT::FRIEND_REQUEST_RECEIVED, + on_friend_request_received, + ) + .on( + WS_EVENT::FRIEND_REQUEST_ACCEPTED, + on_friend_request_accepted, + ) + .on(WS_EVENT::FRIEND_REQUEST_DENIED, on_friend_request_denied) + .on(WS_EVENT::UNFRIENDED, on_unfriended) .auth(json!({ "token": token })) .connect() }) diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index 2bd6302..2e71802 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -5,7 +5,7 @@ use crate::{ app_config::{AppConfig, AuthConfig}, app_data::AppData, }, - remotes::user::UserRemote, + remotes::{friends::FriendRemote, user::UserRemote}, services::auth::{load_auth_pass, AuthPass}, }; use serde_json::json; @@ -99,13 +99,20 @@ pub fn init_fdoll_state() { /// Populate user data in app state from the server. pub async fn init_app_data() { let user_remote = UserRemote::new(); + let friend_remote = FriendRemote::new(); let user = user_remote .get_user(None) .await .expect("TODO: handle user profile fetch failure"); + let friends = friend_remote + .get_friends() + .await + .expect("TODO: handle friends fetch failure"); + { let mut guard = lock_w!(FDOLL); guard.app_data.user = Some(user); + guard.app_data.friends = Some(friends); get_app_handle() .emit("app-data-refreshed", json!(guard.app_data)) .expect("TODO: handle event emit fail"); diff --git a/src/assets/icons/power.svelte b/src/assets/icons/power.svelte new file mode 100644 index 0000000..26a9fc1 --- /dev/null +++ b/src/assets/icons/power.svelte @@ -0,0 +1,13 @@ + diff --git a/src/routes/app-menu/+page.svelte b/src/routes/app-menu/+page.svelte index 7e993fb..8c387c8 100644 --- a/src/routes/app-menu/+page.svelte +++ b/src/routes/app-menu/+page.svelte @@ -2,14 +2,15 @@ import Friends from "./tabs/friends.svelte"; import Preferences from "./tabs/preferences.svelte"; import YourDolls from "./tabs/your-dolls.svelte"; + import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
-
-
+
+
+
+
+
+ +
+
diff --git a/src/routes/app-menu/tabs/preferences.svelte b/src/routes/app-menu/tabs/preferences.svelte index 5f03ca8..d68d6ef 100644 --- a/src/routes/app-menu/tabs/preferences.svelte +++ b/src/routes/app-menu/tabs/preferences.svelte @@ -1,6 +1,7 @@
@@ -11,11 +12,15 @@
+
+ +
+