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

     1  import { setCoinHref } from './coinexplorers'
     2  import Doc from './doc'
     3  import * as intl from './locales'
     4  import { CoreNote, PageElement } from './registry'
     5  import State from './state'
     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  // Bind the webview and webkit message handlers to the window object for darwin.
   139  // Linux and Windows handlers are binded in
   140  // client/cmd/bisonw-desktop/app.go#L399
   141  if (isDesktopWebkit()) {
   142    window.isWebview = () => { return true }
   143    window.sendOSNotification = async (title: string, body?: string) => {
   144      await window.webkit.messageHandlers.bwHandler.postMessage(['sendOSNotification', title, body])
   145    }
   146    window.openUrl = async (url: string) => {
   147      await window.webkit.messageHandlers.bwHandler.postMessage(['openURL', url.toString()])
   148    }
   149    window.open = (url?: string | URL, target?: string, feature?: string): Window | null => {
   150      if (url === undefined) return null
   151      if (target !== undefined || feature !== '') console.warn('open: target and feature are not supported in webview')
   152      window.webkit.messageHandlers.bwHandler.postMessage(['openURL', url.toString()])
   153      return null
   154    }
   155  }
   156  
   157  // determine whether we're running in a webview or in browser, and export
   158  // the appropriate notifier accordingly.
   159  export const Notifier = isDesktopWebview() || isDesktopWebkit() ? OSDesktopNotifier : BrowserNotifier
   160  
   161  export async function desktopNotify (note: CoreNote) {
   162    if (!desktopNtfnSettings.browserNtfnEnabled || !desktopNtfnSettings[note.type]) return
   163    await Notifier.sendDesktopNotification(note.subject, plainNote(note.details))
   164  }
   165  
   166  export function fetchDesktopNtfnSettings (): DesktopNtfnSetting {
   167    if (desktopNtfnSettings !== undefined) {
   168      return desktopNtfnSettings
   169    }
   170    const k = desktopNtfnSettingsKey()
   171    desktopNtfnSettings = (State.fetchLocal(k) ?? {}) as DesktopNtfnSetting
   172    return desktopNtfnSettings
   173  }
   174  
   175  export function updateNtfnSetting (noteType: string, enabled: boolean) {
   176    fetchDesktopNtfnSettings()
   177    desktopNtfnSettings[noteType] = enabled
   178    State.storeLocal(desktopNtfnSettingsKey(), desktopNtfnSettings)
   179  }
   180  
   181  const coinExplorerTokenRe = /\{\{\{([^|]+)\|([^}]+)\}\}\}/g
   182  const orderTokenRe = /\{\{\{order\|([^}]+)\}\}\}/g
   183  
   184  /*
   185   * insertRichNote replaces tx and order hash tokens in the input string with
   186   * <a> elements that link to the asset's chain explorer and order details
   187   * view, and inserts the resulting HTML into the supplied parent element.
   188   */
   189  export function insertRichNote (parent: PageElement, inputString: string) {
   190    const s = inputString.replace(orderTokenRe, (_match, orderToken) => {
   191      const link = document.createElement('a')
   192      link.setAttribute('href', '/order/' + orderToken)
   193      link.setAttribute('class', 'subtlelink')
   194      link.textContent = orderToken.slice(0, 8)
   195      return link.outerHTML
   196    }).replace(coinExplorerTokenRe, (_match, assetID, hash) => {
   197      const link = document.createElement('a')
   198      link.setAttribute('data-explorer-coin', hash)
   199      link.setAttribute('target', '_blank')
   200      link.textContent = hash.slice(0, 8)
   201      setCoinHref(assetID, link)
   202      return link.outerHTML
   203    })
   204    const els = Doc.noderize(s).body
   205    while (els.firstChild) parent.appendChild(els.firstChild)
   206  }
   207  
   208  /*
   209   * plainNote replaces tx and order hash tokens tokens in the input string with
   210   * shortened hashes, for rendering in browser notifications and popups.
   211   */
   212  export function plainNote (inputString: string): string {
   213    const replacedString = inputString.replace(coinExplorerTokenRe, (_match, _assetID, hash) => {
   214      return hash.slice(0, 8)
   215    })
   216    return replacedString
   217  }