2020-11-05 18:58:23 +02:00
import eventManager from '../eventManager' ;
import { Notification } from '../models/Alarm' ;
import shim from '../shim' ;
2020-11-28 14:02:26 +02:00
import Setting from '../models/Setting' ;
2017-11-28 20:47:41 +02:00
const notifier = require ( 'node-notifier' ) ;
2020-10-09 19:35:46 +02:00
interface Options {
2020-11-12 21:29:22 +02:00
appName : string ;
2020-10-09 19:35:46 +02:00
}
export default class AlarmServiceDriverNode {
2017-11-28 20:47:41 +02:00
2020-11-12 21:13:28 +02:00
private appName_ : string ;
private notifications_ : any = { } ;
private service_ : any = null ;
2020-10-09 19:35:46 +02:00
2020-11-12 21:13:28 +02:00
constructor ( options : Options ) {
2017-11-28 20:47:41 +02:00
// Note: appName is required to get the notification to work. It must be the same as the appId defined in package.json
// https://github.com/mikaelbr/node-notifier/issues/144#issuecomment-319324058
this . appName_ = options . appName ;
2019-10-02 20:21:42 +02:00
}
2020-11-12 21:13:28 +02:00
setService ( s : any ) {
2019-10-02 20:21:42 +02:00
this . service_ = s ;
}
logger() {
return this . service_ . logger ( ) ;
2017-11-28 02:22:38 +02:00
}
hasPersistentNotifications() {
return false ;
}
2020-11-12 21:13:28 +02:00
notificationIsSet ( id : number ) {
2017-11-28 02:22:38 +02:00
return id in this . notifications_ ;
}
2020-11-25 16:40:25 +02:00
clearNotification ( id : number ) {
2017-11-28 02:22:38 +02:00
if ( ! this . notificationIsSet ( id ) ) return ;
2020-10-09 19:35:46 +02:00
shim . clearTimeout ( this . notifications_ [ id ] . timeoutId ) ;
2017-11-28 02:22:38 +02:00
delete this . notifications_ [ id ] ;
2017-11-28 00:50:46 +02:00
}
2019-07-29 15:43:53 +02:00
2020-11-28 14:02:26 +02:00
private displayDefaultNotification ( notification : Notification ) {
const o : any = {
appID : this.appName_ ,
title : notification.title ,
2021-10-01 20:35:27 +02:00
icon : ` ${ shim . electronBridge ( ) . electronApp ( ) . buildDir ( ) } /icons/512x512.png ` ,
2020-11-28 14:02:26 +02:00
} ;
if ( 'body' in notification ) o . message = notification . body ;
// Message is required on Windows 7 however we don't want to repeat the title so
// make it an empty string.
// https://github.com/laurent22/joplin/issues/2144
if ( ! o . message ) o . message = '-' ;
this . logger ( ) . info ( 'AlarmServiceDriverNode::scheduleNotification: Triggering notification (default):' , o ) ;
notifier . notify ( o , ( error : any , response : any ) = > {
this . logger ( ) . info ( 'AlarmServiceDriverNode::scheduleNotification: node-notifier response:' , error , response ) ;
} ) ;
}
private displayMacNotification ( notification : Notification ) {
// On macOS, node-notifier is broken:
//
// https://github.com/mikaelbr/node-notifier/issues/352
//
// However we can use the native browser notification as described
// there:
//
// https://www.electronjs.org/docs/tutorial/notifications
//
// In fact it's likely that we could use this on other platforms too
try {
const options : any = {
body : notification.body ? notification . body : '-' ,
onerror : ( error : any ) = > {
this . logger ( ) . error ( 'AlarmServiceDriverNode::displayMacNotification' , error ) ;
} ,
} ;
this . logger ( ) . info ( 'AlarmServiceDriverNode::displayMacNotification: Triggering notification (macOS):' , notification . title , options ) ;
new Notification ( notification . title , options ) ;
} catch ( error ) {
this . logger ( ) . error ( 'AlarmServiceDriverNode::displayMacNotification' , error ) ;
}
}
private async checkPermission() {
if ( shim . isMac ( ) && shim . isElectron ( ) ) {
this . logger ( ) . info ( ` AlarmServiceDriverNode::checkPermission: Permission in settings is " ${ Setting . value ( 'notificationPermission' ) } " ` ) ;
if ( Setting . value ( 'notificationPermission' ) !== '' ) return Setting . value ( 'notificationPermission' ) ;
// In theory `Notification.requestPermission()` should be used to
// ask for permission but in practice this API is unreliable. In
// particular, it returns "granted" immediately even when
// notifications definitely aren't allowed (and creating a new
// notification would fail).
//
// Because of that, our approach is to trigger a notification, which
// should prompt macOS to ask for permission. Once this is done we
// manually save the result in the settings. Of course it means that
// if permission is changed afterwards, for example from the
// notification center, we won't know it and notifications will
// fail.
//
// All this means that for now this checkPermission function always
// returns "granted" and the setting has only two values: "granted"
// or "" (which means we need to do the check permission trick).
//
// The lack of "denied" value is acceptable in our context because
// if a user doesn't want notifications, they can simply not set
// alarms.
new Notification ( 'Checking permissions...' , {
body : 'Permission has been granted' ,
} ) ;
Setting . setValue ( 'notificationPermission' , 'granted' ) ;
}
return 'granted' ;
}
2020-11-12 21:13:28 +02:00
async scheduleNotification ( notification : Notification ) {
2017-11-28 02:22:38 +02:00
const now = Date . now ( ) ;
const interval = notification . date . getTime ( ) - now ;
if ( interval < 0 ) return ;
2017-11-28 00:50:46 +02:00
2018-12-08 01:42:29 +02:00
if ( isNaN ( interval ) ) {
2019-09-19 23:51:18 +02:00
throw new Error ( ` Trying to create a notification from an invalid object: ${ JSON . stringify ( notification ) } ` ) ;
2018-12-08 01:42:29 +02:00
}
2020-11-28 14:02:26 +02:00
const permission = await this . checkPermission ( ) ;
if ( permission !== 'granted' ) {
this . logger ( ) . info ( ` AlarmServiceDriverNode::scheduleNotification: Notification ${ notification . id } : Cancelled because permission was not granted. ` ) ;
return ;
}
2019-10-02 20:21:42 +02:00
this . logger ( ) . info ( ` AlarmServiceDriverNode::scheduleNotification: Notification ${ notification . id } with interval: ${ interval } ms ` ) ;
2020-10-09 19:35:46 +02:00
if ( this . notifications_ [ notification . id ] ) shim . clearTimeout ( this . notifications_ [ notification . id ] . timeoutId ) ;
2019-10-02 20:21:42 +02:00
let timeoutId = null ;
// Note: setTimeout will break for values larger than Number.MAX_VALUE - in which case the timer
// will fire immediately. So instead, if the interval is greater than a set max, reschedule after
// that max interval.
// https://stackoverflow.com/questions/3468607/why-does-settimeout-break-for-large-millisecond-delay-values/3468699
const maxInterval = 60 * 60 * 1000 ;
2021-01-23 17:51:19 +02:00
if ( interval >= maxInterval ) {
2019-10-02 20:21:42 +02:00
this . logger ( ) . info ( ` AlarmServiceDriverNode::scheduleNotification: Notification interval is greater than ${ maxInterval } ms - will reschedule in ${ maxInterval } ms ` ) ;
2020-10-09 19:35:46 +02:00
timeoutId = shim . setTimeout ( ( ) = > {
2019-10-02 20:21:42 +02:00
if ( ! this . notifications_ [ notification . id ] ) {
this . logger ( ) . info ( ` AlarmServiceDriverNode::scheduleNotification: Notification ${ notification . id } has been deleted - not rescheduling it ` ) ;
return ;
}
2020-11-29 02:22:17 +02:00
void this . scheduleNotification ( this . notifications_ [ notification . id ] ) ;
2019-10-02 20:21:42 +02:00
} , maxInterval ) ;
} else {
2020-10-09 19:35:46 +02:00
timeoutId = shim . setTimeout ( ( ) = > {
2020-11-28 14:02:26 +02:00
if ( shim . isMac ( ) && shim . isElectron ( ) ) {
this . displayMacNotification ( notification ) ;
} else {
this . displayDefaultNotification ( notification ) ;
}
2019-10-02 20:21:42 +02:00
this . clearNotification ( notification . id ) ;
2020-10-09 19:35:46 +02:00
eventManager . emit ( 'noteAlarmTrigger' , { noteId : notification.noteId } ) ;
2019-10-02 20:21:42 +02:00
} , interval ) ;
}
2017-11-28 20:47:41 +02:00
this . notifications_ [ notification . id ] = Object . assign ( { } , notification ) ;
this . notifications_ [ notification . id ] . timeoutId = timeoutId ;
2017-11-28 00:50:46 +02:00
}
}