From 03259473a37de4301fd091cb899cda28afb7e4c7 Mon Sep 17 00:00:00 2001 From: emirsassan Date: Tue, 8 Jul 2025 09:54:38 +0300 Subject: [PATCH] fix: notification provider being undefined after a rerender --- .../overlay/popups/NotificationPopup.tsx | 4 +- .../frontend/src/hooks/useNotification.tsx | 68 ++++++++++++++----- .../src/routes/app/cluster/settings.tsx | 2 +- .../frontend/src/routes/app/index.tsx | 14 ---- .../frontend/src/routes/app/route.tsx | 28 ++++++++ .../src/routes/app/settings/minecraft.tsx | 22 +++++- 6 files changed, 101 insertions(+), 37 deletions(-) diff --git a/apps/onelauncher/frontend/src/components/overlay/popups/NotificationPopup.tsx b/apps/onelauncher/frontend/src/components/overlay/popups/NotificationPopup.tsx index 537ec3b2..76e59e92 100644 --- a/apps/onelauncher/frontend/src/components/overlay/popups/NotificationPopup.tsx +++ b/apps/onelauncher/frontend/src/components/overlay/popups/NotificationPopup.tsx @@ -54,13 +54,13 @@ function NotificationPopup() { if (notificationEntries.length === 0) return ( -
+

No notifications

); return ( - +

Notifications

{notificationEntries.length > 0 && ( diff --git a/apps/onelauncher/frontend/src/hooks/useNotification.tsx b/apps/onelauncher/frontend/src/hooks/useNotification.tsx index 02fe79f0..3e7a7de7 100644 --- a/apps/onelauncher/frontend/src/hooks/useNotification.tsx +++ b/apps/onelauncher/frontend/src/hooks/useNotification.tsx @@ -1,7 +1,7 @@ import type { ReactNode } from 'react'; import { bindings } from '@/main'; import { randomString } from '@/utils/index'; -import { createContext, useContext, useState } from 'react'; +import { createContext, memo, useContext, useEffect, useMemo, useState } from 'react'; export interface NotificationData { title: string; @@ -24,40 +24,72 @@ interface NotificationContextValue { setNotifications: React.Dispatch>; } -const NotificationContext = createContext(undefined); +const defaultContextValue: NotificationContextValue = { + notifications: {}, + setNotifications: () => {}, +}; + +const NotificationContext = createContext(defaultContextValue); interface NotificationProviderProps { children: ReactNode; } -export function NotificationProvider({ children }: NotificationProviderProps) { +export const NotificationProvider = memo(({ children }: NotificationProviderProps) => { const [notifications, setNotifications] = useState({}); - bindings.events.ingress.on((e) => { - setNotifications(prev => ({ - ...prev, - [e.id]: { - title: `${e.ingress_type}`, - message: e.message, - }, - })); - }).then(u => u()); + useEffect(() => { + let cleanup: (() => void) | undefined; + + bindings.events.ingress.on((e) => { + setNotifications(prev => ({ + ...prev, + [e.id]: { + title: `${e.ingress_type}`, + message: e.message, + }, + })); + }).then((u) => { + cleanup = u; + }).catch(console.error); + + return () => { + cleanup?.(); + }; + }, []); + + const contextValue = useMemo(() => ({ + notifications, + setNotifications, + }), [notifications, setNotifications]); return ( - + {children} ); -} +}); function useNotifications(): HookReturn { const context = useContext(NotificationContext); - - if (!context) - throw new Error('useNotifications must be used within a NotificationProvider'); - const { notifications, setNotifications } = context; + const isDefaultContext = setNotifications === defaultContextValue.setNotifications; + + if (isDefaultContext) { + if (import.meta.env.DEV) { + console.warn('useNotifications: Using default context (likely due to HMR or missing provider)'); + return { + list: {}, + set: () => {}, + create: () => {}, + remove: () => {}, + clear: () => {}, + }; + } + throw new Error('useNotifications must be used within a NotificationProvider'); + } + const ctx: HookReturn = { list: notifications, set: (id, data) => setNotifications(prev => ({ ...prev, [id]: data })), diff --git a/apps/onelauncher/frontend/src/routes/app/cluster/settings.tsx b/apps/onelauncher/frontend/src/routes/app/cluster/settings.tsx index 93f94dda..c3277183 100644 --- a/apps/onelauncher/frontend/src/routes/app/cluster/settings.tsx +++ b/apps/onelauncher/frontend/src/routes/app/cluster/settings.tsx @@ -38,7 +38,7 @@ function RouteComponent() { - +
diff --git a/apps/onelauncher/frontend/src/routes/app/index.tsx b/apps/onelauncher/frontend/src/routes/app/index.tsx index 2a8b54d7..234ad819 100644 --- a/apps/onelauncher/frontend/src/routes/app/index.tsx +++ b/apps/onelauncher/frontend/src/routes/app/index.tsx @@ -28,20 +28,6 @@ hey future sassan here i guess the issue is solved idk function RouteComponent() { const result = useCommand('getClusters', bindings.core.getClusters); - useEffect(() => { - let unlisten: (() => void) | undefined; - (async () => { - unlisten = await bindings.events.ingress.on((e) => { - if (typeof e.ingress_type === 'object') - if ('PrepareCluster' in e.ingress_type) - // eslint-disable-next-line no-console -- ok - console.log(`Preparing cluster: ${e.ingress_type.PrepareCluster.cluster_name} %${e.percent}`); - }); - })(); - - return () => unlisten?.(); - }, []); - return (
diff --git a/apps/onelauncher/frontend/src/routes/app/route.tsx b/apps/onelauncher/frontend/src/routes/app/route.tsx index e18f1696..d04e56c6 100644 --- a/apps/onelauncher/frontend/src/routes/app/route.tsx +++ b/apps/onelauncher/frontend/src/routes/app/route.tsx @@ -1,13 +1,41 @@ import Navbar from '@/components/Navbar'; +import useNotifications from '@/hooks/useNotification'; +import { bindings } from '@/main'; import { AnimatedOutlet } from '@onelauncher/common/components'; import { createFileRoute } from '@tanstack/react-router'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; +import { useEffect } from 'react'; export const Route = createFileRoute('/app')({ component: RouteComponent, }); function RouteComponent() { + const { set, remove } = useNotifications(); + + useEffect(() => { + let unlisten: (() => void) | undefined; + (async () => { + unlisten = await bindings.events.ingress.on((e) => { + if (typeof e.ingress_type === 'object') + if ('PrepareCluster' in e.ingress_type) { + const { cluster_name } = e.ingress_type.PrepareCluster; + let id = `cluster-${cluster_name}`; + + set(id, { + title: 'Preparing Cluster', + message: e.message, + }); + + if (e.percent === e.total) + remove(id); + } + }); + })(); + + return () => unlisten?.(); + }, [set, remove]); + return ( <>
diff --git a/apps/onelauncher/frontend/src/routes/app/settings/minecraft.tsx b/apps/onelauncher/frontend/src/routes/app/settings/minecraft.tsx index 4dd631f4..789d73d2 100644 --- a/apps/onelauncher/frontend/src/routes/app/settings/minecraft.tsx +++ b/apps/onelauncher/frontend/src/routes/app/settings/minecraft.tsx @@ -35,7 +35,7 @@ function RouteComponent() { {/* */} - +
@@ -129,7 +129,13 @@ export function GameSettings(props: GameSettingProps) { // ); // } -export function ProcessSettings() { +interface ProcessSettingsProps { + settings: SettingProfileModel; +} + +export function ProcessSettings(props: ProcessSettingsProps) { + const { settings } = props; + return ( <> Process @@ -140,6 +146,10 @@ export function ProcessSettings() { title="Pre-Launch Command" > { + settings.hook_pre = e.target.value || null; + }} placeholder="echo 'Game started'" /> @@ -150,6 +160,10 @@ export function ProcessSettings() { title="Wrapper Command" > { + settings.hook_wrapper = e.target.value || null; + }} placeholder="gamescope" /> @@ -160,6 +174,10 @@ export function ProcessSettings() { title="Post-Exit Command" > { + settings.hook_post = e.target.value || null; + }} placeholder="echo 'Game exited'" />