diff --git a/damus/Util/CompatibleAttribute.swift b/damus/Util/CompatibleAttribute.swift index 85e9fbe6..5a3ecdd0 100644 --- a/damus/Util/CompatibleAttribute.swift +++ b/damus/Util/CompatibleAttribute.swift @@ -8,37 +8,96 @@ import Foundation import SwiftUI +// Concatening too many `Text` objects can cause crashes (See https://github.com/damus-io/damus/issues/1826) +fileprivate let MAX_TEXT_ITEMS = 100 + class CompatibleText: Equatable { - var text: Text - var attributed: AttributedString - + var text: some View { + if items.count > MAX_TEXT_ITEMS { + return AnyView( + VStack { + Image("warning") + 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")) + .multilineTextAlignment(.center) + } + .foregroundColor(.secondary) + ) + } + return AnyView( + items.reduce(Text(""), { (accumulated, item) in + return accumulated + item.render_to_text() + }) + ) + } + var attributed: AttributedString { + return items.reduce(AttributedString(stringLiteral: ""), { (accumulated, item) in + guard let item_attributed_string = item.attributed_string() else { return accumulated } + return accumulated + item_attributed_string + }) + } + var items: [Item] + init() { - self.text = Text("") - self.attributed = AttributedString(stringLiteral: "") + self.items = [.attributed_string(AttributedString(stringLiteral: ""))] } - + init(stringLiteral: String) { - self.text = Text(stringLiteral) - self.attributed = AttributedString(stringLiteral: stringLiteral) + self.items = [.attributed_string(AttributedString(stringLiteral: stringLiteral))] } - - init(text: Text, attributed: AttributedString) { - self.text = text - self.attributed = attributed - } - + init(attributed: AttributedString) { - self.text = Text(attributed) - self.attributed = attributed + self.items = [.attributed_string(attributed)] } - + + init(items: [Item]) { + self.items = items + } + static func == (lhs: CompatibleText, rhs: CompatibleText) -> Bool { - return lhs.attributed == rhs.attributed + return lhs.items == rhs.items } - + static func +(lhs: CompatibleText, rhs: CompatibleText) -> CompatibleText { - let combinedText = lhs.text + rhs.text - let combinedAttributes = lhs.attributed + rhs.attributed - return CompatibleText(text: combinedText, attributed: combinedAttributes) + if case .attributed_string(let las) = lhs.items.last, + case .attributed_string(let ras) = rhs.items.first + { + // Concatenate attributed strings whenever possible to reduce item count + let combined_attributed_string = las + ras + return CompatibleText(items: + Array(lhs.items.prefix(upTo: lhs.items.count - 1)) + + [.attributed_string(combined_attributed_string)] + + Array(rhs.items.suffix(from: 1)) + ) + } + else { + return CompatibleText(items: lhs.items + rhs.items) + } + } + +} + +extension CompatibleText { + enum Item: Equatable { + case attributed_string(AttributedString) + case icon(named: String, offset: CGFloat) + + func render_to_text() -> Text { + switch self { + case .attributed_string(let attributed_string): + return Text(attributed_string) + case .icon(named: let image_name, offset: let offset): + return Text(Image(image_name)).baselineOffset(offset) + } + } + + func attributed_string() -> AttributedString? { + switch self { + case .attributed_string(let attributed_string): + return attributed_string + case .icon(named: let name, offset: _): + guard let img = UIImage(named: name) else { return nil } + return icon_attributed_string(img: img) + } + } } } diff --git a/damus/Util/Hashtags.swift b/damus/Util/Hashtags.swift index eabe5312..83f88ec3 100644 --- a/damus/Util/Hashtags.swift +++ b/damus/Util/Hashtags.swift @@ -43,28 +43,20 @@ let custom_hashtags: [String: CustomHashtag] = [ func hashtag_str(_ htag: String) -> CompatibleText { var attributedString = AttributedString(stringLiteral: "#\(htag)") attributedString.link = URL(string: "damus:t:\(htag.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? htag)") - + let lowertag = htag.lowercased() - - var text = Text(attributedString) + if let custom_hashtag = custom_hashtags[lowertag] { if let col = custom_hashtag.color { attributedString.foregroundColor = col } - + let name = custom_hashtag.name - - if let img = UIImage(named: "\(name)-hashtag") { - attributedString = attributedString + " " - attributed_string_attach_icon(&attributedString, img: img) - } - text = Text(attributedString) - let img = Image("\(name)-hashtag") - text = text + Text(img).baselineOffset(custom_hashtag.offset ?? 0.0) + + attributedString = attributedString + " " + return CompatibleText(items: [.attributed_string(attributedString), .icon(named: "\(name)-hashtag", offset: custom_hashtag.offset ?? 0.0)]) } else { attributedString.foregroundColor = DamusColors.purple + return CompatibleText(items: [.attributed_string(attributedString)]) } - - return CompatibleText(text: text, attributed: attributedString) } - diff --git a/damus/Views/NoteContentView.swift b/damus/Views/NoteContentView.swift index 237f5787..049528b7 100644 --- a/damus/Views/NoteContentView.swift +++ b/damus/Views/NoteContentView.swift @@ -297,11 +297,15 @@ struct NoteContentView: View { } func attributed_string_attach_icon(_ astr: inout AttributedString, img: UIImage) { + let wrapped = icon_attributed_string(img: img) + astr.append(wrapped) +} + +func icon_attributed_string(img: UIImage) -> AttributedString { let attachment = NSTextAttachment() attachment.image = img let attachmentString = NSAttributedString(attachment: attachment) - let wrapped = AttributedString(attachmentString) - astr.append(wrapped) + return AttributedString(attachmentString) } func url_str(_ url: URL) -> CompatibleText {