diff --git a/launcher/src-tauri/src/commands.rs b/launcher/src-tauri/src/commands.rs index da107e2..c3a6603 100644 --- a/launcher/src-tauri/src/commands.rs +++ b/launcher/src-tauri/src/commands.rs @@ -1190,3 +1190,51 @@ pub async fn like_user_post(state: State<'_, AppState>, post_id: i64) -> Res Res { + if resp.status().is_success() { + return Ok(resp.json().await.unwrap_or(serde_json::Value::Null)); + } + let body: serde_json::Value = resp.json().await.unwrap_or_default(); + Err(body["error"].as_str().or(body["message"].as_str()).unwrap_or(fallback).to_string()) +} + +/// From the map: ask a player to teleport to, or send them a message in game. +#[tauri::command] +pub async fn map_action(state: State<'_, AppState>, server_id: i64, action: String, target_uuid: String, text: Option) -> Res<()> { + let req = account_api(&state, reqwest::Method::POST, &format!("/servers/{server_id}/map/actions")).await?; + let resp = req + .json(&serde_json::json!({ "action": action, "target_uuid": target_uuid, "text": text.unwrap_or_default() })) + .send() + .await + .map_err(err)?; + ok_or_panel_error(resp, "unable to do that right now").await.map(|_| ()) +} + +/// Invite a player to a guild you lead or officiate. +#[tauri::command] +pub async fn guild_send_invite(state: State<'_, AppState>, guild_id: String, target_uuid: String) -> Res { + let req = account_api(&state, reqwest::Method::POST, &format!("/guilds/{guild_id}/invites")).await?; + let resp = req.json(&serde_json::json!({ "uuid": target_uuid })).send().await.map_err(err)?; + ok_or_panel_error(resp, "unable to send the invitation").await +} + +#[tauri::command] +pub async fn my_guild_invites(state: State<'_, AppState>) -> Res { + let req = account_api(&state, reqwest::Method::GET, "/guilds/invites").await?; + let resp = req.send().await.map_err(err)?; + ok_or_panel_error(resp, "unable to load invitations").await +} + +#[tauri::command] +pub async fn respond_guild_invite(state: State<'_, AppState>, invite_id: i64, accept: bool) -> Res { + let path = format!("/guilds/invites/{invite_id}/{}", if accept { "accept" } else { "decline" }); + let req = account_api(&state, reqwest::Method::POST, &path).await?; + let resp = req.send().await.map_err(err)?; + ok_or_panel_error(resp, "unable to answer the invitation").await +} diff --git a/launcher/src-tauri/src/lib.rs b/launcher/src-tauri/src/lib.rs index ba6833e..88bd91b 100644 --- a/launcher/src-tauri/src/lib.rs +++ b/launcher/src-tauri/src/lib.rs @@ -58,6 +58,10 @@ pub fn run() { commands::request_password_reset, commands::discord_connection, commands::discord_info, + commands::map_action, + commands::guild_send_invite, + commands::my_guild_invites, + commands::respond_guild_invite, commands::discord_link_start, commands::discord_unlink, commands::register_panel, diff --git a/launcher/src-tauri/tauri.conf.json b/launcher/src-tauri/tauri.conf.json index 4dc5a2f..c8188d2 100644 --- a/launcher/src-tauri/tauri.conf.json +++ b/launcher/src-tauri/tauri.conf.json @@ -27,7 +27,7 @@ } ], "security": { - "csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' https: http: data: blob:; media-src 'self' https: http: blob:; font-src 'self' data:; connect-src 'self' ipc: http://ipc.localhost" + "csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' https: http: data: blob:; media-src 'self' https: http: blob:; font-src 'self' data:; frame-src https: http:; connect-src 'self' ipc: http://ipc.localhost" } }, "bundle": { diff --git a/launcher/src/components/BlueMapModal.svelte b/launcher/src/components/BlueMapModal.svelte new file mode 100644 index 0000000..091a1bc --- /dev/null +++ b/launcher/src/components/BlueMapModal.svelte @@ -0,0 +1,80 @@ + + + e.key === 'Escape' && (selected ? (selected = null) : onclose())} /> + + + + diff --git a/launcher/src/components/MapPlayerPanel.svelte b/launcher/src/components/MapPlayerPanel.svelte new file mode 100644 index 0000000..5d21c1e --- /dev/null +++ b/launcher/src/components/MapPlayerPanel.svelte @@ -0,0 +1,164 @@ + + + + + diff --git a/launcher/src/pages/Guilds.svelte b/launcher/src/pages/Guilds.svelte index 56f6741..9d2208f 100644 --- a/launcher/src/pages/Guilds.svelte +++ b/launcher/src/pages/Guilds.svelte @@ -69,7 +69,24 @@ } } + type Invite = { id: number; guild_id: string; guild_name: string; guild_tag: string; icon_url: string; inviter: string }; + let invites = $state([]); + async function loadInvites() { + try { invites = await invoke('my_guild_invites'); } catch { invites = []; } + } + async function answerInvite(inv: Invite, accept: boolean) { + try { + await invoke('respond_guild_invite', { inviteId: inv.id, accept }); + toast(accept ? `Welcome to ${inv.guild_name}!` : 'Invitation declined', 'ok'); + await Promise.all([loadInvites(), loadInstanceGuildData()]); + } catch (e: any) { + toast(typeof e === 'string' ? e : e?.message ?? 'Could not answer the invitation', 'error'); + loadInvites(); + } + } + onMount(async () => { + loadInvites(); try { walletServers = (await invoke<{ servers: typeof walletServers }>('get_public_servers')).servers; } catch { walletServers = []; } }); @@ -169,6 +186,20 @@ + {#if invites.length} +
+

Guild invitations

+ {#each invites as inv (inv.id)} +
+ + [{inv.guild_tag}] {inv.guild_name}Invited by {inv.inviter} + + +
+ {/each} +
+ {/if} + {#if loading && !myGuild && !allGuilds.length}
{:else if myGuild} @@ -416,6 +447,11 @@