mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
Electron: Getting notifications to work
This commit is contained in:
parent
7df6541902
commit
6e23fead59
@ -355,7 +355,15 @@ class Application extends BaseApplication {
|
|||||||
setInterval(() => { runAutoUpdateCheck() }, 2 * 60 * 60 * 1000);
|
setInterval(() => { runAutoUpdateCheck() }, 2 * 60 * 60 * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
reg.scheduleSync();
|
setTimeout(() => {
|
||||||
|
AlarmService.garbageCollect();
|
||||||
|
}, 1000 * 60 * 60);
|
||||||
|
|
||||||
|
reg.scheduleSync().then(() => {
|
||||||
|
// Wait for the first sync before updating the notifications, since synchronisation
|
||||||
|
// might change the notifications.
|
||||||
|
AlarmService.updateAllNotifications();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -84,7 +84,7 @@ class Bridge {
|
|||||||
this.autoUpdateLogger_ = new Logger();
|
this.autoUpdateLogger_ = new Logger();
|
||||||
this.autoUpdateLogger_.addTarget('file', { path: logFilePath });
|
this.autoUpdateLogger_.addTarget('file', { path: logFilePath });
|
||||||
this.autoUpdateLogger_.setLevel(Logger.LEVEL_DEBUG);
|
this.autoUpdateLogger_.setLevel(Logger.LEVEL_DEBUG);
|
||||||
this.autoUpdateLogger_.info('checkForUpdatesAndNotify: Intializing...');
|
this.autoUpdateLogger_.info('checkForUpdatesAndNotify: Initializing...');
|
||||||
this.autoUpdater_ = require("electron-updater").autoUpdater;
|
this.autoUpdater_ = require("electron-updater").autoUpdater;
|
||||||
this.autoUpdater_.logger = this.autoUpdateLogger_;
|
this.autoUpdater_.logger = this.autoUpdateLogger_;
|
||||||
}
|
}
|
||||||
|
@ -348,7 +348,7 @@ class BaseApplication {
|
|||||||
this.dbLogger_.setLevel(initArgs.logLevel);
|
this.dbLogger_.setLevel(initArgs.logLevel);
|
||||||
|
|
||||||
if (Setting.value('env') === 'dev') {
|
if (Setting.value('env') === 'dev') {
|
||||||
this.dbLogger_.setLevel(Logger.LEVEL_DEBUG);
|
this.dbLogger_.setLevel(Logger.LEVEL_WARN);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger_.info('Profile directory: ' + profileDir);
|
this.logger_.info('Profile directory: ' + profileDir);
|
||||||
|
@ -331,14 +331,14 @@ class BaseModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static delete(id, options = null) {
|
static delete(id, options = null) {
|
||||||
options = this.modOptions(options);
|
|
||||||
if (!id) throw new Error('Cannot delete object without an ID');
|
if (!id) throw new Error('Cannot delete object without an ID');
|
||||||
|
options = this.modOptions(options);
|
||||||
return this.db().exec('DELETE FROM ' + this.tableName() + ' WHERE id = ?', [id]);
|
return this.db().exec('DELETE FROM ' + this.tableName() + ' WHERE id = ?', [id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
static batchDelete(ids, options = null) {
|
static batchDelete(ids, options = null) {
|
||||||
|
if (!ids.length) return;
|
||||||
options = this.modOptions(options);
|
options = this.modOptions(options);
|
||||||
if (!ids.length) throw new Error('Cannot delete object without an ID');
|
|
||||||
return this.db().exec('DELETE FROM ' + this.tableName() + ' WHERE id IN ("' + ids.join('","') + '")');
|
return this.db().exec('DELETE FROM ' + this.tableName() + ' WHERE id IN ("' + ids.join('","') + '")');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,16 +14,14 @@ class Alarm extends BaseModel {
|
|||||||
return this.modelSelectOne('SELECT * FROM alarms WHERE note_id = ?', [noteId]);
|
return this.modelSelectOne('SELECT * FROM alarms WHERE note_id = ?', [noteId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async garbageCollect() {
|
static async deleteExpiredAlarms() {
|
||||||
// Delete alarms that have already been triggered
|
return this.db().exec('DELETE FROM alarms WHERE trigger_time <= ?', [Date.now()]);
|
||||||
await this.db().exec('DELETE FROM alarms WHERE trigger_time <= ?', [Date.now()]);
|
}
|
||||||
|
|
||||||
// Delete alarms that correspond to non-existent notes
|
static async alarmIdsWithoutNotes() {
|
||||||
// https://stackoverflow.com/a/4967229/561309
|
// https://stackoverflow.com/a/4967229/561309
|
||||||
await this.db().exec('DELETE FROM alarms WHERE id IN (SELECT alarms.id FROM alarms LEFT JOIN notes ON alarms.note_id = notes.id WHERE notes.id IS NULL)');
|
const alarms = await this.db().selectAll('SELECT alarms.id FROM alarms LEFT JOIN notes ON alarms.note_id = notes.id WHERE notes.id IS NULL');
|
||||||
|
return alarms.map((a) => { return a.id });
|
||||||
// TODO: Check for duplicate alarms for a note
|
|
||||||
// const rows = await this.db().exec('SELECT count(*) as note_count, note_id from alarms group by note_id having note_count >= 2');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -422,7 +422,7 @@ class Note extends BaseItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static dueNotes() {
|
static dueNotes() {
|
||||||
return this.modelSelectAll('SELECT id, title, body, todo_due FROM notes WHERE is_conflict = 0 AND is_todo = 1 AND todo_completed = 0 AND todo_due > ?', [time.unixMs()]);
|
return this.modelSelectAll('SELECT id, title, body, is_todo, todo_due, todo_completed, is_conflict FROM notes WHERE is_conflict = 0 AND is_todo = 1 AND todo_completed = 0 AND todo_due > ?', [time.unixMs()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
static needAlarm(note) {
|
static needAlarm(note) {
|
||||||
|
@ -37,6 +37,11 @@ reg.syncTarget = (syncTargetId = null) => {
|
|||||||
reg.scheduleSync = async (delay = null) => {
|
reg.scheduleSync = async (delay = null) => {
|
||||||
if (delay === null) delay = 1000 * 3;
|
if (delay === null) delay = 1000 * 3;
|
||||||
|
|
||||||
|
let promiseResolve = null;
|
||||||
|
const promise = new Promise((resolve, reject) => {
|
||||||
|
promiseResolve = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
if (reg.scheduleSyncId_) {
|
if (reg.scheduleSyncId_) {
|
||||||
clearTimeout(reg.scheduleSyncId_);
|
clearTimeout(reg.scheduleSyncId_);
|
||||||
reg.scheduleSyncId_ = null;
|
reg.scheduleSyncId_ = null;
|
||||||
@ -57,6 +62,7 @@ reg.scheduleSync = async (delay = null) => {
|
|||||||
|
|
||||||
if (!reg.syncTarget(syncTargetId).isAuthenticated()) {
|
if (!reg.syncTarget(syncTargetId).isAuthenticated()) {
|
||||||
reg.logger().info('Synchroniser is missing credentials - manual sync required to authenticate.');
|
reg.logger().info('Synchroniser is missing credentials - manual sync required to authenticate.');
|
||||||
|
promiseResolve();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,6 +79,7 @@ reg.scheduleSync = async (delay = null) => {
|
|||||||
if (error.code == 'alreadyStarted') {
|
if (error.code == 'alreadyStarted') {
|
||||||
reg.logger().info(error.message);
|
reg.logger().info(error.message);
|
||||||
} else {
|
} else {
|
||||||
|
promiseResolve();
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -82,6 +89,8 @@ reg.scheduleSync = async (delay = null) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reg.setupRecurrentSync();
|
reg.setupRecurrentSync();
|
||||||
|
|
||||||
|
promiseResolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (delay === 0) {
|
if (delay === 0) {
|
||||||
@ -89,6 +98,8 @@ reg.scheduleSync = async (delay = null) => {
|
|||||||
} else {
|
} else {
|
||||||
reg.scheduleSyncId_ = setTimeout(timeoutCallback, delay);
|
reg.scheduleSyncId_ = setTimeout(timeoutCallback, delay);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
reg.setupRecurrentSync = () => {
|
reg.setupRecurrentSync = () => {
|
||||||
|
@ -20,13 +20,52 @@ class AlarmService {
|
|||||||
return this.logger_;
|
return this.logger_;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async updateNoteNotification(noteId, isDeleted = false) {
|
static async garbageCollect() {
|
||||||
const note = await Note.load(noteId);
|
this.logger().info('Garbage collecting alarms...');
|
||||||
|
|
||||||
|
// Delete alarms that have already been triggered
|
||||||
|
await Alarm.deleteExpiredAlarms();
|
||||||
|
|
||||||
|
// Delete alarms that correspond to non-existent notes
|
||||||
|
const alarmIds = await Alarm.alarmIdsWithoutNotes();
|
||||||
|
for (let i = 0; i < alarmIds.length; i++) {
|
||||||
|
this.logger().info('Clearing notification for non-existing note. Alarm ' + alarmIds[i]);
|
||||||
|
await this.driver().clearNotification(alarmIds[i]);
|
||||||
|
}
|
||||||
|
await Alarm.batchDelete(alarmIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// When passing a note, make sure it has all the required properties
|
||||||
|
// (better to pass a complete note or else just the ID)
|
||||||
|
static async updateNoteNotification(noteOrId, isDeleted = false) {
|
||||||
|
let note = null;
|
||||||
|
let noteId = null;
|
||||||
|
|
||||||
|
if (typeof noteOrId === 'object') {
|
||||||
|
note = noteOrId;
|
||||||
|
noteId = note.id;
|
||||||
|
} else {
|
||||||
|
note = await Note.load(noteOrId);
|
||||||
|
noteId = note ? note.id : null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!note && !isDeleted) return;
|
if (!note && !isDeleted) return;
|
||||||
|
|
||||||
|
const driver = this.driver();
|
||||||
|
|
||||||
let alarm = await Alarm.byNoteId(note.id);
|
let alarm = await Alarm.byNoteId(note.id);
|
||||||
let clearAlarm = false;
|
let clearAlarm = false;
|
||||||
|
|
||||||
|
const makeNotificationFromAlarm = (alarm) => {
|
||||||
|
return {
|
||||||
|
id: alarm.id,
|
||||||
|
date: new Date(note.todo_due),
|
||||||
|
title: note.title,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info('NOTE', note, Note.needAlarm(note));
|
||||||
|
|
||||||
if (isDeleted ||
|
if (isDeleted ||
|
||||||
!Note.needAlarm(note) ||
|
!Note.needAlarm(note) ||
|
||||||
(alarm && alarm.trigger_time !== note.todo_due))
|
(alarm && alarm.trigger_time !== note.todo_due))
|
||||||
@ -34,11 +73,24 @@ class AlarmService {
|
|||||||
clearAlarm = !!alarm;
|
clearAlarm = !!alarm;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!clearAlarm && alarm) return; // Alarm already exists and set at the right time
|
if (!clearAlarm && alarm) { // Alarm already exists and set at the right time
|
||||||
|
|
||||||
|
// For persistent notifications (those that stay active after the app has been closed, like on mobile), if we have
|
||||||
|
// an alarm object we can be sure that the notification has already been set, so there's nothing to do.
|
||||||
|
// For non-persistent notifications however we need to check that the notification has been set because, for example,
|
||||||
|
// if the app has just started the notifications need to be set again. so we do this below.
|
||||||
|
if (!driver.hasPersistentNotifications() && !driver.notificationIsSet(alarm.id)) {
|
||||||
|
const notification = makeNotificationFromAlarm(alarm);
|
||||||
|
this.logger().info('Scheduling (non-persistent) notification for note ' + note.id, notification);
|
||||||
|
driver.scheduleNotification(notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (clearAlarm) {
|
if (clearAlarm) {
|
||||||
this.logger().info('Clearing notification for note ' + noteId);
|
this.logger().info('Clearing notification for note ' + noteId);
|
||||||
await this.driver().clearNotification(alarm.id);
|
await driver.clearNotification(alarm.id);
|
||||||
await Alarm.delete(alarm.id);
|
await Alarm.delete(alarm.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,20 +104,28 @@ class AlarmService {
|
|||||||
// Reload alarm to get its ID
|
// Reload alarm to get its ID
|
||||||
alarm = await Alarm.byNoteId(note.id);
|
alarm = await Alarm.byNoteId(note.id);
|
||||||
|
|
||||||
const notification = {
|
const notification = makeNotificationFromAlarm(alarm);
|
||||||
id: alarm.id,
|
|
||||||
date: new Date(note.todo_due),
|
|
||||||
title: note.title,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (note.body) notification.body = note.body;
|
if (note.body) notification.body = note.body;
|
||||||
|
|
||||||
this.logger().info('Scheduling notification for note ' + note.id, notification);
|
this.logger().info('Scheduling notification for note ' + note.id, notification);
|
||||||
await this.driver().scheduleNotification(notification);
|
await driver.scheduleNotification(notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async updateAllNotifications() {
|
||||||
|
this.logger().info('Updading all notifications...');
|
||||||
|
|
||||||
|
await this.garbageCollect();
|
||||||
|
|
||||||
|
const dueNotes = await Note.dueNotes();
|
||||||
|
for (let i = 0; i < dueNotes.length; i++) {
|
||||||
|
await this.updateNoteNotification(dueNotes[i]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: inner notifications (when app is active)
|
// TODO: inner notifications (when app is active)
|
||||||
// TODO: locale-dependent format
|
// TODO: locale-dependent format
|
||||||
|
// TODO: status to view active notifications
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,14 @@ const PushNotification = require('react-native-push-notification');
|
|||||||
|
|
||||||
class AlarmServiceDriver {
|
class AlarmServiceDriver {
|
||||||
|
|
||||||
|
hasPersistentNotifications() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationIsSet(alarmId) {
|
||||||
|
throw new Error('Available only for non-persistent alarms');
|
||||||
|
}
|
||||||
|
|
||||||
async clearNotification(id) {
|
async clearNotification(id) {
|
||||||
PushNotification.cancelLocalNotifications({ id: id });
|
PushNotification.cancelLocalNotifications({ id: id });
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,32 @@
|
|||||||
class AlarmServiceDriverNode {
|
class AlarmServiceDriverNode {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.notifications_ = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
hasPersistentNotifications() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationIsSet(id) {
|
||||||
|
return id in this.notifications_;
|
||||||
|
}
|
||||||
|
|
||||||
async clearNotification(id) {
|
async clearNotification(id) {
|
||||||
console.info('AlarmServiceDriverNode::clearNotification', id);
|
if (!this.notificationIsSet(id)) return;
|
||||||
|
clearTimeout(this.notifications_[id].timeoutId);
|
||||||
|
delete this.notifications_[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
async scheduleNotification(notification) {
|
async scheduleNotification(notification) {
|
||||||
console.info('AlarmServiceDriverNode::scheduleNotification', notification);
|
const now = Date.now();
|
||||||
|
const interval = notification.date.getTime() - now;
|
||||||
|
if (interval < 0) return;
|
||||||
|
|
||||||
// const androidNotification = {
|
const timeoutId = setTimeout(() => {
|
||||||
// id: notification.id,
|
console.info('NOTIFICATION: ', notification);
|
||||||
// message: notification.title.substr(0, 100), // No idea what the limits are for title and body but set something reasonable anyway
|
this.clearNotification(notification.id);
|
||||||
// date: notification.date,
|
}, interval);
|
||||||
// };
|
|
||||||
|
|
||||||
// if ('body' in notification) androidNotification.body = notification.body.substr(0, 512);
|
|
||||||
|
|
||||||
// PushNotification.localNotificationSchedule(androidNotification);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user