From bace5143d28c42ffcc83509b7fcdf02b6cae6934 Mon Sep 17 00:00:00 2001
From: TrainDoctor <traindoctor@protonmail.com>
Date: Sun, 30 Oct 2022 10:32:05 -0700
Subject: Merge Tabs and Injection Fixes, bring back native Valve toaster
 (#238)

* Bring back component patch-based tabshook

* better injection point

* finally fix dumb loading error

* fix QAM injection breaking after lock

* shut up typescript

* fix lock screen focusing issues

* Bring back the Valve toaster!

* Add support for stable steamos

* fix focus bug on lock screen but actually

* oops: remove extra console log

* shut up typescript again

* better fix for lockscreen bug

* better probably

* actually fix focus issues (WTF)

Co-authored-by: AAGaming <aa@mail.catvibers.me>
---
 frontend/src/toaster.tsx | 269 ++++++++++++++++++++++++-----------------------
 1 file changed, 135 insertions(+), 134 deletions(-)

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

diff --git a/frontend/src/toaster.tsx b/frontend/src/toaster.tsx
index 94b08d70..728bbdb8 100644
--- a/frontend/src/toaster.tsx
+++ b/frontend/src/toaster.tsx
@@ -1,10 +1,8 @@
-import { Patch, ToastData, sleep } from 'decky-frontend-lib';
+import { Patch, ToastData, afterPatch, findInReactTree, sleep } from 'decky-frontend-lib';
+import { ReactNode } from 'react';
 
-import DeckyToaster from './components/DeckyToaster';
-import { DeckyToasterState, DeckyToasterStateContextProvider } from './components/DeckyToasterState';
 import Toast from './components/Toast';
 import Logger from './logger';
-import RouterHook from './router-hook';
 
 declare global {
   interface Window {
@@ -14,16 +12,18 @@ declare global {
 }
 
 class Toaster extends Logger {
-  private instanceRetPatch?: Patch;
-  private routerHook: RouterHook;
-  private toasterState: DeckyToasterState = new DeckyToasterState();
+  // private routerHook: RouterHook;
+  // private toasterState: DeckyToasterState = new DeckyToasterState();
   private node: any;
+  private rNode: any;
   private settingsModule: any;
-  private ready: boolean = false;
+  private finishStartup?: () => void;
+  private ready: Promise<void> = new Promise((res) => (this.finishStartup = res));
+  private toasterPatch?: Patch;
 
-  constructor(routerHook: RouterHook) {
+  constructor() {
     super('Toaster');
-    this.routerHook = routerHook;
+    // this.routerHook = routerHook;
 
     window.__TOASTER_INSTANCE?.deinit?.();
     window.__TOASTER_INSTANCE = this;
@@ -31,135 +31,136 @@ class Toaster extends Logger {
   }
 
   async init() {
-    this.routerHook.addGlobalComponent('DeckyToaster', () => (
-      <DeckyToasterStateContextProvider deckyToasterState={this.toasterState}>
-        <DeckyToaster />
-      </DeckyToasterStateContextProvider>
-    ));
-    // let instance: any;
-    // while (true) {
-    //   instance = findInReactTree(
-    //     (document.getElementById('root') as any)._reactRootContainer._internalRoot.current,
-    //     (x) => x?.memoizedProps?.className?.startsWith?.('toastmanager_ToastPlaceholder'),
-    //   );
-    //   if (instance) break;
-    //   this.debug('finding instance');
-    //   await sleep(2000);
-    // }
-    // // const windowManager = findModuleChild((m) => {
-    // //   if (typeof m !== 'object') return false;
-    // //   for (let prop in m) {
-    // //     if (m[prop]?.prototype?.GetRenderElement) return m[prop];
-    // //   }
-    // //   return false;
-    // // });
-    // this.node = instance.return.return;
-    // let toast: any;
-    // let renderedToast: ReactNode = null;
-    // console.log(instance, this.node);
-    // // replacePatch(window.SteamClient.BrowserView, "Destroy", (args: any[]) => {
-    // //   console.debug("destroy", args)
-    // //   return callOriginal;
-    // // })
-    // // let node = this.node.child.updateQueue.lastEffect;
-    // // while (node.next && !node.deckyPatched) {
-    // //   node = node.next;
-    // //   if (node.deps[1] == "notificationtoasts") {
-    // //     console.log("Deleting destroy");
-    // //     node.deckyPatched = true;
-    // //     node.create = () => {console.debug("VVVVVVVVVVV")};
-    // //     node.destroy = () => {console.debug("AAAAAAAAAAAAAAAAaaaaaaaaaaaaaaa")};
-    // //   }
-    // // }
-    // this.node.stateNode.render = (...args: any[]) => {
-    //   const ret = this.node.stateNode.__proto__.render.call(this.node.stateNode, ...args);
-    //   console.log('toast', ret);
-    //     if (ret) {
-    //       console.log(ret)
-    //       // this.instanceRetPatch = replacePatch(ret, 'type', (innerArgs: any) => {
-    //       //   console.log("inner toast", innerArgs)
-    //       //   // @ts-ignore
-    //       //   const oldEffect = window.SP_REACT.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher.current.useEffect;
-    //       //   // @ts-ignore
-    //       //   window.SP_REACT.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher.current.useEffect = (effect, deps) => {
-    //       //     console.log(effect, deps)
-    //       //     if (deps?.[1] == "notificationtoasts") {
-    //       //       console.log("run")
-    //       //       effect();
-    //       //     }
-    //       //     return oldEffect(effect, deps);
-    //       //   }
-    //       //   const ret = this.instanceRetPatch?.original(...args);
-    //       //   console.log("inner ret", ret)
-    //       //   // @ts-ignore
-    //       //   window.SP_REACT.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher.current.useEffect = oldEffect;
-    //       //   return ret
-    //       // });
-    //     }
-    //   //       console.log("toast ret", ret)
-    //   //       if (ret?.props?.children[1]?.children?.props) {
-    //   //         const currentToast = ret.props.children[1].children.props.notification;
-    //   //         if (currentToast?.decky) {
-    //   //           if (currentToast == toast) {
-    //   //             ret.props.children[1].children = renderedToast;
-    //   //           } else {
-    //   //             toast = currentToast;
-    //   //             renderedToast = <Toast toast={toast} />;
-    //   //             ret.props.children[1].children = renderedToast;
-    //   //           }
-    //   //         } else {
-    //   //           toast = null;
-    //   //           renderedToast = null;
-    //   //         }
-    //   //       }
-    //   //       return ret;
-    //   //     });
-    //   //   }
-    //   return ret;
-    // };
-    // this.settingsModule = findModuleChild((m) => {
-    //   if (typeof m !== 'object') return undefined;
-    //   for (let prop in m) {
-    //     if (typeof m[prop]?.settings && m[prop]?.communityPreferences) return m[prop];
-    //   }
-    // });
-    // // const idx = FocusNavController.m_ActiveContext.m_rgGamepadNavigationTrees.findIndex((x: any) => x.m_ID == "ToastContainer");
-    // // if (idx > -1) {
-    // //   FocusNavController.m_ActiveContext.m_rgGamepadNavigationTrees.splice(idx, 1)
-    // // }
-    // this.node.stateNode.forceUpdate();
-    // this.node.stateNode.shouldComponentUpdate = () => {
-    //   return false;
-    // };
-    // this.log('Initialized');
-    // this.ready = true;
+    // this.routerHook.addGlobalComponent('DeckyToaster', () => (
+    //   <DeckyToasterStateContextProvider deckyToasterState={this.toasterState}>
+    //     <DeckyToaster />
+    //   </DeckyToasterStateContextProvider>
+    // ));
+    let instance: any;
+    const tree = (document.getElementById('root') as any)._reactRootContainer._internalRoot.current;
+    const findToasterRoot = (currentNode: any, iters: number): any => {
+      if (iters >= 50) {
+        // currently 40
+        return null;
+      }
+      if (currentNode?.memoizedProps?.className?.startsWith?.('toastmanager_ToastPlaceholder')) {
+        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.error(
+        '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 = this.node.return;
+    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 = this.rNode.stateNode.__proto__.render;
+    let int: NodeJS.Timer | undefined;
+    this.rNode.stateNode.render = (...args: any[]) => {
+      const ret = oRender.call(this.rNode.stateNode, ...args);
+      if (ret && !this?.node?.return?.return) {
+        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.log('Initialized');
+    this.finishStartup?.();
   }
 
-  toast(toast: ToastData) {
-    toast.duration = toast.duration || 5e3;
-    this.toasterState.addToast(toast);
-    // const settings = this.settingsModule?.settings;
-    // let toastData = {
-    //   nNotificationID: window.NotificationStore.m_nNextTestNotificationID++,
-    //   rtCreated: Date.now(),
-    //   eType: 15,
-    //   nToastDurationMS: toast.duration || 5e3,
-    //   data: toast,
-    //   decky: true,
-    // };
-    // // @ts-ignore
-    // toastData.data.appid = () => 0;
-    // if (
-    //   (settings?.bDisableAllToasts && !toast.critical) ||
-    //   (settings?.bDisableToastsInGame && !toast.critical && window.NotificationStore.BIsUserInGame())
-    // )
-    //   return;
-    // window.NotificationStore.m_rgNotificationToasts.push(toastData);
-    // window.NotificationStore.DispatchNextToast();
+  async toast(toast: ToastData) {
+    // toast.duration = toast.duration || 5e3;
+    // this.toasterState.addToast(toast);
+    await this.ready;
+    const settings = this.settingsModule?.settings;
+    let toastData = {
+      nNotificationID: window.NotificationStore.m_nNextTestNotificationID++,
+      rtCreated: Date.now(),
+      eType: 15,
+      nToastDurationMS: toast.duration || (toast.duration = 5e3),
+      data: toast,
+      decky: true,
+    };
+    // @ts-ignore
+    toastData.data.appid = () => 0;
+    if (
+      (settings?.bDisableAllToasts && !toast.critical) ||
+      (settings?.bDisableToastsInGame && !toast.critical && window.NotificationStore.BIsUserInGame())
+    )
+      return;
+    window.NotificationStore.m_rgNotificationToasts.push(toastData);
+    window.NotificationStore.DispatchNextToast();
   }
 
   deinit() {
-    this.routerHook.removeGlobalComponent('DeckyToaster');
+    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');
   }
 }
 
-- 
cgit v1.2.3

