KDChat/KDChat/Views/MessageRow.swift
Blake Oliver f9c751a993
Chat sounds, message card UI, own-message detection, and crash fix
Sounds:
- Add chat sounds (global, map, DM) converted from game client ogg to caf
- Audio category .ambient (mixes with other audio, respects silent switch)
- Per-buffer sound toggles and background buffer sound option in Settings

Messages:
- Card-style message rows with .regularMaterial background, bumped fonts (content .title3, sender .headline)
- Context menu and VoiceOver actions: DM and Show Profile on other players' messages
- Generate local UUIDs for message identity (server ID field is not unique, caused ForEach to collapse messages)

Own-message detection:
- Capture player entity ID from LoadInstance packet (Key 1) on game join
- Hide DM/Profile actions on own messages via senderId comparison

Crash fix:
- Guard all UInt64 timestamp subtractions against underflow (retransmitPending, ping, disconnect timeout)
2026-04-05 08:15:31 -06:00

71 lines
2.4 KiB
Swift

import SwiftUI
struct MessageRow: View {
let message: ChatMessage
var onDM: ((ChatMessage) -> Void)?
var onProfile: ((ChatMessage) -> Void)?
@Environment(ConnectionManager.self) private var manager
private var isOwnMessage: Bool {
message.senderIsRecipient || message.senderId == manager.localSenderId
}
var body: some View {
if message.isSystem {
Text(message.content)
.font(.body)
.foregroundStyle(.secondary)
.italic()
.frame(maxWidth: .infinity, alignment: .center)
.padding(.vertical, 6)
} else {
VStack(alignment: .leading, spacing: 8) {
HStack {
Text(message.senderName)
.font(.headline)
.foregroundStyle(Color.accentColor)
if message.senderIsRecipient {
Text("(you)")
.font(.subheadline)
.foregroundStyle(.secondary)
}
Spacer()
Text(message.timestamp, style: .time)
.font(.subheadline)
.foregroundStyle(.secondary)
}
Text(message.content)
.font(.title3)
}
.padding(.horizontal, 12)
.padding(.vertical, 10)
.background(.regularMaterial)
.clipShape(.rect(cornerRadius: 12))
.contextMenu {
if !isOwnMessage, message.senderId != nil {
if let onDM {
Button("DM \(message.senderName)", systemImage: "envelope") { onDM(message) }
}
if let onProfile {
Button("Show \(message.senderName)'s Profile", systemImage: "person.circle") { onProfile(message) }
}
}
}
.accessibilityElement(children: .combine)
.accessibilityActions {
if !isOwnMessage, message.senderId != nil {
if let onDM {
Button("DM \(message.senderName)") { onDM(message) }
}
if let onProfile {
Button("Show \(message.senderName)'s Profile") { onProfile(message) }
}
}
}
}
}
}