You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	Electron: Getting notifications to work
This commit is contained in:
		| @@ -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(); | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -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_; | ||||
| 		} | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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('","') + '")'); | ||||
| 	}	 | ||||
|  | ||||
|   | ||||
| @@ -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 }); | ||||
| 	} | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -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 = () => { | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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 }); | ||||
| 	} | ||||
|   | ||||
| @@ -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); | ||||
| 	} | ||||
|  | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user