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)
86 lines
3.6 KiB
Swift
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
|
|
}
|
|
}
|