From fd72390a22e0d90994e7256692fcfbce188e5d83 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Fri, 22 Dec 2023 20:44:46 -0500 Subject: [PATCH] Replace SwiftSoup with HTMLStreamer --- Tusker.xcodeproj/project.pbxproj | 26 ++- .../Activities/StatusActivityItemSource.swift | 6 +- Tusker/Filterer.swift | 5 +- Tusker/HTMLConverter.swift | 181 ++++-------------- .../Compose/ComposeReplyContentView.swift | 4 +- .../FeaturedProfileCollectionViewCell.swift | 4 +- ...ggestedProfileCardCollectionViewCell.swift | 4 +- .../TrendingLinkCardCollectionViewCell.swift | 6 +- .../TrendingStatusesViewController.swift | 5 +- ...nNotificationGroupCollectionViewCell.swift | 7 +- ...otificationsCollectionViewController.swift | 5 +- ...nishedNotificationCollectionViewCell.swift | 7 +- ...pdatedNotificationCollectionViewCell.swift | 7 +- .../Preferences/AcknowledgementsView.swift | 25 --- .../ProfileStatusesViewController.swift | 5 +- Tusker/Screens/Report/ReportStatusView.swift | 8 +- .../StatusEditCollectionViewCell.swift | 8 +- .../StatusEditContentTextView.swift | 22 --- .../Timeline/TimelineViewController.swift | 7 +- .../AccountCollectionViewCell.swift | 6 +- .../ConfirmReblogStatusPreviewView.swift | 13 +- Tusker/Views/ContentTextView.swift | 34 ++-- .../Instance Cell/InstanceTableViewCell.swift | 6 +- .../ProfileFieldValueView.swift | 12 +- .../Profile Header/ProfileHeaderView.swift | 4 +- ...ersationMainStatusCollectionViewCell.swift | 18 +- Tusker/Views/Status/StatusCardView.swift | 5 +- .../Status/StatusCollectionViewCell.swift | 4 +- .../TimelineStatusCollectionViewCell.swift | 14 +- Tusker/Views/StatusContentTextView.swift | 8 +- 30 files changed, 144 insertions(+), 322 deletions(-) delete mode 100644 Tusker/Screens/Status Edit History/StatusEditContentTextView.swift diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index b85af33a..5e05dc1e 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -32,7 +32,7 @@ D608470F2A245D1F00C17380 /* ActiveInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = D608470E2A245D1F00C17380 /* ActiveInstance.swift */; }; D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */; }; D6093FB725BE0CF3004811E6 /* TrendHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */; }; - D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = D60CFFDA24A290BA00D00083 /* SwiftSoup */; }; + D60BB3942B30076F00DAEA65 /* HTMLStreamer in Frameworks */ = {isa = PBXBuildFile; productRef = D60BB3932B30076F00DAEA65 /* HTMLStreamer */; }; D60E2F272442372B005F8713 /* StatusMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F232442372B005F8713 /* StatusMO.swift */; }; D60E2F292442372B005F8713 /* AccountMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F252442372B005F8713 /* AccountMO.swift */; }; D60E2F2C24423EAD005F8713 /* LazilyDecoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */; }; @@ -303,7 +303,6 @@ D6D79F262A0C8D2700AB2315 /* FetchStatusSourceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F252A0C8D2700AB2315 /* FetchStatusSourceService.swift */; }; D6D79F292A0D596B00AB2315 /* StatusEditHistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F282A0D596B00AB2315 /* StatusEditHistoryViewController.swift */; }; D6D79F2B2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F2A2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift */; }; - D6D79F2D2A0D61B400AB2315 /* StatusEditContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F2C2A0D61B400AB2315 /* StatusEditContentTextView.swift */; }; D6D79F2F2A0D6A7F00AB2315 /* StatusEditPollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F2E2A0D6A7F00AB2315 /* StatusEditPollView.swift */; }; D6D79F532A0FFE3200AB2315 /* ToggleableButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F522A0FFE3200AB2315 /* ToggleableButton.swift */; }; D6D79F572A1160B800AB2315 /* AccountDisplayNameLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F562A1160B800AB2315 /* AccountDisplayNameLabel.swift */; }; @@ -712,7 +711,6 @@ D6D79F252A0C8D2700AB2315 /* FetchStatusSourceService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchStatusSourceService.swift; sourceTree = ""; }; D6D79F282A0D596B00AB2315 /* StatusEditHistoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditHistoryViewController.swift; sourceTree = ""; }; D6D79F2A2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditCollectionViewCell.swift; sourceTree = ""; }; - D6D79F2C2A0D61B400AB2315 /* StatusEditContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditContentTextView.swift; sourceTree = ""; }; D6D79F2E2A0D6A7F00AB2315 /* StatusEditPollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditPollView.swift; sourceTree = ""; }; D6D79F522A0FFE3200AB2315 /* ToggleableButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleableButton.swift; sourceTree = ""; }; D6D79F562A1160B800AB2315 /* AccountDisplayNameLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDisplayNameLabel.swift; sourceTree = ""; }; @@ -786,10 +784,10 @@ D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */, D659F35E2953A212002D944A /* TTTKit in Frameworks */, D6B0026E29B5248800C70BE2 /* UserAccounts in Frameworks */, - D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */, D60088EF2980D8B5005B4D00 /* StoreKit.framework in Frameworks */, D6CA6ED229EF6091003EC5DF /* TuskerPreferences in Frameworks */, D6552367289870790048A653 /* ScreenCorners in Frameworks */, + D60BB3942B30076F00DAEA65 /* HTMLStreamer in Frameworks */, D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */, D63CC702290EC0B8000E19DE /* Sentry in Frameworks */, D6BEA245291A0EDE002F4D01 /* Duckable in Frameworks */, @@ -1574,7 +1572,6 @@ children = ( D6D79F282A0D596B00AB2315 /* StatusEditHistoryViewController.swift */, D6D79F2A2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift */, - D6D79F2C2A0D61B400AB2315 /* StatusEditContentTextView.swift */, D6D79F2E2A0D6A7F00AB2315 /* StatusEditPollView.swift */, ); path = "Status Edit History"; @@ -1699,7 +1696,6 @@ ); name = Tusker; packageProductDependencies = ( - D60CFFDA24A290BA00D00083 /* SwiftSoup */, D6676CA427A8D0020052936B /* WebURLFoundationExtras */, D674A50827F9128D00BA03AC /* Pachyderm */, D6552366289870790048A653 /* ScreenCorners */, @@ -1711,6 +1707,7 @@ D635237029B78A7D009ED5E7 /* TuskerComponents */, D6BD395829B64426005FFD2B /* ComposeUI */, D6CA6ED129EF6091003EC5DF /* TuskerPreferences */, + D60BB3932B30076F00DAEA65 /* HTMLStreamer */, ); productName = Tusker; productReference = D6D4DDCC212518A000E1C4BB /* Tusker.app */; @@ -1821,10 +1818,10 @@ ); mainGroup = D6D4DDC3212518A000E1C4BB; packageReferences = ( - D60CFFD924A290BA00D00083 /* XCRemoteSwiftPackageReference "SwiftSoup" */, D6676CA127A8D0020052936B /* XCRemoteSwiftPackageReference "swift-url" */, D6552365289870790048A653 /* XCRemoteSwiftPackageReference "ScreenCorners" */, D63CC700290EC0B8000E19DE /* XCRemoteSwiftPackageReference "sentry-cocoa" */, + D60BB3922B30076F00DAEA65 /* XCRemoteSwiftPackageReference "HTMLStreamer" */, ); productRefGroup = D6D4DDCD212518A000E1C4BB /* Products */; projectDirPath = ""; @@ -2181,7 +2178,6 @@ D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */, D68A76F129539116001DA1B3 /* FlipView.swift in Sources */, D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */, - D6D79F2D2A0D61B400AB2315 /* StatusEditContentTextView.swift in Sources */, D647D92824257BEB0005044F /* AttachmentPreviewViewController.swift in Sources */, D61F75B5293BD97400C0B37F /* DeleteFilterService.swift in Sources */, D6DFC6A0242C4CCC00ACC392 /* Weak.swift in Sources */, @@ -2956,12 +2952,12 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - D60CFFD924A290BA00D00083 /* XCRemoteSwiftPackageReference "SwiftSoup" */ = { + D60BB3922B30076F00DAEA65 /* XCRemoteSwiftPackageReference "HTMLStreamer" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/scinfu/SwiftSoup.git"; + repositoryURL = "https://git.shadowfacts.net/shadowfacts/HTMLStreamer.git"; requirement = { - kind = upToNextMinorVersion; - minimumVersion = 2.3.2; + branch = main; + kind = branch; }; }; D63CC700290EC0B8000E19DE /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = { @@ -2991,10 +2987,10 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - D60CFFDA24A290BA00D00083 /* SwiftSoup */ = { + D60BB3932B30076F00DAEA65 /* HTMLStreamer */ = { isa = XCSwiftPackageProductDependency; - package = D60CFFD924A290BA00D00083 /* XCRemoteSwiftPackageReference "SwiftSoup" */; - productName = SwiftSoup; + package = D60BB3922B30076F00DAEA65 /* XCRemoteSwiftPackageReference "HTMLStreamer" */; + productName = HTMLStreamer; }; D61ABEFB28F105DE00B29151 /* Pachyderm */ = { isa = XCSwiftPackageProductDependency; diff --git a/Tusker/Activities/StatusActivityItemSource.swift b/Tusker/Activities/StatusActivityItemSource.swift index 82bda548..b437bec8 100644 --- a/Tusker/Activities/StatusActivityItemSource.swift +++ b/Tusker/Activities/StatusActivityItemSource.swift @@ -8,7 +8,7 @@ import UIKit import LinkPresentation -import SwiftSoup +import HTMLStreamer class StatusActivityItemSource: NSObject, UIActivityItemSource { let status: StatusMO @@ -33,8 +33,8 @@ class StatusActivityItemSource: NSObject, UIActivityItemSource { let metadata = LPLinkMetadata() metadata.originalURL = status.url! metadata.url = status.url! - let doc = try! SwiftSoup.parse(status.content) - let content = try! doc.text() + var converter = TextConverter(callbacks: HTMLConverter.Callbacks.self) + let content = converter.convert(html: status.content) metadata.title = "\(status.account.displayName): \"\(content)\"" if let avatar = status.account.avatar, let data = ImageCache.avatars.getData(avatar), diff --git a/Tusker/Filterer.swift b/Tusker/Filterer.swift index 77b33e8e..fa1a2b9a 100644 --- a/Tusker/Filterer.swift +++ b/Tusker/Filterer.swift @@ -42,7 +42,7 @@ class Filterer { var filtersChanged: ((Bool) -> Void)? - var htmlConverter = HTMLConverter() + private var htmlConverter: HTMLConverter private var hasSetup = false private var matchers = [(NSRegularExpression, Result)]() private var cancellables = Set() @@ -55,9 +55,10 @@ class Filterer { // are no longer valid, without needing to go through and update each of them private var generation = 0 - init(mastodonController: MastodonController, context: FilterV1.Context) { + init(mastodonController: MastodonController, context: FilterV1.Context, htmlConverter: HTMLConverter) { self.mastodonController = mastodonController self.context = context + self.htmlConverter = htmlConverter self.hideReblogsInTimelines = Preferences.shared.hideReblogsInTimelines self.hideRepliesInTimelines = Preferences.shared.hideRepliesInTimelines diff --git a/Tusker/HTMLConverter.swift b/Tusker/HTMLConverter.swift index 8f747811..57bfcc44 100644 --- a/Tusker/HTMLConverter.swift +++ b/Tusker/HTMLConverter.swift @@ -7,11 +7,11 @@ // import UIKit -import SwiftSoup +import HTMLStreamer import WebURL import WebURLFoundationExtras -struct HTMLConverter { +class HTMLConverter { static let defaultFont = UIFont.systemFont(ofSize: 17) static let defaultMonospaceFont = UIFont.monospacedSystemFont(ofSize: 17, weight: .regular) @@ -23,150 +23,47 @@ struct HTMLConverter { return style }() - var font: UIFont = defaultFont - var monospaceFont: UIFont = defaultMonospaceFont - var color: UIColor = defaultColor - var paragraphStyle: NSParagraphStyle = defaultParagraphStyle + private var converter: AttributedStringConverter + init(font: UIFont, monospaceFont: UIFont, color: UIColor, paragraphStyle: NSParagraphStyle) { + let config = AttributedStringConverterConfiguration(font: font, monospaceFont: monospaceFont, color: color, paragraphStyle: paragraphStyle) + self.converter = AttributedStringConverter(configuration: config) + } + func convert(_ html: String) -> NSAttributedString { - let doc = try! SwiftSoup.parseBodyFragment(html) - let body = doc.body()! - - if let attributedText = attributedTextForHTMLNode(body) { - let mutAttrString = NSMutableAttributedString(attributedString: attributedText) - mutAttrString.trimTrailingCharactersInSet(.whitespacesAndNewlines) - mutAttrString.collapseWhitespace() - - // Wait until the end and then fill in the unset paragraph styles, to avoid clobbering the list style. - mutAttrString.enumerateAttribute(.paragraphStyle, in: mutAttrString.fullRange, options: .longestEffectiveRangeNotRequired) { value, range, stop in - if value == nil { - mutAttrString.addAttribute(.paragraphStyle, value: paragraphStyle, range: range) - } - } - - return mutAttrString - } else { - return NSAttributedString() - } + converter.convert(html: html) } - - private func attributedTextForHTMLNode(_ node: Node, usePreformattedText: Bool = false) -> NSAttributedString? { - switch node { - case let node as TextNode: - let text: String - if usePreformattedText { - text = node.getWholeText() - } else { - text = node.text() - } - return NSAttributedString(string: text, attributes: [.font: font, .foregroundColor: color]) - case let node as Element: - if node.tagName() == "ol" || node.tagName() == "ul" { - return attributedTextForList(node, usePreformattedText: usePreformattedText) - } - - let attributed = NSMutableAttributedString(string: "", attributes: [.font: font, .foregroundColor: color]) - for child in node.getChildNodes() { - var appendEllipsis = false - if node.tagName() == "a", - let el = child as? Element { - if el.hasClass("invisible") { - continue - } else if el.hasClass("ellipsis") { - appendEllipsis = true - } - } - - if let childText = attributedTextForHTMLNode(child, usePreformattedText: usePreformattedText || node.tagName() == "pre") { - attributed.append(childText) - } - - if appendEllipsis { - attributed.append(NSAttributedString("…")) - } - } - - lazy var currentFont = if attributed.length == 0 { - font - } else { - attributed.attribute(.font, at: 0, effectiveRange: nil) as? UIFont ?? font - } - - switch node.tagName() { - case "br": - // need to specify defaultFont here b/c otherwise it uses the default 12pt Helvetica which - // screws up its determination of the line height making multiple lines of emojis squash together - attributed.append(NSAttributedString(string: "\n", attributes: [.font: font])) - case "a": - let href = try! node.attr("href") - if let webURL = WebURL(href), - let url = URL(webURL) { - attributed.addAttribute(.link, value: url, range: attributed.fullRange) - } else if let url = URL(string: href) { - attributed.addAttribute(.link, value: url, range: attributed.fullRange) - } - case "p": - attributed.append(NSAttributedString(string: "\n\n", attributes: [.font: font])) - case "em", "i": - attributed.addAttribute(.font, value: currentFont.withTraits(.traitItalic)!, range: attributed.fullRange) - case "strong", "b": - attributed.addAttribute(.font, value: currentFont.withTraits(.traitBold)!, range: attributed.fullRange) - case "del": - attributed.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: attributed.fullRange) - case "code": - attributed.addAttribute(.font, value: monospaceFont, range: attributed.fullRange) - case "pre": - attributed.append(NSAttributedString(string: "\n\n")) - attributed.addAttribute(.font, value: monospaceFont, range: attributed.fullRange) - case "blockquote": - let paragraphStyle = paragraphStyle.mutableCopy() as! NSMutableParagraphStyle - paragraphStyle.headIndent = 32 - paragraphStyle.firstLineHeadIndent = 32 - attributed.addAttributes([ - .font: currentFont.withTraits(.traitItalic)!, - .paragraphStyle: paragraphStyle, - ], range: attributed.fullRange) - default: - break - } - - return attributed - default: - return nil - } - } - - private func attributedTextForList(_ element: Element, usePreformattedText: Bool) -> NSAttributedString { - let list = element.tagName() == "ol" ? OrderedNumberTextList(markerFormat: .decimal, options: 0) : NSTextList(markerFormat: .disc, options: 0) - let paragraphStyle = paragraphStyle.mutableCopy() as! NSMutableParagraphStyle - // I don't like that I can't just use paragraphStyle.textLists, because it makes the list markers - // not use the monospace digit font (it seems to just use whatever font attribute is set for the whole thing), - // and it doesn't right align the list markers. - // Unfortunately, doing it manually means the list markers are incldued in the selectable text. - paragraphStyle.headIndent = 32 - paragraphStyle.firstLineHeadIndent = 0 - // Use 2 tab stops, one for the list marker, the second for the content. - paragraphStyle.tabStops = [NSTextTab(textAlignment: .right, location: 28), NSTextTab(textAlignment: .natural, location: 32)] - let str = NSMutableAttributedString(string: "") - var item = 1 - for child in element.children() where child.tagName() == "li" { - if let childStr = attributedTextForHTMLNode(child, usePreformattedText: usePreformattedText) { - str.append(NSAttributedString(string: "\t\(list.marker(forItemNumber: item))\t", attributes: [ - .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .monospacedDigitSystemFont(ofSize: 17, weight: .regular)), - ])) - str.append(childStr) - str.append(NSAttributedString(string: "\n")) - item += 1 - } - } - str.addAttribute(.paragraphStyle, value: paragraphStyle, range: str.fullRange) - return str - } - } -private class OrderedNumberTextList: NSTextList { - override func marker(forItemNumber itemNumber: Int) -> String { - "\(super.marker(forItemNumber: itemNumber))." +extension HTMLConverter { + struct Callbacks: HTMLConversionCallbacks { + static func makeURL(string: String) -> URL? { + // Converting WebURL to URL is a small but non-trivial expense (since it works by + // serializing the WebURL as a string and then having Foundation parse it again), + // so, if available, use the system parser which doesn't require another round trip. + if #available(iOS 16.0, macOS 13.0, *), + let url = try? URL.ParseStrategy().parse(string) { + url + } else if let web = WebURL(string), + let url = URL(web) { + url + } else { + URL(string: string) + } + } + + static func elementAction(name: String, attributes: [Attribute]) -> ElementAction { + guard name == "span" else { + return .default + } + let clazz = attributes.attributeValue(for: "class") + if clazz == "invisible" { + return .skip + } else if clazz == "ellipsis" { + return .replace("…") + } else { + return .default + } + } } } diff --git a/Tusker/Screens/Compose/ComposeReplyContentView.swift b/Tusker/Screens/Compose/ComposeReplyContentView.swift index 199a1f28..d7c458a1 100644 --- a/Tusker/Screens/Compose/ComposeReplyContentView.swift +++ b/Tusker/Screens/Compose/ComposeReplyContentView.swift @@ -26,11 +26,9 @@ struct ComposeReplyContentView: UIViewRepresentable { view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) view.adjustsFontForContentSizeCategory = true - view.defaultFont = TimelineStatusCollectionViewCell.contentFont - view.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont view.overrideMastodonController = mastodonController - view.setTextFrom(status: status) + view.attributedText = TimelineStatusCollectionViewCell.htmlConverter.convert(status.content) return view } diff --git a/Tusker/Screens/Explore/FeaturedProfileCollectionViewCell.swift b/Tusker/Screens/Explore/FeaturedProfileCollectionViewCell.swift index b346c7c4..52a2a8a9 100644 --- a/Tusker/Screens/Explore/FeaturedProfileCollectionViewCell.swift +++ b/Tusker/Screens/Explore/FeaturedProfileCollectionViewCell.swift @@ -34,8 +34,6 @@ class FeaturedProfileCollectionViewCell: UICollectionViewCell { displayNameLabel.font = UIFontMetrics(forTextStyle: .title1).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold)) displayNameLabel.adjustsFontForContentSizeCategory = true - noteTextView.defaultFont = .preferredFont(forTextStyle: .body) - noteTextView.monospaceFont = UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular)) noteTextView.adjustsFontForContentSizeCategory = true noteTextView.textContainer.lineBreakMode = .byTruncatingTail noteTextView.textContainerInset = UIEdgeInsets(top: 16, left: 4, bottom: 16, right: 4) @@ -60,7 +58,7 @@ class FeaturedProfileCollectionViewCell: UICollectionViewCell { displayNameLabel.updateForAccountDisplayName(account: account) - noteTextView.setTextFromHtml(account.note) + noteTextView.setBodyTextFromHTML(account.note) noteTextView.setEmojis(account.emojis, identifier: account.id) avatarImageView.image = nil diff --git a/Tusker/Screens/Explore/SuggestedProfileCardCollectionViewCell.swift b/Tusker/Screens/Explore/SuggestedProfileCardCollectionViewCell.swift index e1032463..c5106ae0 100644 --- a/Tusker/Screens/Explore/SuggestedProfileCardCollectionViewCell.swift +++ b/Tusker/Screens/Explore/SuggestedProfileCardCollectionViewCell.swift @@ -54,8 +54,6 @@ class SuggestedProfileCardCollectionViewCell: UICollectionViewCell { usernameLabel.font = UIFontMetrics.default.scaledFont(for: .systemFont(ofSize: 15, weight: .light)) usernameLabel.adjustsFontForContentSizeCategory = true - noteTextView.defaultFont = .preferredFont(forTextStyle: .body) - noteTextView.monospaceFont = UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular)) noteTextView.adjustsFontForContentSizeCategory = true noteTextView.textContainer.lineBreakMode = .byTruncatingTail @@ -86,7 +84,7 @@ class SuggestedProfileCardCollectionViewCell: UICollectionViewCell { avatarImageView.update(for: account.avatar) headerImageView.update(for: account.header) usernameLabel.text = "@\(account.acct)" - noteTextView.setTextFromHtml(account.note) + noteTextView.setBodyTextFromHTML(account.note) var config = UIButton.Configuration.plain() config.image = source.image diff --git a/Tusker/Screens/Explore/TrendingLinkCardCollectionViewCell.swift b/Tusker/Screens/Explore/TrendingLinkCardCollectionViewCell.swift index dffe8221..3aabe88e 100644 --- a/Tusker/Screens/Explore/TrendingLinkCardCollectionViewCell.swift +++ b/Tusker/Screens/Explore/TrendingLinkCardCollectionViewCell.swift @@ -9,7 +9,7 @@ import UIKit import Pachyderm import WebURLFoundationExtras -import SwiftSoup +import HTMLStreamer class TrendingLinkCardCollectionViewCell: UICollectionViewCell { @@ -78,8 +78,8 @@ class TrendingLinkCardCollectionViewCell: UICollectionViewCell { let provider = card.providerName!.trimmingCharacters(in: .whitespacesAndNewlines) providerLabel.text = provider - let description = try! SwiftSoup.parseBodyFragment(card.description).text() - descriptionLabel.text = description + var converter = TextConverter(callbacks: HTMLConverter.Callbacks.self) + descriptionLabel.text = converter.convert(html: card.description) descriptionLabel.isHidden = description.isEmpty let sorted = card.history!.sorted(by: { $0.day < $1.day }) diff --git a/Tusker/Screens/Explore/TrendingStatusesViewController.swift b/Tusker/Screens/Explore/TrendingStatusesViewController.swift index 7a391927..f8bc4e73 100644 --- a/Tusker/Screens/Explore/TrendingStatusesViewController.swift +++ b/Tusker/Screens/Explore/TrendingStatusesViewController.swift @@ -23,10 +23,7 @@ class TrendingStatusesViewController: UIViewController, CollectionViewController init(mastodonController: MastodonController) { self.mastodonController = mastodonController - self.filterer = Filterer(mastodonController: mastodonController, context: .public) - self.filterer.htmlConverter.font = TimelineStatusCollectionViewCell.contentFont - self.filterer.htmlConverter.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont - self.filterer.htmlConverter.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle + self.filterer = Filterer(mastodonController: mastodonController, context: .public, htmlConverter: TimelineStatusCollectionViewCell.htmlConverter) super.init(nibName: nil, bundle: nil) diff --git a/Tusker/Screens/Notifications/ActionNotificationGroupCollectionViewCell.swift b/Tusker/Screens/Notifications/ActionNotificationGroupCollectionViewCell.swift index 447cca45..cd80dbe0 100644 --- a/Tusker/Screens/Notifications/ActionNotificationGroupCollectionViewCell.swift +++ b/Tusker/Screens/Notifications/ActionNotificationGroupCollectionViewCell.swift @@ -8,7 +8,7 @@ import UIKit import Pachyderm -import SwiftSoup +import HTMLStreamer class ActionNotificationGroupCollectionViewCell: UICollectionViewListCell { @@ -161,9 +161,8 @@ class ActionNotificationGroupCollectionViewCell: UICollectionViewListCell { actionLabel.setEmojis(pairs: people.map { ($0.displayOrUserName, $0.emojis) }, identifier: group.id) - // todo: use htmlconverter - let doc = try! SwiftSoup.parseBodyFragment(status.content) - statusContentLabel.text = try! doc.text() + var converter = TextConverter(callbacks: HTMLConverter.Callbacks.self) + statusContentLabel.text = converter.convert(html: status.content) } @objc private func updateUIForPreferences() { diff --git a/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift b/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift index 69e3a9c2..fed6947d 100644 --- a/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift +++ b/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift @@ -34,10 +34,7 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle self.allowedTypes = allowedTypes self.mastodonController = mastodonController - self.filterer = Filterer(mastodonController: mastodonController, context: .notifications) - self.filterer.htmlConverter.font = TimelineStatusCollectionViewCell.contentFont - self.filterer.htmlConverter.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont - self.filterer.htmlConverter.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle + self.filterer = Filterer(mastodonController: mastodonController, context: .notifications, htmlConverter: TimelineStatusCollectionViewCell.htmlConverter) super.init(nibName: nil, bundle: nil) diff --git a/Tusker/Screens/Notifications/PollFinishedNotificationCollectionViewCell.swift b/Tusker/Screens/Notifications/PollFinishedNotificationCollectionViewCell.swift index 2c9758d1..756b38b0 100644 --- a/Tusker/Screens/Notifications/PollFinishedNotificationCollectionViewCell.swift +++ b/Tusker/Screens/Notifications/PollFinishedNotificationCollectionViewCell.swift @@ -8,7 +8,7 @@ import UIKit import Pachyderm -import SwiftSoup +import HTMLStreamer class PollFinishedNotificationCollectionViewCell: UICollectionViewListCell { @@ -124,9 +124,8 @@ class PollFinishedNotificationCollectionViewCell: UICollectionViewListCell { updateTimestamp() updateDisplayName(account: account) - // todo: use htmlconverter - let doc = try! SwiftSoup.parseBodyFragment(status.content) - contentLabel.text = try! doc.text() + var converter = TextConverter(callbacks: HTMLConverter.Callbacks.self) + contentLabel.text = converter.convert(html: status.content) pollView.mastodonController = mastodonController pollView.delegate = delegate diff --git a/Tusker/Screens/Notifications/StatusUpdatedNotificationCollectionViewCell.swift b/Tusker/Screens/Notifications/StatusUpdatedNotificationCollectionViewCell.swift index 42d27395..9d7177c9 100644 --- a/Tusker/Screens/Notifications/StatusUpdatedNotificationCollectionViewCell.swift +++ b/Tusker/Screens/Notifications/StatusUpdatedNotificationCollectionViewCell.swift @@ -8,7 +8,7 @@ import UIKit import Pachyderm -import SwiftSoup +import HTMLStreamer class StatusUpdatedNotificationCollectionViewCell: UICollectionViewListCell { @@ -120,9 +120,8 @@ class StatusUpdatedNotificationCollectionViewCell: UICollectionViewListCell { updateTimestamp() updateDisplayName(account: account) - // todo: use htmlconverter - let doc = try! SwiftSoup.parseBodyFragment(status.content) - contentLabel.text = try! doc.text() + var converter = TextConverter(callbacks: HTMLConverter.Callbacks.self) + contentLabel.text = converter.convert(html: status.content) } @objc private func updateUIForPreferences() { diff --git a/Tusker/Screens/Preferences/AcknowledgementsView.swift b/Tusker/Screens/Preferences/AcknowledgementsView.swift index 239f004f..33c2db7b 100644 --- a/Tusker/Screens/Preferences/AcknowledgementsView.swift +++ b/Tusker/Screens/Preferences/AcknowledgementsView.swift @@ -60,31 +60,6 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -^[[SwiftSoup](https://github.com/scinfu/swiftsoup)](headingLevel: 2) -Copyright (c) 2016 Nabil Chatbi - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -Symbols -Symbol outline not available for this file -To inspect a symbol, try clicking on the symbol directly in the code view. -Code navigation supports a limited number of languages. See which languages are supported. - ^[[swift-url](https://github.com/karwa/swift-url)](headingLevel: 2) Apache License diff --git a/Tusker/Screens/Profile/ProfileStatusesViewController.swift b/Tusker/Screens/Profile/ProfileStatusesViewController.swift index 06054869..5f4fb972 100644 --- a/Tusker/Screens/Profile/ProfileStatusesViewController.swift +++ b/Tusker/Screens/Profile/ProfileStatusesViewController.swift @@ -41,10 +41,7 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie self.kind = kind self.owner = owner self.mastodonController = owner.mastodonController - self.filterer = Filterer(mastodonController: mastodonController, context: .account) - self.filterer.htmlConverter.font = TimelineStatusCollectionViewCell.contentFont - self.filterer.htmlConverter.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont - self.filterer.htmlConverter.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle + self.filterer = Filterer(mastodonController: mastodonController, context: .account, htmlConverter: TimelineStatusCollectionViewCell.htmlConverter) super.init(nibName: nil, bundle: nil) diff --git a/Tusker/Screens/Report/ReportStatusView.swift b/Tusker/Screens/Report/ReportStatusView.swift index c9410f9e..2cad3df8 100644 --- a/Tusker/Screens/Report/ReportStatusView.swift +++ b/Tusker/Screens/Report/ReportStatusView.swift @@ -7,9 +7,13 @@ // import SwiftUI -import SwiftSoup -private var converter = HTMLConverter() +private var converter = HTMLConverter( + font: .preferredFont(forTextStyle: .body), + monospaceFont: UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular)), + color: .label, + paragraphStyle: .default +) struct ReportStatusView: View { let status: StatusMO diff --git a/Tusker/Screens/Status Edit History/StatusEditCollectionViewCell.swift b/Tusker/Screens/Status Edit History/StatusEditCollectionViewCell.swift index 7f6cef79..bc40c964 100644 --- a/Tusker/Screens/Status Edit History/StatusEditCollectionViewCell.swift +++ b/Tusker/Screens/Status Edit History/StatusEditCollectionViewCell.swift @@ -70,15 +70,12 @@ class StatusEditCollectionViewCell: UICollectionViewListCell { $0.setContentHuggingPriority(.defaultLow, for: .vertical) } - private let contentTextView = StatusEditContentTextView().configure { + private let contentTextView = ContentTextView().configure { $0.adjustsFontForContentSizeCategory = true $0.isScrollEnabled = false $0.backgroundColor = nil $0.isEditable = false $0.isSelectable = false - $0.defaultFont = TimelineStatusCollectionViewCell.contentFont - $0.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont - $0.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle } private let cardView = StatusCardView().configure { @@ -191,7 +188,8 @@ class StatusEditCollectionViewCell: UICollectionViewListCell { timestampLabel.text = ConversationMainStatusCollectionViewCell.dateFormatter.string(from: edit.createdAt) - contentTextView.setTextFrom(edit: edit, index: index) + contentTextView.attributedText = TimelineStatusCollectionViewCell.htmlConverter.convert(edit.content) + contentTextView.setEmojis(edit.emojis, identifier: index) contentTextView.navigationDelegate = delegate attachmentsView.delegate = self attachmentsView.updateUI(attachments: edit.attachments) diff --git a/Tusker/Screens/Status Edit History/StatusEditContentTextView.swift b/Tusker/Screens/Status Edit History/StatusEditContentTextView.swift deleted file mode 100644 index b3c97552..00000000 --- a/Tusker/Screens/Status Edit History/StatusEditContentTextView.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// StatusEditContentTextView.swift -// Tusker -// -// Created by Shadowfacts on 5/11/23. -// Copyright © 2023 Shadowfacts. All rights reserved. -// - -import UIKit -import Pachyderm -import WebURL - -class StatusEditContentTextView: ContentTextView { - - func setTextFrom(edit: StatusEdit, index: Int) { - setTextFromHtml(edit.content) - setEmojis(edit.emojis, identifier: index) - } - - // mention links aren't included in the edit content, nothing else to do - -} diff --git a/Tusker/Screens/Timeline/TimelineViewController.swift b/Tusker/Screens/Timeline/TimelineViewController.swift index c773e226..a4742f27 100644 --- a/Tusker/Screens/Timeline/TimelineViewController.swift +++ b/Tusker/Screens/Timeline/TimelineViewController.swift @@ -30,7 +30,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro let timeline: Timeline weak var mastodonController: MastodonController! - let filterer: Filterer + private let filterer: Filterer var persistsState = false @@ -59,10 +59,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro default: filterContext = .public } - self.filterer = Filterer(mastodonController: mastodonController, context: filterContext) - self.filterer.htmlConverter.font = TimelineStatusCollectionViewCell.contentFont - self.filterer.htmlConverter.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont - self.filterer.htmlConverter.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle + self.filterer = Filterer(mastodonController: mastodonController, context: filterContext, htmlConverter: TimelineStatusCollectionViewCell.htmlConverter) super.init(nibName: nil, bundle: nil) diff --git a/Tusker/Views/Account Cell/AccountCollectionViewCell.swift b/Tusker/Views/Account Cell/AccountCollectionViewCell.swift index 18c989f1..4f1e13e9 100644 --- a/Tusker/Views/Account Cell/AccountCollectionViewCell.swift +++ b/Tusker/Views/Account Cell/AccountCollectionViewCell.swift @@ -7,7 +7,7 @@ // import UIKit -import SwiftSoup +import HTMLStreamer class AccountCollectionViewCell: UICollectionViewListCell { @@ -134,8 +134,8 @@ class AccountCollectionViewCell: UICollectionViewListCell { displayNameLabel.setEmojis(account.emojis, identifier: account.id) } - let doc = try! SwiftSoup.parseBodyFragment(account.note) - noteLabel.text = try! doc.text() + var converter = TextConverter(callbacks: HTMLConverter.Callbacks.self) + noteLabel.text = converter.convert(html: account.note) noteLabel.setEmojis(account.emojis, identifier: account.id) } diff --git a/Tusker/Views/ConfirmReblogStatusPreviewView.swift b/Tusker/Views/ConfirmReblogStatusPreviewView.swift index 8a7b77e4..2ac6a3c2 100644 --- a/Tusker/Views/ConfirmReblogStatusPreviewView.swift +++ b/Tusker/Views/ConfirmReblogStatusPreviewView.swift @@ -10,6 +10,13 @@ import UIKit class ConfirmReblogStatusPreviewView: UIView { + private static let htmlConverter = HTMLConverter( + font: .preferredFont(forTextStyle: .caption2), + monospaceFont: UIFontMetrics(forTextStyle: .caption2).scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular)), + color: .label, + paragraphStyle: .default + ) + private var avatarTask: Task? init(status: StatusMO) { @@ -60,17 +67,13 @@ class ConfirmReblogStatusPreviewView: UIView { vStack.addArrangedSubview(displayNameLabel) let contentView = StatusContentTextView() - contentView.defaultFont = .preferredFont(forTextStyle: .caption2) - contentView.monospaceFont = UIFontMetrics(forTextStyle: .caption2).scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular)) contentView.isUserInteractionEnabled = false contentView.isScrollEnabled = false contentView.backgroundColor = nil contentView.textContainerInset = .zero contentView.adjustsFontForContentSizeCategory = true - // remove the extra line spacing applied by StatusContentTextView because, since we're using a smaller font, the regular 2pt looks big - contentView.paragraphStyle = .default // TODO: line limit - contentView.setTextFrom(status: status) + contentView.setTextFrom(status: status, content: ConfirmReblogStatusPreviewView.htmlConverter.convert(status.content)) contentView.translatesAutoresizingMaskIntoConstraints = false vStack.addArrangedSubview(contentView) NSLayoutConstraint.activate([ diff --git a/Tusker/Views/ContentTextView.swift b/Tusker/Views/ContentTextView.swift index 75bcef68..d5e0e3ba 100644 --- a/Tusker/Views/ContentTextView.swift +++ b/Tusker/Views/ContentTextView.swift @@ -7,7 +7,6 @@ // import UIKit -import SwiftSoup import Pachyderm import SafariServices import WebURL @@ -22,31 +21,20 @@ class ContentTextView: LinkTextView, BaseEmojiLabel { weak var navigationDelegate: TuskerNavigationDelegate? weak var overrideMastodonController: MastodonController? var mastodonController: MastodonController? { overrideMastodonController ?? navigationDelegate?.apiController } - - private(set) var htmlConverter = HTMLConverter() - var defaultFont: UIFont { - _read { yield htmlConverter.font } - _modify { yield &htmlConverter.font } - } - var monospaceFont: UIFont { - _read { yield htmlConverter.monospaceFont } - _modify { yield &htmlConverter.monospaceFont } - } - var defaultColor: UIColor { - _read { yield htmlConverter.color } - _modify { yield &htmlConverter.color } - } - var paragraphStyle: NSParagraphStyle { - _read { yield htmlConverter.paragraphStyle } - _modify { yield &htmlConverter.paragraphStyle } - } + private static let defaultBodyHTMLConverter = HTMLConverter( + font: .preferredFont(forTextStyle: .body), + monospaceFont: UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular)), + color: .label, + paragraphStyle: .default + ) + private(set) var hasEmojis = false var emojiIdentifier: AnyHashable? var emojiRequests: [ImageCache.Request] = [] - var emojiFont: UIFont { defaultFont } - var emojiTextColor: UIColor { defaultColor } + var emojiFont: UIFont = .preferredFont(forTextStyle: .body) + var emojiTextColor: UIColor = .label // The link range currently being previewed private var currentPreviewedLinkRange: NSRange? @@ -120,8 +108,8 @@ class ContentTextView: LinkTextView, BaseEmojiLabel { } // MARK: - HTML Parsing - func setTextFromHtml(_ html: String) { - self.attributedText = htmlConverter.convert(html) + func setBodyTextFromHTML(_ html: String) { + self.attributedText = ContentTextView.defaultBodyHTMLConverter.convert(html) } // MARK: - Interaction diff --git a/Tusker/Views/Instance Cell/InstanceTableViewCell.swift b/Tusker/Views/Instance Cell/InstanceTableViewCell.swift index 37afd99a..d2108147 100644 --- a/Tusker/Views/Instance Cell/InstanceTableViewCell.swift +++ b/Tusker/Views/Instance Cell/InstanceTableViewCell.swift @@ -34,8 +34,6 @@ class InstanceTableViewCell: UITableViewCell { adultLabel.layer.masksToBounds = true adultLabel.layer.cornerRadius = 0.5 * adultLabel.bounds.height - descriptionTextView.defaultFont = .preferredFont(forTextStyle: .body) - descriptionTextView.monospaceFont = UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular)) descriptionTextView.adjustsFontForContentSizeCategory = true } @@ -49,7 +47,7 @@ class InstanceTableViewCell: UITableViewCell { domainLabel.text = instance.domain adultLabel.isHidden = instance.category != "adult" - descriptionTextView.setTextFromHtml(instance.description) + descriptionTextView.setBodyTextFromHTML(instance.description) updateThumbnail(url: instance.proxiedThumbnailURL) } @@ -59,7 +57,7 @@ class InstanceTableViewCell: UITableViewCell { domainLabel.text = URLComponents(string: instance.uri)?.host ?? instance.uri adultLabel.isHidden = true - descriptionTextView.setTextFromHtml(instance.shortDescription ?? instance.description) + descriptionTextView.setBodyTextFromHTML(instance.shortDescription ?? instance.description) if let thumbnail = instance.thumbnail { updateThumbnail(url: thumbnail) diff --git a/Tusker/Views/Profile Header/ProfileFieldValueView.swift b/Tusker/Views/Profile Header/ProfileFieldValueView.swift index 429eeade..1a6d8740 100644 --- a/Tusker/Views/Profile Header/ProfileFieldValueView.swift +++ b/Tusker/Views/Profile Header/ProfileFieldValueView.swift @@ -14,12 +14,12 @@ import SafariServices class ProfileFieldValueView: UIView { weak var navigationDelegate: TuskerNavigationDelegate? - private static let converter: HTMLConverter = { - var converter = HTMLConverter() - converter.font = .preferredFont(forTextStyle: .body) - converter.monospaceFont = UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular)) - return converter - }() + private static let converter = HTMLConverter( + font: .preferredFont(forTextStyle: .body), + monospaceFont: UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular)), + color: .label, + paragraphStyle: .default + ) private let account: AccountMO private let field: Account.Field diff --git a/Tusker/Views/Profile Header/ProfileHeaderView.swift b/Tusker/Views/Profile Header/ProfileHeaderView.swift index f6dc76d8..5c49eb70 100644 --- a/Tusker/Views/Profile Header/ProfileHeaderView.swift +++ b/Tusker/Views/Profile Header/ProfileHeaderView.swift @@ -95,8 +95,6 @@ class ProfileHeaderView: UIView { relationshipLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 14)) relationshipLabel.adjustsFontForContentSizeCategory = true - noteTextView.defaultFont = .preferredFont(forTextStyle: .body) - noteTextView.monospaceFont = UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular)) noteTextView.adjustsFontForContentSizeCategory = true pagesSegmentedControl = ScrollingSegmentedControl(frame: .zero) @@ -140,7 +138,7 @@ class ProfileHeaderView: UIView { moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: delegate?.actionsForProfile(accountID: accountID, source: .view(moreButton), fetchRelationship: false) ?? []) noteTextView.navigationDelegate = delegate - noteTextView.setTextFromHtml(account.note) + noteTextView.setBodyTextFromHTML(account.note) noteTextView.setEmojis(account.emojis, identifier: account.id) if accountID == mastodonController.account?.id { diff --git a/Tusker/Views/Status/ConversationMainStatusCollectionViewCell.swift b/Tusker/Views/Status/ConversationMainStatusCollectionViewCell.swift index 8f629e3d..0ad6e702 100644 --- a/Tusker/Views/Status/ConversationMainStatusCollectionViewCell.swift +++ b/Tusker/Views/Status/ConversationMainStatusCollectionViewCell.swift @@ -17,6 +17,13 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status static let contentParagraphStyle = HTMLConverter.defaultParagraphStyle static let metaFont = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .systemFont(ofSize: 15)) + private static let htmlConverter = HTMLConverter( + font: ConversationMainStatusCollectionViewCell.contentFont, + monospaceFont: ConversationMainStatusCollectionViewCell.monospaceFont, + color: .label, + paragraphStyle: ConversationMainStatusCollectionViewCell.contentParagraphStyle + ) + static let dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateStyle = .medium @@ -132,9 +139,7 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status $0.backgroundColor = nil $0.isEditable = false $0.isSelectable = true - $0.defaultFont = ConversationMainStatusCollectionViewCell.contentFont - $0.monospaceFont = ConversationMainStatusCollectionViewCell.monospaceFont - $0.paragraphStyle = ConversationMainStatusCollectionViewCell.contentParagraphStyle + $0.emojiFont = ConversationMainStatusCollectionViewCell.contentFont $0.dataDetectorTypes = [.flightNumber, .address, .shipmentTrackingNumber, .phoneNumber] if #available(iOS 16.0, *) { $0.dataDetectorTypes.formUnion([.money, .physicalValue]) @@ -381,10 +386,9 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status self.statusID = statusID self.statusState = state - let attributedTranslatedContent: NSAttributedString? = translation.map { - contentTextView.htmlConverter.convert($0.content) - } - doUpdateUI(status: status, precomputedContent: attributedTranslatedContent) + let html = translation?.content ?? status.content + let attributedContent = ConversationMainStatusCollectionViewCell.htmlConverter.convert(html) + doUpdateUI(status: status, content: attributedContent) if !status.spoilerText.isEmpty, let translated = translation?.spoilerText { diff --git a/Tusker/Views/Status/StatusCardView.swift b/Tusker/Views/Status/StatusCardView.swift index 8dce0a7d..389f78f1 100644 --- a/Tusker/Views/Status/StatusCardView.swift +++ b/Tusker/Views/Status/StatusCardView.swift @@ -10,7 +10,7 @@ import UIKit import Pachyderm import SafariServices import WebURLFoundationExtras -import SwiftSoup +import HTMLStreamer class StatusCardView: UIView { @@ -191,7 +191,8 @@ class StatusCardView: UIView { titleLabel.text = title titleLabel.isHidden = title.isEmpty - let description = try! SwiftSoup.parseBodyFragment(card.description).text() + var converter = TextConverter(callbacks: HTMLConverter.Callbacks.self) + let description = converter.convert(html: card.description) descriptionLabel.text = description descriptionLabel.isHidden = description.isEmpty diff --git a/Tusker/Views/Status/StatusCollectionViewCell.swift b/Tusker/Views/Status/StatusCollectionViewCell.swift index b82ad832..4f52918e 100644 --- a/Tusker/Views/Status/StatusCollectionViewCell.swift +++ b/Tusker/Views/Status/StatusCollectionViewCell.swift @@ -88,13 +88,13 @@ extension StatusCollectionViewCell { .store(in: &cancellables) } - func doUpdateUI(status: StatusMO, precomputedContent: NSAttributedString? = nil) { + func doUpdateUI(status: StatusMO, content: NSAttributedString) { statusID = status.id accountID = status.account.id updateAccountUI(account: status.account) - contentTextView.setTextFrom(status: status, precomputed: precomputedContent) + contentTextView.setTextFrom(status: status, content: content) contentTextView.navigationDelegate = delegate self.updateAttachmentsUI(status: status) pollView.isHidden = status.poll == nil diff --git a/Tusker/Views/Status/TimelineStatusCollectionViewCell.swift b/Tusker/Views/Status/TimelineStatusCollectionViewCell.swift index 99732dd5..a84dd447 100644 --- a/Tusker/Views/Status/TimelineStatusCollectionViewCell.swift +++ b/Tusker/Views/Status/TimelineStatusCollectionViewCell.swift @@ -20,6 +20,13 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti static let monospaceFont = UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 16, weight: .regular)) static let contentParagraphStyle = HTMLConverter.defaultParagraphStyle + static let htmlConverter = HTMLConverter( + font: TimelineStatusCollectionViewCell.contentFont, + monospaceFont: TimelineStatusCollectionViewCell.monospaceFont, + color: .label, + paragraphStyle: TimelineStatusCollectionViewCell.contentParagraphStyle + ) + private static let timelineReasonIconSize: CGFloat = 25 // MARK: Subviews @@ -201,9 +208,7 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti $0.backgroundColor = nil $0.isEditable = false $0.isSelectable = false - $0.defaultFont = TimelineStatusCollectionViewCell.contentFont - $0.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont - $0.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle + $0.emojiFont = TimelineStatusCollectionViewCell.contentFont } let cardView = StatusCardView().configure { @@ -610,7 +615,8 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti mainContainerTopToReblogLabelConstraint.isActive = true } - doUpdateUI(status: status, precomputedContent: precomputedContent) + let content = precomputedContent ?? TimelineStatusCollectionViewCell.htmlConverter.convert(status.content) + doUpdateUI(status: status, content: content) doUpdateTimestamp(status: status) timestampLabel.isHidden = showPinned diff --git a/Tusker/Views/StatusContentTextView.swift b/Tusker/Views/StatusContentTextView.swift index 05f3677b..1fdaee45 100644 --- a/Tusker/Views/StatusContentTextView.swift +++ b/Tusker/Views/StatusContentTextView.swift @@ -14,13 +14,9 @@ class StatusContentTextView: ContentTextView { private var statusID: String? - func setTextFrom(status: some StatusProtocol, precomputed attributedText: NSAttributedString? = nil) { + func setTextFrom(status: some StatusProtocol, content attributedText: NSAttributedString) { statusID = status.id - if let attributedText { - self.attributedText = attributedText - } else { - setTextFromHtml(status.content) - } + self.attributedText = attributedText setEmojis(status.emojis, identifier: status.id) }