// // ChatTranscriptView.swift // kordophone2 // // Created by James Magahern on 8/24/25. // import SwiftUI struct ChatTranscriptView: View { @Binding var model: ViewModel var body: some View { ScrollView { LazyVStack(spacing: 17.0) { ForEach($model.messages.reversed()) { message in MessageCellView(message: message) .id(message.id) .scaleEffect(CGSize(width: 1.0, height: -1.0)) } } .padding() } .scaleEffect(CGSize(width: 1.0, height: -1.0)) } // MARK: - Types @Observable class ViewModel { var messages: [Display.Message] var displayedConversation: Display.Conversation.ID? = nil init(messages: [Display.Message] = []) { self.messages = messages observeDisplayedConversation() } private func observeDisplayedConversation() { withObservationTracking { _ = displayedConversation } onChange: { Task { @MainActor [weak self] in guard let self else { return } await loadMessages() observeDisplayedConversation() } } } private func loadMessages() async { self.messages = [] guard let displayedConversation else { return } do { let client = XPCClient() let messages = try await client.getMessages(conversationId: displayedConversation) self.messages = messages.map { Display.Message(from: $0) } } catch { print("Message fetch error: \(error)") } } } } struct MessageCellView: View { @Binding var message: Display.Message var body: some View { VStack { HStack(alignment: .bottom) { if message.isFromMe { Spacer(minLength: .minimumBubbleHorizontalPadding) } TextCellContentView(text: message.text, isFromMe: message.isFromMe) .mask { UnevenRoundedRectangle(cornerRadii: RectangleCornerRadii( topLeading: message.isFromMe ? .dominantCornerRadius : .minorCornerRadius, bottomLeading: .dominantCornerRadius, bottomTrailing: .dominantCornerRadius, topTrailing: message.isFromMe ? .minorCornerRadius : .dominantCornerRadius, )) } if !message.isFromMe { Spacer(minLength: .minimumBubbleHorizontalPadding) } } } } // MARK: - Types private struct TextCellContentView: View { let text: String let isFromMe: Bool var body: some View { let bubbleColor: Color = isFromMe ? .blue : Color(.systemGray) let textColor: Color = isFromMe ? .white : .primary HStack { Text(text) .foregroundStyle(textColor) .multilineTextAlignment(.leading) } .fixedSize(horizontal: false, vertical: true) .padding(.horizontal, 16.0) .padding(.vertical, 10.0) .background(bubbleColor) } } } fileprivate extension CGFloat { static let dominantCornerRadius = 16.0 static let minorCornerRadius = 4.0 static let minimumBubbleHorizontalPadding = 80.0 } #Preview { @Previewable @State var model = ChatTranscriptView.ViewModel(messages: [ .init(sender: .me, text: "Hello, how are you?"), .init(sender: .counterpart("Bob"), text: "I am doing fine!") ]) ChatTranscriptView(model: $model) }