From 8c368233c45d39b68c6d7ed78c2af806740bf2c5 Mon Sep 17 00:00:00 2001 From: Islam <2553451+qalandarov@users.noreply.github.com> Date: Mon, 6 Oct 2025 16:21:32 +0100 Subject: [PATCH] Extract and simplify `PaymentChipView` (#758) * `cashuToken` -> `cashuLinks` + minor refaactor * Extract and simplify `PaymentChipView` --- .../Views/Components/PaymentChipView.swift | 107 ++++++++++++++++++ bitchat/Views/ContentView.swift | 80 ++----------- bitchat/Views/MessageTextHelpers.swift | 13 ++- 3 files changed, 122 insertions(+), 78 deletions(-) create mode 100644 bitchat/Views/Components/PaymentChipView.swift diff --git a/bitchat/Views/Components/PaymentChipView.swift b/bitchat/Views/Components/PaymentChipView.swift new file mode 100644 index 00000000..cbeedfeb --- /dev/null +++ b/bitchat/Views/Components/PaymentChipView.swift @@ -0,0 +1,107 @@ +// +// PaymentChipView.swift +// bitchat +// +// This is free and unencumbered software released into the public domain. +// For more information, see +// + +import SwiftUI + +struct PaymentChipView: View { + @Environment(\.colorScheme) private var colorScheme + @Environment(\.openURL) private var openURL + + enum PaymentType { + case cashu(String) + case lightning(String) + + var url: URL? { + switch self { + case .cashu(let link), .lightning(let link): + return URL(string: link) + } + } + + var emoji: String { + switch self { + case .cashu: "🥜" + case .lightning: "⚡" + } + } + + var label: String { + switch self { + case .cashu: + String(localized: "content.payment.cashu", comment: "Label for Cashu payment chip") + case .lightning: + String(localized: "content.payment.lightning", comment: "Label for Lightning payment chip") + } + } + } + + let paymentType: PaymentType + + private var fgColor: Color { + colorScheme == .dark ? Color.green : Color(red: 0, green: 0.5, blue: 0) + } + private var bgColor: Color { + colorScheme == .dark ? Color.gray.opacity(0.18) : Color.gray.opacity(0.12) + } + private var border: Color { fgColor.opacity(0.25) } + + var body: some View { + Button { + #if os(iOS) + if let url = paymentType.url { openURL(url) } + #else + if let url = paymentType.url { NSWorkspace.shared.open(url) } + #endif + } label: { + HStack(spacing: 6) { + Text(paymentType.emoji) + Text(paymentType.label) + .font(.bitchatSystem(size: 12, weight: .semibold, design: .monospaced)) + } + .padding(.vertical, 6) + .padding(.horizontal, 12) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(bgColor) + ) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(border, lineWidth: 1) + ) + .foregroundColor(fgColor) + } + .buttonStyle(.plain) + } +} + +#Preview { + let cashuLink = "https://example.com/cashu" + let lightningLink = "https://example.com/lightning" + + List { + HStack { + PaymentChipView(paymentType: .cashu(cashuLink)) + PaymentChipView(paymentType: .lightning(lightningLink)) + } + .listRowSeparator(.hidden) + .listRowInsets(EdgeInsets()) + .listRowBackground(EmptyView()) + } + .environment(\.colorScheme, .light) + + List { + HStack { + PaymentChipView(paymentType: .cashu(cashuLink)) + PaymentChipView(paymentType: .lightning(lightningLink)) + } + .listRowSeparator(.hidden) + .listRowInsets(EdgeInsets()) + .listRowBackground(EmptyView()) + } + .environment(\.colorScheme, .dark) +} diff --git a/bitchat/Views/ContentView.swift b/bitchat/Views/ContentView.swift index 9f3a66dd..06d7fdbc 100644 --- a/bitchat/Views/ContentView.swift +++ b/bitchat/Views/ContentView.swift @@ -311,10 +311,10 @@ struct ContentView: View { // Regular messages with natural text wrapping VStack(alignment: .leading, spacing: 0) { // Precompute heavy token scans once per row - let cashuTokens = message.content.extractCashuTokens() + 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)) && cashuTokens.isEmpty + let isLong = (message.content.count > TransportConfig.uiLongMessageLengthThreshold || message.content.hasVeryLongToken(threshold: TransportConfig.uiVeryLongTokenThreshold)) && cashuLinks.isEmpty let isExpanded = expandedMessageIDs.contains(message.id) Text(viewModel.formatMessageAsText(message, colorScheme: colorScheme)) .fixedSize(horizontal: false, vertical: true) @@ -330,7 +330,7 @@ struct ContentView: View { } // Expand/Collapse for very long messages - if (message.content.count > TransportConfig.uiLongMessageLengthThreshold || message.content.hasVeryLongToken(threshold: TransportConfig.uiVeryLongTokenThreshold)) && cashuTokens.isEmpty { + 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) { @@ -343,37 +343,13 @@ struct ContentView: View { } // Render payment chips (Lightning / Cashu) with rounded background - if !lightningLinks.isEmpty || !cashuTokens.isEmpty { + if !lightningLinks.isEmpty || !cashuLinks.isEmpty { HStack(spacing: 8) { - ForEach(Array(lightningLinks.prefix(3)).indices, id: \.self) { i in - let link = lightningLinks[i] - PaymentChipView( - emoji: "⚡", - label: String(localized: "content.payment.lightning", comment: "Label for Lightning payment chip"), - colorScheme: colorScheme - ) { - #if os(iOS) - if let url = URL(string: link) { UIApplication.shared.open(url) } - #else - if let url = URL(string: link) { NSWorkspace.shared.open(url) } - #endif - } + ForEach(lightningLinks, id: \.self) { link in + PaymentChipView(paymentType: .lightning(link)) } - ForEach(Array(cashuTokens.prefix(3)).indices, id: \.self) { i in - let token = cashuTokens[i] - let enc = token.addingPercentEncoding(withAllowedCharacters: .alphanumerics.union(CharacterSet(charactersIn: "-_"))) ?? token - let urlStr = "cashu:\(enc)" - PaymentChipView( - emoji: "🥜", - label: String(localized: "content.payment.cashu", comment: "Label for Cashu payment chip"), - colorScheme: colorScheme - ) { - #if os(iOS) - if let url = URL(string: urlStr) { UIApplication.shared.open(url) } - #else - if let url = URL(string: urlStr) { NSWorkspace.shared.open(url) } - #endif - } + ForEach(cashuLinks, id: \.self) { link in + PaymentChipView(paymentType: .cashu(link)) } } .padding(.top, 6) @@ -1620,43 +1596,3 @@ extension ContentView { } } } - -// MARK: - Helper Views - -// Rounded payment chip button -private struct PaymentChipView: View { - let emoji: String - let label: String - let colorScheme: ColorScheme - let action: () -> Void - - private var fgColor: Color { - colorScheme == .dark ? Color.green : Color(red: 0, green: 0.5, blue: 0) - } - private var bgColor: Color { - colorScheme == .dark ? Color.gray.opacity(0.18) : Color.gray.opacity(0.12) - } - private var border: Color { fgColor.opacity(0.25) } - - var body: some View { - Button(action: action) { - HStack(spacing: 6) { - Text(emoji) - Text(label) - .font(.bitchatSystem(size: 12, weight: .semibold, design: .monospaced)) - } - .padding(.vertical, 6) - .padding(.horizontal, 12) - .background( - RoundedRectangle(cornerRadius: 12) - .fill(bgColor) - ) - .overlay( - RoundedRectangle(cornerRadius: 12) - .stroke(border, lineWidth: 1) - ) - .foregroundColor(fgColor) - } - .buttonStyle(.plain) - } -} diff --git a/bitchat/Views/MessageTextHelpers.swift b/bitchat/Views/MessageTextHelpers.swift index 1ef4c19e..d4d8095c 100644 --- a/bitchat/Views/MessageTextHelpers.swift +++ b/bitchat/Views/MessageTextHelpers.swift @@ -37,15 +37,16 @@ extension String { } // Extract up to `max` Cashu tokens (cashuA/cashuB). Allow dot '.' and shorter lengths. - func extractCashuTokens(max: Int = 3) -> [String] { + func extractCashuLinks(max: Int = 3) -> [String] { let regex = RegexCache.cashu let ns = self as NSString let range = NSRange(location: 0, length: ns.length) var found: [String] = [] - for m in regex.matches(in: self, options: [], range: range) { + for m in regex.matches(in: self, range: range) { if m.numberOfRanges > 0 { let token = ns.substring(with: m.range(at: 0)) - found.append(token) + let enc = token.addingPercentEncoding(withAllowedCharacters: .alphanumerics.union(CharacterSet(charactersIn: "-_"))) ?? token + found.append("cashu:\(enc)") if found.count >= max { break } } } @@ -58,19 +59,19 @@ extension String { let ns = self as NSString let full = NSRange(location: 0, length: ns.length) // lightning: scheme - for m in RegexCache.lightningScheme.matches(in: self, options: [], range: full) { + for m in RegexCache.lightningScheme.matches(in: self, range: full) { let s = ns.substring(with: m.range(at: 0)) results.append(s) if results.count >= max { return results } } // BOLT11 - for m in RegexCache.bolt11.matches(in: self, options: [], range: full) { + for m in RegexCache.bolt11.matches(in: self, range: full) { let s = ns.substring(with: m.range(at: 0)) results.append("lightning:\(s)") if results.count >= max { return results } } // LNURL bech32 - for m in RegexCache.lnurl.matches(in: self, options: [], range: full) { + for m in RegexCache.lnurl.matches(in: self, range: full) { let s = ns.substring(with: m.range(at: 0)) results.append("lightning:\(s)") if results.count >= max { return results }