platform-codebase/features/conversation-assistant/macos/Sources/Views/MenuBarViewModel.swift

188 lines
6.1 KiB
Swift
Raw Normal View History

import Combine
import SwiftUI
@MainActor
class MenuBarViewModel: ObservableObject {
@Published var isAuthenticated = false
@Published var syncStatus: SyncStatus = .idle
@Published var messageCount = 0
@Published var conversationCount = 0
@Published var lastSyncText = "Never"
@Published var isSyncing = false
@Published var isResetting = false
@Published var needsFullDiskAccess = false
@Published var syncErrorMessage: String?
@Published var registrationCode = ""
@Published var isRegistering = false
@Published var authError: String?
@Published var activityLogEntries: [LogEntry] = []
private let apiClient = APIClient.shared
private let syncManager = SyncManager.shared
private let activityLog = ActivityLog.shared
private var cancellables = Set<AnyCancellable>()
private var pollingTimer: Timer?
init() {
isAuthenticated = apiClient.isAuthenticated
syncManager.$isSyncing
.receive(on: DispatchQueue.main)
.sink { [weak self] syncing in
self?.isSyncing = syncing
self?.syncStatus = syncing ? .syncing : .idle
}
.store(in: &cancellables)
syncManager.$lastSync
.receive(on: DispatchQueue.main)
.sink { [weak self] date in
NSLog("MenuBarViewModel: received lastSync update: \(String(describing: date))")
self?.updateLastSyncText(date)
}
.store(in: &cancellables)
syncManager.$stats
.receive(on: DispatchQueue.main)
.sink { [weak self] stats in
self?.messageCount = stats.messageCount
self?.conversationCount = stats.conversationCount
}
.store(in: &cancellables)
syncManager.$syncError
.receive(on: DispatchQueue.main)
.sink { [weak self] error in
switch error {
case .none:
self?.needsFullDiskAccess = false
self?.syncErrorMessage = nil
self?.syncStatus = .idle
case .fullDiskAccessRequired:
self?.needsFullDiskAccess = true
self?.syncErrorMessage = error.message
self?.syncStatus = .error(error.message)
default:
self?.needsFullDiskAccess = false
self?.syncErrorMessage = error.message
self?.syncStatus = .error(error.message)
}
}
.store(in: &cancellables)
syncManager.$isResetting
.receive(on: DispatchQueue.main)
.sink { [weak self] resetting in
self?.isResetting = resetting
}
.store(in: &cancellables)
activityLog.$entries
.receive(on: DispatchQueue.main)
.sink { [weak self] entries in
self?.activityLogEntries = entries
}
.store(in: &cancellables)
// Register device on startup if not authenticated
if !isAuthenticated {
registerDevice()
}
}
func registerDevice() {
isRegistering = true
authError = nil
Task {
do {
let (_, code) = try await apiClient.registerDevice()
registrationCode = code
startPollingForVerification()
} catch {
authError = error.localizedDescription
}
isRegistering = false
}
}
private func startPollingForVerification() {
// Poll every 3 seconds to check if device has been verified
pollingTimer?.invalidate()
pollingTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { [weak self] _ in
Task { @MainActor in
await self?.checkVerificationStatus()
}
}
}
private func checkVerificationStatus() async {
do {
let verified = try await apiClient.checkVerification()
if verified {
pollingTimer?.invalidate()
pollingTimer = nil
isAuthenticated = true
registrationCode = ""
syncManager.startSync()
}
} catch {
// Silently continue polling
}
}
func refreshCode() {
registrationCode = ""
registerDevice()
}
func triggerSync() {
syncManager.syncNow()
}
func forceSync() {
syncManager.resetAndResync()
}
func openFullDiskAccessSettings() {
syncManager.openFullDiskAccessSettings()
}
func retryConnection() {
syncManager.retryConnection()
}
/// Opens the Settings window, closing the popover first and activating the app
func openSettings() {
// Close the popover first
if let appDelegate = NSApp.delegate as? AppDelegate {
appDelegate.closePopover()
}
// Activate the app (required for menu bar apps to show settings)
NSApp.activate(ignoringOtherApps: true)
// Open settings window - macOS 14+ renamed to showSettingsWindow, older versions use showPreferencesWindow
// Note: Using double-paren selector format to suppress warnings about private Objective-C selectors
if #available(macOS 14, *) {
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
} else {
NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil)
}
}
private func updateLastSyncText(_ date: Date?) {
guard let date = date else {
NSLog("MenuBarViewModel: updateLastSyncText - date is nil, setting to 'Never'")
lastSyncText = "Never"
return
}
let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .abbreviated
let text = formatter.localizedString(for: date, relativeTo: Date())
NSLog("MenuBarViewModel: updateLastSyncText - date: \(date), text: \(text)")
lastSyncText = text
}
}