Files
bitchat/bitchat/Views/Components/TextMessageView.swift
T
764f016d17 [codex] Refactor app runtime and ownership architecture (#1104)
* Refactor app runtime and view model architecture

* Move app ownership into stores and coordinators

* Fix smoke test environment injection

* Stabilize fragmentation package tests

* Fix coordinator build warnings

* Clean up chat view model warnings

* Fix Nostr relay startup coalescing

---------

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
2026-05-30 14:13:26 +02:00

106 lines
4.3 KiB
Swift

//
// TextMessageView.swift
// bitchat
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import SwiftUI
import BitFoundation
struct TextMessageView: View {
@Environment(\.colorScheme) private var colorScheme: ColorScheme
@EnvironmentObject private var conversationUIModel: ConversationUIModel
let message: BitchatMessage
@State private var expandedMessageIDs: Set<String> = []
var body: some View {
VStack(alignment: .leading, spacing: 0) {
// Precompute heavy token scans once per row
let cashuLinks = message.content.extractCashuLinks()
let lightningLinks = message.content.extractLightningLinks()
HStack(alignment: .top, spacing: 0) {
let isLong = (message.content.count > TransportConfig.uiLongMessageLengthThreshold || message.content.hasVeryLongToken(threshold: TransportConfig.uiVeryLongTokenThreshold)) && cashuLinks.isEmpty
let isExpanded = expandedMessageIDs.contains(message.id)
Text(conversationUIModel.formatMessage(message, colorScheme: colorScheme))
.fixedSize(horizontal: false, vertical: true)
.lineLimit(isLong && !isExpanded ? TransportConfig.uiLongMessageLineLimit : nil)
.frame(maxWidth: .infinity, alignment: .leading)
// Delivery status indicator for private messages
if message.isPrivate && conversationUIModel.isSentByCurrentUser(message),
let status = message.deliveryStatus {
DeliveryStatusView(status: status)
.padding(.leading, 4)
}
}
// Expand/Collapse for very long messages
if (message.content.count > TransportConfig.uiLongMessageLengthThreshold || message.content.hasVeryLongToken(threshold: TransportConfig.uiVeryLongTokenThreshold)) && cashuLinks.isEmpty {
let isExpanded = expandedMessageIDs.contains(message.id)
let labelKey = isExpanded ? LocalizedStringKey("content.message.show_less") : LocalizedStringKey("content.message.show_more")
Button(labelKey) {
if isExpanded { expandedMessageIDs.remove(message.id) }
else { expandedMessageIDs.insert(message.id) }
}
.font(.bitchatSystem(size: 11, weight: .medium, design: .monospaced))
.foregroundColor(Color.blue)
.padding(.top, 4)
}
// Render payment chips (Lightning / Cashu) with rounded background
if !lightningLinks.isEmpty || !cashuLinks.isEmpty {
HStack(spacing: 8) {
ForEach(lightningLinks, id: \.self) { link in
PaymentChipView(paymentType: .lightning(link))
}
ForEach(cashuLinks, id: \.self) { link in
PaymentChipView(paymentType: .cashu(link))
}
}
.padding(.top, 6)
.padding(.leading, 2)
}
}
}
}
#Preview {
let keychain = PreviewKeychainManager()
let viewModel = ChatViewModel(
keychain: keychain,
idBridge: NostrIdentityBridge(),
identityManager: SecureIdentityStateManager(keychain)
)
let privateConversationModel = PrivateConversationModel(
chatViewModel: viewModel,
conversationStore: viewModel.conversationStore
)
let conversationUIModel = ConversationUIModel(
chatViewModel: viewModel,
privateConversationModel: privateConversationModel,
conversationStore: viewModel.conversationStore
)
Group {
List {
TextMessageView(message: .preview)
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets())
.listRowBackground(EmptyView())
}
.environment(\.colorScheme, .light)
List {
TextMessageView(message: .preview)
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets())
.listRowBackground(EmptyView())
}
.environment(\.colorScheme, .dark)
}
.environmentObject(conversationUIModel)
}