1
0
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:
Laurent Cozic 2017-11-28 00:22:38 +00:00
parent 7df6541902
commit 6e23fead59
10 changed files with 131 additions and 35 deletions

View File

@ -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();
});
} }
} }

View File

@ -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_;
} }

View File

@ -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);

View File

@ -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('","') + '")');
} }

View File

@ -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');
} }
} }

View File

@ -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) {

View File

@ -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 = () => {

View File

@ -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
} }

View File

@ -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 });
} }

View File

@ -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);
} }
} }