KDChat/KDChat/Views/ChatView.swift
Blake Oliver 2d3fefa013
Initial commit of KDChat
This is an iOS chat client for Kirandur, a multiplayer RPG. Reimplements the LiteNetLib UDP protocol in Swift for reliable ordered messaging. Renders server-driven UI via a RemoteForms widget system for login, character creation and more inside the client.

Uses msgpack-swift for serialization, Keychain for credentials, and targets iOS 18.6+/Xcode 26+.

No message persistence yet. No auto-reconnect. Game-specific packets (movement, combat, etc.) are silently ignored.
No profile viewing yet.

Assisted by Claude Code (Opus 4.6)
2026-04-05 07:11:07 -06:00

86 lines
3.6 KiB
Swift

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?
@AppStorage("haptics_enabled") private var hapticsEnabled = true
@AppStorage("haptics_messages") private var messageHaptics = true
@AppStorage("haptics_status") private var statusHaptics = true
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("Kirandur Chat")
.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)
}
.sensoryFeedback(.impact(flexibility: .soft), trigger: hapticsEnabled && messageHaptics ? manager.messageFeedbackTrigger : 0)
.sensoryFeedback(.warning, trigger: hapticsEnabled && statusHaptics ? manager.systemFeedbackTrigger : 0)
.onChange(of: manager.lastAnnouncement) {
AccessibilityNotification.Announcement(manager.lastAnnouncement).post()
}
}
}
private func canSend(_ buffer: ChatBuffer) -> Bool {
if buffer.name == "direct-messages" { return true }
return buffer.canSend
}
private func iconForBuffer(_ name: String) -> String {
switch name {
case "global": "globe"
case "map": "map"
case "debug": "ladybug"
case "direct-messages": "envelope"
default: "bubble.left"
}
}
private func partnerName(for partnerId: String) -> String {
let dmMessages = manager.messages["direct-messages"] ?? []
return dmMessages.first { $0.senderId == partnerId }?.senderName ?? partnerId
}
private func navigateToDM(_ message: ChatMessage) {
guard let senderId = message.senderId else { return }
selectedBuffer = "direct-messages"
selectedDMPartner = senderId
}
}