friends system (UI WIP)
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
"core:default",
|
"core:default",
|
||||||
"opener:default",
|
"opener:default",
|
||||||
"core:event:allow-listen",
|
"core:event:allow-listen",
|
||||||
"core:event:allow-unlisten"
|
"core:event:allow-unlisten",
|
||||||
|
"core:window:allow-close"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
|
|
||||||
use crate::remotes::user::UserProfile;
|
use crate::remotes::{friends::FriendshipResponseDto, user::UserProfile};
|
||||||
|
|
||||||
#[derive(Default, Serialize, Deserialize, Clone, Debug, TS)]
|
#[derive(Default, Serialize, Deserialize, Clone, Debug, TS)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub struct AppData {
|
pub struct AppData {
|
||||||
pub user: Option<UserProfile>,
|
pub user: Option<UserProfile>,
|
||||||
|
pub friends: Option<Vec<FriendshipResponseDto>>,
|
||||||
}
|
}
|
||||||
|
|||||||
140
src-tauri/src/remotes/friends.rs
Normal file
140
src-tauri/src/remotes/friends.rs
Normal file
@@ -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<Vec<FriendshipResponseDto>, 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<Vec<UserBasicDto>, 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<FriendRequestResponseDto, Error> {
|
||||||
|
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<Vec<FriendRequestResponseDto>, 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<Vec<FriendRequestResponseDto>, 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<FriendRequestResponseDto, Error> {
|
||||||
|
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<FriendRequestResponseDto, Error> {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,2 @@
|
|||||||
|
pub mod friends;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
|||||||
@@ -9,11 +9,22 @@ use crate::{lock_r, services::auth::with_auth, state::FDOLL};
|
|||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub struct UserProfile {
|
pub struct UserProfile {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
pub keycloak_sub: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub username: String,
|
pub username: Option<String>,
|
||||||
|
pub picture: Option<String>,
|
||||||
|
pub roles: Vec<String>,
|
||||||
pub created_at: String,
|
pub created_at: String,
|
||||||
pub last_login_at: String,
|
pub updated_at: String,
|
||||||
|
pub last_login_at: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Serialize, Deserialize, Clone, Debug, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export)]
|
||||||
|
pub struct UpdateUserDto {
|
||||||
|
// Empty as per API schema
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct UserRemote {
|
pub struct UserRemote {
|
||||||
@@ -47,5 +58,25 @@ impl UserRemote {
|
|||||||
Ok(user)
|
Ok(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Add other endpoints as methods
|
pub async fn update_user(
|
||||||
|
&self,
|
||||||
|
user_id: Option<&str>,
|
||||||
|
update: UpdateUserDto,
|
||||||
|
) -> Result<UserProfile, Error> {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
use rust_socketio::{ClientBuilder, Payload, RawClient};
|
use rust_socketio::{ClientBuilder, Payload, RawClient};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use tauri::async_runtime;
|
use tauri::{async_runtime, Emitter};
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
lock_r, lock_w,
|
get_app_handle, lock_r, lock_w, models::app_config::AppConfig,
|
||||||
services::cursor::CursorPosition,
|
services::cursor::CursorPosition, state::FDOLL,
|
||||||
{models::app_config::AppConfig, state::FDOLL},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[allow(non_camel_case_types)] // pretend to be a const like in js
|
#[allow(non_camel_case_types)] // pretend to be a const like in js
|
||||||
@@ -14,13 +13,54 @@ pub struct WS_EVENT;
|
|||||||
|
|
||||||
impl WS_EVENT {
|
impl WS_EVENT {
|
||||||
pub const CURSOR_REPORT_POSITION: &str = "cursor-report-position";
|
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_friend_request_received(payload: Payload, _socket: RawClient) {
|
||||||
fn on_pong(payload: Payload, _socket: RawClient) {
|
|
||||||
match payload {
|
match payload {
|
||||||
Payload::Text(str) => println!("Received pong: {:?}", str),
|
Payload::Text(str) => {
|
||||||
Payload::Binary(bin) => println!("Received pong (binary): {:?}", bin),
|
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!(),
|
_ => 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 || {
|
let client = async_runtime::spawn_blocking(move || {
|
||||||
ClientBuilder::new(api_base_url)
|
ClientBuilder::new(api_base_url)
|
||||||
.namespace("/")
|
.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 }))
|
.auth(json!({ "token": token }))
|
||||||
.connect()
|
.connect()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use crate::{
|
|||||||
app_config::{AppConfig, AuthConfig},
|
app_config::{AppConfig, AuthConfig},
|
||||||
app_data::AppData,
|
app_data::AppData,
|
||||||
},
|
},
|
||||||
remotes::user::UserRemote,
|
remotes::{friends::FriendRemote, user::UserRemote},
|
||||||
services::auth::{load_auth_pass, AuthPass},
|
services::auth::{load_auth_pass, AuthPass},
|
||||||
};
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
@@ -99,13 +99,20 @@ pub fn init_fdoll_state() {
|
|||||||
/// Populate user data in app state from the server.
|
/// Populate user data in app state from the server.
|
||||||
pub async fn init_app_data() {
|
pub async fn init_app_data() {
|
||||||
let user_remote = UserRemote::new();
|
let user_remote = UserRemote::new();
|
||||||
|
let friend_remote = FriendRemote::new();
|
||||||
let user = user_remote
|
let user = user_remote
|
||||||
.get_user(None)
|
.get_user(None)
|
||||||
.await
|
.await
|
||||||
.expect("TODO: handle user profile fetch failure");
|
.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);
|
let mut guard = lock_w!(FDOLL);
|
||||||
guard.app_data.user = Some(user);
|
guard.app_data.user = Some(user);
|
||||||
|
guard.app_data.friends = Some(friends);
|
||||||
get_app_handle()
|
get_app_handle()
|
||||||
.emit("app-data-refreshed", json!(guard.app_data))
|
.emit("app-data-refreshed", json!(guard.app_data))
|
||||||
.expect("TODO: handle event emit fail");
|
.expect("TODO: handle event emit fail");
|
||||||
|
|||||||
13
src/assets/icons/power.svelte
Normal file
13
src/assets/icons/power.svelte
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="lucide lucide-power-icon lucide-power"
|
||||||
|
><path d="M12 2v10" /><path d="M18.4 6.6a9 9 0 1 1-12.77.04" /></svg
|
||||||
|
>
|
||||||
|
After Width: | Height: | Size: 320 B |
@@ -2,14 +2,15 @@
|
|||||||
import Friends from "./tabs/friends.svelte";
|
import Friends from "./tabs/friends.svelte";
|
||||||
import Preferences from "./tabs/preferences.svelte";
|
import Preferences from "./tabs/preferences.svelte";
|
||||||
import YourDolls from "./tabs/your-dolls.svelte";
|
import YourDolls from "./tabs/your-dolls.svelte";
|
||||||
|
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="p-2 h-full absolute inset-0 bg-base-100 border-base-200/50 border border-t-0 rounded-b-xl"
|
class="p-2 h-full absolute inset-0 bg-base-100 border-base-200/50 border border-t-0 rounded-b-xl"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col gap-2 h-full">
|
<div class="flex flex-col gap-2 h-full">
|
||||||
<div class="size-full">
|
<div class="size-full flex flex-col gap-2">
|
||||||
<div class="tabs tabs-lift h-full">
|
<div class="tabs tabs-lift h-full flex-1">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
name="app_menu_tabs"
|
name="app_menu_tabs"
|
||||||
@@ -41,6 +42,17 @@
|
|||||||
<Preferences />
|
<Preferences />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="w-full flex flex-row justify-between">
|
||||||
|
<div></div>
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-outline border-neutral-500/50"
|
||||||
|
onclick={async () => {
|
||||||
|
await getCurrentWebviewWindow().close();
|
||||||
|
}}><p class="px-4">Ok</p></button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { appData } from "../../../events/app-data";
|
import { appData } from "../../../events/app-data";
|
||||||
|
import Power from "../../../assets/icons/power.svelte";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="size-full flex flex-col justify-between">
|
<div class="size-full flex flex-col justify-between">
|
||||||
@@ -11,11 +12,15 @@
|
|||||||
<div></div>
|
<div></div>
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
class="btn btn-error btn-sm btn-soft"
|
class="btn btn-error btn-square btn-soft"
|
||||||
onclick={async () => {
|
onclick={async () => {
|
||||||
await invoke("quit_app");
|
await invoke("quit_app");
|
||||||
}}>Quit Friendolls</button
|
}}
|
||||||
>
|
>
|
||||||
|
<div class="scale-50">
|
||||||
|
<Power />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user