mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 03:45:20 +00:00
Extract and simplify PaymentChipView (#758)
* `cashuToken` -> `cashuLinks` + minor refaactor * Extract and simplify `PaymentChipView`
This commit is contained in:
@@ -0,0 +1,107 @@
|
|||||||
|
//
|
||||||
|
// PaymentChipView.swift
|
||||||
|
// bitchat
|
||||||
|
//
|
||||||
|
// This is free and unencumbered software released into the public domain.
|
||||||
|
// For more information, see <https://unlicense.org>
|
||||||
|
//
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -311,10 +311,10 @@ struct ContentView: View {
|
|||||||
// Regular messages with natural text wrapping
|
// Regular messages with natural text wrapping
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
// Precompute heavy token scans once per row
|
// Precompute heavy token scans once per row
|
||||||
let cashuTokens = message.content.extractCashuTokens()
|
let cashuLinks = message.content.extractCashuLinks()
|
||||||
let lightningLinks = message.content.extractLightningLinks()
|
let lightningLinks = message.content.extractLightningLinks()
|
||||||
HStack(alignment: .top, spacing: 0) {
|
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)
|
let isExpanded = expandedMessageIDs.contains(message.id)
|
||||||
Text(viewModel.formatMessageAsText(message, colorScheme: colorScheme))
|
Text(viewModel.formatMessageAsText(message, colorScheme: colorScheme))
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
@@ -330,7 +330,7 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Expand/Collapse for very long messages
|
// 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 isExpanded = expandedMessageIDs.contains(message.id)
|
||||||
let labelKey = isExpanded ? LocalizedStringKey("content.message.show_less") : LocalizedStringKey("content.message.show_more")
|
let labelKey = isExpanded ? LocalizedStringKey("content.message.show_less") : LocalizedStringKey("content.message.show_more")
|
||||||
Button(labelKey) {
|
Button(labelKey) {
|
||||||
@@ -343,37 +343,13 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Render payment chips (Lightning / Cashu) with rounded background
|
// Render payment chips (Lightning / Cashu) with rounded background
|
||||||
if !lightningLinks.isEmpty || !cashuTokens.isEmpty {
|
if !lightningLinks.isEmpty || !cashuLinks.isEmpty {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
ForEach(Array(lightningLinks.prefix(3)).indices, id: \.self) { i in
|
ForEach(lightningLinks, id: \.self) { link in
|
||||||
let link = lightningLinks[i]
|
PaymentChipView(paymentType: .lightning(link))
|
||||||
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(Array(cashuTokens.prefix(3)).indices, id: \.self) { i in
|
ForEach(cashuLinks, id: \.self) { link in
|
||||||
let token = cashuTokens[i]
|
PaymentChipView(paymentType: .cashu(link))
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.top, 6)
|
.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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -37,15 +37,16 @@ extension String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract up to `max` Cashu tokens (cashuA/cashuB). Allow dot '.' and shorter lengths.
|
// 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 regex = RegexCache.cashu
|
||||||
let ns = self as NSString
|
let ns = self as NSString
|
||||||
let range = NSRange(location: 0, length: ns.length)
|
let range = NSRange(location: 0, length: ns.length)
|
||||||
var found: [String] = []
|
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 {
|
if m.numberOfRanges > 0 {
|
||||||
let token = ns.substring(with: m.range(at: 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 }
|
if found.count >= max { break }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,19 +59,19 @@ extension String {
|
|||||||
let ns = self as NSString
|
let ns = self as NSString
|
||||||
let full = NSRange(location: 0, length: ns.length)
|
let full = NSRange(location: 0, length: ns.length)
|
||||||
// lightning: scheme
|
// 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))
|
let s = ns.substring(with: m.range(at: 0))
|
||||||
results.append(s)
|
results.append(s)
|
||||||
if results.count >= max { return results }
|
if results.count >= max { return results }
|
||||||
}
|
}
|
||||||
// BOLT11
|
// 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))
|
let s = ns.substring(with: m.range(at: 0))
|
||||||
results.append("lightning:\(s)")
|
results.append("lightning:\(s)")
|
||||||
if results.count >= max { return results }
|
if results.count >= max { return results }
|
||||||
}
|
}
|
||||||
// LNURL bech32
|
// 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))
|
let s = ns.substring(with: m.range(at: 0))
|
||||||
results.append("lightning:\(s)")
|
results.append("lightning:\(s)")
|
||||||
if results.count >= max { return results }
|
if results.count >= max { return results }
|
||||||
|
|||||||
Reference in New Issue
Block a user