feat(kopiaui): support for desktop notifications (#4352)

This commit is contained in:
Jarek Kowalski
2025-01-21 20:58:53 -08:00
committed by GitHub
parent 7dec4dea8a
commit a2afcb971d
3 changed files with 109 additions and 13 deletions

View File

@@ -3,6 +3,7 @@ import pkg from "electron-updater";
const autoUpdater = pkg.autoUpdater;
import { iconsPath, publicPath, selectByOS } from './utils.js';
import { toggleLaunchAtStartup, willLaunchAtStartup, refreshWillLaunchAtStartup } from './auto-launch.js';
import { setNotificationLevel, getNotificationLevel } from './notifications.js';
import { serverForRepo } from './server.js';
import { loadConfigs, allConfigs, deleteConfigIfDisconnected, addNewConfig, configDir, isFirstRun, isPortableConfig } from './config.js';
@@ -223,6 +224,7 @@ ipcMain.handle('browse-dir', async (_event, path) => {
ipcMain.on('server-status-updated', updateTrayContextMenu);
ipcMain.on('launch-at-startup-updated', updateTrayContextMenu);
ipcMain.on('notification-config-updated', updateTrayContextMenu);
let updateAvailableInfo = null;
let updateDownloadStatusInfo = "";
@@ -466,8 +468,41 @@ app.on('ready', () => {
}
})
function showRepoNotification(e) {
const nl = getNotificationLevel();
if (nl === 0) {
// notifications disabled
return;
}
if (e.severity < 10 && nl === 1) {
// non-important notifications disabled.
return;
}
let urgency = "normal";
if (e.severity < 0) {
urgency = "low";
} else if (e.severity >= 10) { // warnings and errors
urgency = "critical";
} else {
urgency = "normal";
}
const notification = new Notification({
title: e.notification.subject,
body: e.notification.body,
urgency: urgency
});
notification.on('click', () => showRepoWindow(e.repositoryID));
notification.show();
}
ipcMain.addListener('config-list-updated-event', () => updateTrayContextMenu());
ipcMain.addListener('status-updated-event', () => updateTrayContextMenu());
ipcMain.addListener('repo-notification-event', showRepoNotification);
function addAnotherRepository() {
const repoID = addNewConfig();
@@ -530,6 +565,8 @@ function updateTrayContextMenu() {
autoUpdateMenuItems.push({ label: "KopiaUI is up-to-date: " + app.getVersion(), enabled: false });
}
const nl = getNotificationLevel();
let template = defaultReposTemplates.concat(additionalReposTemplates).concat([
{ type: 'separator' },
{ label: 'Connect To Another Repository...', click: addAnotherRepository },
@@ -538,6 +575,11 @@ function updateTrayContextMenu() {
]).concat(autoUpdateMenuItems).concat([
{ type: 'separator' },
{ label: 'Launch At Startup', type: 'checkbox', click: toggleLaunchAtStartup, checked: willLaunchAtStartup() },
{ label: 'Notifications', type: 'submenu', submenu: [
{ label: 'Enabled', type: 'radio', click: () => setNotificationLevel(2), checked: nl === 2 },
{ label: 'Warnings And Errors', type: 'radio', click: () => setNotificationLevel(1), checked: nl === 1 },
{ label: 'Disabled', type: 'radio', click: () => setNotificationLevel(0), checked: nl === 0 },
] },
{ label: 'Quit', role: 'quit' },
]);

View File

@@ -0,0 +1,35 @@
import { ipcMain } from 'electron';
import { configDir } from './config.js';
const path = await import('path');
const fs = await import('fs');
const LevelDisabled = 0;
const LevelDefault = 1;
const LevelAll = 2;
let level = -1;
export function getNotificationLevel() {
if (level === -1) {
try {
const cfg = fs.readFileSync(path.join(configDir(), 'notifications.json'));
return JSON.parse(cfg).level;
} catch (e) {
level = LevelDefault;
}
}
return level;
}
export function setNotificationLevel(l) {
level = l;
if (level < LevelDisabled || level > LevelAll) {
level = LevelDefault;
}
fs.writeFileSync(path.join(configDir(), 'notifications.json'), JSON.stringify({ level: l }));
ipcMain.emit('notification-config-updated');
}

View File

@@ -41,9 +41,11 @@ function newServerForRepo(repoID) {
'--random-server-control-password',
'--tls-generate-cert',
'--async-repo-connect',
'--error-notifications=always',
'--kopiaui-notifications', // will print notification JSON to stderr
'--shutdown-on-stdin', // shutdown the server when parent dies
'--address=127.0.0.1:0');
args.push("--config-file", path.resolve(configDir(), repoID + ".config"));
if (isPortableConfig()) {
@@ -67,7 +69,7 @@ function newServerForRepo(repoID) {
const statusUpdated = this.raiseStatusUpdatedEvent.bind(this);
const pollInterval = 3000;
const pollInterval = 3000;
function pollOnce() {
if (!runningServerAddress || !runningServerCertificate || !runningServerPassword || !runningServerControlPassword) {
@@ -80,13 +82,13 @@ function newServerForRepo(repoID) {
port: parseInt(new URL(runningServerAddress).port),
method: "GET",
path: "/api/v1/control/status",
timeout: pollInterval,
timeout: pollInterval,
headers: {
'Authorization': 'Basic ' + Buffer.from("server-control" + ':' + runningServerControlPassword).toString('base64')
}
}
}, (resp) => {
if (resp.statusCode === 200) {
resp.on('data', x => {
resp.on('data', x => {
try {
const newDetails = JSON.parse(x);
if (JSON.stringify(newDetails) != JSON.stringify(runningServerStatusDetails)) {
@@ -101,7 +103,7 @@ function newServerForRepo(repoID) {
log.warn('error fetching status', resp.statusMessage);
}
});
req.on('error', (e)=>{
req.on('error', (e) => {
log.info('error fetching status', e);
});
req.end();
@@ -159,6 +161,14 @@ function newServerForRepo(repoID) {
runningServerAddress = value;
this.raiseStatusUpdatedEvent();
break;
case "NOTIFICATION":
try {
this.raiseNotificationEvent(JSON.parse(value));
} catch (e) {
log.warn('unable to parse notification JSON', e);
}
break;
}
}
@@ -231,6 +241,15 @@ function newServerForRepo(repoID) {
ipcMain.emit('status-updated-event', args);
},
raiseNotificationEvent(notification) {
const args = {
repoID: repoID,
notification: notification,
};
ipcMain.emit('repo-notification-event', args);
},
};
};
@@ -243,13 +262,13 @@ ipcMain.on('status-fetch', (event, args) => {
})
export function serverForRepo(repoID) {
let s = servers[repoID];
if (s) {
return s;
}
s = newServerForRepo(repoID);
servers[repoID] = s;
let s = servers[repoID];
if (s) {
return s;
}
s = newServerForRepo(repoID);
servers[repoID] = s;
return s;
}