Compare commits

..

No commits in common. "4d183fe0b230369a481d6ed6e5266d9efde3da66" and "5a4323067ad63772fb9052a4e3aebe69d7305be3" have entirely different histories.

30 changed files with 322 additions and 144 deletions

View File

@ -32,7 +32,7 @@
D608470F2A245D1F00C17380 /* ActiveInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = D608470E2A245D1F00C17380 /* ActiveInstance.swift */; }; D608470F2A245D1F00C17380 /* ActiveInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = D608470E2A245D1F00C17380 /* ActiveInstance.swift */; };
D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */; }; D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */; };
D6093FB725BE0CF3004811E6 /* TrendHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */; }; D6093FB725BE0CF3004811E6 /* TrendHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6093FB625BE0CF3004811E6 /* TrendHistoryView.swift */; };
D60BB3942B30076F00DAEA65 /* HTMLStreamer in Frameworks */ = {isa = PBXBuildFile; productRef = D60BB3932B30076F00DAEA65 /* HTMLStreamer */; }; D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = D60CFFDA24A290BA00D00083 /* SwiftSoup */; };
D60E2F272442372B005F8713 /* StatusMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F232442372B005F8713 /* StatusMO.swift */; }; D60E2F272442372B005F8713 /* StatusMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F232442372B005F8713 /* StatusMO.swift */; };
D60E2F292442372B005F8713 /* AccountMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F252442372B005F8713 /* AccountMO.swift */; }; D60E2F292442372B005F8713 /* AccountMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F252442372B005F8713 /* AccountMO.swift */; };
D60E2F2C24423EAD005F8713 /* LazilyDecoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */; }; D60E2F2C24423EAD005F8713 /* LazilyDecoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */; };
@ -303,6 +303,7 @@
D6D79F262A0C8D2700AB2315 /* FetchStatusSourceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F252A0C8D2700AB2315 /* FetchStatusSourceService.swift */; }; D6D79F262A0C8D2700AB2315 /* FetchStatusSourceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F252A0C8D2700AB2315 /* FetchStatusSourceService.swift */; };
D6D79F292A0D596B00AB2315 /* StatusEditHistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F282A0D596B00AB2315 /* StatusEditHistoryViewController.swift */; }; D6D79F292A0D596B00AB2315 /* StatusEditHistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F282A0D596B00AB2315 /* StatusEditHistoryViewController.swift */; };
D6D79F2B2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F2A2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.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 */; }; D6D79F2F2A0D6A7F00AB2315 /* StatusEditPollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F2E2A0D6A7F00AB2315 /* StatusEditPollView.swift */; };
D6D79F532A0FFE3200AB2315 /* ToggleableButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F522A0FFE3200AB2315 /* ToggleableButton.swift */; }; D6D79F532A0FFE3200AB2315 /* ToggleableButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F522A0FFE3200AB2315 /* ToggleableButton.swift */; };
D6D79F572A1160B800AB2315 /* AccountDisplayNameLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F562A1160B800AB2315 /* AccountDisplayNameLabel.swift */; }; D6D79F572A1160B800AB2315 /* AccountDisplayNameLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F562A1160B800AB2315 /* AccountDisplayNameLabel.swift */; };
@ -711,6 +712,7 @@
D6D79F252A0C8D2700AB2315 /* FetchStatusSourceService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchStatusSourceService.swift; sourceTree = "<group>"; }; D6D79F252A0C8D2700AB2315 /* FetchStatusSourceService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchStatusSourceService.swift; sourceTree = "<group>"; };
D6D79F282A0D596B00AB2315 /* StatusEditHistoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditHistoryViewController.swift; sourceTree = "<group>"; }; D6D79F282A0D596B00AB2315 /* StatusEditHistoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditHistoryViewController.swift; sourceTree = "<group>"; };
D6D79F2A2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditCollectionViewCell.swift; sourceTree = "<group>"; }; D6D79F2A2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditCollectionViewCell.swift; sourceTree = "<group>"; };
D6D79F2C2A0D61B400AB2315 /* StatusEditContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditContentTextView.swift; sourceTree = "<group>"; };
D6D79F2E2A0D6A7F00AB2315 /* StatusEditPollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditPollView.swift; sourceTree = "<group>"; }; D6D79F2E2A0D6A7F00AB2315 /* StatusEditPollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusEditPollView.swift; sourceTree = "<group>"; };
D6D79F522A0FFE3200AB2315 /* ToggleableButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleableButton.swift; sourceTree = "<group>"; }; D6D79F522A0FFE3200AB2315 /* ToggleableButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleableButton.swift; sourceTree = "<group>"; };
D6D79F562A1160B800AB2315 /* AccountDisplayNameLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDisplayNameLabel.swift; sourceTree = "<group>"; }; D6D79F562A1160B800AB2315 /* AccountDisplayNameLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDisplayNameLabel.swift; sourceTree = "<group>"; };
@ -784,10 +786,10 @@
D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */, D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */,
D659F35E2953A212002D944A /* TTTKit in Frameworks */, D659F35E2953A212002D944A /* TTTKit in Frameworks */,
D6B0026E29B5248800C70BE2 /* UserAccounts in Frameworks */, D6B0026E29B5248800C70BE2 /* UserAccounts in Frameworks */,
D60CFFDB24A290BA00D00083 /* SwiftSoup in Frameworks */,
D60088EF2980D8B5005B4D00 /* StoreKit.framework in Frameworks */, D60088EF2980D8B5005B4D00 /* StoreKit.framework in Frameworks */,
D6CA6ED229EF6091003EC5DF /* TuskerPreferences in Frameworks */, D6CA6ED229EF6091003EC5DF /* TuskerPreferences in Frameworks */,
D6552367289870790048A653 /* ScreenCorners in Frameworks */, D6552367289870790048A653 /* ScreenCorners in Frameworks */,
D60BB3942B30076F00DAEA65 /* HTMLStreamer in Frameworks */,
D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */, D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */,
D63CC702290EC0B8000E19DE /* Sentry in Frameworks */, D63CC702290EC0B8000E19DE /* Sentry in Frameworks */,
D6BEA245291A0EDE002F4D01 /* Duckable in Frameworks */, D6BEA245291A0EDE002F4D01 /* Duckable in Frameworks */,
@ -1572,6 +1574,7 @@
children = ( children = (
D6D79F282A0D596B00AB2315 /* StatusEditHistoryViewController.swift */, D6D79F282A0D596B00AB2315 /* StatusEditHistoryViewController.swift */,
D6D79F2A2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift */, D6D79F2A2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift */,
D6D79F2C2A0D61B400AB2315 /* StatusEditContentTextView.swift */,
D6D79F2E2A0D6A7F00AB2315 /* StatusEditPollView.swift */, D6D79F2E2A0D6A7F00AB2315 /* StatusEditPollView.swift */,
); );
path = "Status Edit History"; path = "Status Edit History";
@ -1696,6 +1699,7 @@
); );
name = Tusker; name = Tusker;
packageProductDependencies = ( packageProductDependencies = (
D60CFFDA24A290BA00D00083 /* SwiftSoup */,
D6676CA427A8D0020052936B /* WebURLFoundationExtras */, D6676CA427A8D0020052936B /* WebURLFoundationExtras */,
D674A50827F9128D00BA03AC /* Pachyderm */, D674A50827F9128D00BA03AC /* Pachyderm */,
D6552366289870790048A653 /* ScreenCorners */, D6552366289870790048A653 /* ScreenCorners */,
@ -1707,7 +1711,6 @@
D635237029B78A7D009ED5E7 /* TuskerComponents */, D635237029B78A7D009ED5E7 /* TuskerComponents */,
D6BD395829B64426005FFD2B /* ComposeUI */, D6BD395829B64426005FFD2B /* ComposeUI */,
D6CA6ED129EF6091003EC5DF /* TuskerPreferences */, D6CA6ED129EF6091003EC5DF /* TuskerPreferences */,
D60BB3932B30076F00DAEA65 /* HTMLStreamer */,
); );
productName = Tusker; productName = Tusker;
productReference = D6D4DDCC212518A000E1C4BB /* Tusker.app */; productReference = D6D4DDCC212518A000E1C4BB /* Tusker.app */;
@ -1818,10 +1821,10 @@
); );
mainGroup = D6D4DDC3212518A000E1C4BB; mainGroup = D6D4DDC3212518A000E1C4BB;
packageReferences = ( packageReferences = (
D60CFFD924A290BA00D00083 /* XCRemoteSwiftPackageReference "SwiftSoup" */,
D6676CA127A8D0020052936B /* XCRemoteSwiftPackageReference "swift-url" */, D6676CA127A8D0020052936B /* XCRemoteSwiftPackageReference "swift-url" */,
D6552365289870790048A653 /* XCRemoteSwiftPackageReference "ScreenCorners" */, D6552365289870790048A653 /* XCRemoteSwiftPackageReference "ScreenCorners" */,
D63CC700290EC0B8000E19DE /* XCRemoteSwiftPackageReference "sentry-cocoa" */, D63CC700290EC0B8000E19DE /* XCRemoteSwiftPackageReference "sentry-cocoa" */,
D60BB3922B30076F00DAEA65 /* XCRemoteSwiftPackageReference "HTMLStreamer" */,
); );
productRefGroup = D6D4DDCD212518A000E1C4BB /* Products */; productRefGroup = D6D4DDCD212518A000E1C4BB /* Products */;
projectDirPath = ""; projectDirPath = "";
@ -2178,6 +2181,7 @@
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */, D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */,
D68A76F129539116001DA1B3 /* FlipView.swift in Sources */, D68A76F129539116001DA1B3 /* FlipView.swift in Sources */,
D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */, D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */,
D6D79F2D2A0D61B400AB2315 /* StatusEditContentTextView.swift in Sources */,
D647D92824257BEB0005044F /* AttachmentPreviewViewController.swift in Sources */, D647D92824257BEB0005044F /* AttachmentPreviewViewController.swift in Sources */,
D61F75B5293BD97400C0B37F /* DeleteFilterService.swift in Sources */, D61F75B5293BD97400C0B37F /* DeleteFilterService.swift in Sources */,
D6DFC6A0242C4CCC00ACC392 /* Weak.swift in Sources */, D6DFC6A0242C4CCC00ACC392 /* Weak.swift in Sources */,
@ -2952,12 +2956,12 @@
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */ /* Begin XCRemoteSwiftPackageReference section */
D60BB3922B30076F00DAEA65 /* XCRemoteSwiftPackageReference "HTMLStreamer" */ = { D60CFFD924A290BA00D00083 /* XCRemoteSwiftPackageReference "SwiftSoup" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://git.shadowfacts.net/shadowfacts/HTMLStreamer.git"; repositoryURL = "https://github.com/scinfu/SwiftSoup.git";
requirement = { requirement = {
branch = main; kind = upToNextMinorVersion;
kind = branch; minimumVersion = 2.3.2;
}; };
}; };
D63CC700290EC0B8000E19DE /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = { D63CC700290EC0B8000E19DE /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = {
@ -2987,10 +2991,10 @@
/* End XCRemoteSwiftPackageReference section */ /* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
D60BB3932B30076F00DAEA65 /* HTMLStreamer */ = { D60CFFDA24A290BA00D00083 /* SwiftSoup */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = D60BB3922B30076F00DAEA65 /* XCRemoteSwiftPackageReference "HTMLStreamer" */; package = D60CFFD924A290BA00D00083 /* XCRemoteSwiftPackageReference "SwiftSoup" */;
productName = HTMLStreamer; productName = SwiftSoup;
}; };
D61ABEFB28F105DE00B29151 /* Pachyderm */ = { D61ABEFB28F105DE00B29151 /* Pachyderm */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;

View File

@ -8,7 +8,7 @@
import UIKit import UIKit
import LinkPresentation import LinkPresentation
import HTMLStreamer import SwiftSoup
class StatusActivityItemSource: NSObject, UIActivityItemSource { class StatusActivityItemSource: NSObject, UIActivityItemSource {
let status: StatusMO let status: StatusMO
@ -33,8 +33,8 @@ class StatusActivityItemSource: NSObject, UIActivityItemSource {
let metadata = LPLinkMetadata() let metadata = LPLinkMetadata()
metadata.originalURL = status.url! metadata.originalURL = status.url!
metadata.url = status.url! metadata.url = status.url!
var converter = TextConverter(callbacks: HTMLConverter.Callbacks.self) let doc = try! SwiftSoup.parse(status.content)
let content = converter.convert(html: status.content) let content = try! doc.text()
metadata.title = "\(status.account.displayName): \"\(content)\"" metadata.title = "\(status.account.displayName): \"\(content)\""
if let avatar = status.account.avatar, if let avatar = status.account.avatar,
let data = ImageCache.avatars.getData(avatar), let data = ImageCache.avatars.getData(avatar),

View File

@ -42,7 +42,7 @@ class Filterer {
var filtersChanged: ((Bool) -> Void)? var filtersChanged: ((Bool) -> Void)?
private var htmlConverter: HTMLConverter var htmlConverter = HTMLConverter()
private var hasSetup = false private var hasSetup = false
private var matchers = [(NSRegularExpression, Result)]() private var matchers = [(NSRegularExpression, Result)]()
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
@ -55,10 +55,9 @@ class Filterer {
// are no longer valid, without needing to go through and update each of them // are no longer valid, without needing to go through and update each of them
private var generation = 0 private var generation = 0
init(mastodonController: MastodonController, context: FilterV1.Context, htmlConverter: HTMLConverter) { init(mastodonController: MastodonController, context: FilterV1.Context) {
self.mastodonController = mastodonController self.mastodonController = mastodonController
self.context = context self.context = context
self.htmlConverter = htmlConverter
self.hideReblogsInTimelines = Preferences.shared.hideReblogsInTimelines self.hideReblogsInTimelines = Preferences.shared.hideReblogsInTimelines
self.hideRepliesInTimelines = Preferences.shared.hideRepliesInTimelines self.hideRepliesInTimelines = Preferences.shared.hideRepliesInTimelines

View File

@ -7,11 +7,11 @@
// //
import UIKit import UIKit
import HTMLStreamer import SwiftSoup
import WebURL import WebURL
import WebURLFoundationExtras import WebURLFoundationExtras
class HTMLConverter { struct HTMLConverter {
static let defaultFont = UIFont.systemFont(ofSize: 17) static let defaultFont = UIFont.systemFont(ofSize: 17)
static let defaultMonospaceFont = UIFont.monospacedSystemFont(ofSize: 17, weight: .regular) static let defaultMonospaceFont = UIFont.monospacedSystemFont(ofSize: 17, weight: .regular)
@ -23,47 +23,150 @@ class HTMLConverter {
return style return style
}() }()
private var converter: AttributedStringConverter<Callbacks> var font: UIFont = defaultFont
var monospaceFont: UIFont = defaultMonospaceFont
var color: UIColor = defaultColor
var paragraphStyle: NSParagraphStyle = defaultParagraphStyle
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 { func convert(_ html: String) -> NSAttributedString {
converter.convert(html: html) 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()
}
} }
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
}
} }
extension HTMLConverter { private class OrderedNumberTextList: NSTextList {
struct Callbacks: HTMLConversionCallbacks { override func marker(forItemNumber itemNumber: Int) -> String {
static func makeURL(string: String) -> URL? { "\(super.marker(forItemNumber: itemNumber))."
// 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
}
}
} }
} }

View File

@ -26,9 +26,11 @@ struct ComposeReplyContentView: UIViewRepresentable {
view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
view.adjustsFontForContentSizeCategory = true view.adjustsFontForContentSizeCategory = true
view.defaultFont = TimelineStatusCollectionViewCell.contentFont
view.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont
view.overrideMastodonController = mastodonController view.overrideMastodonController = mastodonController
view.attributedText = TimelineStatusCollectionViewCell.htmlConverter.convert(status.content) view.setTextFrom(status: status)
return view return view
} }

View File

@ -34,6 +34,8 @@ class FeaturedProfileCollectionViewCell: UICollectionViewCell {
displayNameLabel.font = UIFontMetrics(forTextStyle: .title1).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold)) displayNameLabel.font = UIFontMetrics(forTextStyle: .title1).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold))
displayNameLabel.adjustsFontForContentSizeCategory = true displayNameLabel.adjustsFontForContentSizeCategory = true
noteTextView.defaultFont = .preferredFont(forTextStyle: .body)
noteTextView.monospaceFont = UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular))
noteTextView.adjustsFontForContentSizeCategory = true noteTextView.adjustsFontForContentSizeCategory = true
noteTextView.textContainer.lineBreakMode = .byTruncatingTail noteTextView.textContainer.lineBreakMode = .byTruncatingTail
noteTextView.textContainerInset = UIEdgeInsets(top: 16, left: 4, bottom: 16, right: 4) noteTextView.textContainerInset = UIEdgeInsets(top: 16, left: 4, bottom: 16, right: 4)
@ -58,7 +60,7 @@ class FeaturedProfileCollectionViewCell: UICollectionViewCell {
displayNameLabel.updateForAccountDisplayName(account: account) displayNameLabel.updateForAccountDisplayName(account: account)
noteTextView.setBodyTextFromHTML(account.note) noteTextView.setTextFromHtml(account.note)
noteTextView.setEmojis(account.emojis, identifier: account.id) noteTextView.setEmojis(account.emojis, identifier: account.id)
avatarImageView.image = nil avatarImageView.image = nil

View File

@ -54,6 +54,8 @@ class SuggestedProfileCardCollectionViewCell: UICollectionViewCell {
usernameLabel.font = UIFontMetrics.default.scaledFont(for: .systemFont(ofSize: 15, weight: .light)) usernameLabel.font = UIFontMetrics.default.scaledFont(for: .systemFont(ofSize: 15, weight: .light))
usernameLabel.adjustsFontForContentSizeCategory = true usernameLabel.adjustsFontForContentSizeCategory = true
noteTextView.defaultFont = .preferredFont(forTextStyle: .body)
noteTextView.monospaceFont = UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular))
noteTextView.adjustsFontForContentSizeCategory = true noteTextView.adjustsFontForContentSizeCategory = true
noteTextView.textContainer.lineBreakMode = .byTruncatingTail noteTextView.textContainer.lineBreakMode = .byTruncatingTail
@ -84,7 +86,7 @@ class SuggestedProfileCardCollectionViewCell: UICollectionViewCell {
avatarImageView.update(for: account.avatar) avatarImageView.update(for: account.avatar)
headerImageView.update(for: account.header) headerImageView.update(for: account.header)
usernameLabel.text = "@\(account.acct)" usernameLabel.text = "@\(account.acct)"
noteTextView.setBodyTextFromHTML(account.note) noteTextView.setTextFromHtml(account.note)
var config = UIButton.Configuration.plain() var config = UIButton.Configuration.plain()
config.image = source.image config.image = source.image

View File

@ -9,7 +9,7 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
import WebURLFoundationExtras import WebURLFoundationExtras
import HTMLStreamer import SwiftSoup
class TrendingLinkCardCollectionViewCell: UICollectionViewCell { class TrendingLinkCardCollectionViewCell: UICollectionViewCell {
@ -78,8 +78,8 @@ class TrendingLinkCardCollectionViewCell: UICollectionViewCell {
let provider = card.providerName!.trimmingCharacters(in: .whitespacesAndNewlines) let provider = card.providerName!.trimmingCharacters(in: .whitespacesAndNewlines)
providerLabel.text = provider providerLabel.text = provider
var converter = TextConverter(callbacks: HTMLConverter.Callbacks.self) let description = try! SwiftSoup.parseBodyFragment(card.description).text()
descriptionLabel.text = converter.convert(html: card.description) descriptionLabel.text = description
descriptionLabel.isHidden = description.isEmpty descriptionLabel.isHidden = description.isEmpty
let sorted = card.history!.sorted(by: { $0.day < $1.day }) let sorted = card.history!.sorted(by: { $0.day < $1.day })

View File

@ -23,7 +23,10 @@ class TrendingStatusesViewController: UIViewController, CollectionViewController
init(mastodonController: MastodonController) { init(mastodonController: MastodonController) {
self.mastodonController = mastodonController self.mastodonController = mastodonController
self.filterer = Filterer(mastodonController: mastodonController, context: .public, htmlConverter: TimelineStatusCollectionViewCell.htmlConverter) 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
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)

View File

@ -8,7 +8,7 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
import HTMLStreamer import SwiftSoup
class ActionNotificationGroupCollectionViewCell: UICollectionViewListCell { class ActionNotificationGroupCollectionViewCell: UICollectionViewListCell {
@ -161,8 +161,9 @@ class ActionNotificationGroupCollectionViewCell: UICollectionViewListCell {
actionLabel.setEmojis(pairs: people.map { ($0.displayOrUserName, $0.emojis) }, identifier: group.id) actionLabel.setEmojis(pairs: people.map { ($0.displayOrUserName, $0.emojis) }, identifier: group.id)
var converter = TextConverter(callbacks: HTMLConverter.Callbacks.self) // todo: use htmlconverter
statusContentLabel.text = converter.convert(html: status.content) let doc = try! SwiftSoup.parseBodyFragment(status.content)
statusContentLabel.text = try! doc.text()
} }
@objc private func updateUIForPreferences() { @objc private func updateUIForPreferences() {

View File

@ -34,7 +34,10 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle
self.allowedTypes = allowedTypes self.allowedTypes = allowedTypes
self.mastodonController = mastodonController self.mastodonController = mastodonController
self.filterer = Filterer(mastodonController: mastodonController, context: .notifications, htmlConverter: TimelineStatusCollectionViewCell.htmlConverter) 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
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)

View File

@ -8,7 +8,7 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
import HTMLStreamer import SwiftSoup
class PollFinishedNotificationCollectionViewCell: UICollectionViewListCell { class PollFinishedNotificationCollectionViewCell: UICollectionViewListCell {
@ -124,8 +124,9 @@ class PollFinishedNotificationCollectionViewCell: UICollectionViewListCell {
updateTimestamp() updateTimestamp()
updateDisplayName(account: account) updateDisplayName(account: account)
var converter = TextConverter(callbacks: HTMLConverter.Callbacks.self) // todo: use htmlconverter
contentLabel.text = converter.convert(html: status.content) let doc = try! SwiftSoup.parseBodyFragment(status.content)
contentLabel.text = try! doc.text()
pollView.mastodonController = mastodonController pollView.mastodonController = mastodonController
pollView.delegate = delegate pollView.delegate = delegate

View File

@ -8,7 +8,7 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
import HTMLStreamer import SwiftSoup
class StatusUpdatedNotificationCollectionViewCell: UICollectionViewListCell { class StatusUpdatedNotificationCollectionViewCell: UICollectionViewListCell {
@ -120,8 +120,9 @@ class StatusUpdatedNotificationCollectionViewCell: UICollectionViewListCell {
updateTimestamp() updateTimestamp()
updateDisplayName(account: account) updateDisplayName(account: account)
var converter = TextConverter(callbacks: HTMLConverter.Callbacks.self) // todo: use htmlconverter
contentLabel.text = converter.convert(html: status.content) let doc = try! SwiftSoup.parseBodyFragment(status.content)
contentLabel.text = try! doc.text()
} }
@objc private func updateUIForPreferences() { @objc private func updateUIForPreferences() {

View File

@ -60,6 +60,31 @@ 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 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. 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) ^[[swift-url](https://github.com/karwa/swift-url)](headingLevel: 2)
Apache License Apache License

View File

@ -41,7 +41,10 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
self.kind = kind self.kind = kind
self.owner = owner self.owner = owner
self.mastodonController = owner.mastodonController self.mastodonController = owner.mastodonController
self.filterer = Filterer(mastodonController: mastodonController, context: .account, htmlConverter: TimelineStatusCollectionViewCell.htmlConverter) 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
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)

View File

@ -7,13 +7,9 @@
// //
import SwiftUI 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 { struct ReportStatusView: View {
let status: StatusMO let status: StatusMO

View File

@ -70,12 +70,15 @@ class StatusEditCollectionViewCell: UICollectionViewListCell {
$0.setContentHuggingPriority(.defaultLow, for: .vertical) $0.setContentHuggingPriority(.defaultLow, for: .vertical)
} }
private let contentTextView = ContentTextView().configure { private let contentTextView = StatusEditContentTextView().configure {
$0.adjustsFontForContentSizeCategory = true $0.adjustsFontForContentSizeCategory = true
$0.isScrollEnabled = false $0.isScrollEnabled = false
$0.backgroundColor = nil $0.backgroundColor = nil
$0.isEditable = false $0.isEditable = false
$0.isSelectable = false $0.isSelectable = false
$0.defaultFont = TimelineStatusCollectionViewCell.contentFont
$0.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont
$0.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle
} }
private let cardView = StatusCardView().configure { private let cardView = StatusCardView().configure {
@ -188,8 +191,7 @@ class StatusEditCollectionViewCell: UICollectionViewListCell {
timestampLabel.text = ConversationMainStatusCollectionViewCell.dateFormatter.string(from: edit.createdAt) timestampLabel.text = ConversationMainStatusCollectionViewCell.dateFormatter.string(from: edit.createdAt)
contentTextView.attributedText = TimelineStatusCollectionViewCell.htmlConverter.convert(edit.content) contentTextView.setTextFrom(edit: edit, index: index)
contentTextView.setEmojis(edit.emojis, identifier: index)
contentTextView.navigationDelegate = delegate contentTextView.navigationDelegate = delegate
attachmentsView.delegate = self attachmentsView.delegate = self
attachmentsView.updateUI(attachments: edit.attachments) attachmentsView.updateUI(attachments: edit.attachments)

View File

@ -0,0 +1,22 @@
//
// 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
}

View File

@ -30,7 +30,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
let timeline: Timeline let timeline: Timeline
weak var mastodonController: MastodonController! weak var mastodonController: MastodonController!
private let filterer: Filterer let filterer: Filterer
var persistsState = false var persistsState = false
@ -59,7 +59,10 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
default: default:
filterContext = .public filterContext = .public
} }
self.filterer = Filterer(mastodonController: mastodonController, context: filterContext, htmlConverter: TimelineStatusCollectionViewCell.htmlConverter) 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
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)

View File

@ -7,7 +7,7 @@
// //
import UIKit import UIKit
import HTMLStreamer import SwiftSoup
class AccountCollectionViewCell: UICollectionViewListCell { class AccountCollectionViewCell: UICollectionViewListCell {
@ -134,8 +134,8 @@ class AccountCollectionViewCell: UICollectionViewListCell {
displayNameLabel.setEmojis(account.emojis, identifier: account.id) displayNameLabel.setEmojis(account.emojis, identifier: account.id)
} }
var converter = TextConverter(callbacks: HTMLConverter.Callbacks.self) let doc = try! SwiftSoup.parseBodyFragment(account.note)
noteLabel.text = converter.convert(html: account.note) noteLabel.text = try! doc.text()
noteLabel.setEmojis(account.emojis, identifier: account.id) noteLabel.setEmojis(account.emojis, identifier: account.id)
} }

View File

@ -10,13 +10,6 @@ import UIKit
class ConfirmReblogStatusPreviewView: UIView { 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<Void, Error>? private var avatarTask: Task<Void, Error>?
init(status: StatusMO) { init(status: StatusMO) {
@ -67,13 +60,17 @@ class ConfirmReblogStatusPreviewView: UIView {
vStack.addArrangedSubview(displayNameLabel) vStack.addArrangedSubview(displayNameLabel)
let contentView = StatusContentTextView() let contentView = StatusContentTextView()
contentView.defaultFont = .preferredFont(forTextStyle: .caption2)
contentView.monospaceFont = UIFontMetrics(forTextStyle: .caption2).scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular))
contentView.isUserInteractionEnabled = false contentView.isUserInteractionEnabled = false
contentView.isScrollEnabled = false contentView.isScrollEnabled = false
contentView.backgroundColor = nil contentView.backgroundColor = nil
contentView.textContainerInset = .zero contentView.textContainerInset = .zero
contentView.adjustsFontForContentSizeCategory = true 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 // TODO: line limit
contentView.setTextFrom(status: status, content: ConfirmReblogStatusPreviewView.htmlConverter.convert(status.content)) contentView.setTextFrom(status: status)
contentView.translatesAutoresizingMaskIntoConstraints = false contentView.translatesAutoresizingMaskIntoConstraints = false
vStack.addArrangedSubview(contentView) vStack.addArrangedSubview(contentView)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([

View File

@ -7,6 +7,7 @@
// //
import UIKit import UIKit
import SwiftSoup
import Pachyderm import Pachyderm
import SafariServices import SafariServices
import WebURL import WebURL
@ -21,20 +22,31 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
weak var navigationDelegate: TuskerNavigationDelegate? weak var navigationDelegate: TuskerNavigationDelegate?
weak var overrideMastodonController: MastodonController? weak var overrideMastodonController: MastodonController?
var mastodonController: MastodonController? { overrideMastodonController ?? navigationDelegate?.apiController } var mastodonController: MastodonController? { overrideMastodonController ?? navigationDelegate?.apiController }
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 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(set) var hasEmojis = false private(set) var hasEmojis = false
var emojiIdentifier: AnyHashable? var emojiIdentifier: AnyHashable?
var emojiRequests: [ImageCache.Request] = [] var emojiRequests: [ImageCache.Request] = []
var emojiFont: UIFont = .preferredFont(forTextStyle: .body) var emojiFont: UIFont { defaultFont }
var emojiTextColor: UIColor = .label var emojiTextColor: UIColor { defaultColor }
// The link range currently being previewed // The link range currently being previewed
private var currentPreviewedLinkRange: NSRange? private var currentPreviewedLinkRange: NSRange?
@ -108,8 +120,8 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
} }
// MARK: - HTML Parsing // MARK: - HTML Parsing
func setBodyTextFromHTML(_ html: String) { func setTextFromHtml(_ html: String) {
self.attributedText = ContentTextView.defaultBodyHTMLConverter.convert(html) self.attributedText = htmlConverter.convert(html)
} }
// MARK: - Interaction // MARK: - Interaction

View File

@ -34,6 +34,8 @@ class InstanceTableViewCell: UITableViewCell {
adultLabel.layer.masksToBounds = true adultLabel.layer.masksToBounds = true
adultLabel.layer.cornerRadius = 0.5 * adultLabel.bounds.height 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 descriptionTextView.adjustsFontForContentSizeCategory = true
} }
@ -47,7 +49,7 @@ class InstanceTableViewCell: UITableViewCell {
domainLabel.text = instance.domain domainLabel.text = instance.domain
adultLabel.isHidden = instance.category != "adult" adultLabel.isHidden = instance.category != "adult"
descriptionTextView.setBodyTextFromHTML(instance.description) descriptionTextView.setTextFromHtml(instance.description)
updateThumbnail(url: instance.proxiedThumbnailURL) updateThumbnail(url: instance.proxiedThumbnailURL)
} }
@ -57,7 +59,7 @@ class InstanceTableViewCell: UITableViewCell {
domainLabel.text = URLComponents(string: instance.uri)?.host ?? instance.uri domainLabel.text = URLComponents(string: instance.uri)?.host ?? instance.uri
adultLabel.isHidden = true adultLabel.isHidden = true
descriptionTextView.setBodyTextFromHTML(instance.shortDescription ?? instance.description) descriptionTextView.setTextFromHtml(instance.shortDescription ?? instance.description)
if let thumbnail = instance.thumbnail { if let thumbnail = instance.thumbnail {
updateThumbnail(url: thumbnail) updateThumbnail(url: thumbnail)

View File

@ -14,12 +14,12 @@ import SafariServices
class ProfileFieldValueView: UIView { class ProfileFieldValueView: UIView {
weak var navigationDelegate: TuskerNavigationDelegate? weak var navigationDelegate: TuskerNavigationDelegate?
private static let converter = HTMLConverter( private static let converter: HTMLConverter = {
font: .preferredFont(forTextStyle: .body), var converter = HTMLConverter()
monospaceFont: UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular)), converter.font = .preferredFont(forTextStyle: .body)
color: .label, converter.monospaceFont = UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular))
paragraphStyle: .default return converter
) }()
private let account: AccountMO private let account: AccountMO
private let field: Account.Field private let field: Account.Field

View File

@ -95,6 +95,8 @@ class ProfileHeaderView: UIView {
relationshipLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 14)) relationshipLabel.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 14))
relationshipLabel.adjustsFontForContentSizeCategory = true relationshipLabel.adjustsFontForContentSizeCategory = true
noteTextView.defaultFont = .preferredFont(forTextStyle: .body)
noteTextView.monospaceFont = UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular))
noteTextView.adjustsFontForContentSizeCategory = true noteTextView.adjustsFontForContentSizeCategory = true
pagesSegmentedControl = ScrollingSegmentedControl(frame: .zero) pagesSegmentedControl = ScrollingSegmentedControl(frame: .zero)
@ -138,7 +140,7 @@ class ProfileHeaderView: UIView {
moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: delegate?.actionsForProfile(accountID: accountID, source: .view(moreButton), fetchRelationship: false) ?? []) moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: delegate?.actionsForProfile(accountID: accountID, source: .view(moreButton), fetchRelationship: false) ?? [])
noteTextView.navigationDelegate = delegate noteTextView.navigationDelegate = delegate
noteTextView.setBodyTextFromHTML(account.note) noteTextView.setTextFromHtml(account.note)
noteTextView.setEmojis(account.emojis, identifier: account.id) noteTextView.setEmojis(account.emojis, identifier: account.id)
if accountID == mastodonController.account?.id { if accountID == mastodonController.account?.id {

View File

@ -17,13 +17,6 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
static let contentParagraphStyle = HTMLConverter.defaultParagraphStyle static let contentParagraphStyle = HTMLConverter.defaultParagraphStyle
static let metaFont = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .systemFont(ofSize: 15)) 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 = { static let dateFormatter: DateFormatter = {
let formatter = DateFormatter() let formatter = DateFormatter()
formatter.dateStyle = .medium formatter.dateStyle = .medium
@ -139,7 +132,9 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
$0.backgroundColor = nil $0.backgroundColor = nil
$0.isEditable = false $0.isEditable = false
$0.isSelectable = true $0.isSelectable = true
$0.emojiFont = ConversationMainStatusCollectionViewCell.contentFont $0.defaultFont = ConversationMainStatusCollectionViewCell.contentFont
$0.monospaceFont = ConversationMainStatusCollectionViewCell.monospaceFont
$0.paragraphStyle = ConversationMainStatusCollectionViewCell.contentParagraphStyle
$0.dataDetectorTypes = [.flightNumber, .address, .shipmentTrackingNumber, .phoneNumber] $0.dataDetectorTypes = [.flightNumber, .address, .shipmentTrackingNumber, .phoneNumber]
if #available(iOS 16.0, *) { if #available(iOS 16.0, *) {
$0.dataDetectorTypes.formUnion([.money, .physicalValue]) $0.dataDetectorTypes.formUnion([.money, .physicalValue])
@ -386,9 +381,10 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
self.statusID = statusID self.statusID = statusID
self.statusState = state self.statusState = state
let html = translation?.content ?? status.content let attributedTranslatedContent: NSAttributedString? = translation.map {
let attributedContent = ConversationMainStatusCollectionViewCell.htmlConverter.convert(html) contentTextView.htmlConverter.convert($0.content)
doUpdateUI(status: status, content: attributedContent) }
doUpdateUI(status: status, precomputedContent: attributedTranslatedContent)
if !status.spoilerText.isEmpty, if !status.spoilerText.isEmpty,
let translated = translation?.spoilerText { let translated = translation?.spoilerText {

View File

@ -10,7 +10,7 @@ import UIKit
import Pachyderm import Pachyderm
import SafariServices import SafariServices
import WebURLFoundationExtras import WebURLFoundationExtras
import HTMLStreamer import SwiftSoup
class StatusCardView: UIView { class StatusCardView: UIView {
@ -191,8 +191,7 @@ class StatusCardView: UIView {
titleLabel.text = title titleLabel.text = title
titleLabel.isHidden = title.isEmpty titleLabel.isHidden = title.isEmpty
var converter = TextConverter(callbacks: HTMLConverter.Callbacks.self) let description = try! SwiftSoup.parseBodyFragment(card.description).text()
let description = converter.convert(html: card.description)
descriptionLabel.text = description descriptionLabel.text = description
descriptionLabel.isHidden = description.isEmpty descriptionLabel.isHidden = description.isEmpty

View File

@ -88,13 +88,13 @@ extension StatusCollectionViewCell {
.store(in: &cancellables) .store(in: &cancellables)
} }
func doUpdateUI(status: StatusMO, content: NSAttributedString) { func doUpdateUI(status: StatusMO, precomputedContent: NSAttributedString? = nil) {
statusID = status.id statusID = status.id
accountID = status.account.id accountID = status.account.id
updateAccountUI(account: status.account) updateAccountUI(account: status.account)
contentTextView.setTextFrom(status: status, content: content) contentTextView.setTextFrom(status: status, precomputed: precomputedContent)
contentTextView.navigationDelegate = delegate contentTextView.navigationDelegate = delegate
self.updateAttachmentsUI(status: status) self.updateAttachmentsUI(status: status)
pollView.isHidden = status.poll == nil pollView.isHidden = status.poll == nil

View File

@ -20,13 +20,6 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
static let monospaceFont = UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 16, weight: .regular)) static let monospaceFont = UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 16, weight: .regular))
static let contentParagraphStyle = HTMLConverter.defaultParagraphStyle 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 private static let timelineReasonIconSize: CGFloat = 25
// MARK: Subviews // MARK: Subviews
@ -208,7 +201,9 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
$0.backgroundColor = nil $0.backgroundColor = nil
$0.isEditable = false $0.isEditable = false
$0.isSelectable = false $0.isSelectable = false
$0.emojiFont = TimelineStatusCollectionViewCell.contentFont $0.defaultFont = TimelineStatusCollectionViewCell.contentFont
$0.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont
$0.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle
} }
let cardView = StatusCardView().configure { let cardView = StatusCardView().configure {
@ -615,8 +610,7 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
mainContainerTopToReblogLabelConstraint.isActive = true mainContainerTopToReblogLabelConstraint.isActive = true
} }
let content = precomputedContent ?? TimelineStatusCollectionViewCell.htmlConverter.convert(status.content) doUpdateUI(status: status, precomputedContent: precomputedContent)
doUpdateUI(status: status, content: content)
doUpdateTimestamp(status: status) doUpdateTimestamp(status: status)
timestampLabel.isHidden = showPinned timestampLabel.isHidden = showPinned

View File

@ -14,9 +14,13 @@ class StatusContentTextView: ContentTextView {
private var statusID: String? private var statusID: String?
func setTextFrom(status: some StatusProtocol, content attributedText: NSAttributedString) { func setTextFrom(status: some StatusProtocol, precomputed attributedText: NSAttributedString? = nil) {
statusID = status.id statusID = status.id
self.attributedText = attributedText if let attributedText {
self.attributedText = attributedText
} else {
setTextFromHtml(status.content)
}
setEmojis(status.emojis, identifier: status.id) setEmojis(status.emojis, identifier: status.id)
} }