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)
This commit is contained in:
AAGaming
2024-07-18 01:20:29 -04:00
parent 28c7254ef6
commit 88e7919a12
5 changed files with 86 additions and 173 deletions
+36 -136
View File
@@ -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;
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} />);
}
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);
}
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();
}
}