KDChat/KDChat/ContentView.swift
Blake Oliver 8a39e5939f
Add swiftData message persistence and reactive side effects
Persistence:
- SavedMessage SwiftData model with index on [buffer, timestamp]
- @Query in ChatBufferView/DMListView/DMConversationView — SwiftData is the single source of truth, no in-memory dictionary
- Settings: Save Messages toggle, Save Debug Buffer toggle, Delete After picker (1d/1w/1m/1y/never), Delete All with confirmation dialog
- Expired and debug messages purged on launch based on settings
- Say messages go to debug buffer only (not all buffers)

Reactive side effects:
- Sound, haptic, and VoiceOver announcement all fire from ChatBufferView's onChange(of: messages.count)
- Sound plays for active buffer or background buffers if enabled
- Haptic and announcement fire on active buffer only, current session only (I'll add a toggle to announce other buffers next)
- Status haptic for Say messages stays imperative in model (global event, not per-buffer)
- Removed trigger counters and lastAnnouncement from ConnectionManager

DM conversation fix:
- Track DM recipient via lastDMRecipientId; tag outgoing echoes with partner ID in characterId
- Conversation view filters on characterId == partnerId (shows only that conversation)

Other:
- modelContext set in ContentView.onAppear (available for all screens)
- Instant scroll (no animation), respects Reduce Motion
2026-04-05 18:46:38 -06:00

61 lines
2 KiB
Swift

import SwiftData
import SwiftUI
struct ContentView: View {
@Environment(ConnectionManager.self) private var manager
@Environment(\.modelContext) private var modelContext
var body: some View {
Group {
switch manager.screen {
case .login:
LoginView()
case .connecting:
ConnectingView()
case .forms:
RemoteFormView()
case .chat:
ChatView()
}
}
.animation(.default, value: manager.screen)
.onAppear {
manager.modelContext = modelContext
purgeExpiredMessages()
}
}
private func purgeExpiredMessages() {
// Purge all if save is off
if !UserDefaults.standard.bool(forKey: "save_messages", default: true) {
try? modelContext.delete(model: SavedMessage.self)
try? modelContext.save()
return
}
// Purge expired messages
let deleteAfterRaw = UserDefaults.standard.integer(forKey: "delete_after")
let days = deleteAfterRaw == 0 ? 0 : (DeleteAfter(rawValue: deleteAfterRaw) ?? .oneMonth).rawValue
if days > 0 {
let cutoff = Calendar.current.date(byAdding: .day, value: -days, to: .now) ?? .now
let expired = FetchDescriptor<SavedMessage>(
predicate: #Predicate { $0.timestamp < cutoff }
)
if let old = try? modelContext.fetch(expired) {
for message in old { modelContext.delete(message) }
}
}
// Purge debug messages if not saving them
if !UserDefaults.standard.bool(forKey: "save_debug_buffer") {
let debugDesc = FetchDescriptor<SavedMessage>(
predicate: #Predicate { $0.buffer == "debug" }
)
if let debugMsgs = try? modelContext.fetch(debugDesc) {
for message in debugMsgs { modelContext.delete(message) }
}
}
try? modelContext.save()
}
}