Compare commits

..

2 Commits

Author SHA1 Message Date
Shadowfacts 0948371f83 Improve appearance of lists when converting from HTML
Closes #434
2023-09-27 17:35:36 -04:00
Shadowfacts 3ba1a00257 Reconfigure visible updates when refreshing
Closes #300
2023-09-26 09:42:39 -04:00
5 changed files with 91 additions and 20 deletions

View File

@ -37,7 +37,12 @@ struct HTMLConverter {
mutAttrString.trimTrailingCharactersInSet(.whitespacesAndNewlines)
mutAttrString.collapseWhitespace()
mutAttrString.addAttribute(.paragraphStyle, value: paragraphStyle, range: mutAttrString.fullRange)
// 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 {
@ -56,6 +61,10 @@ struct HTMLConverter {
}
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
@ -115,25 +124,6 @@ struct HTMLConverter {
case "pre":
attributed.append(NSAttributedString(string: "\n\n"))
attributed.addAttribute(.font, value: monospaceFont, range: attributed.fullRange)
case "ol", "ul":
attributed.append(NSAttributedString(string: "\n\n"))
attributed.trimLeadingCharactersInSet(.whitespacesAndNewlines)
case "li":
let parentEl = node.parent()!
let parentTag = parentEl.tagName()
let bullet: NSAttributedString
if parentTag == "ol" {
let index = (try? node.elementSiblingIndex()) ?? 0
// we use the monospace digit font so that the periods of all the list items line up
// TODO: this probably breaks with dynamic type
bullet = NSAttributedString(string: "\(index + 1).\t", attributes: [.font: monospaceFont, .foregroundColor: color])
} else if parentTag == "ul" {
bullet = NSAttributedString(string: "\u{2022}\t", attributes: [.font: font, .foregroundColor: color])
} else {
bullet = NSAttributedString()
}
attributed.insert(bullet, at: 0)
attributed.append(NSAttributedString(string: "\n", attributes: [.font: font]))
default:
break
}
@ -144,5 +134,37 @@ struct HTMLConverter {
}
}
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))."
}
}

View File

@ -25,6 +25,8 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle
private(set) var collectionView: UICollectionView!
private(set) var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
var reconfigureVisibleItemsOnEndDecelerating: Bool = false
private var newer: RequestRange?
private var older: RequestRange?
@ -662,6 +664,13 @@ extension NotificationsCollectionViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
MenuPreviewHelper.willPerformPreviewAction(animator: animator, presenter: self)
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if reconfigureVisibleItemsOnEndDecelerating {
reconfigureVisibleItemsOnEndDecelerating = false
reconfigureVisibleCells()
}
}
}
extension NotificationsCollectionViewController: UICollectionViewDragDelegate {

View File

@ -32,6 +32,8 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
private(set) var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
private(set) var headerCell: ProfileHeaderCollectionViewCell?
var reconfigureVisibleItemsOnEndDecelerating: Bool = false
private(set) var state: State = .unloaded
init(accountID: String?, kind: Kind, owner: ProfileViewController) {
@ -627,6 +629,13 @@ extension ProfileStatusesViewController: UICollectionViewDelegate {
return true
}
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if reconfigureVisibleItemsOnEndDecelerating {
reconfigureVisibleItemsOnEndDecelerating = false
reconfigureVisibleCells()
}
}
}
extension ProfileStatusesViewController: UICollectionViewDragDelegate {

View File

@ -42,6 +42,8 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
private(set) var collectionView: UICollectionView!
private(set) var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
var reconfigureVisibleItemsOnEndDecelerating: Bool = false
private var cancellables = Set<AnyCancellable>()
private var userActivityNeedsUpdate = PassthroughSubject<Void, Never>()
// the last time this VC disappeared or the scene was backgrounded while it was active, used to decide if we want to check for present when reappearing
@ -1317,6 +1319,11 @@ extension TimelineViewController: UICollectionViewDelegate {
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
userActivityNeedsUpdate.send()
if reconfigureVisibleItemsOnEndDecelerating {
reconfigureVisibleItemsOnEndDecelerating = false
reconfigureVisibleCells()
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {

View File

@ -21,6 +21,8 @@ protocol TimelineLikeCollectionViewController: UIViewController, TimelineLikeCon
var collectionView: UICollectionView! { get }
var dataSource: UICollectionViewDiffableDataSource<Section, Item>! { get }
var reconfigureVisibleItemsOnEndDecelerating: Bool { get set }
}
protocol TimelineLikeCollectionViewSection: Hashable, Sendable {
@ -125,6 +127,18 @@ extension TimelineLikeCollectionViewController {
var config: ToastConfiguration
if let error = error as? Self.Error,
error == .allCaughtUp {
// Reconfigure visible items to update timestamps.
#if targetEnvironment(macCatalyst)
let isRefreshing = false
#else
let isRefreshing = collectionView.refreshControl?.isRefreshing ?? false
#endif
if isRefreshing {
reconfigureVisibleItemsOnEndDecelerating = true
} else {
reconfigureVisibleCells()
}
config = ToastConfiguration(title: "You're all caught up")
config.edge = .top
config.dismissAutomaticallyAfter = 2
@ -204,6 +218,16 @@ extension TimelineLikeCollectionViewController {
await task.value
}
@MainActor
func reconfigureVisibleCells() {
let items = collectionView.indexPathsForVisibleItems.compactMap { dataSource.itemIdentifier(for: $0) }
if !items.isEmpty {
var snapshot = dataSource.snapshot()
snapshot.reconfigureItems(items)
dataSource.apply(snapshot, animatingDifferences: false)
}
}
func registerTimelineLikeCells() {
collectionView.register(LoadingCollectionViewCell.self, forCellWithReuseIdentifier: "loadingIndicator")
collectionView.register(ConfirmLoadMoreCollectionViewCell.self, forCellWithReuseIdentifier: "confirmLoadMore")