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
61 lines
2 KiB
Swift
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()
|
|
}
|
|
}
|