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);
}
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_.addTarget('file', { path: logFilePath });
this.autoUpdateLogger_.setLevel(Logger.LEVEL_DEBUG);
this.autoUpdateLogger_.info('checkForUpdatesAndNotify: Intializing...');
this.autoUpdateLogger_.info('checkForUpdatesAndNotify: Initializing...');
this.autoUpdater_ = require("electron-updater").autoUpdater;
this.autoUpdater_.logger = this.autoUpdateLogger_;
}

View File

@ -348,7 +348,7 @@ class BaseApplication {
this.dbLogger_.setLevel(initArgs.logLevel);
if (Setting.value('env') === 'dev') {
this.dbLogger_.setLevel(Logger.LEVEL_DEBUG);
this.dbLogger_.setLevel(Logger.LEVEL_WARN);
}
this.logger_.info('Profile directory: ' + profileDir);

View File

@ -331,14 +331,14 @@ class BaseModel {
}
static delete(id, options = null) {
options = this.modOptions(options);
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]);
}
static batchDelete(ids, options = null) {
if (!ids.length) return;
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('","') + '")');
}

View File

@ -14,16 +14,14 @@ class Alarm extends BaseModel {
return this.modelSelectOne('SELECT * FROM alarms WHERE note_id = ?', [noteId]);
}
static async garbageCollect() {
// Delete alarms that have already been triggered
await this.db().exec('DELETE FROM alarms WHERE trigger_time <= ?', [Date.now()]);
static async deleteExpiredAlarms() {
return 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
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)');
// 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');
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 });
}
}

View File

@ -422,7 +422,7 @@ class Note extends BaseItem {
}
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) {

View File

@ -37,6 +37,11 @@ reg.syncTarget = (syncTargetId = null) => {
reg.scheduleSync = async (delay = null) => {
if (delay === null) delay = 1000 * 3;
let promiseResolve = null;
const promise = new Promise((resolve, reject) => {
promiseResolve = resolve;
});
if (reg.scheduleSyncId_) {
clearTimeout(reg.scheduleSyncId_);
reg.scheduleSyncId_ = null;
@ -57,6 +62,7 @@ reg.scheduleSync = async (delay = null) => {
if (!reg.syncTarget(syncTargetId).isAuthenticated()) {
reg.logger().info('Synchroniser is missing credentials - manual sync required to authenticate.');
promiseResolve();
return;
}
@ -73,6 +79,7 @@ reg.scheduleSync = async (delay = null) => {
if (error.code == 'alreadyStarted') {
reg.logger().info(error.message);
} else {
promiseResolve();
throw error;
}
}
@ -82,6 +89,8 @@ reg.scheduleSync = async (delay = null) => {
}
reg.setupRecurrentSync();
promiseResolve();
};
if (delay === 0) {
@ -89,6 +98,8 @@ reg.scheduleSync = async (delay = null) => {
} else {
reg.scheduleSyncId_ = setTimeout(timeoutCallback, delay);
}
return promise;
}
reg.setupRecurrentSync = () => {

View File

@ -20,13 +20,52 @@ class AlarmService {
return this.logger_;
}
static async updateNoteNotification(noteId, isDeleted = false) {
const note = await Note.load(noteId);
static async garbageCollect() {
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;
const driver = this.driver();
let alarm = await Alarm.byNoteId(note.id);
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 ||
!Note.needAlarm(note) ||
(alarm && alarm.trigger_time !== note.todo_due))
@ -34,11 +73,24 @@ class AlarmService {
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) {
this.logger().info('Clearing notification for note ' + noteId);
await this.driver().clearNotification(alarm.id);
await driver.clearNotification(alarm.id);
await Alarm.delete(alarm.id);
}
@ -52,20 +104,28 @@ class AlarmService {
// Reload alarm to get its ID
alarm = await Alarm.byNoteId(note.id);
const notification = {
id: alarm.id,
date: new Date(note.todo_due),
title: note.title,
};
const notification = makeNotificationFromAlarm(alarm);
if (note.body) notification.body = note.body;
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: locale-dependent format
// TODO: status to view active notifications
}

View File

@ -2,6 +2,14 @@ const PushNotification = require('react-native-push-notification');
class AlarmServiceDriver {
hasPersistentNotifications() {
return true;
}
notificationIsSet(alarmId) {
throw new Error('Available only for non-persistent alarms');
}
async clearNotification(id) {
PushNotification.cancelLocalNotifications({ id: id });
}

View File

@ -1,21 +1,32 @@
class AlarmServiceDriverNode {
constructor() {
this.notifications_ = {};
}
hasPersistentNotifications() {
return false;
}
notificationIsSet(id) {
return id in this.notifications_;
}
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) {
console.info('AlarmServiceDriverNode::scheduleNotification', notification);
const now = Date.now();
const interval = notification.date.getTime() - now;
if (interval < 0) return;
// const androidNotification = {
// id: notification.id,
// message: notification.title.substr(0, 100), // No idea what the limits are for title and body but set something reasonable anyway
// date: notification.date,
// };
// if ('body' in notification) androidNotification.body = notification.body.substr(0, 512);
// PushNotification.localNotificationSchedule(androidNotification);
const timeoutId = setTimeout(() => {
console.info('NOTIFICATION: ', notification);
this.clearNotification(notification.id);
}, interval);
}
}