From 1648364540e577843b8990eaa1dec5f69a37caec Mon Sep 17 00:00:00 2001 From: Michael Mainguy Date: Mon, 10 Nov 2025 17:53:27 -0600 Subject: [PATCH] Add Discord widget integration with dynamic script loading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Created TypeScript wrapper for Widgetbot Crate - Dynamically loads Discord widget from CDN at runtime - Removed @widgetbot/crate npm package to avoid React dependency (182 packages removed) - Integrated with VR mode: auto-hides in VR, auto-shows in desktop mode - Connected to Discord server 1112846185913401475, channel 1437561367908581406 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- package-lock.json | 14 +++ package.json | 4 +- src/discordWidget.ts | 199 +++++++++++++++++++++++++++++++++++++++++++ src/main.ts | 35 ++++++++ src/vite-env.d.ts | 5 ++ 5 files changed, 255 insertions(+), 2 deletions(-) create mode 100644 src/discordWidget.ts diff --git a/package-lock.json b/package-lock.json index e047578..053ed45 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1513,6 +1513,20 @@ "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } + }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "dev": true, + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } } } } diff --git a/package.json b/package.json index 1cab2ce..fd02a89 100644 --- a/package.json +++ b/package.json @@ -15,15 +15,15 @@ "export-blend:batch": "tsx scripts/exportBlend.ts --batch" }, "dependencies": { + "@auth0/auth0-spa-js": "^2.8.0", "@babylonjs/core": "8.36.1", "@babylonjs/gui": "^8.36.1", "@babylonjs/havok": "1.3.10", "@babylonjs/inspector": "8.36.1", "@babylonjs/loaders": "8.36.1", "@babylonjs/materials": "8.36.1", - "@babylonjs/serializers": "8.36.1", "@babylonjs/procedural-textures": "8.36.1", - "@auth0/auth0-spa-js": "^2.8.0", + "@babylonjs/serializers": "8.36.1", "openai": "4.52.3" }, "devDependencies": { diff --git a/src/discordWidget.ts b/src/discordWidget.ts new file mode 100644 index 0000000..3753570 --- /dev/null +++ b/src/discordWidget.ts @@ -0,0 +1,199 @@ +/** + * Discord Widget Integration using Widgetbot Crate + * Dynamically loads the widget script to avoid npm bundling issues + */ + +export interface DiscordWidgetOptions { + server: string; + channel: string; + location?: string[]; + color?: string; + glyph?: string[]; + notifications?: boolean; + indicator?: boolean; + allChannelNotifications?: boolean; +} + +export class DiscordWidget { + private crate: any = null; + private scriptLoaded = false; + private isVisible = false; + + /** + * Initialize the Discord widget + * @param options - Widget configuration + */ + async initialize(options: DiscordWidgetOptions): Promise { + // Load the Crate script if not already loaded + if (!this.scriptLoaded) { + console.log('[DiscordWidget] Loading Crate script...'); + await this.loadCrateScript(); + this.scriptLoaded = true; + } + + // Wait for Crate to be available on window + await this.waitForCrate(); + + // Initialize the Crate widget + const defaultOptions: DiscordWidgetOptions = { + location: ['bottom', 'right'], + color: '#7289DA', + glyph: ['💬', '✖️'], + notifications: true, + indicator: true, + ...options + }; + + console.log('[DiscordWidget] Initializing Crate with options:', defaultOptions); + + // @ts-ignore - Crate is loaded from CDN + this.crate = new window.Crate(defaultOptions); + + this.setupEventListeners(); + console.log('[DiscordWidget] Successfully initialized'); + } + + /** + * Dynamically load the Crate script from CDN + */ + private loadCrateScript(): Promise { + return new Promise((resolve, reject) => { + // Check if script already exists + const existingScript = document.querySelector('script[src*="widgetbot"]'); + if (existingScript) { + resolve(); + return; + } + + const script = document.createElement('script'); + script.src = 'https://cdn.jsdelivr.net/npm/@widgetbot/crate@3'; + script.async = true; + script.defer = true; + + script.onload = () => { + console.log('[DiscordWidget] Script loaded successfully'); + resolve(); + }; + + script.onerror = () => { + console.error('[DiscordWidget] Failed to load script'); + reject(new Error('Failed to load Widgetbot Crate script')); + }; + + document.head.appendChild(script); + }); + } + + /** + * Wait for Crate constructor to be available on window + */ + private waitForCrate(): Promise { + return new Promise((resolve) => { + const checkCrate = () => { + // @ts-ignore + if (window.Crate) { + resolve(); + } else { + setTimeout(checkCrate, 50); + } + }; + checkCrate(); + }); + } + + /** + * Setup event listeners for widget events + */ + private setupEventListeners(): void { + if (!this.crate) return; + + // Listen for when user signs in + this.crate.on('signIn', (user: any) => { + console.log('[DiscordWidget] User signed in:', user.username); + }); + + // Listen for widget visibility changes + this.crate.on('toggleChat', (visible: boolean) => { + this.isVisible = visible; + console.log('[DiscordWidget] Chat visibility:', visible); + }); + } + + /** + * Toggle the Discord chat widget + */ + toggle(): void { + if (this.crate) { + this.crate.toggle(); + } + } + + /** + * Show a notification on the widget button + * @param message - Notification message + */ + notify(message: string): void { + if (this.crate) { + this.crate.notify(message); + } + } + + /** + * Show the widget + */ + show(): void { + if (this.crate && !this.isVisible) { + this.crate.show(); + this.isVisible = true; + } + } + + /** + * Hide the widget + */ + hide(): void { + if (this.crate && this.isVisible) { + this.crate.hide(); + this.isVisible = false; + } + } + + /** + * Check if widget is currently visible + */ + getIsVisible(): boolean { + return this.isVisible; + } + + /** + * Emit a custom event to the widget + * @param event - Event name + * @param data - Event data + */ + emit(event: string, data?: any): void { + if (this.crate) { + this.crate.emit(event, data); + } + } + + /** + * Listen for widget events + * @param event - Event name + * @param callback - Event callback + */ + on(event: string, callback: (data: any) => void): void { + if (this.crate) { + this.crate.on(event, callback); + } + } + + /** + * Send a message to the Discord channel (if user is signed in) + * @param message - Message text + */ + sendMessage(message: string): void { + if (this.crate) { + this.emit('sendMessage', message); + } + } +} diff --git a/src/main.ts b/src/main.ts index 91d74ac..a8a7ee0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -32,6 +32,7 @@ import {ReplayManager} from "./replay/ReplayManager"; import {AuthService} from "./authService"; import {updateUserProfile} from "./loginScreen"; import {Preloader} from "./preloader"; +import {DiscordWidget} from "./discordWidget"; // Set to true to run minimal controller debug test const DEBUG_CONTROLLERS = false; @@ -473,6 +474,20 @@ export class Main { pointerFeature.detach(); debugLog("Pointer selection feature stored and detached"); } + + // Hide Discord widget when entering VR, show when exiting + DefaultScene.XR.baseExperience.onStateChangedObservable.add((state) => { + const discord = (window as any).__discordWidget as DiscordWidget; + if (discord) { + if (state === 2) { // WebXRState.IN_XR + debugLog('[Main] Entering VR - hiding Discord widget'); + discord.hide(); + } else if (state === 0) { // WebXRState.NOT_IN_XR + debugLog('[Main] Exiting VR - showing Discord widget'); + discord.show(); + } + } + }); } this.reportProgress(40, 'VR support enabled'); } catch (error) { @@ -610,6 +625,26 @@ router.on('/', async () => { // Initialize demo mode without engine (just for UI purposes) const demo = new Demo(main); } + + // Initialize Discord widget (if not already initialized) + if (!(window as any).__discordWidget) { + debugLog('[Router] Initializing Discord widget'); + const discord = new DiscordWidget(); + + // Initialize with your server and channel IDs + discord.initialize({ + server: '1112846185913401475', // Replace with your Discord server ID + channel: '1437561367908581406', // Replace with your Discord channel ID + color: '#667eea', + glyph: ['💬', '✖️'], + notifications: true + }).then(() => { + debugLog('[Router] Discord widget ready'); + (window as any).__discordWidget = discord; + }).catch(error => { + console.error('[Router] Failed to initialize Discord widget:', error); + }); + } } debugLog('[Router] Home route handler complete'); diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 11f02fe..e453901 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1 +1,6 @@ /// + +// Widgetbot Crate global type +interface Window { + Crate: new (options: any) => any; +}