Compare commits

...

5 Commits

Author SHA1 Message Date
Shadowfacts 157c8629a9 Add underline links preference
Closes #397
2023-10-24 16:02:03 -04:00
Shadowfacts bde21fbc6c Fix crash due to prematurely pruned statuses being fetched
If the app hasn't launched in long enough, we may be displaying old statuses as a result of state restoration. If the user leaves the app, those statuses can't get pruned, because the user may return. We need to make sure the lastFetchedAt date is current, since awakeFromFetch won't be called until the object is faulted in (which wasn't happening immediately during state restoration).
2023-10-24 15:50:58 -04:00
Shadowfacts 74820e8922 Underline links when button shapes accessibility setting is on 2023-10-24 15:50:58 -04:00
Shadowfacts f7a9075b77 Fix timeline jump button having background when button shapes accessibility setting is on 2023-10-24 15:50:58 -04:00
Shadowfacts 4af56e48bf Clean up TimelineLikeCollectionViewController.apply(_:animatingDifferences:) 2023-10-24 14:56:39 -04:00
8 changed files with 55 additions and 7 deletions

View File

@ -61,6 +61,7 @@ public final class Preferences: Codable, ObservableObject {
self.leadingStatusSwipeActions = try container.decodeIfPresent([StatusSwipeAction].self, forKey: .leadingStatusSwipeActions) ?? leadingStatusSwipeActions self.leadingStatusSwipeActions = try container.decodeIfPresent([StatusSwipeAction].self, forKey: .leadingStatusSwipeActions) ?? leadingStatusSwipeActions
self.trailingStatusSwipeActions = try container.decodeIfPresent([StatusSwipeAction].self, forKey: .trailingStatusSwipeActions) ?? trailingStatusSwipeActions self.trailingStatusSwipeActions = try container.decodeIfPresent([StatusSwipeAction].self, forKey: .trailingStatusSwipeActions) ?? trailingStatusSwipeActions
self.widescreenNavigationMode = try container.decodeIfPresent(WidescreenNavigationMode.self, forKey: .widescreenNavigationMode) ?? Self.defaultWidescreenNavigationMode self.widescreenNavigationMode = try container.decodeIfPresent(WidescreenNavigationMode.self, forKey: .widescreenNavigationMode) ?? Self.defaultWidescreenNavigationMode
self.underlineTextLinks = try container.decodeIfPresent(Bool.self, forKey: .underlineTextLinks) ?? false
self.defaultPostVisibility = try container.decode(Visibility.self, forKey: .defaultPostVisibility) self.defaultPostVisibility = try container.decode(Visibility.self, forKey: .defaultPostVisibility)
self.defaultReplyVisibility = try container.decodeIfPresent(ReplyVisibility.self, forKey: .defaultReplyVisibility) ?? .sameAsPost self.defaultReplyVisibility = try container.decodeIfPresent(ReplyVisibility.self, forKey: .defaultReplyVisibility) ?? .sameAsPost
@ -121,6 +122,7 @@ public final class Preferences: Codable, ObservableObject {
try container.encode(leadingStatusSwipeActions, forKey: .leadingStatusSwipeActions) try container.encode(leadingStatusSwipeActions, forKey: .leadingStatusSwipeActions)
try container.encode(trailingStatusSwipeActions, forKey: .trailingStatusSwipeActions) try container.encode(trailingStatusSwipeActions, forKey: .trailingStatusSwipeActions)
try container.encode(widescreenNavigationMode, forKey: .widescreenNavigationMode) try container.encode(widescreenNavigationMode, forKey: .widescreenNavigationMode)
try container.encode(underlineTextLinks, forKey: .underlineTextLinks)
try container.encode(defaultPostVisibility, forKey: .defaultPostVisibility) try container.encode(defaultPostVisibility, forKey: .defaultPostVisibility)
try container.encode(defaultReplyVisibility, forKey: .defaultReplyVisibility) try container.encode(defaultReplyVisibility, forKey: .defaultReplyVisibility)
@ -175,6 +177,7 @@ public final class Preferences: Codable, ObservableObject {
@Published public var trailingStatusSwipeActions: [StatusSwipeAction] = [.reply, .share] @Published public var trailingStatusSwipeActions: [StatusSwipeAction] = [.reply, .share]
private static var defaultWidescreenNavigationMode = WidescreenNavigationMode.splitScreen private static var defaultWidescreenNavigationMode = WidescreenNavigationMode.splitScreen
@Published public var widescreenNavigationMode = Preferences.defaultWidescreenNavigationMode @Published public var widescreenNavigationMode = Preferences.defaultWidescreenNavigationMode
@Published public var underlineTextLinks = false
// MARK: Composing // MARK: Composing
@Published public var defaultPostVisibility = Visibility.public @Published public var defaultPostVisibility = Visibility.public
@ -245,6 +248,7 @@ public final class Preferences: Codable, ObservableObject {
case leadingStatusSwipeActions case leadingStatusSwipeActions
case trailingStatusSwipeActions case trailingStatusSwipeActions
case widescreenNavigationMode case widescreenNavigationMode
case underlineTextLinks
case defaultPostVisibility case defaultPostVisibility
case defaultReplyVisibility case defaultReplyVisibility

View File

@ -59,9 +59,14 @@ public final class AccountMO: NSManagedObject, AccountProtocol {
super.awakeFromFetch() super.awakeFromFetch()
managedObjectContext?.perform { managedObjectContext?.perform {
self.lastFetchedAt = Date() self.touch()
} }
} }
/// Update the `lastFetchedAt` date so this object isn't pruned early.
func touch() {
lastFetchedAt = Date()
}
} }

View File

@ -89,10 +89,15 @@ public final class StatusMO: NSManagedObject, StatusProtocol {
super.awakeFromFetch() super.awakeFromFetch()
managedObjectContext?.perform { managedObjectContext?.perform {
self.lastFetchedAt = Date() self.touch()
} }
} }
/// Update the `lastFetchedAt` date so this object isn't pruned early.
func touch() {
lastFetchedAt = Date()
}
} }
extension StatusMO { extension StatusMO {

View File

@ -124,6 +124,9 @@ struct AppearancePrefsView : View {
Toggle(isOn: $preferences.showLinkPreviews) { Toggle(isOn: $preferences.showLinkPreviews) {
Text("Show Link Previews") Text("Show Link Previews")
} }
Toggle(isOn: $preferences.underlineTextLinks) {
Text("Underline Links")
}
NavigationLink("Leading Swipe Actions") { NavigationLink("Leading Swipe Actions") {
SwipeActionsPrefsView(selection: $preferences.leadingStatusSwipeActions) SwipeActionsPrefsView(selection: $preferences.leadingStatusSwipeActions)
.edgesIgnoringSafeArea(.all) .edgesIgnoringSafeArea(.all)

View File

@ -20,6 +20,8 @@ class TimelineJumpButton: UIView {
var config = UIButton.Configuration.plain() var config = UIButton.Configuration.plain()
config.image = UIImage(systemName: "arrow.up") config.image = UIImage(systemName: "arrow.up")
config.contentInsets = .zero config.contentInsets = .zero
// We don't want a background for this button, even when accessibility button shapes are enabled, because it's in the navbar.
config.background.backgroundColor = .clear
return UIButton(configuration: config) return UIButton(configuration: config)
}() }()

View File

@ -440,7 +440,15 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
private func loadStatusesToRestore(position: TimelinePosition) async -> Bool { private func loadStatusesToRestore(position: TimelinePosition) async -> Bool {
let originalPositionStatusIDs = position.statusIDs let originalPositionStatusIDs = position.statusIDs
let unloaded = position.statusIDs.filter({ mastodonController.persistentContainer.status(for: $0) == nil }) var unloaded = [String]()
for id in position.statusIDs {
if let status = mastodonController.persistentContainer.status(for: id) {
// touch the status so that, even if it's old, it doesn't get pruned when we go into the background
status.touch()
} else {
unloaded.append(id)
}
}
guard !unloaded.isEmpty else { guard !unloaded.isEmpty else {
return true return true
} }

View File

@ -211,11 +211,9 @@ extension TimelineLikeCollectionViewController {
extension TimelineLikeCollectionViewController { extension TimelineLikeCollectionViewController {
// apply(_:animatingDifferences:) is marked as nonisolated, so just awaiting it doesn't dispatch to the main thread, unlike other async @MainActor methods // apply(_:animatingDifferences:) is marked as nonisolated, so just awaiting it doesn't dispatch to the main thread, unlike other async @MainActor methods
// but we always want to update the data source on the main thread for consistency, so this method does that // but we always want to update the data source on the main thread for consistency, so this method does that
@MainActor
func apply(_ snapshot: NSDiffableDataSourceSnapshot<Section, Item>, animatingDifferences: Bool) async { func apply(_ snapshot: NSDiffableDataSourceSnapshot<Section, Item>, animatingDifferences: Bool) async {
let task = Task { @MainActor in await self.dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
self.dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
}
await task.value
} }
@MainActor @MainActor

View File

@ -12,6 +12,7 @@ import Pachyderm
import SafariServices import SafariServices
import WebURL import WebURL
import WebURLFoundationExtras import WebURLFoundationExtras
import Combine
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: []) private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
private let dataDetectorsScheme = "x-apple-data-detectors" private let dataDetectorsScheme = "x-apple-data-detectors"
@ -52,6 +53,8 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
// The preview created in the previewForHighlighting method, so that we can use the same one in previewForDismissing. // The preview created in the previewForHighlighting method, so that we can use the same one in previewForDismissing.
private weak var currentTargetedPreview: UITargetedPreview? private weak var currentTargetedPreview: UITargetedPreview?
private var underlineTextLinksCancellable: AnyCancellable?
override init(frame: CGRect, textContainer: NSTextContainer?) { override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer) super.init(frame: frame, textContainer: textContainer)
commonInit() commonInit()
@ -78,10 +81,30 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
linkTextAttributes = [ linkTextAttributes = [
.foregroundColor: UIColor.tintColor .foregroundColor: UIColor.tintColor
] ]
updateLinkUnderlineStyle()
// the text view's builtin link interaction code is tied to isSelectable, so we need to use our own tap recognizer // the text view's builtin link interaction code is tied to isSelectable, so we need to use our own tap recognizer
let recognizer = UITapGestureRecognizer(target: self, action: #selector(textTapped(_:))) let recognizer = UITapGestureRecognizer(target: self, action: #selector(textTapped(_:)))
addGestureRecognizer(recognizer) addGestureRecognizer(recognizer)
NotificationCenter.default.addObserver(self, selector: #selector(_updateLinkUnderlineStyle), name: UIAccessibility.buttonShapesEnabledStatusDidChangeNotification, object: nil)
underlineTextLinksCancellable =
Preferences.shared.$underlineTextLinks
.sink { [unowned self] in
self.updateLinkUnderlineStyle(preference: $0)
}
}
@objc private func _updateLinkUnderlineStyle() {
updateLinkUnderlineStyle()
}
private func updateLinkUnderlineStyle(preference: Bool = Preferences.shared.underlineTextLinks) {
if UIAccessibility.buttonShapesEnabled || preference {
linkTextAttributes[.underlineStyle] = NSUnderlineStyle.single.rawValue
} else {
linkTextAttributes.removeValue(forKey: .underlineStyle)
}
} }
// MARK: - Emojis // MARK: - Emojis