Compare commits
6 Commits
3691c3f483
...
ff4dff1147
Author | SHA1 | Date |
---|---|---|
Shadowfacts | ff4dff1147 | |
Shadowfacts | ba1eed7a85 | |
Shadowfacts | 0c9f6e02bd | |
Shadowfacts | 565d17970f | |
Shadowfacts | dc3c2d027c | |
Shadowfacts | ba2c34fdd6 |
|
@ -305,6 +305,7 @@
|
||||||
D6D4DDE5212518A200E1C4BB /* TuskerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDE4212518A200E1C4BB /* TuskerTests.swift */; };
|
D6D4DDE5212518A200E1C4BB /* TuskerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDE4212518A200E1C4BB /* TuskerTests.swift */; };
|
||||||
D6D4DDF0212518A200E1C4BB /* TuskerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */; };
|
D6D4DDF0212518A200E1C4BB /* TuskerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */; };
|
||||||
D6D706A029466649000827ED /* ScrollingSegmentedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D7069F29466649000827ED /* ScrollingSegmentedControl.swift */; };
|
D6D706A029466649000827ED /* ScrollingSegmentedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D7069F29466649000827ED /* ScrollingSegmentedControl.swift */; };
|
||||||
|
D6D706A72948D4D0000827ED /* TimlineState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D706A62948D4D0000827ED /* TimlineState.swift */; };
|
||||||
D6DD2A3F273C1F4900386A6C /* ComposeAttachmentImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD2A3E273C1F4900386A6C /* ComposeAttachmentImage.swift */; };
|
D6DD2A3F273C1F4900386A6C /* ComposeAttachmentImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD2A3E273C1F4900386A6C /* ComposeAttachmentImage.swift */; };
|
||||||
D6DD2A45273D6C5700386A6C /* GIFImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD2A44273D6C5700386A6C /* GIFImageView.swift */; };
|
D6DD2A45273D6C5700386A6C /* GIFImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD2A44273D6C5700386A6C /* GIFImageView.swift */; };
|
||||||
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */; };
|
D6DD353D22F28CD000A9563A /* ContentWarningCopyMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */; };
|
||||||
|
@ -698,6 +699,7 @@
|
||||||
D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerUITests.swift; sourceTree = "<group>"; };
|
D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerUITests.swift; sourceTree = "<group>"; };
|
||||||
D6D4DDF1212518A200E1C4BB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
D6D4DDF1212518A200E1C4BB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
D6D7069F29466649000827ED /* ScrollingSegmentedControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollingSegmentedControl.swift; sourceTree = "<group>"; };
|
D6D7069F29466649000827ED /* ScrollingSegmentedControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollingSegmentedControl.swift; sourceTree = "<group>"; };
|
||||||
|
D6D706A62948D4D0000827ED /* TimlineState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimlineState.swift; sourceTree = "<group>"; };
|
||||||
D6DD2A3E273C1F4900386A6C /* ComposeAttachmentImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeAttachmentImage.swift; sourceTree = "<group>"; };
|
D6DD2A3E273C1F4900386A6C /* ComposeAttachmentImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeAttachmentImage.swift; sourceTree = "<group>"; };
|
||||||
D6DD2A44273D6C5700386A6C /* GIFImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GIFImageView.swift; sourceTree = "<group>"; };
|
D6DD2A44273D6C5700386A6C /* GIFImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GIFImageView.swift; sourceTree = "<group>"; };
|
||||||
D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningCopyMode.swift; sourceTree = "<group>"; };
|
D6DD353C22F28CD000A9563A /* ContentWarningCopyMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningCopyMode.swift; sourceTree = "<group>"; };
|
||||||
|
@ -933,6 +935,7 @@
|
||||||
D61F75932936F0DA00C0B37F /* FollowedHashtag.swift */,
|
D61F75932936F0DA00C0B37F /* FollowedHashtag.swift */,
|
||||||
D61F759A29384F9C00C0B37F /* FilterMO.swift */,
|
D61F759A29384F9C00C0B37F /* FilterMO.swift */,
|
||||||
D61F75AA293AF11400C0B37F /* FilterKeywordMO.swift */,
|
D61F75AA293AF11400C0B37F /* FilterKeywordMO.swift */,
|
||||||
|
D6D706A62948D4D0000827ED /* TimlineState.swift */,
|
||||||
D60E2F2D244248BF005F8713 /* MastodonCachePersistentStore.swift */,
|
D60E2F2D244248BF005F8713 /* MastodonCachePersistentStore.swift */,
|
||||||
D6B936702829F72900237D0E /* NSManagedObjectContext+Helpers.swift */,
|
D6B936702829F72900237D0E /* NSManagedObjectContext+Helpers.swift */,
|
||||||
);
|
);
|
||||||
|
@ -2071,6 +2074,7 @@
|
||||||
D6114E1327F89B440080E273 /* TrendingLinkTableViewCell.swift in Sources */,
|
D6114E1327F89B440080E273 /* TrendingLinkTableViewCell.swift in Sources */,
|
||||||
D6262C9A28D01C4B00390C1F /* LoadingTableViewCell.swift in Sources */,
|
D6262C9A28D01C4B00390C1F /* LoadingTableViewCell.swift in Sources */,
|
||||||
D68C2AE325869BAB00548EFF /* AuxiliarySceneDelegate.swift in Sources */,
|
D68C2AE325869BAB00548EFF /* AuxiliarySceneDelegate.swift in Sources */,
|
||||||
|
D6D706A72948D4D0000827ED /* TimlineState.swift in Sources */,
|
||||||
D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */,
|
D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */,
|
||||||
D61F758D2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.swift in Sources */,
|
D61F758D2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.swift in Sources */,
|
||||||
D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */,
|
D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */,
|
||||||
|
|
|
@ -355,6 +355,15 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getTimelineState(timeline: Timeline) -> TimelineState? {
|
||||||
|
do {
|
||||||
|
let req = TimelineState.fetchRequest(timeline: timeline)
|
||||||
|
return try viewContext.fetch(req).first
|
||||||
|
} catch {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func managedObjectsDidChange(_ notification: Foundation.Notification) {
|
@objc private func managedObjectsDidChange(_ notification: Foundation.Notification) {
|
||||||
let changes = hasChangedSavedHashtagsOrInstances(notification)
|
let changes = hasChangedSavedHashtagsOrInstances(notification)
|
||||||
if changes.hashtags {
|
if changes.hashtags {
|
||||||
|
|
|
@ -18,6 +18,12 @@ public final class StatusMO: NSManagedObject, StatusProtocol {
|
||||||
return NSFetchRequest<StatusMO>(entityName: "Status")
|
return NSFetchRequest<StatusMO>(entityName: "Status")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@nonobjc public class func fetchRequest(id: String) -> NSFetchRequest<StatusMO> {
|
||||||
|
let req = Self.fetchRequest()
|
||||||
|
req.predicate = NSPredicate(format: "id = %@", id)
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
@NSManaged public var applicationName: String?
|
@NSManaged public var applicationName: String?
|
||||||
@NSManaged private var attachmentsData: Data?
|
@NSManaged private var attachmentsData: Data?
|
||||||
@NSManaged private var bookmarkedInternal: Bool
|
@NSManaged private var bookmarkedInternal: Bool
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
//
|
||||||
|
// TimlineState.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 12/13/22.
|
||||||
|
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
|
@objc(TimelineState)
|
||||||
|
public final class TimelineState: NSManagedObject {
|
||||||
|
|
||||||
|
@nonobjc public class func fetchRequest(timeline: Timeline) -> NSFetchRequest<TimelineState> {
|
||||||
|
let req = NSFetchRequest<TimelineState>(entityName: "TimelineState")
|
||||||
|
req.predicate = NSPredicate(format: "timelineKind = %@", toTimelineKind(timeline))
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
@NSManaged private var timelineKind: String
|
||||||
|
@NSManaged public var centerStatusID: String?
|
||||||
|
@NSManaged private var statuses: NSOrderedSet
|
||||||
|
|
||||||
|
var timeline: Timeline {
|
||||||
|
get { fromTimelineKind(timelineKind) }
|
||||||
|
set { timelineKind = toTimelineKind(newValue) }
|
||||||
|
}
|
||||||
|
|
||||||
|
var statusMOs: [StatusMO] {
|
||||||
|
statuses.array as! [StatusMO]
|
||||||
|
}
|
||||||
|
|
||||||
|
convenience init(timeline: Timeline, context: NSManagedObjectContext) {
|
||||||
|
self.init(context: context)
|
||||||
|
self.timeline = timeline
|
||||||
|
}
|
||||||
|
|
||||||
|
func setStatuses(_ statusIDs: [String]) {
|
||||||
|
let context = managedObjectContext!
|
||||||
|
// todo: this feels really inefficient, but I'm not sure if it's better or worse than doing a single "id IN %@" fetch and sorting after
|
||||||
|
let mos = statusIDs.compactMap { try? context.fetch(StatusMO.fetchRequest(id: $0)).first }
|
||||||
|
self.statuses = NSOrderedSet(array: mos)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// blergh, this is the simplest way of getting the Timeline into a format that A) CoreData can handle and B) is usable in the predicate
|
||||||
|
private func toTimelineKind(_ timeline: Timeline) -> String {
|
||||||
|
switch timeline {
|
||||||
|
case .home:
|
||||||
|
return "home"
|
||||||
|
case .public(local: true):
|
||||||
|
return "local"
|
||||||
|
case .public(local: false):
|
||||||
|
return "federated"
|
||||||
|
case .direct:
|
||||||
|
return "direct"
|
||||||
|
case .tag(hashtag: let name):
|
||||||
|
return "hashtag:\(name)"
|
||||||
|
case .list(id: let id):
|
||||||
|
return "list:\(id)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func fromTimelineKind(_ kind: String) -> Timeline {
|
||||||
|
if kind == "home" {
|
||||||
|
return .home
|
||||||
|
} else if kind == "local" {
|
||||||
|
return .public(local: true)
|
||||||
|
} else if kind == "federated" {
|
||||||
|
return .public(local: false)
|
||||||
|
} else if kind == "direct" {
|
||||||
|
return .direct
|
||||||
|
} else if kind.starts(with: "hashtag:") {
|
||||||
|
return .tag(hashtag: String(trimmingPrefix("hashtag:", of: kind)))
|
||||||
|
} else if kind.starts(with: "list:") {
|
||||||
|
return .list(id: String(trimmingPrefix("list:", of: kind)))
|
||||||
|
} else {
|
||||||
|
fatalError("invalid timeline kind \(kind)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace with Collection.trimmingPrefix
|
||||||
|
@available(iOS, obsoleted: 16.0)
|
||||||
|
private func trimmingPrefix(_ prefix: String, of str: String) -> Substring {
|
||||||
|
return str[str.index(str.startIndex, offsetBy: prefix.count)...]
|
||||||
|
}
|
|
@ -105,10 +105,16 @@
|
||||||
<attribute name="visibilityString" attributeType="String"/>
|
<attribute name="visibilityString" attributeType="String"/>
|
||||||
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="statuses" inverseEntity="Account"/>
|
<relationship name="account" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="statuses" inverseEntity="Account"/>
|
||||||
<relationship name="reblog" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status"/>
|
<relationship name="reblog" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Status"/>
|
||||||
|
<relationship name="timelines" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="TimelineState" inverseName="statuses" inverseEntity="TimelineState"/>
|
||||||
<uniquenessConstraints>
|
<uniquenessConstraints>
|
||||||
<uniquenessConstraint>
|
<uniquenessConstraint>
|
||||||
<constraint value="id"/>
|
<constraint value="id"/>
|
||||||
</uniquenessConstraint>
|
</uniquenessConstraint>
|
||||||
</uniquenessConstraints>
|
</uniquenessConstraints>
|
||||||
</entity>
|
</entity>
|
||||||
|
<entity name="TimelineState" representedClassName="TimelineState" syncable="YES">
|
||||||
|
<attribute name="centerStatusID" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="timelineKind" attributeType="String" valueTransformerName="pachydermTimeline" customClassName="Tusker.TimelineContainer"/>
|
||||||
|
<relationship name="statuses" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="Status" inverseName="timelines" inverseEntity="Status"/>
|
||||||
|
</entity>
|
||||||
</model>
|
</model>
|
|
@ -131,7 +131,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
|
||||||
minDate.addTimeInterval(-7 * 24 * 60 * 60)
|
minDate.addTimeInterval(-7 * 24 * 60 * 60)
|
||||||
|
|
||||||
let statusReq: NSFetchRequest<NSFetchRequestResult> = StatusMO.fetchRequest()
|
let statusReq: NSFetchRequest<NSFetchRequestResult> = StatusMO.fetchRequest()
|
||||||
statusReq.predicate = NSPredicate(format: "(lastFetchedAt = nil) OR (lastFetchedAt < %@)", minDate as NSDate)
|
statusReq.predicate = NSPredicate(format: "((lastFetchedAt = nil) OR (lastFetchedAt < %@)) AND (timelines.@count = 0)", minDate as NSDate)
|
||||||
let deleteStatusReq = NSBatchDeleteRequest(fetchRequest: statusReq)
|
let deleteStatusReq = NSBatchDeleteRequest(fetchRequest: statusReq)
|
||||||
deleteStatusReq.resultType = .resultTypeCount
|
deleteStatusReq.resultType = .resultTypeCount
|
||||||
if let res = try? context.execute(deleteStatusReq) as? NSBatchDeleteResult {
|
if let res = try? context.execute(deleteStatusReq) as? NSBatchDeleteResult {
|
||||||
|
|
|
@ -17,8 +17,7 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
||||||
|
|
||||||
@IBOutlet weak var scrollView: UIScrollView!
|
@IBOutlet weak var scrollView: UIScrollView!
|
||||||
@IBOutlet weak var topControlsView: UIView!
|
@IBOutlet weak var topControlsView: UIView!
|
||||||
@IBOutlet weak var bottomControlsView: UIView!
|
@IBOutlet weak var descriptionTextView: UITextView!
|
||||||
@IBOutlet weak var descriptionLabel: UILabel!
|
|
||||||
|
|
||||||
private var shareContainer: UIView!
|
private var shareContainer: UIView!
|
||||||
private var closeContainer: UIView!
|
private var closeContainer: UIView!
|
||||||
|
@ -47,6 +46,8 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
||||||
}
|
}
|
||||||
var shrinkGestureEnabled = true
|
var shrinkGestureEnabled = true
|
||||||
|
|
||||||
|
private var isInitialAppearance = true
|
||||||
|
private var skipUpdatingControlsWhileZooming = false
|
||||||
private var prevZoomScale: CGFloat?
|
private var prevZoomScale: CGFloat?
|
||||||
private var isGrayscale = false
|
private var isGrayscale = false
|
||||||
private var contentViewSizeObservation: NSKeyValueObservation?
|
private var contentViewSizeObservation: NSKeyValueObservation?
|
||||||
|
@ -99,9 +100,14 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
||||||
|
|
||||||
if let imageDescription = imageDescription,
|
if let imageDescription = imageDescription,
|
||||||
!imageDescription.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
!imageDescription.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||||
descriptionLabel.text = imageDescription.trimmingCharacters(in: .whitespacesAndNewlines)
|
descriptionTextView.text = imageDescription.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
descriptionTextView.textContainerInset = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
|
||||||
|
// i'm not sure why .automatic doesn't work for this
|
||||||
|
descriptionTextView.contentInsetAdjustmentBehavior = .always
|
||||||
|
let height = min(150, descriptionTextView.contentSize.height)
|
||||||
|
descriptionTextView.topAnchor.constraint(equalTo: descriptionTextView.safeAreaLayoutGuide.bottomAnchor, constant: -(height + 16)).isActive = true
|
||||||
} else {
|
} else {
|
||||||
bottomControlsView.isHidden = true
|
descriptionTextView.isHidden = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if shrinkGestureEnabled {
|
if shrinkGestureEnabled {
|
||||||
|
@ -121,7 +127,7 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
||||||
accessibilityElements = [
|
accessibilityElements = [
|
||||||
topControlsView!,
|
topControlsView!,
|
||||||
contentView,
|
contentView,
|
||||||
bottomControlsView!,
|
descriptionTextView!,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -211,9 +217,11 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
||||||
let heightScale = maxHeight / contentView.intrinsicContentSize.height
|
let heightScale = maxHeight / contentView.intrinsicContentSize.height
|
||||||
let widthScale = view.bounds.width / contentView.intrinsicContentSize.width
|
let widthScale = view.bounds.width / contentView.intrinsicContentSize.width
|
||||||
let minScale = min(widthScale, heightScale)
|
let minScale = min(widthScale, heightScale)
|
||||||
|
skipUpdatingControlsWhileZooming = true
|
||||||
scrollView.minimumZoomScale = minScale
|
scrollView.minimumZoomScale = minScale
|
||||||
scrollView.zoomScale = minScale
|
scrollView.zoomScale = minScale
|
||||||
scrollView.maximumZoomScale = minScale >= 1 ? minScale + 2 : 2
|
scrollView.maximumZoomScale = minScale >= 1 ? minScale + 2 : 2
|
||||||
|
skipUpdatingControlsWhileZooming = false
|
||||||
|
|
||||||
centerImage()
|
centerImage()
|
||||||
|
|
||||||
|
@ -243,6 +251,26 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func viewSafeAreaInsetsDidChange() {
|
||||||
|
super.viewSafeAreaInsetsDidChange()
|
||||||
|
// the controls view transforms take the safe area insets into account, so they need to be updated
|
||||||
|
updateControlsView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
|
// on the first appearance, the text view flashes its own scroll indicators automatically
|
||||||
|
// so we only need to do it on subsequent appearances
|
||||||
|
if isInitialAppearance {
|
||||||
|
isInitialAppearance = false
|
||||||
|
} else {
|
||||||
|
if animated && controlsVisible && !descriptionTextView.isHidden {
|
||||||
|
descriptionTextView.flashScrollIndicators()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func preferencesChanged() {
|
@objc private func preferencesChanged() {
|
||||||
if isGrayscale != Preferences.shared.grayscaleImages {
|
if isGrayscale != Preferences.shared.grayscaleImages {
|
||||||
isGrayscale = Preferences.shared.grayscaleImages
|
isGrayscale = Preferences.shared.grayscaleImages
|
||||||
|
@ -257,17 +285,20 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
||||||
self.contentView.setControlsVisible(controlsVisible)
|
self.contentView.setControlsVisible(controlsVisible)
|
||||||
self.updateControlsView()
|
self.updateControlsView()
|
||||||
}
|
}
|
||||||
|
if controlsVisible && !descriptionTextView.isHidden {
|
||||||
|
descriptionTextView.flashScrollIndicators()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
updateControlsView()
|
updateControlsView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateControlsView() {
|
func updateControlsView() {
|
||||||
let topOffset = self.controlsVisible ? 0 : -self.topControlsView.bounds.height
|
let topOffset = self.controlsVisible ? 0 : -(self.topControlsView.bounds.height + self.view.safeAreaInsets.top)
|
||||||
self.topControlsView.transform = CGAffineTransform(translationX: 0, y: topOffset)
|
self.topControlsView.transform = CGAffineTransform(translationX: 0, y: topOffset)
|
||||||
if self.imageDescription != nil {
|
if self.imageDescription != nil {
|
||||||
let bottomOffset = self.controlsVisible ? 0 : self.bottomControlsView.bounds.height + self.view.safeAreaInsets.bottom
|
let bottomOffset = self.controlsVisible ? 0 : self.descriptionTextView.bounds.height + self.view.safeAreaInsets.bottom
|
||||||
self.bottomControlsView.transform = CGAffineTransform(translationX: 0, y: bottomOffset)
|
self.descriptionTextView.transform = CGAffineTransform(translationX: 0, y: bottomOffset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -277,10 +308,12 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
||||||
|
|
||||||
func scrollViewDidZoom(_ scrollView: UIScrollView) {
|
func scrollViewDidZoom(_ scrollView: UIScrollView) {
|
||||||
let prevZoomScale = self.prevZoomScale ?? scrollView.minimumZoomScale
|
let prevZoomScale = self.prevZoomScale ?? scrollView.minimumZoomScale
|
||||||
if scrollView.zoomScale <= scrollView.minimumZoomScale {
|
if !skipUpdatingControlsWhileZooming {
|
||||||
setControlsVisible(true, animated: true)
|
if scrollView.zoomScale <= scrollView.minimumZoomScale {
|
||||||
} else if scrollView.zoomScale > prevZoomScale {
|
setControlsVisible(true, animated: true)
|
||||||
setControlsVisible(false, animated: true)
|
} else if scrollView.zoomScale > prevZoomScale {
|
||||||
|
setControlsVisible(false, animated: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.prevZoomScale = scrollView.zoomScale
|
self.prevZoomScale = scrollView.zoomScale
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,7 @@
|
||||||
<objects>
|
<objects>
|
||||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="LargeImageViewController" customModule="Tusker" customModuleProvider="target">
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="LargeImageViewController" customModule="Tusker" customModuleProvider="target">
|
||||||
<connections>
|
<connections>
|
||||||
<outlet property="bottomControlsView" destination="rPa-Zu-T6g" id="Rgz-AQ-9nt"/>
|
<outlet property="descriptionTextView" destination="JZk-BO-2Vh" id="cby-Hc-ezg"/>
|
||||||
<outlet property="descriptionLabel" destination="eo5-fc-RV8" id="vrW-RJ-y5k"/>
|
|
||||||
<outlet property="scrollView" destination="Skj-xq-AgQ" id="TFb-zF-m1b"/>
|
<outlet property="scrollView" destination="Skj-xq-AgQ" id="TFb-zF-m1b"/>
|
||||||
<outlet property="topControlsView" destination="kHo-B9-R7a" id="8sJ-xQ-7ix"/>
|
<outlet property="topControlsView" destination="kHo-B9-R7a" id="8sJ-xQ-7ix"/>
|
||||||
<outlet property="view" destination="BJw-5C-9nT" id="1C2-VA-mNf"/>
|
<outlet property="view" destination="BJw-5C-9nT" id="1C2-VA-mNf"/>
|
||||||
|
@ -29,41 +28,34 @@
|
||||||
<view contentMode="scaleToFill" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kHo-B9-R7a">
|
<view contentMode="scaleToFill" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kHo-B9-R7a">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="36"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="36"/>
|
||||||
</view>
|
</view>
|
||||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="rPa-Zu-T6g">
|
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" editable="NO" textAlignment="natural" adjustsFontForContentSizeCategory="YES" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="JZk-BO-2Vh">
|
||||||
<rect key="frame" x="0.0" y="622.5" width="375" height="44.5"/>
|
<rect key="frame" x="0.0" y="517" width="375" height="150"/>
|
||||||
<subviews>
|
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.5" colorSpace="custom" customColorSpace="displayP3"/>
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="eo5-fc-RV8">
|
|
||||||
<rect key="frame" x="16" y="8" width="343" height="20.5"/>
|
|
||||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
|
||||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
|
||||||
<nil key="highlightedColor"/>
|
|
||||||
</label>
|
|
||||||
</subviews>
|
|
||||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.5" colorSpace="custom" customColorSpace="sRGB"/>
|
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstItem="eo5-fc-RV8" firstAttribute="top" secondItem="rPa-Zu-T6g" secondAttribute="top" constant="8" id="6n3-E0-2G6"/>
|
<constraint firstAttribute="height" constant="150" placeholder="YES" id="YfV-kQ-0RT"/>
|
||||||
<constraint firstAttribute="trailing" secondItem="eo5-fc-RV8" secondAttribute="trailing" constant="16" id="6uL-vY-tqk"/>
|
|
||||||
<constraint firstItem="eo5-fc-RV8" firstAttribute="leading" secondItem="rPa-Zu-T6g" secondAttribute="leading" constant="16" id="KIF-vw-K7n"/>
|
|
||||||
<constraint firstAttribute="bottom" secondItem="eo5-fc-RV8" secondAttribute="bottom" constant="16" id="v43-mS-tyR"/>
|
|
||||||
</constraints>
|
</constraints>
|
||||||
</view>
|
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
|
||||||
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||||
|
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||||
|
</textView>
|
||||||
</subviews>
|
</subviews>
|
||||||
<viewLayoutGuide key="safeArea" id="w1g-VC-Ll9"/>
|
<viewLayoutGuide key="safeArea" id="w1g-VC-Ll9"/>
|
||||||
<color key="backgroundColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
<color key="backgroundColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
<gestureRecognizers/>
|
<gestureRecognizers/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstItem="Skj-xq-AgQ" firstAttribute="centerY" secondItem="BJw-5C-9nT" secondAttribute="centerY" id="0Xb-ib-2hg"/>
|
<constraint firstItem="Skj-xq-AgQ" firstAttribute="centerY" secondItem="BJw-5C-9nT" secondAttribute="centerY" id="0Xb-ib-2hg"/>
|
||||||
<constraint firstItem="w1g-VC-Ll9" firstAttribute="trailing" secondItem="rPa-Zu-T6g" secondAttribute="trailing" id="2GG-7P-Qv1"/>
|
<constraint firstAttribute="bottom" secondItem="JZk-BO-2Vh" secondAttribute="bottom" id="7Z2-gW-sPj"/>
|
||||||
<constraint firstItem="w1g-VC-Ll9" firstAttribute="bottom" secondItem="rPa-Zu-T6g" secondAttribute="bottom" id="3qf-5e-vl0"/>
|
|
||||||
<constraint firstItem="kHo-B9-R7a" firstAttribute="leading" secondItem="w1g-VC-Ll9" secondAttribute="leading" id="IvH-gU-Kie"/>
|
<constraint firstItem="kHo-B9-R7a" firstAttribute="leading" secondItem="w1g-VC-Ll9" secondAttribute="leading" id="IvH-gU-Kie"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="JZk-BO-2Vh" secondAttribute="trailing" id="JgV-jy-qjS"/>
|
||||||
<constraint firstItem="Skj-xq-AgQ" firstAttribute="centerX" secondItem="BJw-5C-9nT" secondAttribute="centerX" id="KMe-Zc-NZq"/>
|
<constraint firstItem="Skj-xq-AgQ" firstAttribute="centerX" secondItem="BJw-5C-9nT" secondAttribute="centerX" id="KMe-Zc-NZq"/>
|
||||||
<constraint firstItem="Skj-xq-AgQ" firstAttribute="width" secondItem="BJw-5C-9nT" secondAttribute="width" id="Onj-l9-fBu"/>
|
<constraint firstItem="Skj-xq-AgQ" firstAttribute="width" secondItem="BJw-5C-9nT" secondAttribute="width" id="Onj-l9-fBu"/>
|
||||||
<constraint firstItem="w1g-VC-Ll9" firstAttribute="trailing" secondItem="kHo-B9-R7a" secondAttribute="trailing" id="Uh0-ub-R9V"/>
|
<constraint firstItem="w1g-VC-Ll9" firstAttribute="trailing" secondItem="kHo-B9-R7a" secondAttribute="trailing" id="Uh0-ub-R9V"/>
|
||||||
<constraint firstItem="rPa-Zu-T6g" firstAttribute="leading" secondItem="w1g-VC-Ll9" secondAttribute="leading" id="asz-Xj-FUC"/>
|
|
||||||
<constraint firstItem="Skj-xq-AgQ" firstAttribute="height" secondItem="BJw-5C-9nT" secondAttribute="height" id="jvz-QW-n9c"/>
|
<constraint firstItem="Skj-xq-AgQ" firstAttribute="height" secondItem="BJw-5C-9nT" secondAttribute="height" id="jvz-QW-n9c"/>
|
||||||
|
<constraint firstItem="JZk-BO-2Vh" firstAttribute="leading" secondItem="BJw-5C-9nT" secondAttribute="leading" id="kkj-O9-1rE"/>
|
||||||
<constraint firstItem="kHo-B9-R7a" firstAttribute="top" secondItem="BJw-5C-9nT" secondAttribute="top" id="n1O-C3-yQR"/>
|
<constraint firstItem="kHo-B9-R7a" firstAttribute="top" secondItem="BJw-5C-9nT" secondAttribute="top" id="n1O-C3-yQR"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
<point key="canvasLocation" x="-164" y="476"/>
|
<point key="canvasLocation" x="-164" y="475.41229385307349"/>
|
||||||
</view>
|
</view>
|
||||||
</objects>
|
</objects>
|
||||||
</document>
|
</document>
|
||||||
|
|
|
@ -15,6 +15,8 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
let filterer: Filterer
|
let filterer: Filterer
|
||||||
|
|
||||||
|
var persistsState = false
|
||||||
|
|
||||||
private(set) var controller: TimelineLikeController<TimelineItem>!
|
private(set) var controller: TimelineLikeController<TimelineItem>!
|
||||||
let confirmLoadMore = PassthroughSubject<Void, Never>()
|
let confirmLoadMore = PassthroughSubject<Void, Never>()
|
||||||
// stored separately because i don't want to query the snapshot every time the user scrolls
|
// stored separately because i don't want to query the snapshot every time the user scrolls
|
||||||
|
@ -196,7 +198,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
}
|
}
|
||||||
|
|
||||||
if case .notLoadedInitial = controller.state {
|
if case .notLoadedInitial = controller.state {
|
||||||
if doRestore() {
|
if restoreState() {
|
||||||
Task {
|
Task {
|
||||||
await checkPresent(jumpImmediately: false)
|
await checkPresent(jumpImmediately: false)
|
||||||
}
|
}
|
||||||
|
@ -227,22 +229,23 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
super.viewDidDisappear(animated)
|
super.viewDidDisappear(animated)
|
||||||
|
|
||||||
disappearedAt = Date()
|
disappearedAt = Date()
|
||||||
|
saveState()
|
||||||
}
|
}
|
||||||
|
|
||||||
func stateRestorationActivity() -> NSUserActivity? {
|
private func saveState() {
|
||||||
guard isViewLoaded else {
|
guard isViewLoaded,
|
||||||
return nil
|
persistsState else {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
let visible = collectionView.indexPathsForVisibleItems.sorted()
|
let visible = collectionView.indexPathsForVisibleItems.sorted()
|
||||||
let snapshot = dataSource.snapshot()
|
let snapshot = dataSource.snapshot()
|
||||||
let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.bounds.size)
|
let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.bounds.size)
|
||||||
let midPoint = CGPoint(x: visibleRect.midX, y: visibleRect.midY)
|
let midPoint = CGPoint(x: visibleRect.midX, y: visibleRect.midY)
|
||||||
guard let currentAccountID = mastodonController.accountInfo?.id,
|
guard !visible.isEmpty,
|
||||||
!visible.isEmpty,
|
|
||||||
let statusesSection = snapshot.sectionIdentifiers.firstIndex(of: .statuses),
|
let statusesSection = snapshot.sectionIdentifiers.firstIndex(of: .statuses),
|
||||||
let rawCenterVisible = collectionView.indexPathForItem(at: midPoint),
|
let rawCenterVisible = collectionView.indexPathForItem(at: midPoint),
|
||||||
let centerVisible = visible.first(where: { $0.section == statusesSection && $0 >= rawCenterVisible }) else {
|
let centerVisible = visible.first(where: { $0.section == statusesSection && $0 >= rawCenterVisible }) else {
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
let allItems = snapshot.itemIdentifiers(inSection: .statuses)
|
let allItems = snapshot.itemIdentifiers(inSection: .statuses)
|
||||||
|
|
||||||
|
@ -282,44 +285,40 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
} else {
|
} else {
|
||||||
fatalError()
|
fatalError()
|
||||||
}
|
}
|
||||||
stateRestorationLogger.debug("TimelineViewController: creating state restoration activity with topID \(centerVisibleID)")
|
stateRestorationLogger.debug("TimelineViewController: saving state to persistent store with with centerID \(centerVisibleID)")
|
||||||
|
|
||||||
|
let state = mastodonController.persistentContainer.getTimelineState(timeline: timeline) ?? TimelineState(timeline: timeline, context: mastodonController.persistentContainer.viewContext)
|
||||||
|
state.setStatuses(ids)
|
||||||
|
state.centerStatusID = centerVisibleID
|
||||||
|
mastodonController.persistentContainer.save(context: mastodonController.persistentContainer.viewContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
func stateRestorationActivity() -> NSUserActivity? {
|
||||||
|
guard isViewLoaded,
|
||||||
|
let currentAccountID = mastodonController.accountInfo?.id else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
let activity = UserActivityManager.showTimelineActivity(timeline: timeline, accountID: currentAccountID)!
|
let activity = UserActivityManager.showTimelineActivity(timeline: timeline, accountID: currentAccountID)!
|
||||||
activity.addUserInfoEntries(from: [
|
|
||||||
"statusIDs": ids,
|
|
||||||
"centerID": centerVisibleID,
|
|
||||||
])
|
|
||||||
activity.isEligibleForPrediction = false
|
activity.isEligibleForPrediction = false
|
||||||
return activity
|
return activity
|
||||||
}
|
}
|
||||||
|
|
||||||
func restoreActivity(_ activity: NSUserActivity) {
|
private func restoreState() -> Bool {
|
||||||
self.activityToRestore = activity
|
guard persistsState,
|
||||||
}
|
Preferences.shared.timelineStateRestoration,
|
||||||
|
let state = mastodonController.persistentContainer.getTimelineState(timeline: timeline) else {
|
||||||
private func doRestore() -> Bool {
|
|
||||||
guard let activity = activityToRestore,
|
|
||||||
Preferences.shared.timelineStateRestoration else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
guard let statusIDs = activity.userInfo?["statusIDs"] as? [String] else {
|
|
||||||
stateRestorationLogger.fault("TimelineViewController: activity missing statusIDs")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
activityToRestore = nil
|
|
||||||
let existingStatuses = statusIDs.filter { mastodonController.persistentContainer.status(for: $0) != nil }
|
|
||||||
guard !existingStatuses.isEmpty else {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
let statusIDs = state.statusMOs.map(\.id)
|
||||||
loadViewIfNeeded()
|
loadViewIfNeeded()
|
||||||
controller.restoreInitial {
|
controller.restoreInitial {
|
||||||
var snapshot = dataSource.snapshot()
|
var snapshot = dataSource.snapshot()
|
||||||
snapshot.appendSections([.statuses])
|
snapshot.appendSections([.statuses])
|
||||||
let items = existingStatuses.map { Item.status(id: $0, collapseState: .unknown, filterState: .unknown) }
|
let items = statusIDs.map { Item.status(id: $0, collapseState: .unknown, filterState: .unknown) }
|
||||||
snapshot.appendItems(items, toSection: .statuses)
|
snapshot.appendItems(items, toSection: .statuses)
|
||||||
dataSource.apply(snapshot, animatingDifferences: false) {
|
dataSource.apply(snapshot, animatingDifferences: false) {
|
||||||
if let centerID = activity.userInfo?["centerID"] as? String ?? activity.userInfo?["topID"] as? String,
|
if let centerID = state.centerStatusID,
|
||||||
let index = existingStatuses.firstIndex(of: centerID),
|
let index = statusIDs.firstIndex(of: centerID),
|
||||||
let indexPath = self.dataSource.indexPath(for: items[index]) {
|
let indexPath = self.dataSource.indexPath(for: items[index]) {
|
||||||
// it sometimes takes multiple attempts to convert on the right scroll position
|
// it sometimes takes multiple attempts to convert on the right scroll position
|
||||||
// since we're dealing with a bunch of unmeasured cells, so just try a few times in a loop
|
// since we're dealing with a bunch of unmeasured cells, so just try a few times in a loop
|
||||||
|
@ -400,6 +399,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
disappearedAt = Date()
|
disappearedAt = Date()
|
||||||
|
saveState()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func refresh() {
|
@objc func refresh() {
|
||||||
|
|
|
@ -22,12 +22,15 @@ class TimelinesPageViewController: SegmentedPageViewController<TimelinesPageView
|
||||||
|
|
||||||
let home = TimelineViewController(for: .home, mastodonController: mastodonController)
|
let home = TimelineViewController(for: .home, mastodonController: mastodonController)
|
||||||
home.title = homeTitle
|
home.title = homeTitle
|
||||||
|
home.persistsState = true
|
||||||
|
|
||||||
let federated = TimelineViewController(for: .public(local: false), mastodonController: mastodonController)
|
let federated = TimelineViewController(for: .public(local: false), mastodonController: mastodonController)
|
||||||
federated.title = federatedTitle
|
federated.title = federatedTitle
|
||||||
|
federated.persistsState = true
|
||||||
|
|
||||||
let local = TimelineViewController(for: .public(local: true), mastodonController: mastodonController)
|
let local = TimelineViewController(for: .public(local: true), mastodonController: mastodonController)
|
||||||
local.title = localTitle
|
local.title = localTitle
|
||||||
|
local.persistsState = true
|
||||||
|
|
||||||
super.init(pages: [
|
super.init(pages: [
|
||||||
(.home, "Home", home),
|
(.home, "Home", home),
|
||||||
|
@ -83,9 +86,6 @@ class TimelinesPageViewController: SegmentedPageViewController<TimelinesPageView
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
selectPage(page, animated: false)
|
selectPage(page, animated: false)
|
||||||
// can't use currentIndex here because the view isn't loaded yet, and so the page wasn't actually updated by the selectPage call
|
|
||||||
let timelineVC = pageControllers[pages.firstIndex(of: page)!] as! TimelineViewController
|
|
||||||
timelineVC.restoreActivity(activity)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func filtersPressed() {
|
@objc private func filtersPressed() {
|
||||||
|
|
|
@ -394,6 +394,8 @@ class CustomAlertActionButton: UIControl {
|
||||||
self.isContextMenuInteractionEnabled = true
|
self.isContextMenuInteractionEnabled = true
|
||||||
self.showsMenuAsPrimaryAction = action.handler == nil
|
self.showsMenuAsPrimaryAction = action.handler == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addGestureRecognizer(UIHoverGestureRecognizer(target: self, action: #selector(hoverRecognized)))
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
|
@ -429,6 +431,17 @@ class CustomAlertActionButton: UIControl {
|
||||||
super.contextMenuInteraction(interaction, willEndFor: configuration, animator: animator)
|
super.contextMenuInteraction(interaction, willEndFor: configuration, animator: animator)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func hoverRecognized(_ recognizer: UIHoverGestureRecognizer) {
|
||||||
|
switch recognizer.state {
|
||||||
|
case .began, .changed:
|
||||||
|
backgroundColor = .secondarySystemFill
|
||||||
|
case .ended:
|
||||||
|
backgroundColor = nil
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
super.touchesBegan(touches, with: event)
|
super.touchesBegan(touches, with: event)
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ class StatusMetaIndicatorsView: UIView {
|
||||||
var secondaryAxisAlignment: Alignment = .leading
|
var secondaryAxisAlignment: Alignment = .leading
|
||||||
private var images: [UIImageView] = []
|
private var images: [UIImageView] = []
|
||||||
private var isUsingSingleAxis = false
|
private var isUsingSingleAxis = false
|
||||||
|
private var statusID: String?
|
||||||
|
|
||||||
private var needsSingleAxis: Bool {
|
private var needsSingleAxis: Bool {
|
||||||
traitCollection.preferredContentSizeCategory > .extraLarge
|
traitCollection.preferredContentSizeCategory > .extraLarge
|
||||||
|
@ -61,6 +62,11 @@ class StatusMetaIndicatorsView: UIView {
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUI(status: StatusMO) {
|
func updateUI(status: StatusMO) {
|
||||||
|
guard statusID != status.id else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
statusID = status.id
|
||||||
|
|
||||||
var images: [UIImage] = []
|
var images: [UIImage] = []
|
||||||
|
|
||||||
if allowedIndicators.contains(.reply) && Preferences.shared.showIsStatusReplyIcon && status.inReplyToID != nil {
|
if allowedIndicators.contains(.reply) && Preferences.shared.showIsStatusReplyIcon && status.inReplyToID != nil {
|
||||||
|
|
Loading…
Reference in New Issue