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)
84 lines
3.1 KiB
Swift
84 lines
3.1 KiB
Swift
import SwiftUI
|
|
|
|
struct ChatBufferView: View {
|
|
let bufferName: String
|
|
let canSend: Bool
|
|
var onDM: ((ChatMessage) -> Void)?
|
|
@Environment(ConnectionManager.self) private var manager
|
|
@State private var messageText = ""
|
|
|
|
private var messages: [ChatMessage] {
|
|
manager.messages[bufferName] ?? []
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
ScrollViewReader { proxy in
|
|
ScrollView {
|
|
LazyVStack(alignment: .leading, spacing: 8) {
|
|
ForEach(messages) { message in
|
|
MessageRow(
|
|
message: message,
|
|
onTapSender: message.isInteractable ? { startDM(to: message) } : nil,
|
|
onDM: onDM,
|
|
onProfile: nil // TODO: profile viewing
|
|
)
|
|
.id(message.id)
|
|
}
|
|
}
|
|
.padding()
|
|
}
|
|
.onChange(of: messages.count) {
|
|
if let lastId = messages.last?.id {
|
|
withAnimation(.easeOut(duration: 0.2)) {
|
|
proxy.scrollTo(lastId, anchor: .bottom)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if canSend {
|
|
HStack(spacing: 8) {
|
|
TextField("Message...", text: $messageText, axis: .vertical)
|
|
.textFieldStyle(.roundedBorder)
|
|
.lineLimit(1...4)
|
|
.onSubmit { sendMessage() }
|
|
.toolbar {
|
|
ToolbarItemGroup(placement: .keyboard) {
|
|
Button("/command", systemImage: "slash.circle") {
|
|
if !messageText.hasPrefix("/") {
|
|
messageText = "/" + messageText
|
|
}
|
|
}
|
|
.labelStyle(.titleAndIcon)
|
|
Spacer()
|
|
}
|
|
}
|
|
|
|
Button("Send", systemImage: "arrow.up.circle.fill", action: sendMessage)
|
|
.labelStyle(.iconOnly)
|
|
.font(.title)
|
|
.buttonStyle(.borderedProminent)
|
|
.buttonBorderShape(.circle)
|
|
.disabled(messageText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
|
|
}
|
|
.padding(.horizontal)
|
|
.padding(.vertical, 8)
|
|
.background(.bar)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func sendMessage() {
|
|
let text = messageText.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard !text.isEmpty else { return }
|
|
|
|
manager.sendChatMessage(buffer: bufferName, content: text)
|
|
messageText = ""
|
|
}
|
|
|
|
private func startDM(to message: ChatMessage) {
|
|
guard message.senderId != nil else { return }
|
|
// Full DM flow would use sendDirectMessage with a selected recipient
|
|
}
|
|
}
|