Files
andros 2cc77d804d Smoother chat open and waving-hand reaction
- Hide the chat content until the initial fetch completes and the
  ScrollView has laid out at its bottom anchor, then fade it in. No
  more visible scroll jump when opening a conversation.
- Use defaultScrollAnchor(.bottom) so the first layout is already at
  the newest message.
- Add the waving hand emoji to the reaction picker.
2026-04-15 08:31:18 +02:00

116 lines
2.6 KiB
Swift

import SwiftUI
struct MessageRow: View {
let message: Message
let senderName: String
let isSelf: Bool
let replyToName: String?
let canDMSender: Bool
let onReply: () -> Void
let onReact: (String) -> Void
let onDMSender: () -> Void
static let reactionEmojis = ["👋", "👍", "❤️", "😂", "😮", "😢", "🙏"]
private static let timeFmt: DateFormatter = {
let f = DateFormatter()
f.dateFormat = "HH:mm"
return f
}()
var body: some View {
HStack {
if isSelf { Spacer(minLength: 40) }
VStack(alignment: isSelf ? .trailing : .leading, spacing: 2) {
header
bubble
.contextMenu { menuContent }
}
if !isSelf { Spacer(minLength: 40) }
}
}
private var header: some View {
HStack(spacing: 6) {
if !isSelf {
Text(senderName)
.font(.caption.weight(.semibold))
.foregroundStyle(.tint)
}
Text(Self.timeFmt.string(from: message.timestamp))
.font(.caption2)
.foregroundStyle(.secondary)
if isSelf {
Text(senderName)
.font(.caption.weight(.semibold))
.foregroundStyle(.tint)
}
}
}
private var bubble: some View {
HStack(alignment: .bottom, spacing: 4) {
VStack(alignment: .leading, spacing: 3) {
if let name = replyToName {
HStack(spacing: 3) {
Image(systemName: "arrowshape.turn.up.left.fill")
.font(.caption2)
Text(name).font(.caption.weight(.semibold))
}
.foregroundStyle(isSelf ? Color.white.opacity(0.85) : Color.secondary)
}
Text(message.text ?? "")
.font(.body)
.foregroundStyle(isSelf ? .white : .primary)
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(
RoundedRectangle(cornerRadius: 14, style: .continuous)
.fill(isSelf
? AnyShapeStyle(Color.accentColor)
: AnyShapeStyle(Color(.systemGray5)))
)
if isSelf, message.delivery != .none {
Image(systemName: message.delivery.icon)
.font(.caption2)
.foregroundStyle(deliveryColor)
}
}
}
@ViewBuilder
private var menuContent: some View {
Menu("React", systemImage: "face.smiling") {
ForEach(Self.reactionEmojis, id: \.self) { e in
Button {
onReact(e)
} label: {
Text(e)
}
}
}
Button {
onReply()
} label: {
Label("Reply", systemImage: "arrowshape.turn.up.left")
}
if canDMSender {
Button {
onDMSender()
} label: {
Label("Message \(senderName)", systemImage: "bubble.left.and.bubble.right")
}
}
}
private var deliveryColor: Color {
switch message.delivery {
case .confirmed: return .green
case .failed: return .red
case .pending: return .gray
case .none: return .clear
}
}
}