Compare commits
4 Commits
6501343f24
...
e3e55de55b
Author | SHA1 | Date |
---|---|---|
Shadowfacts | e3e55de55b | |
Shadowfacts | 54857a3bf3 | |
Shadowfacts | b28f616e85 | |
Shadowfacts | 97c7104dbc |
|
@ -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 */,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
@ -42,6 +41,8 @@ class StatusContentContainer: UIView {
|
||||||
private var verticalConstraints: [NSLayoutConstraint] = []
|
private var verticalConstraints: [NSLayoutConstraint] = []
|
||||||
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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
setTextFromHtml(status.content)
|
if let attributedText {
|
||||||
|
self.attributedText = attributedText
|
||||||
|
} else {
|
||||||
|
setTextFromHtml(status.content)
|
||||||
|
}
|
||||||
setEmojis(status.emojis, identifier: status.id)
|
setEmojis(status.emojis, identifier: status.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue