From f1152d3dbb4c6deefea168d66f15f77b7155a5fe Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Wed, 15 Mar 2023 17:26:13 +0100 Subject: Basic D-Bus API (#866) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: basic D-Bus API Expose muted state and the number of unread message over D-Bus when running on Linux. This is useful for, e.g., displaying notifications on a window manager status bar. Signed-off-by: Kristóf Marussy * docs: create docs directory Move the documentation to a separate directory so that new documentation can be added into one place. We keep the following files still in the repository root by convention: * CHANGELOG.md * CODE_OF_CONDUCT.md * CONTRIBUTING.md * LICENSE.md * README.md * SECURITY.md Signed-off-by: Kristóf Marussy * docs: D-Bus usage example Signed-off-by: Kristóf Marussy * fix: remove unneeded D-Bus signals Only notify clients that the message counts or the mute status has changed if there actually was a change. Signed-off-by: Kristóf Marussy * docs: rewrite sample bar client * docs: better unread --services help * docs: update dbus docs * docs: use ferdium-dbus in dbus bar example * docs: make command argument required in bar example --------- Signed-off-by: Kristóf Marussy Co-authored-by: Victor Bonnelle --- src/lib/DBus.ts | 69 +++++++++++++++++++++++++++++++++-- src/lib/dbus/Ferdium.ts | 88 +++++++++++++++++++++++++++++++++++++++++++++ src/stores/ServicesStore.ts | 52 ++++++++++++++++----------- 3 files changed, 186 insertions(+), 23 deletions(-) create mode 100644 src/lib/dbus/Ferdium.ts (limited to 'src') diff --git a/src/lib/DBus.ts b/src/lib/DBus.ts index bbff405c4..530e30c85 100644 --- a/src/lib/DBus.ts +++ b/src/lib/DBus.ts @@ -1,28 +1,92 @@ +import { ipcMain } from 'electron'; +import { comparer } from 'mobx'; + import { MessageBus, sessionBus } from 'dbus-next'; import { isLinux } from '../environment'; import TrayIcon from './Tray'; +import Ferdium, { type UnreadServices } from './dbus/Ferdium'; export default class DBus { - bus: MessageBus | null = null; + private bus: MessageBus | null = null; trayIcon: TrayIcon; + private ferdium: Ferdium | null = null; + + muted = false; + + unreadDirectMessageCount = 0; + + unreadIndirectMessageCount = 0; + + unreadServices: UnreadServices = []; + constructor(trayIcon: TrayIcon) { this.trayIcon = trayIcon; + ipcMain.on('initialAppSettings', (_, appSettings) => { + this.updateSettings(appSettings); + }); + ipcMain.on('updateAppSettings', (_, appSettings) => { + this.updateSettings(appSettings); + }); + ipcMain.on( + 'updateDBusUnread', + ( + _, + unreadDirectMessageCount, + unreadIndirectMessageCount, + unreadServices, + ) => { + this.setUnread( + unreadDirectMessageCount, + unreadIndirectMessageCount, + unreadServices, + ); + }, + ); + } + + private updateSettings(appSettings): void { + const muted = !!appSettings.data.isAppMuted; + if (this.muted !== muted) { + this.muted = muted; + this.ferdium?.emitMutedChanged(); + } } - start() { + private setUnread( + unreadDirectMessageCount: number, + unreadIndirectMessageCount: number, + unreadServices: UnreadServices, + ): void { + if ( + this.unreadDirectMessageCount !== unreadDirectMessageCount || + this.unreadIndirectMessageCount !== unreadIndirectMessageCount || + !comparer.structural(this.unreadServices, unreadServices) + ) { + this.unreadDirectMessageCount = unreadDirectMessageCount; + this.unreadIndirectMessageCount = unreadIndirectMessageCount; + this.unreadServices = unreadServices; + this.ferdium?.emitUnreadChanged(); + } + } + + async start() { if (!isLinux || this.bus) { return; } try { this.bus = sessionBus(); + await this.bus.requestName('org.ferdium.Ferdium', 0); } catch { // Error connecting to the bus. return; } + this.ferdium = new Ferdium(this); + this.bus.export('/org/ferdium', this.ferdium); + // HACK Hook onto the MessageBus to track StatusNotifierWatchers // @ts-expect-error Property '_addMatch' does not exist on type 'MessageBus'. this.bus._addMatch( @@ -56,5 +120,6 @@ export default class DBus { this.bus.disconnect(); this.bus = null; + this.ferdium = null; } } diff --git a/src/lib/dbus/Ferdium.ts b/src/lib/dbus/Ferdium.ts new file mode 100644 index 000000000..b2a9105f4 --- /dev/null +++ b/src/lib/dbus/Ferdium.ts @@ -0,0 +1,88 @@ +import * as dbus from 'dbus-next'; + +import type DBus from '../DBus'; + +export type UnreadServices = [string, number, number][]; + +export default class Ferdium extends dbus.interface.Interface { + constructor(private readonly dbus: DBus) { + super('org.ferdium.Ferdium'); + } + + emitMutedChanged(): void { + Ferdium.emitPropertiesChanged(this, { Muted: this.dbus.muted }, []); + } + + get Muted(): boolean { + return this.dbus.muted; + } + + set Muted(muted: boolean) { + if (this.dbus.muted !== muted) { + this.ToggleMute(); + } + } + + ToggleMute(): void { + this.dbus.trayIcon.mainWindow?.webContents.send('muteApp'); + } + + ToggleWindow(): void { + this.dbus.trayIcon._toggleWindow(); + } + + emitUnreadChanged(): void { + Ferdium.emitPropertiesChanged( + this, + { + UnreadDirectMessageCount: this.dbus.unreadDirectMessageCount, + UnreadIndirectMessageCount: this.dbus.unreadIndirectMessageCount, + UnreadServices: this.dbus.unreadServices, + }, + [], + ); + } + + get UnreadDirectMessageCount(): number { + return this.dbus.unreadDirectMessageCount; + } + + get UnreadIndirectMessageCount(): number { + return this.dbus.unreadIndirectMessageCount; + } + + get UnreadServices(): UnreadServices { + return this.dbus.unreadServices; + } +} + +Ferdium.configureMembers({ + methods: { + ToggleMute: { + inSignature: '', + outSignature: '', + }, + ToggleWindow: { + inSignature: '', + outSignature: '', + }, + }, + properties: { + Muted: { + signature: 'b', + access: dbus.interface.ACCESS_READWRITE, + }, + UnreadDirectMessageCount: { + signature: 'u', + access: dbus.interface.ACCESS_READ, + }, + UnreadIndirectMessageCount: { + signature: 'u', + access: dbus.interface.ACCESS_READ, + }, + UnreadServices: { + signature: 'a(suu)', + access: dbus.interface.ACCESS_READ, + }, + }, +}); diff --git a/src/stores/ServicesStore.ts b/src/stores/ServicesStore.ts index 0ab4dbc5b..829c64d76 100644 --- a/src/stores/ServicesStore.ts +++ b/src/stores/ServicesStore.ts @@ -1,4 +1,4 @@ -import { shell } from 'electron'; +import { ipcRenderer, shell } from 'electron'; import { action, reaction, computed, observable, makeObservable } from 'mobx'; import { debounce, remove } from 'lodash'; import ms from 'ms'; @@ -23,6 +23,7 @@ import { cleanseJSObject } from '../jsUtils'; import { SPELLCHECKER_LOCALES } from '../i18n/languages'; import { ferdiumVersion } from '../environment-remote'; import TypedStore from './lib/TypedStore'; +import type { UnreadServices } from '../lib/dbus/Ferdium'; const debug = require('../preload-safe-debug')('Ferdium:ServiceStore'); @@ -1230,26 +1231,29 @@ export default class ServicesStore extends TypedStore { const { showMessageBadgeWhenMuted } = this.stores.settings.all.app; const { showMessageBadgesEvenWhenMuted } = this.stores.ui; - const unreadDirectMessageCount = this.allDisplayed - .filter( - s => - (showMessageBadgeWhenMuted || s.isNotificationEnabled) && - showMessageBadgesEvenWhenMuted && - s.isBadgeEnabled, - ) - .map(s => s.unreadDirectMessageCount) - .reduce((a, b) => a + b, 0); - - const unreadIndirectMessageCount = this.allDisplayed - .filter( - s => - showMessageBadgeWhenMuted && - showMessageBadgesEvenWhenMuted && - s.isBadgeEnabled && - s.isIndirectMessageBadgeEnabled, - ) - .map(s => s.unreadIndirectMessageCount) - .reduce((a, b) => a + b, 0); + const unreadServices: UnreadServices = []; + let unreadDirectMessageCount = 0; + let unreadIndirectMessageCount = 0; + + if (showMessageBadgesEvenWhenMuted) { + for (const s of this.allDisplayed) { + if (s.isBadgeEnabled) { + const direct = + showMessageBadgeWhenMuted || s.isNotificationEnabled + ? s.unreadDirectMessageCount + : 0; + const indirect = + showMessageBadgeWhenMuted && s.isIndirectMessageBadgeEnabled + ? s.unreadIndirectMessageCount + : 0; + unreadDirectMessageCount += direct; + unreadIndirectMessageCount += indirect; + if (direct > 0 || indirect > 0) { + unreadServices.push([s.name, direct, indirect]); + } + } + } + } // We can't just block this earlier, otherwise the mobx reaction won't be aware of the vars to watch in some cases if (showMessageBadgesEvenWhenMuted) { @@ -1257,6 +1261,12 @@ export default class ServicesStore extends TypedStore { unreadDirectMessageCount, unreadIndirectMessageCount, }); + ipcRenderer.send( + 'updateDBusUnread', + unreadDirectMessageCount, + unreadIndirectMessageCount, + unreadServices, + ); } } -- cgit v1.2.3-54-g00ecf