Compare commits

...

4 Commits

13 changed files with 140 additions and 43 deletions

View File

@ -67,6 +67,7 @@
D61F75B7293C119700C0B37F /* Filterer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75B6293C119700C0B37F /* Filterer.swift */; }; D61F75B7293C119700C0B37F /* Filterer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75B6293C119700C0B37F /* Filterer.swift */; };
D61F75B9293C15A000C0B37F /* ZeroHeightCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75B8293C15A000C0B37F /* ZeroHeightCollectionViewCell.swift */; }; D61F75B9293C15A000C0B37F /* ZeroHeightCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75B8293C15A000C0B37F /* ZeroHeightCollectionViewCell.swift */; };
D61F75BB293C183100C0B37F /* HTMLConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75BA293C183100C0B37F /* HTMLConverter.swift */; }; D61F75BB293C183100C0B37F /* HTMLConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75BA293C183100C0B37F /* HTMLConverter.swift */; };
D61F75BD293D099600C0B37F /* Lazy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75BC293D099600C0B37F /* Lazy.swift */; };
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483323D3801D008A63EF /* LinkTextView.swift */; }; D620483423D3801D008A63EF /* LinkTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483323D3801D008A63EF /* LinkTextView.swift */; };
D620483623D38075008A63EF /* ContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483523D38075008A63EF /* ContentTextView.swift */; }; D620483623D38075008A63EF /* ContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483523D38075008A63EF /* ContentTextView.swift */; };
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483723D38190008A63EF /* StatusContentTextView.swift */; }; D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483723D38190008A63EF /* StatusContentTextView.swift */; };
@ -450,6 +451,7 @@
D61F75B6293C119700C0B37F /* Filterer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Filterer.swift; sourceTree = "<group>"; }; D61F75B6293C119700C0B37F /* Filterer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Filterer.swift; sourceTree = "<group>"; };
D61F75B8293C15A000C0B37F /* ZeroHeightCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZeroHeightCollectionViewCell.swift; sourceTree = "<group>"; }; D61F75B8293C15A000C0B37F /* ZeroHeightCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZeroHeightCollectionViewCell.swift; sourceTree = "<group>"; };
D61F75BA293C183100C0B37F /* HTMLConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLConverter.swift; sourceTree = "<group>"; }; D61F75BA293C183100C0B37F /* HTMLConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLConverter.swift; sourceTree = "<group>"; };
D61F75BC293D099600C0B37F /* Lazy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Lazy.swift; sourceTree = "<group>"; };
D620483323D3801D008A63EF /* LinkTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkTextView.swift; sourceTree = "<group>"; }; D620483323D3801D008A63EF /* LinkTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkTextView.swift; sourceTree = "<group>"; };
D620483523D38075008A63EF /* ContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentTextView.swift; sourceTree = "<group>"; }; D620483523D38075008A63EF /* ContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentTextView.swift; sourceTree = "<group>"; };
D620483723D38190008A63EF /* StatusContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentTextView.swift; sourceTree = "<group>"; }; D620483723D38190008A63EF /* StatusContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentTextView.swift; sourceTree = "<group>"; };
@ -1463,6 +1465,7 @@
D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */, D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */,
D61F75BA293C183100C0B37F /* HTMLConverter.swift */, D61F75BA293C183100C0B37F /* HTMLConverter.swift */,
D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */, D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */,
D61F75BC293D099600C0B37F /* Lazy.swift */,
D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */, D60E2F2B24423EAD005F8713 /* LazilyDecoding.swift */,
D64D0AAC2128D88B005A6F37 /* LocalData.swift */, D64D0AAC2128D88B005A6F37 /* LocalData.swift */,
D61DC84528F498F200B82C6E /* Logging.swift */, D61DC84528F498F200B82C6E /* Logging.swift */,
@ -2004,6 +2007,7 @@
D6420AEE26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.swift in Sources */, D6420AEE26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.swift in Sources */,
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */, D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */,
D6B93667281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift in Sources */, D6B93667281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift in Sources */,
D61F75BD293D099600C0B37F /* Lazy.swift in Sources */,
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */, D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */,
D69693F42585941A00F4E116 /* UIWindowSceneDelegate+Close.swift in Sources */, D69693F42585941A00F4E116 /* UIWindowSceneDelegate+Close.swift in Sources */,
D6C143DA253510F4007DC240 /* ComposeEmojiTextField.swift in Sources */, D6C143DA253510F4007DC240 /* ComposeEmojiTextField.swift in Sources */,

View File

@ -42,7 +42,7 @@ class Filterer {
var filtersChanged: ((Bool) -> Void)? var filtersChanged: ((Bool) -> Void)?
private let 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 allFiltersObserver: AnyCancellable? private var allFiltersObserver: AnyCancellable?
@ -69,7 +69,7 @@ class Filterer {
matchers = [] matchers = []
filterObservers = [] filterObservers = []
for filter in filters where filter.contexts.contains(context) { for filter in filters where (filter.expiresAt == nil || filter.expiresAt! > Date()) && filter.contexts.contains(context) {
guard let matcher = filter.createMatcher() else { guard let matcher = filter.createMatcher() else {
continue continue
} }
@ -114,16 +114,16 @@ class Filterer {
} }
// Use a closure for the status in case the result is cached and we don't need to look it up // Use a closure for the status in case the result is cached and we don't need to look it up
func resolve(state: FilterState, status: () -> StatusMO) -> Filterer.Result { func resolve(state: FilterState, status: () -> StatusMO) -> (Filterer.Result, NSAttributedString?) {
switch state.state { switch state.state {
case .known(_, generation: let knownGen) where knownGen < generation: case .known(_, generation: let knownGen) where knownGen < generation:
fallthrough fallthrough
case .unknown: case .unknown:
let result = doResolve(status: status()) let (result, attributedString) = doResolve(status: status())
state.state = .known(result, generation: generation) state.state = .known(result, generation: generation)
return result return (result, attributedString)
case .known(let result, _): case .known(let result, _):
return result return (result, nil)
} }
} }
@ -131,21 +131,30 @@ class Filterer {
state.state = .known(result, generation: generation) state.state = .known(result, generation: generation)
} }
private func doResolve(status: StatusMO) -> Result { func isKnownHide(state: FilterState) -> Bool {
switch state.state {
case .known(.hide, generation: let gen) where gen >= generation:
return true
default:
return false
}
}
private func doResolve(status: StatusMO) -> (Result, NSAttributedString?) {
if !hasSetup { if !hasSetup {
setupFilters(filters: mastodonController.filters) setupFilters(filters: mastodonController.filters)
} }
if matchers.isEmpty { if matchers.isEmpty {
return .allow return (.allow, nil)
} }
lazy var text = htmlConverter.convert(status.content).string @Lazy var text = self.htmlConverter.convert(status.content)
for (regex, result) in matchers { for (regex, result) in matchers {
if (!status.spoilerText.isEmpty && regex.numberOfMatches(in: status.spoilerText, range: NSRange(location: 0, length: status.spoilerText.utf16.count)) > 0) if (!status.spoilerText.isEmpty && regex.numberOfMatches(in: status.spoilerText, range: NSRange(location: 0, length: status.spoilerText.utf16.count)) > 0)
|| regex.numberOfMatches(in: text, range: NSRange(location: 0, length: text.utf16.count)) > 0 { || regex.numberOfMatches(in: text.string, range: NSRange(location: 0, length: text.length)) > 0 {
return result return (result, _text.valueIfInitialized)
} }
} }
return .allow return (.allow, _text.valueIfInitialized)
} }
enum Result: Equatable { enum Result: Equatable {

54
Tusker/Lazy.swift Normal file
View File

@ -0,0 +1,54 @@
//
// Lazy.swift
// Tusker
//
// Created by Shadowfacts on 12/4/22.
// Copyright © 2022 Shadowfacts. All rights reserved.
//
import Foundation
/// A lazy initialization property wrapper that allows checking the initialization state.
@propertyWrapper
enum Lazy<Value> {
case uninitialized(() -> Value)
case initialized(Value)
init(wrappedValue: @autoclosure @escaping () -> Value) {
self = .uninitialized(wrappedValue)
}
/// Returns the contained value, initializing it if the value hasn't been accessed before.
var wrappedValue: Value {
mutating get {
switch self {
case .uninitialized(let closure):
let value = closure()
self = .initialized(value)
return value
case .initialized(let value):
return value
}
}
}
/// Whether this Lazy has been initialized yet.
var isInitialized: Bool {
switch self {
case .uninitialized(_):
return false
case .initialized(_):
return true
}
}
/// If this Lazy is initialized, this returns the value. Otherwise, it returns `nil`.
var valueIfInitialized: Value? {
switch self {
case .uninitialized(_):
return nil
case .initialized(let value):
return value
}
}
}

View File

@ -13,6 +13,7 @@ struct Logging {
private init() {} private init() {}
static let general = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "General") static let general = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "General")
static let generalSignposter = OSSignposter(logger: general)
static func getLogData() -> Data? { static func getLogData() -> Data? {
do { do {

View File

@ -65,7 +65,7 @@ class TrendingStatusesViewController: UIViewController {
let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, (String, CollapseState)> { [unowned self] cell, indexPath, item in let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, (String, CollapseState)> { [unowned self] cell, indexPath, item in
cell.delegate = self cell.delegate = self
// TODO: filter these // TODO: filter these
cell.updateUI(statusID: item.0, state: item.1, filterResult: .allow) cell.updateUI(statusID: item.0, state: item.1, filterResult: .allow, precomputedContent: nil)
} }
let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, _, _ in let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, _, _ in
cell.indicator.startAnimating() cell.indicator.startAnimating()

View File

@ -40,6 +40,8 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
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)
self.filterer.htmlConverter.font = TimelineStatusCollectionViewCell.contentFont
self.filterer.htmlConverter.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
@ -68,8 +70,11 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
if item.hideSeparators { if item.hideSeparators {
config.topSeparatorVisibility = .hidden config.topSeparatorVisibility = .hidden
config.bottomSeparatorVisibility = .hidden config.bottomSeparatorVisibility = .hidden
} } else if case .status(id: _, collapseState: _, filterState: let filterState, pinned: _) = item,
if case .status(_, _, _, _) = item { filterer.isKnownHide(state: filterState) {
config.topSeparatorVisibility = .hidden
config.bottomSeparatorVisibility = .hidden
} else if case .status(_, _, _, _) = item {
config.topSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets config.topSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
config.bottomSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets config.bottomSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
} }
@ -116,10 +121,12 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> { private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
collectionView.register(ProfileHeaderCollectionViewCell.self, forCellWithReuseIdentifier: "headerCell") collectionView.register(ProfileHeaderCollectionViewCell.self, forCellWithReuseIdentifier: "headerCell")
let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, (String, CollapseState, Filterer.Result, Bool)> { [unowned self] cell, indexPath, item in let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, (String, CollapseState, Filterer.Result, NSAttributedString?, Bool)> { [unowned self] cell, indexPath, item in
cell.delegate = self cell.delegate = self
cell.showPinned = item.3 cell.showPinned = item.4
cell.updateUI(statusID: item.0, state: item.1, filterResult: item.2) cell.updateUI(statusID: item.0, state: item.1, filterResult: item.2, precomputedContent: item.3)
}
let zeroHeightCell = UICollectionView.CellRegistration<ZeroHeightCollectionViewCell, Void> { _, _, _ in
} }
return UICollectionViewDiffableDataSource(collectionView: collectionView) { [unowned self] collectionView, indexPath, itemIdentifier in return UICollectionViewDiffableDataSource(collectionView: collectionView) { [unowned self] collectionView, indexPath, itemIdentifier in
switch itemIdentifier { switch itemIdentifier {
@ -146,8 +153,13 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
return cell return cell
} }
case .status(id: let id, collapseState: let collapseState, filterState: let filterState, pinned: let pinned): case .status(id: let id, collapseState: let collapseState, filterState: let filterState, pinned: let pinned):
let result = filterResult(state: filterState, statusID: id) let (result, precomputedContent) = filterResult(state: filterState, statusID: id)
return collectionView.dequeueConfiguredReusableCell(using: statusCell, for: indexPath, item: (id, collapseState, result, pinned)) switch result {
case .allow, .warn(_):
return collectionView.dequeueConfiguredReusableCell(using: statusCell, for: indexPath, item: (id, collapseState, result, precomputedContent, pinned))
case .hide:
return collectionView.dequeueConfiguredReusableCell(using: zeroHeightCell, for: indexPath, item: ())
}
case .loadingIndicator: case .loadingIndicator:
return loadingIndicatorCell(for: indexPath) return loadingIndicatorCell(for: indexPath)
case .confirmLoadMore: case .confirmLoadMore:
@ -245,7 +257,7 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
await apply(snapshot, animatingDifferences: true) await apply(snapshot, animatingDifferences: true)
} }
private func filterResult(state: FilterState, statusID: String) -> Filterer.Result { private func filterResult(state: FilterState, statusID: String) -> (Filterer.Result, NSAttributedString?) {
let status = { let status = {
let status = self.mastodonController.persistentContainer.status(for: statusID)! let status = self.mastodonController.persistentContainer.status(for: statusID)!
// if the status is a reblog of another one, filter based on that one // if the status is a reblog of another one, filter based on that one

View File

@ -80,7 +80,7 @@ class StatusActionAccountListViewController: UIViewController {
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> { private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, Void> { [unowned self] cell, indexPath, _ in let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, Void> { [unowned self] cell, indexPath, _ in
cell.delegate = self cell.delegate = self
cell.updateUI(statusID: self.statusID, state: self.statusState, filterResult: .allow) cell.updateUI(statusID: self.statusID, state: self.statusState, filterResult: .allow, precomputedContent: nil)
} }
let accountCell = UICollectionView.CellRegistration<AccountCollectionViewCell, String> { [unowned self] cell, indexPath, item in let accountCell = UICollectionView.CellRegistration<AccountCollectionViewCell, String> { [unowned self] cell, indexPath, item in
cell.delegate = self cell.delegate = self

View File

@ -69,10 +69,10 @@ class InstanceTimelineViewController: TimelineViewController {
toggleSaveButton.title = toggleSaveButtonTitle toggleSaveButton.title = toggleSaveButtonTitle
} }
override func configureStatusCell(_ cell: TimelineStatusCollectionViewCell, id: String, state: CollapseState, filterResult: Filterer.Result) { override func configureStatusCell(_ cell: TimelineStatusCollectionViewCell, id: String, state: CollapseState, filterResult: Filterer.Result, precomputedContent: NSAttributedString?) {
cell.delegate = browsingEnabled ? self : nil cell.delegate = browsingEnabled ? self : nil
cell.overrideMastodonController = mastodonController cell.overrideMastodonController = mastodonController
cell.updateUI(statusID: id, state: state, filterResult: filterResult) cell.updateUI(statusID: id, state: state, filterResult: filterResult, precomputedContent: precomputedContent)
} }
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {

View File

@ -37,6 +37,8 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
filterContext = .public filterContext = .public
} }
self.filterer = Filterer(mastodonController: mastodonController, context: filterContext) self.filterer = Filterer(mastodonController: mastodonController, context: filterContext)
self.filterer.htmlConverter.font = TimelineStatusCollectionViewCell.contentFont
self.filterer.htmlConverter.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
@ -68,8 +70,8 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
if item.hideSeparators { if item.hideSeparators {
config.topSeparatorVisibility = .hidden config.topSeparatorVisibility = .hidden
config.bottomSeparatorVisibility = .hidden config.bottomSeparatorVisibility = .hidden
} else if case .status(id: let id, collapseState: _, filterState: let filterState) = item, } else if case .status(id: _, collapseState: _, filterState: let filterState) = item,
case .hide = filterResult(state: filterState, statusID: id) { filterer.isKnownHide(state: filterState) {
config.topSeparatorVisibility = .hidden config.topSeparatorVisibility = .hidden
config.bottomSeparatorVisibility = .hidden config.bottomSeparatorVisibility = .hidden
} else { } else {
@ -116,19 +118,19 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
} }
// separate method because InstanceTimelineViewController needs to be able to customize it // separate method because InstanceTimelineViewController needs to be able to customize it
func configureStatusCell(_ cell: TimelineStatusCollectionViewCell, id: String, state: CollapseState, filterResult: Filterer.Result) { func configureStatusCell(_ cell: TimelineStatusCollectionViewCell, id: String, state: CollapseState, filterResult: Filterer.Result, precomputedContent: NSAttributedString?) {
cell.delegate = self cell.delegate = self
if case .home = timeline { if case .home = timeline {
cell.showFollowedHashtags = true cell.showFollowedHashtags = true
} else { } else {
cell.showFollowedHashtags = false cell.showFollowedHashtags = false
} }
cell.updateUI(statusID: id, state: state, filterResult: filterResult) cell.updateUI(statusID: id, state: state, filterResult: filterResult, precomputedContent: precomputedContent)
} }
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> { private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, (String, CollapseState, Filterer.Result)> { [unowned self] cell, indexPath, item in let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, (String, CollapseState, Filterer.Result, NSAttributedString?)> { [unowned self] cell, indexPath, item in
self.configureStatusCell(cell, id: item.0, state: item.1, filterResult: item.2) self.configureStatusCell(cell, id: item.0, state: item.1, filterResult: item.2, precomputedContent: item.3)
} }
let zeroHeightCell = UICollectionView.CellRegistration<ZeroHeightCollectionViewCell, Void> { _, _, _ in let zeroHeightCell = UICollectionView.CellRegistration<ZeroHeightCollectionViewCell, Void> { _, _, _ in
} }
@ -148,10 +150,10 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
return UICollectionViewDiffableDataSource(collectionView: collectionView) { [unowned self] collectionView, indexPath, itemIdentifier in return UICollectionViewDiffableDataSource(collectionView: collectionView) { [unowned self] collectionView, indexPath, itemIdentifier in
switch itemIdentifier { switch itemIdentifier {
case .status(id: let id, collapseState: let state, filterState: let filterState): case .status(id: let id, collapseState: let state, filterState: let filterState):
let result = filterResult(state: filterState, statusID: id) let (result, attributedString) = filterResult(state: filterState, statusID: id)
switch result { switch result {
case .allow, .warn(_): case .allow, .warn(_):
return collectionView.dequeueConfiguredReusableCell(using: statusCell, for: indexPath, item: (id, state, result)) return collectionView.dequeueConfiguredReusableCell(using: statusCell, for: indexPath, item: (id, state, result, nil))
case .hide: case .hide:
return collectionView.dequeueConfiguredReusableCell(using: zeroHeightCell, for: indexPath, item: ()) return collectionView.dequeueConfiguredReusableCell(using: zeroHeightCell, for: indexPath, item: ())
} }
@ -329,7 +331,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
isShowingTimelineDescription = false isShowingTimelineDescription = false
} }
private func filterResult(state: FilterState, statusID: String) -> Filterer.Result { private func filterResult(state: FilterState, statusID: String) -> (Filterer.Result, NSAttributedString?) {
let status = { let status = {
let status = self.mastodonController.persistentContainer.status(for: statusID)! let status = self.mastodonController.persistentContainer.status(for: statusID)!
// if the status is a reblog of another one, filter based on that one // if the status is a reblog of another one, filter based on that one

View File

@ -76,14 +76,14 @@ extension StatusCollectionViewCell {
.store(in: &cancellables) .store(in: &cancellables)
} }
func doUpdateUI(status: StatusMO) { 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)
updateUIForPreferences(status: status) updateUIForPreferences(status: status)
contentContainer.contentTextView.setTextFrom(status: status) contentContainer.contentTextView.setTextFrom(status: status, precomputed: precomputedContent)
contentContainer.contentTextView.navigationDelegate = delegate contentContainer.contentTextView.navigationDelegate = delegate
contentContainer.attachmentsView.delegate = self contentContainer.attachmentsView.delegate = self
contentContainer.attachmentsView.updateUI(status: status) contentContainer.attachmentsView.updateUI(status: status)
@ -173,7 +173,7 @@ extension StatusCollectionViewCell {
func updateGrayscaleableUI(status: StatusMO) { func updateGrayscaleableUI(status: StatusMO) {
isGrayscale = Preferences.shared.grayscaleImages isGrayscale = Preferences.shared.grayscaleImages
if contentContainer.contentTextView.hasEmojis { if contentContainer.contentTextView.hasEmojis {
contentContainer.contentTextView.setTextFrom(status: status) contentContainer.contentTextView.setEmojis(status.emojis, identifier: status.id)
} }
displayNameLabel.updateForAccountDisplayName(account: status.account) displayNameLabel.updateForAccountDisplayName(account: status.account)
} }

View File

@ -11,7 +11,6 @@ import UIKit
class StatusContentContainer: UIView { class StatusContentContainer: UIView {
let contentTextView = StatusContentTextView().configure { let contentTextView = StatusContentTextView().configure {
$0.defaultFont = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 16))
$0.adjustsFontForContentSizeCategory = true $0.adjustsFontForContentSizeCategory = true
$0.isScrollEnabled = false $0.isScrollEnabled = false
$0.backgroundColor = nil $0.backgroundColor = nil
@ -43,6 +42,8 @@ class StatusContentContainer: UIView {
private var lastSubviewBottomConstraint: NSLayoutConstraint? private var lastSubviewBottomConstraint: NSLayoutConstraint?
private var zeroHeightConstraint: NSLayoutConstraint! private var zeroHeightConstraint: NSLayoutConstraint!
private var isCollapsed = false
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
@ -97,13 +98,20 @@ class StatusContentContainer: UIView {
lastSubviewBottomConstraint?.isActive = false lastSubviewBottomConstraint?.isActive = false
// this constraint needs to have low priority so that during the collapse/expand animation, the content container is the view that shrinks/expands // this constraint needs to have low priority so that during the collapse/expand animation, the content container is the view that shrinks/expands
lastSubviewBottomConstraint = subviews.last(where: { !$0.isHidden })!.bottomAnchor.constraint(equalTo: bottomAnchor) lastSubviewBottomConstraint = subviews.last(where: { !$0.isHidden })!.bottomAnchor.constraint(equalTo: bottomAnchor)
lastSubviewBottomConstraint!.isActive = true lastSubviewBottomConstraint!.isActive = !isCollapsed
lastSubviewBottomConstraint!.priority = .defaultLow lastSubviewBottomConstraint!.priority = .defaultLow
zeroHeightConstraint.isActive = isCollapsed
super.updateConstraints() super.updateConstraints()
} }
func setCollapsed(_ collapsed: Bool) { func setCollapsed(_ collapsed: Bool) {
guard collapsed != isCollapsed else {
return
}
isCollapsed = collapsed
// ensure that we have a lastSubviewBottomConstraint // ensure that we have a lastSubviewBottomConstraint
updateConstraintsIfNeeded() updateConstraintsIfNeeded()
// force unwrap because the content container should always have at least one view // force unwrap because the content container should always have at least one view

View File

@ -16,6 +16,8 @@ private let hashtagIcon = UIImage(systemName: "number")
class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollectionViewCell { class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollectionViewCell {
static let separatorInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 0) static let separatorInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 0)
static let contentFont = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 16))
static let contentParagraphStyle = HTMLConverter.defaultParagraphStyle
// MARK: Subviews // MARK: Subviews
@ -164,6 +166,8 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
} }
let contentContainer = StatusContentContainer().configure { let contentContainer = StatusContentContainer().configure {
$0.contentTextView.font = TimelineStatusCollectionViewCell.contentFont
$0.contentTextView.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle
$0.setContentHuggingPriority(.defaultLow, for: .vertical) $0.setContentHuggingPriority(.defaultLow, for: .vertical)
} }
private var contentTextView: StatusContentTextView { private var contentTextView: StatusContentTextView {
@ -478,8 +482,7 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
} }
} }
func updateUI(statusID: String, state: CollapseState, filterResult: Filterer.Result, precomputedContent: NSAttributedString?) {
func updateUI(statusID: String, state: CollapseState, filterResult: Filterer.Result) {
guard var status = mastodonController.persistentContainer.status(for: statusID) else { guard var status = mastodonController.persistentContainer.status(for: statusID) else {
fatalError() fatalError()
} }
@ -531,7 +534,7 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
mainContainerTopToReblogLabelConstraint.isActive = !hideTimelineReason mainContainerTopToReblogLabelConstraint.isActive = !hideTimelineReason
mainContainerTopToSelfConstraint.isActive = hideTimelineReason mainContainerTopToSelfConstraint.isActive = hideTimelineReason
doUpdateUI(status: status) doUpdateUI(status: status, precomputedContent: precomputedContent)
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: StatusMO) { func setTextFrom(status: StatusMO, precomputed attributedText: NSAttributedString? = nil) {
statusID = status.id statusID = status.id
if let attributedText {
self.attributedText = attributedText
} else {
setTextFromHtml(status.content) setTextFromHtml(status.content)
}
setEmojis(status.emojis, identifier: status.id) setEmojis(status.emojis, identifier: status.id)
} }