();
+ let mut lock = mutex.0.lock().await;
+ *lock = Some(client)
+ }
+ tauri::async_runtime::spawn(async move {
+ drop_client_on_loss(app, user_wheel).await;
+ });
+ });
+ }
+
+ Ok(())
+}
diff --git a/gui/src/App.tsx b/gui/src/App.tsx
index 02d09dbd5..6185fd0b5 100644
--- a/gui/src/App.tsx
+++ b/gui/src/App.tsx
@@ -54,6 +54,7 @@ import { error, log } from './utils/logging';
import { AppLayout } from './AppLayout';
import { Preload } from './components/Preload';
import { UnknownDeviceModal } from './components/UnknownDeviceModal';
+import { useDiscordPresence } from './hooks/discord-presence';
export const GH_REPO = 'SlimeVR/SlimeVR-Server';
export const VersionContext = createContext('');
@@ -62,6 +63,7 @@ export const SLIMEVR_DISCORD = 'https://discord.gg/slimevr';
function Layout() {
const { isMobile } = useBreakpoint('mobile');
+ useDiscordPresence();
return (
<>
diff --git a/gui/src/components/settings/pages/InterfaceSettings.tsx b/gui/src/components/settings/pages/InterfaceSettings.tsx
index 4c165d766..11cc46d48 100644
--- a/gui/src/components/settings/pages/InterfaceSettings.tsx
+++ b/gui/src/components/settings/pages/InterfaceSettings.tsx
@@ -30,6 +30,7 @@ interface InterfaceSettingsForm {
feedbackSoundVolume: number;
connectedTrackersWarning: boolean;
useTray: boolean;
+ discordPresence: boolean;
};
}
@@ -55,6 +56,8 @@ export function InterfaceSettings() {
config?.connectedTrackersWarning ??
defaultConfig.connectedTrackersWarning,
useTray: config?.useTray ?? defaultConfig.useTray ?? false,
+ discordPresence:
+ config?.discordPresence ?? defaultConfig.discordPresence,
},
},
});
@@ -70,6 +73,7 @@ export function InterfaceSettings() {
textSize: values.appearance.textSize,
connectedTrackersWarning: values.notifications.connectedTrackersWarning,
useTray: values.notifications.useTray,
+ discordPresence: values.notifications.discordPresence,
});
};
@@ -204,6 +208,28 @@ export function InterfaceSettings() {
)}
/>
+
+
+ {l10n.getString('settings-general-interface-discord_presence')}
+
+
+
+ {l10n.getString(
+ 'settings-general-interface-discord_presence-description'
+ )}
+
+
+
+
+
>
diff --git a/gui/src/hooks/config.ts b/gui/src/hooks/config.ts
index c37309c5c..639c9e1f8 100644
--- a/gui/src/hooks/config.ts
+++ b/gui/src/hooks/config.ts
@@ -29,6 +29,7 @@ export interface Config {
useTray: boolean | null;
doneManualMounting: boolean;
mirrorView: boolean;
+ discordPresence: boolean;
}
export interface ConfigContext {
@@ -54,6 +55,7 @@ export const defaultConfig: Omit = {
useTray: null,
doneManualMounting: false,
mirrorView: true,
+ discordPresence: false,
};
interface CrossStorage {
diff --git a/gui/src/hooks/discord-presence.ts b/gui/src/hooks/discord-presence.ts
new file mode 100644
index 000000000..8f9e8191a
--- /dev/null
+++ b/gui/src/hooks/discord-presence.ts
@@ -0,0 +1,71 @@
+import { useCallback, useEffect } from 'react';
+import { useConfig } from './config';
+import { useInterval } from './timeout';
+import { useTrackers } from './tracker';
+import { invoke } from '@tauri-apps/api/core';
+import { warn } from '@/utils/logging';
+import { useLocalization } from '@fluent/react';
+
+export function useDiscordPresence() {
+ const { config } = useConfig();
+ const { useConnectedIMUTrackers } = useTrackers();
+ const { l10n } = useLocalization();
+ const imuTrackers = useConnectedIMUTrackers();
+
+ const updatePresence = useCallback(() => {
+ (async () => {
+ try {
+ if (await checkDiscordClient()) {
+ // If discord client exists, try updating presence
+ await updateDiscordPresence({
+ details: l10n.getString(
+ 'settings-general-interface-discord_presence-message',
+ { amount: imuTrackers.length }
+ ),
+ });
+ } else {
+ // else, try creating a discord client
+ await createDiscordClient();
+ }
+ } catch (e) {
+ warn(`failed to update presence, error: ${e}`);
+ }
+ })();
+ }, [imuTrackers.length, l10n]);
+
+ // Update presence every 6.9 seconds
+ useInterval(updatePresence, config?.discordPresence ? 6900 : null);
+
+ // Clear presence on config being disabled
+ useEffect(() => {
+ if (config?.discordPresence !== false) return;
+
+ (async () => {
+ if (!(await checkDiscordClient())) return;
+ clearDiscordPresence().catch((e) =>
+ warn(`failed to clear discord presence, error: ${e}`)
+ );
+ })();
+ }, [config?.discordPresence]);
+}
+
+export function checkDiscordClient(): Promise {
+ return invoke('discord_client_exists');
+}
+
+export function createDiscordClient(): Promise {
+ return invoke('create_discord_client');
+}
+
+export function clearDiscordPresence(): Promise {
+ return invoke('clear_presence');
+}
+
+export function updateDiscordPresence(obj: {
+ details: string;
+ state?: string;
+ small_icon?: [string, string];
+ button?: { label: string; url: string };
+}): Promise {
+ return invoke('update_presence', obj);
+}
diff --git a/gui/src/hooks/timeout.ts b/gui/src/hooks/timeout.ts
index 39af2a317..bb1c74302 100644
--- a/gui/src/hooks/timeout.ts
+++ b/gui/src/hooks/timeout.ts
@@ -1,17 +1,31 @@
-import { useEffect } from 'react';
+import { useEffect, useLayoutEffect, useRef } from 'react';
+
+export function useTimeout(fn: () => void, delay: number | null) {
+ const saved = useRef(fn);
+
+ useLayoutEffect(() => {
+ saved.current = fn;
+ }, [fn]);
-export function useTimeout(fn: () => void, delay: number) {
useEffect(() => {
- const id = setTimeout(fn, delay);
+ if (delay === null) return;
+ const id = setTimeout(() => saved.current(), delay);
return () => clearTimeout(id);
- });
+ }, [delay]);
}
-export function useInterval(fn: () => void, delay: number) {
+export function useInterval(fn: () => void, delay: number | null) {
+ const saved = useRef(fn);
+
+ useLayoutEffect(() => {
+ saved.current = fn;
+ }, [fn]);
+
useEffect(() => {
- const id = setInterval(fn, delay);
+ if (delay === null) return;
+ const id = setInterval(() => saved.current(), delay);
return () => clearInterval(id);
- });
+ }, [delay]);
}
export const useDebouncedEffect = (effect: () => void, deps: any[], delay: number) => {