import Foundation import SwiftUI struct ConversationMessageRow: View { @EnvironmentObject var store: AppStore let message: Message @State private var offset: CGSize = .zero var body: some View { VStack(spacing: 0) { HStack(spacing: 0) { if isOutgoing() { Spacer() MessageAttr(message: message) .padding(.trailing, 4) } ConversationMessageContainer(message: message, isOutgoing: isOutgoing()) .background(isOutgoing() ? Color.Material.greenDark100 : Color.Main.white) .clipShape(ConversationMessageBubble(isOutgoing: isOutgoing())) if !isOutgoing() { MessageAttr(message: message) .padding(.leading, 4) Spacer() } } .padding(.vertical, 10) .padding(.horizontal, 16) .background(Color.clearTappable) .offset(offset) .gesture( DragGesture(minimumDistance: 20, coordinateSpace: .local) .onEnded { value in withAnimation(.easeOut(duration: 0.1)) { if value.translation.width < 0 { offset = CGSize(width: -50, height: 0) Vibration.success.vibrate() DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { store.dispatch(.conversationAction(.setReplyText(message.body ?? ""))) withAnimation(.easeOut(duration: 0.1)) { offset = .zero } } } else { offset = .zero } } } ) } .sharedListRow() } private func isOutgoing() -> Bool { message.from == store.state.conversationsState.currentChat?.account } } 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) } }