summaryrefslogtreecommitdiff
path: root/frontend/src/toaster.tsx
blob: 54c0d7fbd752df99352644043e95e411d6684e9b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import type { ToastData, ToastNotification } from '@decky/api';
import { ErrorBoundary, 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;
    settingsStore: any;
    NotificationStore: any;
  }
}

class Toaster extends Logger {
  private toastPatch?: Patch;

  constructor() {
    super('Toaster');

    window.__TOASTER_INSTANCE?.deinit?.();
    window.__TOASTER_INSTANCE = this;

    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) => (
          <ErrorBoundary>
            <Toast toast={notification.data} newIndicator={notification.bNewIndicator} location={args?.[0]?.location} />
          </ErrorBoundary>
        ));
      }
      return callOriginal;
    });

    this.log('Initialized');
  }

  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 || 13,
      eSource: 1, // Client
      nToastDurationMS: toast.duration || (toast.duration = 5e3),
      data: toast,
      decky: true,
    };
    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);
    }
    try {
      window.NotificationStore.ProcessNotification(info, toastData, ToastType.New);
    } catch (e) {
      this.error('Error while sending toast:', e);
    }
    return toastResult;
  }

  deinit() {
    this.toastPatch?.unpatch();
  }
}

export default Toaster;