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 }