Added desktop tray controls and reliable notification activation (#550)
This commit is contained in:
parent
a7ca6bb9fb
commit
ed7641117a
10 changed files with 214 additions and 20 deletions
|
|
@ -1,11 +1,72 @@
|
|||
import {readFileSync} from "node:fs"
|
||||
import {join} from "node:path"
|
||||
import {app} from "electron"
|
||||
import {app, Menu, Tray} from "electron"
|
||||
import {createCapacitorElectronApp} from "@capawesome/capacitor-electron"
|
||||
|
||||
const {appId, appName} = JSON.parse(
|
||||
readFileSync(join(app.getAppPath(), "generated/capacitor.config.json"), "utf8"),
|
||||
)
|
||||
let tray: Tray | undefined
|
||||
let quitting = false
|
||||
|
||||
const destroyTray = () => {
|
||||
tray?.destroy()
|
||||
tray = undefined
|
||||
}
|
||||
|
||||
app.on("before-quit", () => {
|
||||
quitting = true
|
||||
destroyTray()
|
||||
})
|
||||
|
||||
createCapacitorElectronApp({
|
||||
window: {width: 1200, height: 800},
|
||||
hooks: {
|
||||
beforeReady: () => {
|
||||
if (process.platform === "win32") {
|
||||
app.setAppUserModelId(appId)
|
||||
}
|
||||
},
|
||||
onWindowCreated: window => {
|
||||
const show = () => {
|
||||
if (window.isMinimized()) {
|
||||
window.restore()
|
||||
}
|
||||
window.show()
|
||||
window.focus()
|
||||
}
|
||||
if (process.platform === "darwin") {
|
||||
window.on("close", event => {
|
||||
if (!quitting) {
|
||||
event.preventDefault()
|
||||
window.hide()
|
||||
}
|
||||
})
|
||||
app.on("activate", show)
|
||||
window.on("closed", () => app.removeListener("activate", show))
|
||||
}
|
||||
const icon =
|
||||
process.platform === "darwin"
|
||||
? "trayTemplate.png"
|
||||
: process.platform === "win32"
|
||||
? "icon.ico"
|
||||
: "icon.png"
|
||||
tray = new Tray(
|
||||
join(
|
||||
app.getAppPath(),
|
||||
app.isPackaged ? "generated" : "app",
|
||||
app.isPackaged ? icon : "pwa-192x192.png",
|
||||
),
|
||||
)
|
||||
tray.setToolTip(appName)
|
||||
tray.setContextMenu(
|
||||
Menu.buildFromTemplate([
|
||||
{label: "Show", click: show},
|
||||
{label: "Quit", click: () => app.quit()},
|
||||
]),
|
||||
)
|
||||
tray.on("click", show)
|
||||
window.on("closed", destroyTray)
|
||||
if (process.platform === "linux" && app.isPackaged) {
|
||||
window.setIcon(join(app.getAppPath(), "generated/icon.png"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
"bump": "node scripts/bump-version.mjs",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --check src e2e playwright.config.ts capacitor.config.ts electron/*.ts electron/electron-builder.config.mjs scripts/dev-desktop.mjs scripts/package-desktop.mjs && eslint src e2e capacitor.config.ts electron/*.ts electron/electron-builder.config.mjs scripts/dev-desktop.mjs scripts/package-desktop.mjs",
|
||||
"lint": "prettier --check src e2e packages playwright.config.ts capacitor.config.ts electron/*.ts electron/electron-builder.config.mjs scripts/dev-desktop.mjs scripts/package-desktop.mjs && eslint src e2e packages capacitor.config.ts electron/*.ts electron/electron-builder.config.mjs scripts/dev-desktop.mjs scripts/package-desktop.mjs",
|
||||
"test": "playwright test",
|
||||
"test:desktop": "playwright test --config e2e/desktop/playwright.config.ts",
|
||||
"test:ui": "playwright test --ui",
|
||||
|
|
@ -74,6 +74,7 @@
|
|||
"@capawesome/capacitor-android-dark-mode-support": "^8.0.0",
|
||||
"@capawesome/capacitor-badge": "^8.0.0",
|
||||
"@capawesome/capacitor-electron": "0.1.1",
|
||||
"@flotilla/desktop-window": "file:packages/desktop-window",
|
||||
"@getalby/lightning-tools": "^6.1.0",
|
||||
"@getalby/sdk": "^5.1.2",
|
||||
"@hono/node-server": "^2.0.0",
|
||||
|
|
|
|||
4
packages/desktop-window/README.md
Normal file
4
packages/desktop-window/README.md
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# Desktop window plugin
|
||||
|
||||
Capawesome 0.1.1 discovers Electron plugins at `<capacitor.electron.src>/dist/plugin.mjs`.
|
||||
This package contains plain JavaScript at that required path; it needs no compilation step.
|
||||
24
packages/desktop-window/electron/dist/plugin.mjs
vendored
Normal file
24
packages/desktop-window/electron/dist/plugin.mjs
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import {app} from "electron"
|
||||
|
||||
export class DesktopWindow {
|
||||
static __capacitorElectronPlugin = {name: "DesktopWindow", methods: ["show"]}
|
||||
|
||||
window
|
||||
|
||||
load() {
|
||||
app.on("browser-window-created", (_event, window) => {
|
||||
this.window = window
|
||||
})
|
||||
}
|
||||
|
||||
async show() {
|
||||
if (!this.window || this.window.isDestroyed()) {
|
||||
throw new Error("The application window is closed")
|
||||
}
|
||||
if (this.window.isMinimized()) {
|
||||
this.window.restore()
|
||||
}
|
||||
this.window.show()
|
||||
this.window.focus()
|
||||
}
|
||||
}
|
||||
11
packages/desktop-window/package.json
Normal file
11
packages/desktop-window/package.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"name": "@flotilla/desktop-window",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"capacitor": {
|
||||
"electron": {
|
||||
"src": "electron"
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
pnpm-lock.yaml
BIN
pnpm-lock.yaml
Binary file not shown.
|
|
@ -43,6 +43,18 @@ try {
|
|||
|
||||
await run("bash", ["scripts/build-desktop.sh"])
|
||||
await cp(join(root, env.VITE_PLATFORM_LOGO), join(root, "electron/generated/icon.png"))
|
||||
await cp(join(root, "static/favicon.ico"), join(root, "electron/generated/icon.ico"))
|
||||
for (const [size, name] of [
|
||||
[16, "trayTemplate.png"],
|
||||
[32, "trayTemplate@2x.png"],
|
||||
]) {
|
||||
await sharp({create: {width: size, height: size, channels: 3, background: "black"}})
|
||||
.joinChannel(
|
||||
await sharp(logo).resize(size, size).ensureAlpha().extractChannel("alpha").toBuffer(),
|
||||
)
|
||||
.png()
|
||||
.toFile(join(root, "electron/generated", name))
|
||||
}
|
||||
await cp(join(root, "LICENSE"), join(root, "electron/generated/LICENSE"))
|
||||
await run(
|
||||
process.execPath,
|
||||
|
|
|
|||
|
|
@ -58,7 +58,6 @@ export const onNotification = call(() => {
|
|||
{kinds: [MESSAGE, ...CONTENT_KINDS, ...DM_KINDS]},
|
||||
makeCommentFilter(CONTENT_KINDS),
|
||||
]
|
||||
const filters = allFilters.map(assoc("since", now()))
|
||||
const subscribers: Subscriber<TrustedEvent>[] = []
|
||||
|
||||
let unsubscribe: Unsubscriber | undefined
|
||||
|
|
@ -67,29 +66,36 @@ export const onNotification = call(() => {
|
|||
subscribers.push(f)
|
||||
|
||||
if (!unsubscribe) {
|
||||
unsubscribe = on(app.get().repository, "update", ({added}: RepositoryUpdate) => {
|
||||
const $pubkey = app.get().user?.pubkey
|
||||
let unsubscribeRepository: Unsubscriber | undefined
|
||||
const unsubscribeApp = app.subscribe($app => {
|
||||
unsubscribeRepository?.()
|
||||
const filters = allFilters.map(assoc("since", now()))
|
||||
unsubscribeRepository = on($app.repository, "update", ({added}: RepositoryUpdate) => {
|
||||
const $pubkey = $app.user?.pubkey
|
||||
|
||||
for (const event of added) {
|
||||
if (event.pubkey == $pubkey) {
|
||||
continue
|
||||
}
|
||||
for (const event of added) {
|
||||
if (event.pubkey == $pubkey) {
|
||||
continue
|
||||
}
|
||||
|
||||
const h = tagValue(tagSpec("h"), event.tags)
|
||||
const h = tagValue(tagSpec("h"), event.tags)
|
||||
|
||||
if (
|
||||
Array.from(app.get().tracker.getRelays(event.id)).every(url => !shouldNotify(url, h))
|
||||
) {
|
||||
continue
|
||||
}
|
||||
if (Array.from($app.tracker.getRelays(event.id)).every(url => !shouldNotify(url, h))) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (matchFilters(filters, event)) {
|
||||
for (const f of subscribers) {
|
||||
f(event)
|
||||
if (matchFilters(filters, event)) {
|
||||
for (const f of subscribers) {
|
||||
f(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
unsubscribe = () => {
|
||||
unsubscribeApp()
|
||||
unsubscribeRepository?.()
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
|
|
|
|||
72
src/app/push/adapters/electron.ts
Normal file
72
src/app/push/adapters/electron.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import {registerPlugin} from "@capacitor/core"
|
||||
import type {Unsubscriber} from "svelte/store"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {app} from "@app/core"
|
||||
import {WebNotifications} from "@app/push/adapters/web"
|
||||
import {goToEvent} from "@app/routes"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
||||
const DesktopWindow = registerPlugin<{show: () => Promise<void>}>("DesktopWindow")
|
||||
|
||||
export class ElectronNotifications extends WebNotifications {
|
||||
_unsubscribeApp: Unsubscriber | undefined
|
||||
_notifications = new Set<Notification>()
|
||||
|
||||
_closeNotifications = () => {
|
||||
for (const notification of this._notifications) {
|
||||
notification.close()
|
||||
}
|
||||
this._notifications.clear()
|
||||
}
|
||||
|
||||
_onVisibilityChange = () => {
|
||||
if (document.visibilityState === "visible") {
|
||||
this._closeNotifications()
|
||||
}
|
||||
}
|
||||
|
||||
_notify(event: TrustedEvent, title: string, body: string) {
|
||||
const owner = app.get()
|
||||
const notification = new Notification(title, {
|
||||
body,
|
||||
tag: event.id,
|
||||
icon: "/icon.png",
|
||||
badge: "/icon.png",
|
||||
})
|
||||
|
||||
this._notifications.add(notification)
|
||||
notification.onclose = () => this._notifications.delete(notification)
|
||||
notification.onclick = async () => {
|
||||
try {
|
||||
if (app.get() === owner) {
|
||||
await DesktopWindow.show()
|
||||
if (app.get() === owner) {
|
||||
goToEvent(event)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
pushToast({message: "Unable to open the notification", theme: "error"})
|
||||
} finally {
|
||||
notification.close()
|
||||
this._notifications.delete(notification)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async enable() {
|
||||
if (!this._unsubscriber) {
|
||||
this._unsubscribeApp = app.subscribe(this._closeNotifications)
|
||||
document.addEventListener("visibilitychange", this._onVisibilityChange)
|
||||
}
|
||||
return super.enable()
|
||||
}
|
||||
|
||||
async disable() {
|
||||
this._unsubscribeApp?.()
|
||||
this._unsubscribeApp = undefined
|
||||
document.removeEventListener("visibilitychange", this._onVisibilityChange)
|
||||
this._closeNotifications()
|
||||
return super.disable()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import {Capacitor} from "@capacitor/core"
|
||||
import {notificationSettings} from "@app/settings"
|
||||
import {pushState} from "@app/push/adapters/common"
|
||||
import {ElectronNotifications} from "@app/push/adapters/electron"
|
||||
import {WebNotifications} from "@app/push/adapters/web"
|
||||
import {CapacitorNotifications} from "@app/push/adapters/capacitor"
|
||||
import {AndroidFallbackNotifications} from "@app/push/adapters/android"
|
||||
|
|
@ -15,7 +16,9 @@ export class Push {
|
|||
if (!Push._adapter) {
|
||||
const {useFallback} = pushState.get()
|
||||
|
||||
if (Capacitor.getPlatform() === "android" && useFallback) {
|
||||
if (Capacitor.getPlatform() === "electron") {
|
||||
Push._adapter = new ElectronNotifications()
|
||||
} else if (Capacitor.getPlatform() === "android" && useFallback) {
|
||||
Push._adapter = new AndroidFallbackNotifications()
|
||||
} else if (Capacitor.isPluginAvailable("PushNotifications")) {
|
||||
Push._adapter = new CapacitorNotifications()
|
||||
|
|
|
|||
Loading…
Reference in a new issue