|
| 1 | +# Notification (Common) |
| 2 | + |
| 3 | +Cross-platform notification abstraction that unifies Linux, Windows, and macOS notification APIs behind a single Kotlin DSL. Send notifications with title, message, images, action buttons, and lifecycle callbacks — the module routes to the right platform backend at runtime. |
| 4 | + |
| 5 | +!!! info "Simple subset by design" |
| 6 | + This module exposes the **intersection** of what all three platforms support: title, message, large image, small icon, up to 5 action buttons, and lifecycle callbacks. For platform-specific features (progress bars, input fields, scheduling, categories), use the dedicated [Linux](notification-linux.md), [Windows](notification-windows.md), or [macOS](notification-macos.md) modules directly. |
| 7 | + |
| 8 | +## Installation |
| 9 | + |
| 10 | +```kotlin |
| 11 | +dependencies { |
| 12 | + implementation("io.github.kdroidfilter:nucleus.notification-common:<version>") |
| 13 | +} |
| 14 | +``` |
| 15 | + |
| 16 | +This single dependency pulls in all three platform modules (Linux, Windows, macOS) and `core-runtime` transitively. The module detects the current OS at runtime and delegates to the appropriate backend — non-native libraries are simply unused on other platforms. |
| 17 | + |
| 18 | +## Quick Start |
| 19 | + |
| 20 | +```kotlin |
| 21 | +import io.github.kdroidfilter.nucleus.notification.common.* |
| 22 | + |
| 23 | +// Build a notification |
| 24 | +val n = notification( |
| 25 | + title = "Download Complete", |
| 26 | + message = "report.pdf has been saved", |
| 27 | +) { |
| 28 | + button("Open") { openFile() } |
| 29 | + button("Show in Folder") { showInFolder() } |
| 30 | +} |
| 31 | + |
| 32 | +// Send it |
| 33 | +n.send() |
| 34 | +``` |
| 35 | + |
| 36 | +## Full Example |
| 37 | + |
| 38 | +```kotlin |
| 39 | +import io.github.kdroidfilter.nucleus.notification.common.* |
| 40 | + |
| 41 | +val myNotification = notification( |
| 42 | + title = "New Message from Alice", |
| 43 | + message = "Hey! Have you seen the latest build?", |
| 44 | + largeImage = Res.getUri("drawable/alice_avatar.png"), |
| 45 | + smallIcon = Res.getUri("drawable/app_icon.png"), |
| 46 | + onActivated = { openConversation("alice") }, |
| 47 | + onDismissed = { reason -> println("Dismissed: $reason") }, |
| 48 | + onFailed = { println("Failed to show notification") }, |
| 49 | +) { |
| 50 | + button("Reply") { showReplyDialog("alice") } |
| 51 | + button("Archive") { archiveConversation("alice") } |
| 52 | +} |
| 53 | + |
| 54 | +// Check availability before sending |
| 55 | +if (NotificationManager.isAvailable()) { |
| 56 | + when (val result = myNotification.send()) { |
| 57 | + is NotificationResult.Success -> { |
| 58 | + // Store the handle to dismiss later |
| 59 | + val handle = result.handle |
| 60 | + // ... |
| 61 | + handle.dismiss() |
| 62 | + } |
| 63 | + is NotificationResult.Failure -> { |
| 64 | + println("Could not send: ${result.reason}") |
| 65 | + } |
| 66 | + } |
| 67 | +} |
| 68 | +``` |
| 69 | + |
| 70 | +## API Reference |
| 71 | + |
| 72 | +### `notification()` — DSL Entry Point |
| 73 | + |
| 74 | +Top-level function that builds a `Notification` instance. |
| 75 | + |
| 76 | +```kotlin |
| 77 | +fun notification( |
| 78 | + title: String, |
| 79 | + message: String = "", |
| 80 | + largeImage: String? = null, |
| 81 | + smallIcon: String? = null, |
| 82 | + onActivated: (() -> Unit)? = null, |
| 83 | + onDismissed: ((DismissReason) -> Unit)? = null, |
| 84 | + onFailed: (() -> Unit)? = null, |
| 85 | + buttons: (NotificationButtonBuilder.() -> Unit)? = null, |
| 86 | +): Notification |
| 87 | +``` |
| 88 | + |
| 89 | +| Parameter | Type | Default | Description | |
| 90 | +|---|---|---|---| |
| 91 | +| `title` | `String` | *(required)* | Notification title. | |
| 92 | +| `message` | `String` | `""` | Body text. | |
| 93 | +| `largeImage` | `String?` | `null` | URI to a large image (hero image on Windows, image hint on Linux, attachment on macOS). | |
| 94 | +| `smallIcon` | `String?` | `null` | URI to a small icon (app logo on Windows, app icon on Linux, ignored on macOS). | |
| 95 | +| `onActivated` | `(() -> Unit)?` | `null` | Called when the user clicks the notification body. | |
| 96 | +| `onDismissed` | `((DismissReason) -> Unit)?` | `null` | Called when the notification is dismissed. | |
| 97 | +| `onFailed` | `(() -> Unit)?` | `null` | Called if the notification fails to display. | |
| 98 | +| `buttons` | DSL block | `null` | Builder block to add up to 5 action buttons. | |
| 99 | + |
| 100 | +### `NotificationButtonBuilder` |
| 101 | + |
| 102 | +Available inside the `notification { }` trailing lambda. |
| 103 | + |
| 104 | +| Method | Description | |
| 105 | +|---|---| |
| 106 | +| `button(title: String, onClick: () -> Unit)` | Add an action button. Maximum 5 buttons (Windows limit). | |
| 107 | + |
| 108 | +--- |
| 109 | + |
| 110 | +### `Notification` |
| 111 | + |
| 112 | +Immutable notification object returned by the `notification()` function. The same instance can be sent multiple times — each call creates a new system notification. |
| 113 | + |
| 114 | +| Method | Returns | Description | |
| 115 | +|---|---|---| |
| 116 | +| `send()` | `NotificationResult` | Sends the notification to the OS. | |
| 117 | + |
| 118 | +--- |
| 119 | + |
| 120 | +### `NotificationResult` |
| 121 | + |
| 122 | +Sealed class returned by `send()`. |
| 123 | + |
| 124 | +| Subclass | Properties | Description | |
| 125 | +|---|---|---| |
| 126 | +| `Success` | `handle: NotificationHandle` | Notification sent successfully. | |
| 127 | +| `Failure` | `reason: String` | Notification could not be sent. | |
| 128 | + |
| 129 | +--- |
| 130 | + |
| 131 | +### `NotificationHandle` |
| 132 | + |
| 133 | +Opaque handle to a sent notification. |
| 134 | + |
| 135 | +| Method | Description | |
| 136 | +|---|---| |
| 137 | +| `dismiss()` | Programmatically close the notification if still visible. | |
| 138 | + |
| 139 | +--- |
| 140 | + |
| 141 | +### `NotificationManager` |
| 142 | + |
| 143 | +Singleton facade for platform detection and notification dispatch. |
| 144 | + |
| 145 | +| Method | Returns | Description | |
| 146 | +|---|---|---| |
| 147 | +| `isAvailable()` | `Boolean` | `true` if the current platform's notification module is on the classpath and functional. | |
| 148 | +| `initialize()` | `Unit` | Eagerly initialize the notification subsystem (Windows only — called lazily on first `send()` otherwise). | |
| 149 | +| `send(notification)` | `NotificationResult` | Send a notification. Prefer using `notification.send()` directly. | |
| 150 | + |
| 151 | +--- |
| 152 | + |
| 153 | +### `DismissReason` |
| 154 | + |
| 155 | +Unified enum for why a notification was dismissed. |
| 156 | + |
| 157 | +| Value | Description | Linux | Windows | macOS | |
| 158 | +|---|---|---|---|---| |
| 159 | +| `USER_DISMISSED` | User explicitly dismissed | `DISMISSED` | `USER_CANCELED` | Custom dismiss action | |
| 160 | +| `TIMED_OUT` | Auto-expired after timeout | `EXPIRED` | `TIMED_OUT` | — | |
| 161 | +| `APPLICATION` | Closed programmatically | `CLOSED` | `APPLICATION_HIDDEN` | — | |
| 162 | +| `UNKNOWN` | Could not be determined | `UNDEFINED` | — | — | |
| 163 | + |
| 164 | +--- |
| 165 | + |
| 166 | +## Platform Mapping |
| 167 | + |
| 168 | +How each parameter maps to platform-specific APIs: |
| 169 | + |
| 170 | +| Common | Linux | Windows | macOS | |
| 171 | +|---|---|---|---| |
| 172 | +| `title` | `summary` | First `AdaptiveText` (bold) | `content.title` | |
| 173 | +| `message` | `body` | Second `AdaptiveText` | `content.body` | |
| 174 | +| `largeImage` | `hints.imagePath` | Hero image (top banner) | `attachments[0]` | |
| 175 | +| `smallIcon` | `appIcon` | App logo override (left of text) | Ignored (uses bundle icon) | |
| 176 | +| `buttons` | `actions` list | `ToastButton` list | Auto-generated `NotificationCategory` | |
| 177 | +| `onActivated` | `onActionInvoked` with `"default"` key | `onActivated` with empty arguments | `didReceive` with `DEFAULT_ACTION` | |
| 178 | +| `onDismissed` | `onClosed` signal | `onDismissed` event | Requires `CUSTOM_DISMISS_ACTION` | |
| 179 | +| `onFailed` | `notify()` returns 0 | `onFailed` event | `add()` callback error | |
| 180 | + |
| 181 | +## Platform Details |
| 182 | + |
| 183 | +### Windows |
| 184 | + |
| 185 | +- **Initialization**: `WindowsNotificationCenter.initialize()` is called automatically on the first `send()`. Call `NotificationManager.initialize()` explicitly for early setup. |
| 186 | +- **Tag/Group**: Each notification gets a unique tag (`n1`, `n2`, ...) under the `"ncm"` group. |
| 187 | +- **Images**: `largeImage` maps to a hero image at the top of the toast. `smallIcon` maps to the app logo override (displayed left of the text). Both accept `file:///` URIs and HTTP URLs. |
| 188 | +- **Buttons**: Up to 5, rendered as standard toast action buttons. |
| 189 | + |
| 190 | +### macOS |
| 191 | + |
| 192 | +- **App bundle required**: Notifications only work inside a packaged `.app` bundle (e.g. via `./gradlew runDistributable`). `isAvailable()` returns `false` when running via `./gradlew run`. |
| 193 | +- **Authorization**: The user must have granted notification permissions. The common module does **not** auto-request authorization — use `NotificationCenter.requestAuthorization()` from the macOS module before sending. |
| 194 | +- **Buttons**: Require pre-registered `NotificationCategory` objects. The common module handles this automatically — it generates and caches categories per unique button configuration. |
| 195 | +- **Dismiss callback**: macOS does not natively fire dismiss events. The common module enables `CUSTOM_DISMISS_ACTION` on generated categories so `onDismissed` fires when the user explicitly dismisses. |
| 196 | +- **Small icon**: Ignored — macOS always uses the app icon from the bundle. |
| 197 | +- **Large image**: Mapped to a notification attachment (displayed as a thumbnail). |
| 198 | + |
| 199 | +### Linux |
| 200 | + |
| 201 | +- **No initialization needed**: The D-Bus connection is established automatically. |
| 202 | +- **Images**: `largeImage` maps to the `imagePath` hint (icon name or `file://` URI). `smallIcon` maps to `appIcon`. See the [Linux notification docs](notification-linux.md#icons) for icon priority. |
| 203 | +- **Default action**: A `"default"` action is automatically added when `onActivated` is set, so clicking the notification body triggers the callback. |
| 204 | +- **All callbacks on Swing EDT**: Safe to update Compose state directly from callbacks. |
| 205 | + |
| 206 | +## Compose Desktop Integration |
| 207 | + |
| 208 | +```kotlin |
| 209 | +@Composable |
| 210 | +fun NotificationDemo() { |
| 211 | + var lastResult by remember { mutableStateOf<NotificationResult?>(null) } |
| 212 | + |
| 213 | + Button(onClick = { |
| 214 | + val n = notification( |
| 215 | + title = "Build Finished", |
| 216 | + message = "nucleus-1.3.0 compiled in 42s", |
| 217 | + onActivated = { println("Notification clicked") }, |
| 218 | + ) { |
| 219 | + button("View Logs") { openLogs() } |
| 220 | + } |
| 221 | + lastResult = n.send() |
| 222 | + }) { |
| 223 | + Text("Send Notification") |
| 224 | + } |
| 225 | + |
| 226 | + lastResult?.let { result -> |
| 227 | + when (result) { |
| 228 | + is NotificationResult.Success -> Text("Sent!") |
| 229 | + is NotificationResult.Failure -> Text("Failed: ${result.reason}") |
| 230 | + } |
| 231 | + } |
| 232 | +} |
| 233 | +``` |
| 234 | + |
| 235 | +!!! tip "Getting the best experience across platforms" |
| 236 | + Always provide both `largeImage` and `smallIcon` for the richest display. On platforms that don't support one (e.g. `smallIcon` on macOS), it is silently ignored. |
| 237 | + |
| 238 | +## Architecture |
| 239 | + |
| 240 | +The module uses a dispatcher pattern inspired by `taskbar-progress`: |
| 241 | + |
| 242 | +``` |
| 243 | +NotificationManager (singleton) |
| 244 | + └─ DispatcherFactory (selects by os.name) |
| 245 | + ├─ LinuxDispatcher → LinuxNotificationCenter |
| 246 | + ├─ WindowsDispatcher → WindowsNotificationCenter |
| 247 | + └─ MacOsDispatcher → NotificationCenter (macOS) |
| 248 | +``` |
| 249 | + |
| 250 | +Each dispatcher: |
| 251 | + |
| 252 | +1. Checks for the platform module on the classpath via `Class.forName` (no `NoClassDefFoundError` if absent) |
| 253 | +2. Registers **one global listener** on the platform's notification center |
| 254 | +3. Routes callbacks to per-notification lambdas via a `ConcurrentHashMap<platformId, callbacks>` registry |
| 255 | +4. Cleans up callback entries on dismiss/failure events |
| 256 | + |
| 257 | +## ProGuard |
| 258 | + |
| 259 | +No additional ProGuard rules are needed for `notification-common` itself. Ensure the platform module rules are applied — see [Linux](notification-linux.md#proguard), [Windows](notification-windows.md#proguard), [macOS](notification-macos.md#proguard). |
| 260 | + |
| 261 | +## GraalVM |
| 262 | + |
| 263 | +No additional GraalVM metadata is needed for `notification-common`. The platform modules ship their own `reachability-metadata.json`. See [Linux](notification-linux.md#graalvm), [Windows](notification-windows.md#graalvm), [macOS](notification-macos.md#graalvm). |
0 commit comments