CompatibleAttribute.swift (3685B)
1 // 2 // CompatibleAttribute.swift 3 // damus 4 // 5 // Created by William Casarin on 2023-04-06. 6 // 7 8 import Foundation 9 import SwiftUI 10 11 // Concatening too many `Text` objects can cause crashes (See https://github.com/damus-io/damus/issues/1826) 12 fileprivate let MAX_TEXT_ITEMS = 100 13 14 class CompatibleText: Equatable { 15 var text: some View { 16 if items.count > MAX_TEXT_ITEMS { 17 return AnyView( 18 VStack { 19 Image("warning") 20 Text(NSLocalizedString("This note contains too many items and cannot be rendered", comment: "Error message indicating that a note is too big and cannot be rendered")) 21 .multilineTextAlignment(.center) 22 } 23 .foregroundColor(.secondary) 24 ) 25 } 26 return AnyView( 27 items.reduce(Text(""), { (accumulated, item) in 28 return accumulated + item.render_to_text() 29 }) 30 ) 31 } 32 var attributed: AttributedString { 33 return items.reduce(AttributedString(stringLiteral: ""), { (accumulated, item) in 34 guard let item_attributed_string = item.attributed_string() else { return accumulated } 35 return accumulated + item_attributed_string 36 }) 37 } 38 var items: [Item] 39 40 init() { 41 self.items = [.attributed_string(AttributedString(stringLiteral: ""))] 42 } 43 44 init(stringLiteral: String) { 45 self.items = [.attributed_string(AttributedString(stringLiteral: stringLiteral))] 46 } 47 48 init(attributed: AttributedString) { 49 self.items = [.attributed_string(attributed)] 50 } 51 52 init(items: [Item]) { 53 self.items = items 54 } 55 56 static func == (lhs: CompatibleText, rhs: CompatibleText) -> Bool { 57 return lhs.items == rhs.items 58 } 59 60 static func +(lhs: CompatibleText, rhs: CompatibleText) -> CompatibleText { 61 if case .attributed_string(let las) = lhs.items.last, 62 case .attributed_string(let ras) = rhs.items.first 63 { 64 // Concatenate attributed strings whenever possible to reduce item count 65 let combined_attributed_string = las + ras 66 return CompatibleText(items: 67 Array(lhs.items.prefix(upTo: lhs.items.count - 1)) + 68 [.attributed_string(combined_attributed_string)] + 69 Array(rhs.items.suffix(from: 1)) 70 ) 71 } 72 else { 73 return CompatibleText(items: lhs.items + rhs.items) 74 } 75 } 76 77 } 78 79 extension CompatibleText { 80 enum Item: Equatable { 81 case attributed_string(AttributedString) 82 case icon(named: String, offset: CGFloat) 83 84 func render_to_text() -> Text { 85 switch self { 86 case .attributed_string(let attributed_string): 87 return Text(attributed_string) 88 case .icon(named: let image_name, offset: let offset): 89 return Text(Image(image_name)).baselineOffset(offset) 90 } 91 } 92 93 func attributed_string() -> AttributedString? { 94 switch self { 95 case .attributed_string(let attributed_string): 96 return attributed_string 97 case .icon(named: let name, offset: _): 98 guard let img = UIImage(named: name) else { return nil } 99 return icon_attributed_string(img: img) 100 } 101 } 102 } 103 } 104 105 106 func icon_attributed_string(img: UIImage) -> AttributedString { 107 let attachment = NSTextAttachment() 108 attachment.image = img 109 let attachmentString = NSAttributedString(attachment: attachment) 110 return AttributedString(attachmentString) 111 } 112 113