Compare commits
7 Commits
5a4323067a
...
fb5581ae67
Author | SHA1 | Date |
---|---|---|
Shadowfacts | fb5581ae67 | |
Shadowfacts | cd01d2f8c3 | |
Shadowfacts | 65c3c8026d | |
Shadowfacts | 534f83e716 | |
Shadowfacts | 93c859a3c4 | |
Shadowfacts | 4d183fe0b2 | |
Shadowfacts | fd72390a22 |
|
@ -1,5 +1,8 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2024.1 (111)
|
||||||
|
This build contains a complete rewrite of the HTML parsing pipeline for displaying posts. If you notice any issues with how post text appears—especially when it differs from on the web—please report it!
|
||||||
|
|
||||||
## 2023.8 (110)
|
## 2023.8 (110)
|
||||||
Bugfixes:
|
Bugfixes:
|
||||||
- Fix potential crash after deleting List on Explore screen
|
- Fix potential crash after deleting List on Explore screen
|
||||||
|
|
|
@ -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 */; };
|
||||||
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 */; };
|
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,7 +303,6 @@
|
||||||
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 */; };
|
||||||
|
@ -712,7 +711,6 @@
|
||||||
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>"; };
|
||||||
|
@ -786,10 +784,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 */,
|
||||||
|
@ -1574,7 +1572,6 @@
|
||||||
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";
|
||||||
|
@ -1699,7 +1696,6 @@
|
||||||
);
|
);
|
||||||
name = Tusker;
|
name = Tusker;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
D60CFFDA24A290BA00D00083 /* SwiftSoup */,
|
|
||||||
D6676CA427A8D0020052936B /* WebURLFoundationExtras */,
|
D6676CA427A8D0020052936B /* WebURLFoundationExtras */,
|
||||||
D674A50827F9128D00BA03AC /* Pachyderm */,
|
D674A50827F9128D00BA03AC /* Pachyderm */,
|
||||||
D6552366289870790048A653 /* ScreenCorners */,
|
D6552366289870790048A653 /* ScreenCorners */,
|
||||||
|
@ -1711,6 +1707,7 @@
|
||||||
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 */;
|
||||||
|
@ -1821,10 +1818,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 = "";
|
||||||
|
@ -2181,7 +2178,6 @@
|
||||||
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 */,
|
||||||
|
@ -2455,6 +2451,7 @@
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
INFOPLIST_FILE = TuskerUITests/Info.plist;
|
INFOPLIST_FILE = TuskerUITests/Info.plist;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -2808,6 +2805,7 @@
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
INFOPLIST_FILE = TuskerUITests/Info.plist;
|
INFOPLIST_FILE = TuskerUITests/Info.plist;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -2827,6 +2825,7 @@
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
INFOPLIST_FILE = TuskerUITests/Info.plist;
|
INFOPLIST_FILE = TuskerUITests/Info.plist;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
@ -2956,12 +2955,12 @@
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
/* Begin XCRemoteSwiftPackageReference section */
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
D60CFFD924A290BA00D00083 /* XCRemoteSwiftPackageReference "SwiftSoup" */ = {
|
D60BB3922B30076F00DAEA65 /* XCRemoteSwiftPackageReference "HTMLStreamer" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/scinfu/SwiftSoup.git";
|
repositoryURL = "https://git.shadowfacts.net/shadowfacts/HTMLStreamer.git";
|
||||||
requirement = {
|
requirement = {
|
||||||
kind = upToNextMinorVersion;
|
kind = upToNextMinorVersion;
|
||||||
minimumVersion = 2.3.2;
|
minimumVersion = 0.1.0;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
D63CC700290EC0B8000E19DE /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = {
|
D63CC700290EC0B8000E19DE /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = {
|
||||||
|
@ -2991,10 +2990,10 @@
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
D60CFFDA24A290BA00D00083 /* SwiftSoup */ = {
|
D60BB3932B30076F00DAEA65 /* HTMLStreamer */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = D60CFFD924A290BA00D00083 /* XCRemoteSwiftPackageReference "SwiftSoup" */;
|
package = D60BB3922B30076F00DAEA65 /* XCRemoteSwiftPackageReference "HTMLStreamer" */;
|
||||||
productName = SwiftSoup;
|
productName = HTMLStreamer;
|
||||||
};
|
};
|
||||||
D61ABEFB28F105DE00B29151 /* Pachyderm */ = {
|
D61ABEFB28F105DE00B29151 /* Pachyderm */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import LinkPresentation
|
import LinkPresentation
|
||||||
import SwiftSoup
|
import HTMLStreamer
|
||||||
|
|
||||||
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!
|
||||||
let doc = try! SwiftSoup.parse(status.content)
|
var converter = TextConverter(configuration: .init(insertNewlines: false), callbacks: HTMLConverter.Callbacks.self)
|
||||||
let content = try! doc.text()
|
let content = converter.convert(html: status.content)
|
||||||
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),
|
||||||
|
|
|
@ -30,20 +30,31 @@ extension NSAttributedString {
|
||||||
extension NSMutableAttributedString {
|
extension NSMutableAttributedString {
|
||||||
|
|
||||||
func trimLeadingCharactersInSet(_ charSet: CharacterSet) {
|
func trimLeadingCharactersInSet(_ charSet: CharacterSet) {
|
||||||
var range = (string as NSString).rangeOfCharacter(from: charSet)
|
var end = string.startIndex
|
||||||
|
while end < string.endIndex && charSet.contains(string.unicodeScalars[end]) {
|
||||||
while range.length != 0 && range.location == 0 {
|
end = string.unicodeScalars.index(after: end)
|
||||||
replaceCharacters(in: range, with: "")
|
}
|
||||||
range = (string as NSString).rangeOfCharacter(from: charSet)
|
if end > string.startIndex {
|
||||||
|
let length = string.utf16.distance(from: string.startIndex, to: end)
|
||||||
|
replaceCharacters(in: NSRange(location: 0, length: length), with: "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func trimTrailingCharactersInSet(_ charSet: CharacterSet) {
|
func trimTrailingCharactersInSet(_ charSet: CharacterSet) {
|
||||||
var range = (string as NSString).rangeOfCharacter(from: charSet, options: .backwards)
|
if string.isEmpty {
|
||||||
|
return
|
||||||
while range.length != 0 && range.length + range.location == length {
|
}
|
||||||
replaceCharacters(in: range, with: "")
|
var start = string.index(before: string.endIndex)
|
||||||
range = (string as NSString).rangeOfCharacter(from: charSet, options: .backwards)
|
while start > string.startIndex && charSet.contains(string.unicodeScalars[start]) {
|
||||||
|
start = string.unicodeScalars.index(before: start)
|
||||||
|
}
|
||||||
|
if start < string.endIndex {
|
||||||
|
if start != string.startIndex || !charSet.contains(string.unicodeScalars[start]) {
|
||||||
|
start = string.unicodeScalars.index(after: start)
|
||||||
|
}
|
||||||
|
let location = string.utf16.distance(from: string.startIndex, to: start)
|
||||||
|
let length = string.utf16.distance(from: start, to: string.endIndex)
|
||||||
|
replaceCharacters(in: NSRange(location: location, length: length), with: "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,7 @@ class Filterer {
|
||||||
|
|
||||||
var filtersChanged: ((Bool) -> Void)?
|
var filtersChanged: ((Bool) -> Void)?
|
||||||
|
|
||||||
var htmlConverter = HTMLConverter()
|
private 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,9 +55,10 @@ 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) {
|
init(mastodonController: MastodonController, context: FilterV1.Context, htmlConverter: HTMLConverter) {
|
||||||
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
|
||||||
|
|
||||||
|
|
|
@ -7,11 +7,11 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import SwiftSoup
|
import HTMLStreamer
|
||||||
import WebURL
|
import WebURL
|
||||||
import WebURLFoundationExtras
|
import WebURLFoundationExtras
|
||||||
|
|
||||||
struct HTMLConverter {
|
class 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,150 +23,47 @@ struct HTMLConverter {
|
||||||
return style
|
return style
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var font: UIFont = defaultFont
|
private var converter: AttributedStringConverter<Callbacks>
|
||||||
var monospaceFont: UIFont = defaultMonospaceFont
|
|
||||||
var color: UIColor = defaultColor
|
init(font: UIFont, monospaceFont: UIFont, color: UIColor, paragraphStyle: NSParagraphStyle) {
|
||||||
var paragraphStyle: NSParagraphStyle = defaultParagraphStyle
|
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 {
|
||||||
let doc = try! SwiftSoup.parseBodyFragment(html)
|
converter.convert(html: 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
|
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 {
|
} else {
|
||||||
return NSAttributedString()
|
URL(string: string)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func attributedTextForHTMLNode(_ node: Node, usePreformattedText: Bool = false) -> NSAttributedString? {
|
static func elementAction(name: String, attributes: [Attribute]) -> ElementAction {
|
||||||
switch node {
|
guard name == "span" else {
|
||||||
case let node as TextNode:
|
return .default
|
||||||
let text: String
|
}
|
||||||
if usePreformattedText {
|
let clazz = attributes.attributeValue(for: "class")
|
||||||
text = node.getWholeText()
|
if clazz == "invisible" {
|
||||||
|
return .skip
|
||||||
|
} else if clazz == "ellipsis" {
|
||||||
|
return .append("…")
|
||||||
} else {
|
} else {
|
||||||
text = node.text()
|
return .default
|
||||||
}
|
|
||||||
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))."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,11 +26,9 @@ 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.setTextFrom(status: status)
|
view.attributedText = TimelineStatusCollectionViewCell.htmlConverter.convert(status.content)
|
||||||
|
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,8 +34,6 @@ 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)
|
||||||
|
@ -60,7 +58,7 @@ class FeaturedProfileCollectionViewCell: UICollectionViewCell {
|
||||||
|
|
||||||
displayNameLabel.updateForAccountDisplayName(account: account)
|
displayNameLabel.updateForAccountDisplayName(account: account)
|
||||||
|
|
||||||
noteTextView.setTextFromHtml(account.note)
|
noteTextView.setBodyTextFromHTML(account.note)
|
||||||
noteTextView.setEmojis(account.emojis, identifier: account.id)
|
noteTextView.setEmojis(account.emojis, identifier: account.id)
|
||||||
|
|
||||||
avatarImageView.image = nil
|
avatarImageView.image = nil
|
||||||
|
|
|
@ -54,8 +54,6 @@ 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
|
||||||
|
|
||||||
|
@ -86,7 +84,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.setTextFromHtml(account.note)
|
noteTextView.setBodyTextFromHTML(account.note)
|
||||||
|
|
||||||
var config = UIButton.Configuration.plain()
|
var config = UIButton.Configuration.plain()
|
||||||
config.image = source.image
|
config.image = source.image
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import WebURLFoundationExtras
|
import WebURLFoundationExtras
|
||||||
import SwiftSoup
|
import HTMLStreamer
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
let description = try! SwiftSoup.parseBodyFragment(card.description).text()
|
var converter = TextConverter(configuration: .init(insertNewlines: false), callbacks: HTMLConverter.Callbacks.self)
|
||||||
descriptionLabel.text = description
|
descriptionLabel.text = converter.convert(html: card.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 })
|
||||||
|
|
|
@ -23,10 +23,7 @@ class TrendingStatusesViewController: UIViewController, CollectionViewController
|
||||||
|
|
||||||
init(mastodonController: MastodonController) {
|
init(mastodonController: MastodonController) {
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
self.filterer = Filterer(mastodonController: mastodonController, context: .public)
|
self.filterer = Filterer(mastodonController: mastodonController, context: .public, htmlConverter: TimelineStatusCollectionViewCell.htmlConverter)
|
||||||
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)
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import SwiftSoup
|
import HTMLStreamer
|
||||||
|
|
||||||
class ActionNotificationGroupCollectionViewCell: UICollectionViewListCell {
|
class ActionNotificationGroupCollectionViewCell: UICollectionViewListCell {
|
||||||
|
|
||||||
|
@ -161,9 +161,8 @@ 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)
|
||||||
|
|
||||||
// todo: use htmlconverter
|
var converter = TextConverter(configuration: .init(insertNewlines: false), callbacks: HTMLConverter.Callbacks.self)
|
||||||
let doc = try! SwiftSoup.parseBodyFragment(status.content)
|
statusContentLabel.text = converter.convert(html: status.content)
|
||||||
statusContentLabel.text = try! doc.text()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func updateUIForPreferences() {
|
@objc private func updateUIForPreferences() {
|
||||||
|
|
|
@ -34,10 +34,7 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle
|
||||||
self.allowedTypes = allowedTypes
|
self.allowedTypes = allowedTypes
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
|
|
||||||
self.filterer = Filterer(mastodonController: mastodonController, context: .notifications)
|
self.filterer = Filterer(mastodonController: mastodonController, context: .notifications, htmlConverter: TimelineStatusCollectionViewCell.htmlConverter)
|
||||||
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)
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import SwiftSoup
|
import HTMLStreamer
|
||||||
|
|
||||||
class PollFinishedNotificationCollectionViewCell: UICollectionViewListCell {
|
class PollFinishedNotificationCollectionViewCell: UICollectionViewListCell {
|
||||||
|
|
||||||
|
@ -124,9 +124,8 @@ class PollFinishedNotificationCollectionViewCell: UICollectionViewListCell {
|
||||||
updateTimestamp()
|
updateTimestamp()
|
||||||
updateDisplayName(account: account)
|
updateDisplayName(account: account)
|
||||||
|
|
||||||
// todo: use htmlconverter
|
var converter = TextConverter(configuration: .init(insertNewlines: false), callbacks: HTMLConverter.Callbacks.self)
|
||||||
let doc = try! SwiftSoup.parseBodyFragment(status.content)
|
contentLabel.text = converter.convert(html: status.content)
|
||||||
contentLabel.text = try! doc.text()
|
|
||||||
|
|
||||||
pollView.mastodonController = mastodonController
|
pollView.mastodonController = mastodonController
|
||||||
pollView.delegate = delegate
|
pollView.delegate = delegate
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import SwiftSoup
|
import HTMLStreamer
|
||||||
|
|
||||||
class StatusUpdatedNotificationCollectionViewCell: UICollectionViewListCell {
|
class StatusUpdatedNotificationCollectionViewCell: UICollectionViewListCell {
|
||||||
|
|
||||||
|
@ -120,9 +120,8 @@ class StatusUpdatedNotificationCollectionViewCell: UICollectionViewListCell {
|
||||||
updateTimestamp()
|
updateTimestamp()
|
||||||
updateDisplayName(account: account)
|
updateDisplayName(account: account)
|
||||||
|
|
||||||
// todo: use htmlconverter
|
var converter = TextConverter(configuration: .init(insertNewlines: false), callbacks: HTMLConverter.Callbacks.self)
|
||||||
let doc = try! SwiftSoup.parseBodyFragment(status.content)
|
contentLabel.text = converter.convert(html: status.content)
|
||||||
contentLabel.text = try! doc.text()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func updateUIForPreferences() {
|
@objc private func updateUIForPreferences() {
|
||||||
|
|
|
@ -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
|
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
|
||||||
|
|
|
@ -41,10 +41,7 @@ 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)
|
self.filterer = Filterer(mastodonController: mastodonController, context: .account, htmlConverter: TimelineStatusCollectionViewCell.htmlConverter)
|
||||||
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)
|
||||||
|
|
||||||
|
|
|
@ -7,9 +7,13 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
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
|
||||||
|
|
|
@ -70,15 +70,12 @@ class StatusEditCollectionViewCell: UICollectionViewListCell {
|
||||||
$0.setContentHuggingPriority(.defaultLow, for: .vertical)
|
$0.setContentHuggingPriority(.defaultLow, for: .vertical)
|
||||||
}
|
}
|
||||||
|
|
||||||
private let contentTextView = StatusEditContentTextView().configure {
|
private let contentTextView = ContentTextView().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 {
|
||||||
|
@ -191,7 +188,8 @@ class StatusEditCollectionViewCell: UICollectionViewListCell {
|
||||||
|
|
||||||
timestampLabel.text = ConversationMainStatusCollectionViewCell.dateFormatter.string(from: edit.createdAt)
|
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
|
contentTextView.navigationDelegate = delegate
|
||||||
attachmentsView.delegate = self
|
attachmentsView.delegate = self
|
||||||
attachmentsView.updateUI(attachments: edit.attachments)
|
attachmentsView.updateUI(attachments: edit.attachments)
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
||||||
}
|
|
|
@ -30,7 +30,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
|
|
||||||
let timeline: Timeline
|
let timeline: Timeline
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
let filterer: Filterer
|
private let filterer: Filterer
|
||||||
|
|
||||||
var persistsState = false
|
var persistsState = false
|
||||||
|
|
||||||
|
@ -59,10 +59,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
default:
|
default:
|
||||||
filterContext = .public
|
filterContext = .public
|
||||||
}
|
}
|
||||||
self.filterer = Filterer(mastodonController: mastodonController, context: filterContext)
|
self.filterer = Filterer(mastodonController: mastodonController, context: filterContext, htmlConverter: TimelineStatusCollectionViewCell.htmlConverter)
|
||||||
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)
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import SwiftSoup
|
import HTMLStreamer
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
let doc = try! SwiftSoup.parseBodyFragment(account.note)
|
var converter = TextConverter(configuration: .init(insertNewlines: false), callbacks: HTMLConverter.Callbacks.self)
|
||||||
noteLabel.text = try! doc.text()
|
noteLabel.text = converter.convert(html: account.note)
|
||||||
noteLabel.setEmojis(account.emojis, identifier: account.id)
|
noteLabel.setEmojis(account.emojis, identifier: account.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,13 @@ 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) {
|
||||||
|
@ -60,17 +67,13 @@ 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)
|
contentView.setTextFrom(status: status, content: ConfirmReblogStatusPreviewView.htmlConverter.convert(status.content))
|
||||||
contentView.translatesAutoresizingMaskIntoConstraints = false
|
contentView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
vStack.addArrangedSubview(contentView)
|
vStack.addArrangedSubview(contentView)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import SwiftSoup
|
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import SafariServices
|
import SafariServices
|
||||||
import WebURL
|
import WebURL
|
||||||
|
@ -23,30 +22,19 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
|
||||||
weak var overrideMastodonController: MastodonController?
|
weak var overrideMastodonController: MastodonController?
|
||||||
var mastodonController: MastodonController? { overrideMastodonController ?? navigationDelegate?.apiController }
|
var mastodonController: MastodonController? { overrideMastodonController ?? navigationDelegate?.apiController }
|
||||||
|
|
||||||
private(set) var htmlConverter = HTMLConverter()
|
private static let defaultBodyHTMLConverter = HTMLConverter(
|
||||||
var defaultFont: UIFont {
|
font: .preferredFont(forTextStyle: .body),
|
||||||
_read { yield htmlConverter.font }
|
monospaceFont: UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular)),
|
||||||
_modify { yield &htmlConverter.font }
|
color: .label,
|
||||||
}
|
paragraphStyle: .default
|
||||||
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 { defaultFont }
|
var emojiFont: UIFont = .preferredFont(forTextStyle: .body)
|
||||||
var emojiTextColor: UIColor { defaultColor }
|
var emojiTextColor: UIColor = .label
|
||||||
|
|
||||||
// The link range currently being previewed
|
// The link range currently being previewed
|
||||||
private var currentPreviewedLinkRange: NSRange?
|
private var currentPreviewedLinkRange: NSRange?
|
||||||
|
@ -120,8 +108,8 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - HTML Parsing
|
// MARK: - HTML Parsing
|
||||||
func setTextFromHtml(_ html: String) {
|
func setBodyTextFromHTML(_ html: String) {
|
||||||
self.attributedText = htmlConverter.convert(html)
|
self.attributedText = ContentTextView.defaultBodyHTMLConverter.convert(html)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Interaction
|
// MARK: - Interaction
|
||||||
|
|
|
@ -34,8 +34,6 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +47,7 @@ class InstanceTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
domainLabel.text = instance.domain
|
domainLabel.text = instance.domain
|
||||||
adultLabel.isHidden = instance.category != "adult"
|
adultLabel.isHidden = instance.category != "adult"
|
||||||
descriptionTextView.setTextFromHtml(instance.description)
|
descriptionTextView.setBodyTextFromHTML(instance.description)
|
||||||
updateThumbnail(url: instance.proxiedThumbnailURL)
|
updateThumbnail(url: instance.proxiedThumbnailURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,7 +57,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.setTextFromHtml(instance.shortDescription ?? instance.description)
|
descriptionTextView.setBodyTextFromHTML(instance.shortDescription ?? instance.description)
|
||||||
|
|
||||||
if let thumbnail = instance.thumbnail {
|
if let thumbnail = instance.thumbnail {
|
||||||
updateThumbnail(url: thumbnail)
|
updateThumbnail(url: thumbnail)
|
||||||
|
|
|
@ -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(
|
||||||
var converter = HTMLConverter()
|
font: .preferredFont(forTextStyle: .body),
|
||||||
converter.font = .preferredFont(forTextStyle: .body)
|
monospaceFont: UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular)),
|
||||||
converter.monospaceFont = UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular))
|
color: .label,
|
||||||
return converter
|
paragraphStyle: .default
|
||||||
}()
|
)
|
||||||
|
|
||||||
private let account: AccountMO
|
private let account: AccountMO
|
||||||
private let field: Account.Field
|
private let field: Account.Field
|
||||||
|
|
|
@ -95,8 +95,6 @@ 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)
|
||||||
|
@ -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) ?? [])
|
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.setTextFromHtml(account.note)
|
noteTextView.setBodyTextFromHTML(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 {
|
||||||
|
|
|
@ -17,6 +17,13 @@ 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
|
||||||
|
@ -132,9 +139,7 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
|
||||||
$0.backgroundColor = nil
|
$0.backgroundColor = nil
|
||||||
$0.isEditable = false
|
$0.isEditable = false
|
||||||
$0.isSelectable = true
|
$0.isSelectable = true
|
||||||
$0.defaultFont = ConversationMainStatusCollectionViewCell.contentFont
|
$0.emojiFont = 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])
|
||||||
|
@ -381,10 +386,13 @@ class ConversationMainStatusCollectionViewCell: UICollectionViewListCell, Status
|
||||||
self.statusID = statusID
|
self.statusID = statusID
|
||||||
self.statusState = state
|
self.statusState = state
|
||||||
|
|
||||||
let attributedTranslatedContent: NSAttributedString? = translation.map {
|
let html = translation?.content ?? status.content
|
||||||
contentTextView.htmlConverter.convert($0.content)
|
let attributedContent = ConversationMainStatusCollectionViewCell.htmlConverter.convert(html)
|
||||||
}
|
let collapsedContent = NSMutableAttributedString(attributedString: attributedContent)
|
||||||
doUpdateUI(status: status, precomputedContent: attributedTranslatedContent)
|
collapsedContent.collapseWhitespace()
|
||||||
|
collapsedContent.trimLeadingCharactersInSet(.whitespacesAndNewlines)
|
||||||
|
collapsedContent.trimTrailingCharactersInSet(.whitespacesAndNewlines)
|
||||||
|
doUpdateUI(status: status, content: collapsedContent)
|
||||||
|
|
||||||
if !status.spoilerText.isEmpty,
|
if !status.spoilerText.isEmpty,
|
||||||
let translated = translation?.spoilerText {
|
let translated = translation?.spoilerText {
|
||||||
|
|
|
@ -10,7 +10,7 @@ import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import SafariServices
|
import SafariServices
|
||||||
import WebURLFoundationExtras
|
import WebURLFoundationExtras
|
||||||
import SwiftSoup
|
import HTMLStreamer
|
||||||
|
|
||||||
class StatusCardView: UIView {
|
class StatusCardView: UIView {
|
||||||
|
|
||||||
|
@ -191,7 +191,8 @@ class StatusCardView: UIView {
|
||||||
titleLabel.text = title
|
titleLabel.text = title
|
||||||
titleLabel.isHidden = title.isEmpty
|
titleLabel.isHidden = title.isEmpty
|
||||||
|
|
||||||
let description = try! SwiftSoup.parseBodyFragment(card.description).text()
|
var converter = TextConverter(configuration: .init(insertNewlines: false), callbacks: HTMLConverter.Callbacks.self)
|
||||||
|
let description = converter.convert(html: card.description)
|
||||||
descriptionLabel.text = description
|
descriptionLabel.text = description
|
||||||
descriptionLabel.isHidden = description.isEmpty
|
descriptionLabel.isHidden = description.isEmpty
|
||||||
|
|
||||||
|
|
|
@ -88,13 +88,13 @@ extension StatusCollectionViewCell {
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
func doUpdateUI(status: StatusMO, precomputedContent: NSAttributedString? = nil) {
|
func doUpdateUI(status: StatusMO, content: NSAttributedString) {
|
||||||
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, precomputed: precomputedContent)
|
contentTextView.setTextFrom(status: status, content: content)
|
||||||
contentTextView.navigationDelegate = delegate
|
contentTextView.navigationDelegate = delegate
|
||||||
self.updateAttachmentsUI(status: status)
|
self.updateAttachmentsUI(status: status)
|
||||||
pollView.isHidden = status.poll == nil
|
pollView.isHidden = status.poll == nil
|
||||||
|
|
|
@ -20,6 +20,13 @@ 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
|
||||||
|
@ -201,9 +208,7 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
||||||
$0.backgroundColor = nil
|
$0.backgroundColor = nil
|
||||||
$0.isEditable = false
|
$0.isEditable = false
|
||||||
$0.isSelectable = false
|
$0.isSelectable = false
|
||||||
$0.defaultFont = TimelineStatusCollectionViewCell.contentFont
|
$0.emojiFont = TimelineStatusCollectionViewCell.contentFont
|
||||||
$0.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont
|
|
||||||
$0.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let cardView = StatusCardView().configure {
|
let cardView = StatusCardView().configure {
|
||||||
|
@ -610,7 +615,12 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
||||||
mainContainerTopToReblogLabelConstraint.isActive = true
|
mainContainerTopToReblogLabelConstraint.isActive = true
|
||||||
}
|
}
|
||||||
|
|
||||||
doUpdateUI(status: status, precomputedContent: precomputedContent)
|
let content = precomputedContent ?? TimelineStatusCollectionViewCell.htmlConverter.convert(status.content)
|
||||||
|
let collapsedContent = NSMutableAttributedString(attributedString: content)
|
||||||
|
collapsedContent.collapseWhitespace()
|
||||||
|
collapsedContent.trimLeadingCharactersInSet(.whitespacesAndNewlines)
|
||||||
|
collapsedContent.trimTrailingCharactersInSet(.whitespacesAndNewlines)
|
||||||
|
doUpdateUI(status: status, content: collapsedContent)
|
||||||
|
|
||||||
doUpdateTimestamp(status: status)
|
doUpdateTimestamp(status: status)
|
||||||
timestampLabel.isHidden = showPinned
|
timestampLabel.isHidden = showPinned
|
||||||
|
|
|
@ -14,13 +14,9 @@ class StatusContentTextView: ContentTextView {
|
||||||
|
|
||||||
private var statusID: String?
|
private var statusID: String?
|
||||||
|
|
||||||
func setTextFrom(status: some StatusProtocol, precomputed attributedText: NSAttributedString? = nil) {
|
func setTextFrom(status: some StatusProtocol, content attributedText: NSAttributedString) {
|
||||||
statusID = status.id
|
statusID = status.id
|
||||||
if let attributedText {
|
|
||||||
self.attributedText = attributedText
|
self.attributedText = attributedText
|
||||||
} else {
|
|
||||||
setTextFromHtml(status.content)
|
|
||||||
}
|
|
||||||
setEmojis(status.emojis, identifier: status.id)
|
setEmojis(status.emojis, identifier: status.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,36 @@ class AttributedStringHelperTests: XCTestCase {
|
||||||
override func tearDown() {
|
override func tearDown() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testTrimLeading() {
|
||||||
|
let a = NSMutableAttributedString(string: " a ")
|
||||||
|
a.trimLeadingCharactersInSet(.whitespaces)
|
||||||
|
XCTAssertEqual(a, NSAttributedString(string: "a "))
|
||||||
|
let b = NSMutableAttributedString(string: " ")
|
||||||
|
b.trimLeadingCharactersInSet(.whitespaces)
|
||||||
|
XCTAssertEqual(b, NSAttributedString(string: ""))
|
||||||
|
let c = NSMutableAttributedString(string: "")
|
||||||
|
c.trimLeadingCharactersInSet(.whitespaces)
|
||||||
|
XCTAssertEqual(c, NSAttributedString(string: ""))
|
||||||
|
let d = NSMutableAttributedString(string: "abc")
|
||||||
|
d.trimLeadingCharactersInSet(.whitespaces)
|
||||||
|
XCTAssertEqual(d, NSAttributedString(string: "abc"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTrimTrailing() {
|
||||||
|
let a = NSMutableAttributedString(string: " a ")
|
||||||
|
a.trimTrailingCharactersInSet(.whitespaces)
|
||||||
|
XCTAssertEqual(a, NSAttributedString(string: " a"))
|
||||||
|
let b = NSMutableAttributedString(string: " ")
|
||||||
|
b.trimTrailingCharactersInSet(.whitespaces)
|
||||||
|
XCTAssertEqual(b, NSAttributedString(string: ""))
|
||||||
|
let c = NSMutableAttributedString(string: "")
|
||||||
|
c.trimTrailingCharactersInSet(.whitespaces)
|
||||||
|
XCTAssertEqual(c, NSAttributedString(string: ""))
|
||||||
|
let d = NSMutableAttributedString(string: "abc")
|
||||||
|
d.trimTrailingCharactersInSet(.whitespaces)
|
||||||
|
XCTAssertEqual(d, NSAttributedString(string: "abc"))
|
||||||
|
}
|
||||||
|
|
||||||
func testCollapsingWhitespace() {
|
func testCollapsingWhitespace() {
|
||||||
var str = NSAttributedString(string: "test 1\n")
|
var str = NSAttributedString(string: "test 1\n")
|
||||||
XCTAssertEqual(str.collapsingWhitespace(), NSAttributedString(string: "test 1\n"))
|
XCTAssertEqual(str.collapsingWhitespace(), NSAttributedString(string: "test 1\n"))
|
||||||
|
|
|
@ -9,8 +9,8 @@
|
||||||
// Configuration settings file format documentation can be found at:
|
// Configuration settings file format documentation can be found at:
|
||||||
// https://help.apple.com/xcode/#/dev745c5c974
|
// https://help.apple.com/xcode/#/dev745c5c974
|
||||||
|
|
||||||
MARKETING_VERSION = 2023.8
|
MARKETING_VERSION = 2024.1
|
||||||
CURRENT_PROJECT_VERSION = 110
|
CURRENT_PROJECT_VERSION = 111
|
||||||
CURRENT_PROJECT_VERSION = $(inherited)$(CURRENT_PROJECT_VERSION_BUILD_SUFFIX_$(CONFIGURATION))
|
CURRENT_PROJECT_VERSION = $(inherited)$(CURRENT_PROJECT_VERSION_BUILD_SUFFIX_$(CONFIGURATION))
|
||||||
|
|
||||||
CURRENT_PROJECT_VERSION_BUILD_SUFFIX_Debug=-dev
|
CURRENT_PROJECT_VERSION_BUILD_SUFFIX_Debug=-dev
|
||||||
|
|
Loading…
Reference in New Issue