refactor(conversation-assistant): remove unused SwiftUI popover views

Delete MenuBarView.swift and MenuBarViewModel.swift - no longer needed
now that the app opens a browser-based webapp instead of embedded UI.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Quinn Ftw 2025-12-30 04:39:06 -08:00
parent 8a31285265
commit 414c34ad0f
2 changed files with 0 additions and 526 deletions

View file

@ -1,339 +0,0 @@
import SwiftUI
struct MenuBarView: View {
@StateObject private var viewModel = MenuBarViewModel()
var body: some View {
VStack(spacing: 0) {
// Header
HStack {
Image(systemName: "bubble.left.and.bubble.right.fill")
.foregroundColor(.accentColor)
.accessibilityHidden(true)
Text("Conversation Assistant")
.font(.headline)
Spacer()
}
.padding()
.background(Color(.windowBackgroundColor))
Divider()
if viewModel.isAuthenticated {
authenticatedView
} else {
authenticationView
}
Divider()
// Footer
HStack {
// Open Settings - use SettingsLink on macOS 14+, fallback to NSApp.sendAction
if #available(macOS 14, *) {
SettingsLink {
Image(systemName: "gear")
.accessibilityLabel("Settings")
}
.buttonStyle(.borderless)
} else {
Button {
// Close popover first
if let appDelegate = NSApp.delegate as? AppDelegate {
appDelegate.closePopover()
}
NSApp.activate(ignoringOtherApps: true)
NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil)
} label: {
Image(systemName: "gear")
.accessibilityLabel("Settings")
}
.buttonStyle(.borderless)
}
Spacer()
Text(AppVersion.displayVersion)
.font(.caption2)
.foregroundStyle(.tertiary)
Spacer()
Button("Quit") {
NSApplication.shared.terminate(nil)
}
.buttonStyle(.borderless)
}
.padding()
}
.frame(width: 320)
}
private var authenticatedView: some View {
VStack(spacing: 12) {
// Sync Status
HStack {
Circle()
.fill(viewModel.syncStatus.color)
.frame(width: 8, height: 8)
Text(viewModel.syncStatus.text)
.font(.subheadline)
Spacer()
}
.padding(.horizontal)
// Full Disk Access Warning
if viewModel.needsFullDiskAccess {
VStack(spacing: 8) {
Image(systemName: "exclamationmark.shield.fill")
.font(.system(size: 32))
.foregroundColor(.orange)
.accessibilityLabel("Warning")
Text("Full Disk Access Required")
.font(.headline)
Text("Grant access to read iMessage database")
.font(.caption)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
HStack(spacing: 12) {
Button("Open Settings") {
viewModel.openFullDiskAccessSettings()
}
.buttonStyle(.borderedProminent)
Button("Retry") {
viewModel.retryConnection()
}
.buttonStyle(.bordered)
}
}
.padding()
.background(Color.orange.opacity(0.1))
.cornerRadius(8)
.padding(.horizontal)
} else {
// Stats
VStack(alignment: .leading, spacing: 8) {
StatRow(label: "Messages Synced", value: "\(viewModel.messageCount)")
StatRow(label: "Conversations", value: "\(viewModel.conversationCount)")
StatRow(label: "Last Sync", value: viewModel.lastSyncText)
}
.padding(.horizontal)
// Activity Log
ActivityLogSection(entries: viewModel.activityLogEntries)
}
Spacer()
// Sync Buttons
HStack(spacing: 8) {
Button(action: viewModel.triggerSync) {
HStack {
if viewModel.isSyncing && !viewModel.isResetting {
ProgressView()
.scaleEffect(0.7)
}
Text(viewModel.isSyncing && !viewModel.isResetting ? "Syncing..." : "Sync Now")
}
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.disabled(viewModel.isSyncing || viewModel.isResetting || viewModel.needsFullDiskAccess)
Button(action: viewModel.forceSync) {
HStack {
if viewModel.isResetting {
ProgressView()
.scaleEffect(0.7)
}
Text(viewModel.isResetting ? "Resetting..." : "Force Sync")
}
.frame(maxWidth: .infinity)
}
.buttonStyle(.bordered)
.disabled(viewModel.isSyncing || viewModel.isResetting || viewModel.needsFullDiskAccess)
}
.padding()
}
.padding(.vertical)
}
private var authenticationView: some View {
VStack(spacing: 16) {
Image(systemName: "link.badge.plus")
.font(.system(size: 48))
.foregroundColor(.accentColor)
.accessibilityLabel("Connect device")
Text("Connect This Device")
.font(.headline)
if viewModel.isRegistering {
ProgressView("Registering...")
} else if !viewModel.registrationCode.isEmpty {
Text("Enter this code in your admin panel:")
.font(.caption)
.foregroundColor(.secondary)
Text(viewModel.registrationCode)
.font(.system(size: 32, weight: .bold, design: .monospaced))
.foregroundColor(.accentColor)
.padding(.vertical, 8)
.onTapGesture {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(viewModel.registrationCode, forType: .string)
}
Text("Tap to copy • Waiting for verification...")
.font(.caption2)
.foregroundColor(.secondary)
Button("Get New Code") {
viewModel.refreshCode()
}
.buttonStyle(.link)
} else {
Text("Unable to connect to server")
.font(.caption)
.foregroundColor(.secondary)
Button("Retry") {
viewModel.registerDevice()
}
.buttonStyle(.borderedProminent)
}
if let error = viewModel.authError {
Text(error)
.font(.caption)
.foregroundColor(.red)
}
}
.padding()
}
}
struct StatRow: View {
let label: String
let value: String
var body: some View {
HStack {
Text(label)
.foregroundColor(.secondary)
Spacer()
Text(value)
.fontWeight(.medium)
}
}
}
struct ActivityLogSection: View {
let entries: [LogEntry]
/// Maximum entries to display
private let maxDisplayed = 8
var body: some View {
VStack(alignment: .leading, spacing: 4) {
HStack {
Text("Activity")
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(.secondary)
Spacer()
}
.padding(.horizontal)
.padding(.top, 8)
if entries.isEmpty {
Text("No recent activity")
.font(.caption2)
.foregroundStyle(.tertiary)
.padding(.horizontal)
} else {
ScrollView {
VStack(alignment: .leading, spacing: 2) {
ForEach(entries.prefix(maxDisplayed)) { entry in
ActivityLogRow(entry: entry)
}
}
}
.frame(maxHeight: 120)
.padding(.horizontal)
}
}
}
}
struct ActivityLogRow: View {
let entry: LogEntry
private var timeText: String {
let formatter = RelativeDateTimeFormatter()
formatter.unitsStyle = .abbreviated
return formatter.localizedString(for: entry.timestamp, relativeTo: Date())
}
private var iconColor: Color {
switch entry.level {
case .info: return .blue
case .success: return .green
case .warning: return .orange
case .error: return .red
}
}
var body: some View {
HStack(alignment: .top, spacing: 6) {
Image(systemName: entry.level.icon)
.font(.caption2)
.foregroundColor(iconColor)
.frame(width: 12)
.accessibilityHidden(true)
Text(entry.message)
.font(.caption2)
.foregroundColor(.primary)
.lineLimit(2)
Spacer()
Text(timeText)
.font(.caption2)
.foregroundStyle(.tertiary)
}
.padding(.vertical, 2)
}
}
enum SyncStatus {
case idle
case syncing
case error(String)
var color: Color {
switch self {
case .idle: return .green
case .syncing: return .orange
case .error: return .red
}
}
var text: String {
switch self {
case .idle: return "Connected"
case .syncing: return "Syncing..."
case .error(let message): return message
}
}
}
#Preview {
MenuBarView()
.frame(height: 400)
}

View file

@ -1,187 +0,0 @@
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
}
}