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>
This commit is contained in:
TrainDoctor
2022-10-30 10:32:05 -07:00
committed by GitHub
parent f5fc205384
commit bace5143d2
9 changed files with 391 additions and 248 deletions
+135 -134
View File
@@ -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');
}
}