import Foundation import SwiftUI struct ConversationMessageRow: View { @EnvironmentObject var chatWrapper: MonalChatWrapper let message: Message @State private var offset: CGSize = .zero var body: some View { VStack(spacing: 0) { HStack(spacing: 0) { if !message.isInbound { Spacer() MessageAttr(message: message) .padding(.trailing, 4) } ConversationMessageContainer(message: message) .background(message.isInbound ? Color.Material.Shape.white : Color.Material.Shape.alternate) .clipShape(ConversationMessageBubble(isOutgoing: !message.isInbound)) if message.isInbound { MessageAttr(message: message) .padding(.leading, 4) Spacer() } } .padding(.vertical, 10) .padding(.horizontal, 16) .background(Color.clearTappable) .offset(offset) .gesture( DragGesture(minimumDistance: 30, coordinateSpace: .local) .onChanged { value in var width = value.translation.width width = width > 0 ? 0 : width offset = CGSize(width: width, height: 0) } .onEnded { value in let targetWidth: CGFloat = -90 withAnimation(.easeOut(duration: 0.1)) { if value.translation.width <= targetWidth { Vibration.success.vibrate() DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { withAnimation(.easeOut(duration: 0.1)) { offset = .zero } } } else { offset = .zero } } if value.translation.width <= targetWidth { DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { // messages.replyText = message.body ?? "" } } } ) } .listRowInsets(.zero) .listRowSeparator(.hidden) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.Material.Background.light) } } struct ConversationMessageBubble: Shape { let isOutgoing: Bool func path(in rect: CGRect) -> Path { let path = UIBezierPath( roundedRect: rect, byRoundingCorners: isOutgoing ? [.topLeft, .bottomLeft, .bottomRight] : [.topRight, .bottomLeft, .bottomRight], cornerRadii: CGSize(width: 8, height: 10) ) return Path(path.cgPath) } }