From 88e7919a12fd56b297e73afb3fb05483f5893f4d Mon Sep 17 00:00:00 2001
From: AAGaming <aagaming@riseup.net>
Date: Thu, 18 Jul 2024 01:20:29 -0400
Subject: implement new toaster hook

this also supports the notification list and probably also desktop toasts (UI wip, read location enum prop from toast component probably)
---
 frontend/src/toaster.tsx | 172 ++++++++++-------------------------------------
 1 file changed, 36 insertions(+), 136 deletions(-)

(limited to 'frontend/src/toaster.tsx')

diff --git a/frontend/src/toaster.tsx b/frontend/src/toaster.tsx
index 4bc08772..611806d2 100644
--- a/frontend/src/toaster.tsx
+++ b/frontend/src/toaster.tsx
@@ -1,19 +1,16 @@
 import type { ToastData } from '@decky/api';
-import {
-  Export,
-  Patch,
-  afterPatch,
-  findClassByName,
-  findInReactTree,
-  findModuleExport,
-  getReactRoot,
-  sleep,
-} from '@decky/ui';
-import { ReactNode } from 'react';
+import { Patch, callOriginal, findModuleExport, injectFCTrampoline, replacePatch } from '@decky/ui';
 
 import Toast from './components/Toast';
 import Logger from './logger';
 
+// TODO export
+enum ToastType {
+  New,
+  Update,
+  Remove,
+}
+
 declare global {
   interface Window {
     __TOASTER_INSTANCE: any;
@@ -23,18 +20,12 @@ declare global {
 }
 
 class Toaster extends Logger {
-  // private routerHook: RouterHook;
-  // private toasterState: DeckyToasterState = new DeckyToasterState();
-  private node: any;
-  private rNode: any;
-  private audioModule: any;
   private finishStartup?: () => void;
   private ready: Promise<void> = new Promise((res) => (this.finishStartup = res));
-  private toasterPatch?: Patch;
+  private toastPatch?: Patch;
 
   constructor() {
     super('Toaster');
-    // this.routerHook = routerHook;
 
     window.__TOASTER_INSTANCE?.deinit?.();
     window.__TOASTER_INSTANCE = this;
@@ -42,115 +33,18 @@ class Toaster extends Logger {
   }
 
   async init() {
-    // this.routerHook.addGlobalComponent('DeckyToaster', () => (
-    //   <DeckyToasterStateContextProvider deckyToasterState={this.toasterState}>
-    //     <DeckyToaster />
-    //   </DeckyToasterStateContextProvider>
-    // ));
-    let instance: any;
-    const tree = getReactRoot(document.getElementById('root') as any);
-    const toasterClass1 = findClassByName('GamepadToastPlaceholder');
-    const toasterClass2 = findClassByName('ToastPlaceholder');
-    const toasterClass3 = findClassByName('ToastPopup');
-    const toasterClass4 = findClassByName('GamepadToastPopup');
-    const findToasterRoot = (currentNode: any, iters: number): any => {
-      if (iters >= 80) {
-        // currently 66
-        return null;
-      }
-      if (
-        currentNode?.memoizedProps?.className?.startsWith?.(toasterClass1) ||
-        currentNode?.memoizedProps?.className?.startsWith?.(toasterClass2) ||
-        currentNode?.memoizedProps?.className?.startsWith?.(toasterClass3) ||
-        currentNode?.memoizedProps?.className?.startsWith?.(toasterClass4)
-      ) {
-        this.log(`Toaster root was found in ${iters} recursion cycles`);
-        return currentNode;
-      }
-      if (currentNode.sibling) {
-        let node = findToasterRoot(currentNode.sibling, iters + 1);
-        if (node !== null) return node;
-      }
-      if (currentNode.child) {
-        let node = findToasterRoot(currentNode.child, iters + 1);
-        if (node !== null) return node;
-      }
-      return null;
-    };
-    instance = findToasterRoot(tree, 0);
-    while (!instance) {
-      this.warn(
-        'Failed to find Toaster root node, reattempting in 5 seconds. A developer may need to increase the recursion limit.',
-      );
-      await sleep(5000);
-      instance = findToasterRoot(tree, 0);
-    }
-    this.node = instance.return;
-    this.rNode = findInReactTree(
-      this.node.return.return,
-      (node) => node?.stateNode && node.type?.InstallErrorReportingStore,
-    );
-    let toast: any;
-    let renderedToast: ReactNode = null;
-    let innerPatched: any;
-    const repatch = () => {
-      if (this.node && !this.node.type.decky) {
-        this.toasterPatch = afterPatch(this.node, 'type', (_: any, ret: any) => {
-          const inner = findInReactTree(ret.props.children, (x) => x?.props?.onDismiss);
-          if (innerPatched) {
-            inner.type = innerPatched;
-          } else {
-            afterPatch(inner, 'type', (innerArgs: any, ret: any) => {
-              const currentToast = innerArgs[0]?.notification;
-              if (currentToast?.decky) {
-                if (currentToast == toast) {
-                  ret.props.children = renderedToast;
-                } else {
-                  toast = currentToast;
-                  renderedToast = <Toast toast={toast.data} />;
-                  ret.props.children = renderedToast;
-                }
-              } else {
-                toast = null;
-                renderedToast = null;
-              }
-              return ret;
-            });
-            innerPatched = inner.type;
-          }
-          return ret;
-        });
-        this.node.type.decky = true;
-        this.node.alternate.type = this.node.type;
-      }
-    };
-    const oRender = Object.getPrototypeOf(this.rNode.stateNode).render;
-    let int: number | undefined;
-    this.rNode.stateNode.render = (...args: any[]) => {
-      const ret = oRender.call(this.rNode.stateNode, ...args);
-      if (ret && !this?.node?.return?.return) {
-        int && clearInterval(int);
-        int = setInterval(() => {
-          const n = findToasterRoot(tree, 0);
-          if (n?.return) {
-            clearInterval(int);
-            this.node = n.return;
-            this.rNode = this.node.return;
-            repatch();
-          } else {
-            this.error('Failed to re-grab Toaster node, trying again...');
-          }
-        }, 1200);
+    const ToastRenderer = findModuleExport((e) => e?.toString()?.includes(`controller:"notification",method:`));
+    this.debug('toastrenderer', ToastRenderer);
+    // TODO find a way to undo this if possible?
+    const patchedRenderer = injectFCTrampoline(ToastRenderer);
+    this.toastPatch = replacePatch(patchedRenderer, 'component', (args: any[]) => {
+      this.debug('render toast', args);
+      if (args?.[0]?.group?.decky || args?.[0]?.group?.notifications?.[0]?.decky) {
+        this.debug('rendering decky toast');
+        return args[0].group.notifications.map((notification: any) => <Toast toast={notification.data} />);
       }
-      repatch();
-      return ret;
-    };
-
-    this.rNode.stateNode.shouldComponentUpdate = () => true;
-    this.rNode.stateNode.forceUpdate();
-    delete this.rNode.stateNode.shouldComponentUpdate;
-
-    this.audioModule = findModuleExport((e: Export) => e.PlayNavSound && e.RegisterCallbackOnPlaySound);
+      return callOriginal;
+    });
 
     this.log('Initialized');
     this.finishStartup?.();
@@ -168,8 +62,6 @@ class Toaster extends Logger {
       data: toast,
       decky: true,
     };
-    // @ts-ignore
-    toastData.data.appid = () => 0;
     if (toast.sound === undefined) toast.sound = 6;
     if (toast.playSound === undefined) toast.playSound = true;
     if (toast.showToast === undefined) toast.showToast = true;
@@ -180,19 +72,27 @@ class Toaster extends Logger {
         window.NotificationStore.BIsUserInGame())
     )
       return;
-    if (toast.playSound) this.audioModule?.PlayNavSound(toast.sound);
     if (toast.showToast) {
-      window.NotificationStore.m_rgNotificationToasts.push(toastData);
-      window.NotificationStore.DispatchNextToast();
+      function fnTray(toast: any, tray: any) {
+        let group = {
+          eType: toast.eType,
+          notifications: [toast],
+        };
+        tray.unshift(group);
+      }
+      const info = {
+        showToast: toast.showToast,
+        sound: toast.sound,
+        eFeature: 0,
+        toastDurationMS: toastData.nToastDurationMS,
+        fnTray,
+      };
+      window.NotificationStore.ProcessNotification(info, toastData, ToastType.New);
     }
   }
 
   deinit() {
-    this.toasterPatch?.unpatch();
-    this.node.alternate.type = this.node.type;
-    delete this.rNode.stateNode.render;
-    this.ready = new Promise((res) => (this.finishStartup = res));
-    // this.routerHook.removeGlobalComponent('DeckyToaster');
+    this.toastPatch?.unpatch();
   }
 }
 
-- 
cgit v1.2.3

