Skip to content

Commit 704dc31

Browse files
committed
Add NWPathMonitor reconnect and dynamic menu bar icon
- Reconnect immediately when network becomes available (NWPathMonitor) - Menu bar icon switches to hollow bell when any server is disconnected - Track failed connection attempts to show disconnected state even on first failure
1 parent 90732f8 commit 704dc31

File tree

2 files changed

+44
-1
lines changed

2 files changed

+44
-1
lines changed

Sources/NtfyClient.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Foundation
2+
import Network
23

34
@preconcurrency protocol NtfyClientDelegate: AnyObject {
45
func ntfyClient(_ client: NtfyClient, didReceiveMessage message: NtfyMessage)
@@ -107,6 +108,10 @@ final class NtfyClient: NSObject, @unchecked Sendable {
107108
private var watchdogTimer: Timer?
108109
private var lastDataReceived: Date = .distantPast
109110

111+
// Network path monitor: reconnect immediately when network becomes available
112+
private var pathMonitor: NWPathMonitor?
113+
private var isPathSatisfied = true // Assume connected initially
114+
110115
init(serverURL: String, topics: [String], authToken: String? = nil, fetchMissed: Bool = false, watchdogInterval: TimeInterval = 120.0, baseReconnectDelay: TimeInterval = 2.0, urlSessionConfiguration: URLSessionConfiguration? = nil) {
111116
self.serverURL = serverURL
112117
self.topics = topics
@@ -182,6 +187,8 @@ final class NtfyClient: NSObject, @unchecked Sendable {
182187
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
183188
}
184189

190+
startPathMonitor()
191+
185192
buffer.removeAll()
186193
dataTask = session.dataTask(with: request)
187194
dataTask?.resume()
@@ -194,6 +201,7 @@ final class NtfyClient: NSObject, @unchecked Sendable {
194201
reconnectTimer?.invalidate()
195202
reconnectTimer = nil
196203
stopWatchdog()
204+
stopPathMonitor()
197205
dataTask?.cancel()
198206
dataTask = nil
199207
isConnecting = false
@@ -221,6 +229,28 @@ final class NtfyClient: NSObject, @unchecked Sendable {
221229
watchdogTimer = nil
222230
}
223231

232+
private func startPathMonitor() {
233+
guard pathMonitor == nil else { return }
234+
let monitor = NWPathMonitor()
235+
pathMonitor = monitor
236+
monitor.pathUpdateHandler = { [weak self] path in
237+
guard let self else { return }
238+
let satisfied = path.status == .satisfied
239+
if satisfied && !self.isPathSatisfied {
240+
Log.info("Network became available, reconnecting...")
241+
self.reconnect()
242+
}
243+
self.isPathSatisfied = satisfied
244+
}
245+
monitor.start(queue: .global(qos: .background))
246+
}
247+
248+
private func stopPathMonitor() {
249+
pathMonitor?.cancel()
250+
pathMonitor = nil
251+
isPathSatisfied = true
252+
}
253+
224254
func updateAuthToken(_ token: String?) {
225255
self.authToken = token
226256
if dataTask != nil {

Sources/StatusBarController.swift

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,12 @@ class StatusBarController: NSObject {
2727
let topics: [String]
2828
var isConnected: Bool
2929
var hasEverConnected: Bool // Track if we've ever successfully connected
30+
var hasFailedAttempt: Bool = false // Track if at least one attempt ended in failure
3031

3132
var state: ConnectionState {
3233
if isConnected {
3334
return .connected
34-
} else if hasEverConnected {
35+
} else if hasEverConnected || hasFailedAttempt {
3536
return .disconnected
3637
} else {
3738
return .connecting
@@ -318,6 +319,8 @@ class StatusBarController: NSObject {
318319
status.isConnected = connected
319320
if connected {
320321
status.hasEverConnected = true
322+
} else {
323+
status.hasFailedAttempt = true
321324
}
322325
serverStatuses[serverUrl] = status
323326
refreshServersSubmenu()
@@ -366,13 +369,23 @@ class StatusBarController: NSObject {
366369
refreshServersSubmenu()
367370
}
368371

372+
private func updateMenuBarIcon(hasDisconnected: Bool) {
373+
let symbolName = hasDisconnected ? "bell" : "bell.fill"
374+
if let image = NSImage(systemSymbolName: symbolName, accessibilityDescription: "ntfy") {
375+
image.isTemplate = true
376+
statusItem?.button?.image = image
377+
}
378+
}
379+
369380
private func refreshMainStatus() {
370381
let totalServers = serverStatuses.count
371382
let connectedServers = serverStatuses.values.filter { $0.state == .connected }.count
372383
let connectingServers = serverStatuses.values.filter { $0.state == .connecting }.count
373384
let disconnectedServers = serverStatuses.values.filter { $0.state == .disconnected }.count
374385
let totalTopics = serverStatuses.values.flatMap { $0.topics }.count
375386

387+
updateMenuBarIcon(hasDisconnected: disconnectedServers > 0)
388+
376389
let attributedTitle = NSMutableAttributedString()
377390
let textAttrs: [NSAttributedString.Key: Any] = [
378391
.font: NSFont.systemFont(ofSize: 13)

0 commit comments

Comments
 (0)