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
97 lines
3.6 KiB
Swift
97 lines
3.6 KiB
Swift
import SwiftData
|
|
import SwiftUI
|
|
|
|
struct ChatView: View {
|
|
@Environment(ConnectionManager.self) private var manager
|
|
@State private var selectedBuffer: String = "global"
|
|
@State private var showSettings = false
|
|
@State private var selectedDMPartner: String?
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
TabView(selection: $selectedBuffer) {
|
|
ForEach(manager.buffers) { buffer in
|
|
if buffer.name == "direct-messages" {
|
|
Tab(buffer.displayName, systemImage: iconForBuffer(buffer.name), value: buffer.name) {
|
|
DMListView(selectedConversation: $selectedDMPartner)
|
|
}
|
|
} else {
|
|
Tab(buffer.displayName, systemImage: iconForBuffer(buffer.name), value: buffer.name) {
|
|
ChatBufferView(bufferName: buffer.name, canSend: canSend(buffer)) { message in
|
|
navigateToDM(message)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle(titleForBuffer(selectedBuffer))
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .topBarTrailing) {
|
|
Menu("More", systemImage: "ellipsis.circle") {
|
|
Button("Switch Character", systemImage: "person.2") {
|
|
manager.switchCharacter()
|
|
}
|
|
Button("Settings", systemImage: "gearshape") {
|
|
showSettings = true
|
|
}
|
|
Button("Log Out", systemImage: "rectangle.portrait.and.arrow.right", role: .destructive) {
|
|
manager.logout()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.navigationDestination(isPresented: $showSettings) {
|
|
SettingsView()
|
|
}
|
|
.navigationDestination(for: String.self) { partnerId in
|
|
let name = partnerName(for: partnerId)
|
|
DMConversationView(partnerId: partnerId, partnerName: name)
|
|
}
|
|
.onChange(of: selectedBuffer) {
|
|
manager.activeBuffer = selectedBuffer
|
|
}
|
|
}
|
|
}
|
|
|
|
private func canSend(_ buffer: ChatBuffer) -> Bool {
|
|
if buffer.name == "direct-messages" { return true }
|
|
return buffer.canSend
|
|
}
|
|
|
|
private func titleForBuffer(_ name: String) -> String {
|
|
switch name {
|
|
case "global": "Kirandur Global Chat"
|
|
case "map": "Kirandur Map Chat"
|
|
case "debug": "Kirandur Debug Output"
|
|
case "direct-messages": "Kirandur DMs"
|
|
default: "Kirandur \(name.capitalized)"
|
|
}
|
|
}
|
|
|
|
private func iconForBuffer(_ name: String) -> String {
|
|
switch name {
|
|
case "global": "globe"
|
|
case "map": "map"
|
|
case "debug": "ladybug"
|
|
case "direct-messages": "envelope"
|
|
default: "bubble.left"
|
|
}
|
|
}
|
|
|
|
@Environment(\.modelContext) private var modelContext
|
|
|
|
private func partnerName(for partnerId: String) -> String {
|
|
let descriptor = FetchDescriptor<SavedMessage>(
|
|
predicate: #Predicate { $0.buffer == "direct-messages" && $0.senderId == partnerId }
|
|
)
|
|
let results = (try? modelContext.fetch(descriptor)) ?? []
|
|
return results.first?.senderName ?? partnerId
|
|
}
|
|
|
|
private func navigateToDM(_ message: SavedMessage) {
|
|
guard let senderId = message.senderId else { return }
|
|
selectedBuffer = "direct-messages"
|
|
selectedDMPartner = senderId
|
|
}
|
|
}
|