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 }