/** * Auto-connect the staff-chat WebSocket once the app is up — but ONLY for * non-limited staff and ONLY when the chat module is enabled. Limited ('c') * roles and disabled-chat builds never open a socket. * * Mirrors plugins/03.notifications-stream.client.ts (delayed start so we're past * initial hydration). Defensive: any failure here is swallowed so it can never * affect app boot. * * Eligibility is resolved against the SERVER (resolveChatEligible) rather than the * cookie, so a colleague who got IsChatEnabled set after their last login connects * without re-logging-in. That call also refreshes an expired token cookie, so the * socket below connects with a fresh token on its first try (no red-dot dance). * Unread badge counts are (re)seeded inside useChatSocket on every 'connected' * frame, so they no longer need a separate call here. * * Also consumes the /?chat= deep-link that chat push notifications target * (buildChatMessagePayload → sw-notifications.js openWindow): on it, open the * drawer on that conversation. Chat-only code → isolation preserved. */ export default defineNuxtPlugin((nuxtApp) => { if (!process.client) return try { // Cheapest synchronous short-circuit: a disabled build never connects. if (useRuntimeConfig().public.chatEnabled !== true) return let bootstrapped = false // socket up — permanent once eligible let bootstrapping = false // in-flight guard against concurrent navigations // Resolve eligibility against the server and, if eligible, open the socket. // useChatSocket() is captured HERE (not at plugin init) so its `me` is read from // the *logged-in* user cookie: the plugin runs once at app boot — on a fresh // login that's the signin page (no user cookie yet), so a handle captured then // would carry me=0 and mis-route every inbound message. const bootstrap = async () => { if (bootstrapped || bootstrapping) return bootstrapping = true try { const eligible = await resolveChatEligible() if (!eligible) return bootstrapped = true const { connect } = useChatSocket() connect() // Open the drawer on the conversation named by /?chat= (push // click target), and on any later in-app navigation to the same param. const route = useRoute() const openFromQuery = (val: any) => { const id = Number(val) if (!id) return useChatOpen().value = true useChatActivePeer().value = id } openFromQuery(route.query.chat) watch(() => route.query.chat, (val) => openFromQuery(val)) } catch (e) { console.error('[ChatSocketPlugin] connect failed:', e) } finally { bootstrapping = false } } // First attempt, past initial hydration. Covers a logged-in reload / deep link. setTimeout(() => nuxtApp.runWithContext(() => bootstrap()), 2000) // Re-attempt after an SPA login. The plugin runs ONCE per page load; on a fresh // login the app boots logged-out (signin page), so the first attempt resolves // "not eligible". Every later navigation that lands while still un-bootstrapped // clears any stale logged-out verdict and retries with the now-present login // cookies — so the chat icon AND socket come up without a manual page reload. // Cheap no-op once bootstrapped; while logged-out resolveChatEligible() // short-circuits client-side (no server round-trip per navigation). useRouter().afterEach(() => { if (bootstrapped) return nuxtApp.runWithContext(() => { const resolved = useState('chatEligibleResolved') if (resolved.value === false) resolved.value = null return bootstrap() }) }) } catch (e) { console.error('[ChatSocketPlugin] init skipped:', e) } })