decred.org/dcrdex@v1.0.3/client/webserver/site/src/js/notifications.ts (about)

     1  import { CoreNote, PageElement } from './registry'
     2  import * as intl from './locales'
     3  import State from './state'
     4  import { setCoinHref } from './coinexplorers'
     5  import Doc from './doc'
     6  
     7  export const IGNORE = 0
     8  export const DATA = 1
     9  export const POKE = 2
    10  export const SUCCESS = 3
    11  export const WARNING = 4
    12  export const ERROR = 5
    13  
    14  /*
    15   * make constructs a new notification. The notification structure is a mirror of
    16   * the structure of notifications sent from the web server.
    17   * NOTE: I'm hoping to make this function obsolete, since errors generated in
    18   * javascript should usually be displayed/cached somewhere better. For example,
    19   * if the error is generated during submission of a form, the error should be
    20   * displayed on or near the form itself, not in the notifications.
    21   */
    22  export function make (subject: string, details: string, severity: number): CoreNote {
    23    return {
    24      subject: subject,
    25      details: details,
    26      severity: severity,
    27      stamp: new Date().getTime(),
    28      acked: false,
    29      type: 'internal',
    30      topic: 'internal',
    31      id: ''
    32    }
    33  }
    34  
    35  const NoteTypeOrder = 'order'
    36  const NoteTypeMatch = 'match'
    37  const NoteTypeBondPost = 'bondpost'
    38  const NoteTypeConnEvent = 'conn'
    39  
    40  type DesktopNtfnSettingLabel = {
    41    [x: string]: string
    42  }
    43  
    44  export type DesktopNtfnSetting = {
    45    [x: string]: boolean
    46  }
    47  
    48  function desktopNtfnSettingsKey (): string {
    49    return `desktop_notifications-${window.location.host}`
    50  }
    51  
    52  export const desktopNtfnLabels: DesktopNtfnSettingLabel = {
    53    [NoteTypeOrder]: intl.ID_BROWSER_NTFN_ORDERS,
    54    [NoteTypeMatch]: intl.ID_BROWSER_NTFN_MATCHES,
    55    [NoteTypeBondPost]: intl.ID_BROWSER_NTFN_BONDS,
    56    [NoteTypeConnEvent]: intl.ID_BROWSER_NTFN_CONNECTIONS
    57  }
    58  
    59  export const defaultDesktopNtfnSettings: DesktopNtfnSetting = {
    60    [NoteTypeOrder]: true,
    61    [NoteTypeMatch]: true,
    62    [NoteTypeBondPost]: true,
    63    [NoteTypeConnEvent]: true
    64  }
    65  
    66  let desktopNtfnSettings: DesktopNtfnSetting
    67  
    68  // BrowserNotifier is a wrapper around the browser's notification API.
    69  class BrowserNotifier {
    70    static ntfnPermissionGranted (): boolean {
    71      return window.Notification.permission === 'granted'
    72    }
    73  
    74    static ntfnPermissionDenied (): boolean {
    75      return window.Notification.permission === 'denied'
    76    }
    77  
    78    static async requestNtfnPermission (): Promise<void> {
    79      if (!('Notification' in window)) {
    80        return
    81      }
    82      if (BrowserNotifier.ntfnPermissionGranted()) {
    83        BrowserNotifier.sendDesktopNotification(intl.prep(intl.ID_BROWSER_NTFN_ENABLED))
    84      } else if (!BrowserNotifier.ntfnPermissionDenied()) {
    85        await Notification.requestPermission()
    86        BrowserNotifier.sendDesktopNotification(intl.prep(intl.ID_BROWSER_NTFN_ENABLED))
    87      }
    88    }
    89  
    90    static async sendDesktopNotification (title: string, body?: string) {
    91      if (!BrowserNotifier.ntfnPermissionGranted()) return
    92      const ntfn = new window.Notification(title, {
    93        body: body,
    94        icon: '/img/softened-icon.png'
    95      })
    96      return ntfn
    97    }
    98  }
    99  
   100  // OSDesktopNotifier manages OS desktop notifications via the same interface
   101  // as BrowserNotifier, but sends notifications using an underlying Go
   102  // notification library exposed to the webview.
   103  class OSDesktopNotifier {
   104    static ntfnPermissionGranted (): boolean {
   105      return true
   106    }
   107  
   108    static ntfnPermissionDenied (): boolean {
   109      return false
   110    }
   111  
   112    static async requestNtfnPermission (): Promise<void> {
   113      await OSDesktopNotifier.sendDesktopNotification(intl.prep(intl.ID_BROWSER_NTFN_ENABLED))
   114      return Promise.resolve()
   115    }
   116  
   117    static async sendDesktopNotification (title: string, body?: string): Promise<void> {
   118      // webview/linux or webview/windows
   119      if (isDesktopWebview()) await window.sendOSNotification(title, body)
   120      // webkit/darwin
   121      // See: client/cmd/bisonw-desktop/app_darwin.go#L673-#L697
   122      else if (isDesktopWebkit()) await window.webkit.messageHandlers.bwHandler.postMessage(['sendOSNotification', title, body])
   123      else console.error('sendDesktopNotification: unknown environment')
   124    }
   125  }
   126  
   127  // isDesktopWebview checks if we are running in webview
   128  function isDesktopWebview (): boolean {
   129    return window.isWebview !== undefined
   130  }
   131  
   132  // isDesktopDarwin returns true if we are running in a webview on darwin
   133  // It tests for the existence of the bwHandler webkit message handler.
   134  function isDesktopWebkit (): boolean {
   135    return window.webkit?.messageHandlers?.bwHandler !== undefined
   136  }
   137  
   138  // determine whether we're running in a webview or in browser, and export
   139  // the appropriate notifier accordingly.
   140  export const Notifier = isDesktopWebview() || isDesktopWebkit() ? OSDesktopNotifier : BrowserNotifier
   141  
   142  export async function desktopNotify (note: CoreNote) {
   143    if (!desktopNtfnSettings.browserNtfnEnabled || !desktopNtfnSettings[note.type]) return
   144    await Notifier.sendDesktopNotification(note.subject, plainNote(note.details))
   145  }
   146  
   147  export function fetchDesktopNtfnSettings (): DesktopNtfnSetting {
   148    if (desktopNtfnSettings !== undefined) {
   149      return desktopNtfnSettings
   150    }
   151    const k = desktopNtfnSettingsKey()
   152    desktopNtfnSettings = (State.fetchLocal(k) ?? {}) as DesktopNtfnSetting
   153    return desktopNtfnSettings
   154  }
   155  
   156  export function updateNtfnSetting (noteType: string, enabled: boolean) {
   157    fetchDesktopNtfnSettings()
   158    desktopNtfnSettings[noteType] = enabled
   159    State.storeLocal(desktopNtfnSettingsKey(), desktopNtfnSettings)
   160  }
   161  
   162  const coinExplorerTokenRe = /\{\{\{([^|]+)\|([^}]+)\}\}\}/g
   163  const orderTokenRe = /\{\{\{order\|([^}]+)\}\}\}/g
   164  
   165  /*
   166   * insertRichNote replaces tx and order hash tokens in the input string with
   167   * <a> elements that link to the asset's chain explorer and order details
   168   * view, and inserts the resulting HTML into the supplied parent element.
   169   */
   170  export function insertRichNote (parent: PageElement, inputString: string) {
   171    const s = inputString.replace(orderTokenRe, (_match, orderToken) => {
   172      const link = document.createElement('a')
   173      link.setAttribute('href', '/order/' + orderToken)
   174      link.setAttribute('class', 'subtlelink')
   175      link.textContent = orderToken.slice(0, 8)
   176      return link.outerHTML
   177    }).replace(coinExplorerTokenRe, (_match, assetID, hash) => {
   178      const link = document.createElement('a')
   179      link.setAttribute('data-explorer-coin', hash)
   180      link.setAttribute('target', '_blank')
   181      link.textContent = hash.slice(0, 8)
   182      setCoinHref(assetID, link)
   183      return link.outerHTML
   184    })
   185    const els = Doc.noderize(s).body
   186    while (els.firstChild) parent.appendChild(els.firstChild)
   187  }
   188  
   189  /*
   190   * plainNote replaces tx and order hash tokens tokens in the input string with
   191   * shortened hashes, for rendering in browser notifications and popups.
   192   */
   193  export function plainNote (inputString: string): string {
   194    const replacedString = inputString.replace(coinExplorerTokenRe, (_match, _assetID, hash) => {
   195      return hash.slice(0, 8)
   196    })
   197    return replacedString
   198  }