From 131f0961ff451ec47376483178e092c8d7403b27 Mon Sep 17 00:00:00 2001
From: AAGaming <aagaming@riseup.net>
Date: Mon, 5 Aug 2024 14:07:10 -0400
Subject: Rewrite router/tabs/toaster hooks (#661)

---
 frontend/src/toaster.tsx | 238 ++++++++++++++++-------------------------------
 1 file changed, 79 insertions(+), 159 deletions(-)

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

diff --git a/frontend/src/toaster.tsx b/frontend/src/toaster.tsx
index 4bc08772..e45b14a4 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 type { ToastData, ToastNotification } from '@decky/api';
+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,176 +20,99 @@ 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;
-    this.init();
-  }
 
-  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;
+    const ValveToastRenderer = findModuleExport((e) => e?.toString()?.includes(`controller:"notification",method:`));
+    // TODO find a way to undo this if possible?
+    const patchedRenderer = injectFCTrampoline(ValveToastRenderer);
+    this.toastPatch = replacePatch(patchedRenderer, 'component', (args: any[]) => {
+      if (args?.[0]?.group?.decky || args?.[0]?.group?.notifications?.[0]?.decky) {
+        return args[0].group.notifications.map((notification: any) => (
+          <Toast toast={notification.data} newIndicator={notification.bNewIndicator} location={args?.[0]?.location} />
+        ));
       }
-      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);
-      }
-      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?.();
   }
 
-  async toast(toast: ToastData) {
-    // toast.duration = toast.duration || 5e3;
-    // this.toasterState.addToast(toast);
-    await this.ready;
+  toast(toast: ToastData): ToastNotification {
+    if (toast.sound === undefined) toast.sound = 6;
+    if (toast.playSound === undefined) toast.playSound = true;
+    if (toast.showToast === undefined) toast.showToast = true;
+    if (toast.timestamp === undefined) toast.timestamp = new Date();
+    if (toast.showNewIndicator === undefined) toast.showNewIndicator = true;
+    /* eType 13 
+      13: {
+        proto: m.mu,
+        fnTray: null,
+        showToast: !0,
+        sound: f.PN.ToastMisc,
+        eFeature: l.uX
+      }
+    */
     let toastData = {
       nNotificationID: window.NotificationStore.m_nNextTestNotificationID++,
+      bNewIndicator: toast.showNewIndicator,
       rtCreated: Date.now(),
-      eType: toast.eType || 11,
+      eType: toast.eType || 13,
+      eSource: 1, // Client
       nToastDurationMS: toast.duration || (toast.duration = 5e3),
       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;
-    if (
-      (window.settingsStore.settings.bDisableAllToasts && !toast.critical) ||
-      (window.settingsStore.settings.bDisableToastsInGame &&
-        !toast.critical &&
-        window.NotificationStore.BIsUserInGame())
-    )
-      return;
-    if (toast.playSound) this.audioModule?.PlayNavSound(toast.sound);
-    if (toast.showToast) {
-      window.NotificationStore.m_rgNotificationToasts.push(toastData);
-      window.NotificationStore.DispatchNextToast();
+    let group: any;
+    function fnTray(toast: any, tray: any) {
+      group = {
+        eType: toast.eType,
+        notifications: [toast],
+      };
+      tray.unshift(group);
+    }
+    const info = {
+      showToast: toast.showToast,
+      sound: toast.sound,
+      eFeature: 0,
+      toastDurationMS: toastData.nToastDurationMS,
+      bCritical: toast.critical,
+      fnTray,
+    };
+    const self = this;
+    let expirationTimeout: number;
+    const toastResult: ToastNotification = {
+      data: toast,
+      dismiss() {
+        // it checks against the id of notifications[0]
+        try {
+          expirationTimeout && clearTimeout(expirationTimeout);
+          group && window.NotificationStore.RemoveGroupFromTray(group);
+        } catch (e) {
+          self.error('Error while dismissing toast:', e);
+        }
+      },
+    };
+    if (toast.expiration) {
+      expirationTimeout = setTimeout(() => {
+        try {
+          group && window.NotificationStore.RemoveGroupFromTray(group);
+        } catch (e) {
+          this.error('Error while dismissing expired toast:', e);
+        }
+      }, toast.expiration);
     }
+    window.NotificationStore.ProcessNotification(info, toastData, ToastType.New);
+    return toastResult;
   }
 
   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

