1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-21 09:38:01 +02:00
joplin/packages/lib/services/AlarmServiceDriverNode.ts

191 lines
7.1 KiB
TypeScript

import eventManager, { EventName } from '../eventManager';
import { Notification } from '../models/Alarm';
import shim from '../shim';
import Setting from '../models/Setting';
const notifier = require('node-notifier');
interface Options {
appName: string;
}
export default class AlarmServiceDriverNode {
private appName_: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
private notifications_: any = {};
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
private service_: any = null;
public constructor(options: Options) {
// 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;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
public setService(s: any) {
this.service_ = s;
}
public logger() {
return this.service_.logger();
}
public hasPersistentNotifications() {
return false;
}
public notificationIsSet(id: number) {
return id in this.notifications_;
}
public clearNotification(id: number) {
if (!this.notificationIsSet(id)) return;
shim.clearTimeout(this.notifications_[id].timeoutId);
delete this.notifications_[id];
}
private displayDefaultNotification(notification: Notification) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const o: any = {
appID: this.appName_,
title: notification.title,
icon: `${shim.electronBridge().electronApp().buildDir()}/icons/512x512.png`,
};
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);
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
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 {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const options: any = {
body: notification.body ? notification.body : '-',
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
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';
}
public async scheduleNotification(notification: Notification) {
const now = Date.now();
const interval = notification.date.getTime() - now;
if (interval < 0) return;
if (isNaN(interval)) {
throw new Error(`Trying to create a notification from an invalid object: ${JSON.stringify(notification)}`);
}
const permission = await this.checkPermission();
if (permission !== 'granted') {
this.logger().info(`AlarmServiceDriverNode::scheduleNotification: Notification ${notification.id}: Cancelled because permission was not granted.`);
return;
}
this.logger().info(`AlarmServiceDriverNode::scheduleNotification: Notification ${notification.id} with interval: ${interval}ms`);
if (this.notifications_[notification.id]) shim.clearTimeout(this.notifications_[notification.id].timeoutId);
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;
if (interval >= maxInterval) {
this.logger().info(`AlarmServiceDriverNode::scheduleNotification: Notification interval is greater than ${maxInterval}ms - will reschedule in ${maxInterval}ms`);
timeoutId = shim.setTimeout(() => {
if (!this.notifications_[notification.id]) {
this.logger().info(`AlarmServiceDriverNode::scheduleNotification: Notification ${notification.id} has been deleted - not rescheduling it`);
return;
}
void this.scheduleNotification(this.notifications_[notification.id]);
}, maxInterval);
} else {
timeoutId = shim.setTimeout(() => {
if (shim.isMac() && shim.isElectron()) {
this.displayMacNotification(notification);
} else {
this.displayDefaultNotification(notification);
}
this.clearNotification(notification.id);
eventManager.emit(EventName.NoteAlarmTrigger, { noteId: notification.noteId });
}, interval);
}
this.notifications_[notification.id] = { ...notification };
this.notifications_[notification.id].timeoutId = timeoutId;
}
}