Compare commits

..

No commits in common. "238f246d644fb36c17c912aabcffb112d2aa2d9d" and "c99c397cf6d4ba2ac36bf40a72874dc8576e67db" have entirely different histories.

52 changed files with 427 additions and 975 deletions

View File

@ -1,13 +1,3 @@
## 2024.5
Features/Improvements:
- Improve gallery animations
Bugfixes:
- Handle right-to-left text in display names
- Fix crash during gifv playback
- iPadOS: Fix app becoming unresponsive when switching accounts
- iPadOS/macOS: Fix Cmd+R shortcuts not working
## 2024.4 ## 2024.4
This release introduces support for iOS 18, including a new sidebar/tab bar on iPad, as well as bugfixes and improvements. This release introduces support for iOS 18, including a new sidebar/tab bar on iPad, as well as bugfixes and improvements.

View File

@ -1,33 +1,5 @@
# Changelog # Changelog
## 2024.5 (141)
Bugfixes:
- Fix gallery controls being positioned incorrectly during dismiss animation on certain devices
- Fix gallery controls being positioned incorrectly in landscape orientations
## 2024.5 (139)
Bugfixes:
- Fix error decoding certain posts
## 2024.5 (138)
Bugfixes:
- Fix potential crash when displaying certain attachments
- Fix potential crash due to race condition when opening push notification in app
- Fix misaligned text between profile field values/labels
- Fix rate limited error message not including reset timestamp
- iPadOS/macOS: Fix Cmd+R shortcut not working
## 2024.5 (137)
Features/Improvements:
- Improve gallery presentation/dismissal transitions
Bugfixes:
- Account for bidirectional text in display names
- Fix crash when playing back gifv
- Fix gallery controls not hiding if video loading fails
- iPadOS: Fix incorrect gallery dismiss animation on non-fullscreen windows
- iPadOS: Fix hang when switching accounts
## 2024.4 (136) ## 2024.4 (136)
Features/Improvements: Features/Improvements:
- Import image description when adding attachments from Photos if possible - Import image description when adding attachments from Photos if possible

View File

@ -1,4 +1,4 @@
// swift-tools-version: 6.0 // swift-tools-version: 5.7
// The swift-tools-version declares the minimum version of Swift required to build this package. // The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription import PackageDescription
@ -26,15 +26,9 @@ let package = Package(
// Targets can depend on other targets in this package, and on products in packages this package depends on. // Targets can depend on other targets in this package, and on products in packages this package depends on.
.target( .target(
name: "ComposeUI", name: "ComposeUI",
dependencies: ["Pachyderm", "InstanceFeatures", "TuskerComponents", "MatchedGeometryPresentation"], dependencies: ["Pachyderm", "InstanceFeatures", "TuskerComponents", "MatchedGeometryPresentation"]),
swiftSettings: [
.swiftLanguageMode(.v5)
]),
.testTarget( .testTarget(
name: "ComposeUITests", name: "ComposeUITests",
dependencies: ["ComposeUI"], dependencies: ["ComposeUI"]),
swiftSettings: [
.swiftLanguageMode(.v5)
]),
] ]
) )

View File

@ -1,4 +1,4 @@
// swift-tools-version: 6.0 // swift-tools-version: 5.7
// The swift-tools-version declares the minimum version of Swift required to build this package. // The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription import PackageDescription
@ -23,10 +23,7 @@ let package = Package(
// Targets can depend on other targets in this package, and on products in packages this package depends on. // Targets can depend on other targets in this package, and on products in packages this package depends on.
.target( .target(
name: "Duckable", name: "Duckable",
dependencies: [], dependencies: []),
swiftSettings: [
.swiftLanguageMode(.v5)
]),
// .testTarget( // .testTarget(
// name: "DuckableTests", // name: "DuckableTests",
// dependencies: ["Duckable"]), // dependencies: ["Duckable"]),

View File

@ -1,4 +1,4 @@
// swift-tools-version: 6.0 // swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package. // The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription import PackageDescription
@ -14,23 +14,13 @@ let package = Package(
name: "GalleryVC", name: "GalleryVC",
targets: ["GalleryVC"]), targets: ["GalleryVC"]),
], ],
dependencies: [
.package(path: "../TuskerComponents"),
],
targets: [ targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite. // Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies. // Targets can depend on other targets in this package and products from dependencies.
.target( .target(
name: "GalleryVC", name: "GalleryVC"),
dependencies: ["TuskerComponents"],
swiftSettings: [
.swiftLanguageMode(.v5)
]),
.testTarget( .testTarget(
name: "GalleryVCTests", name: "GalleryVCTests",
dependencies: ["GalleryVC"], dependencies: ["GalleryVC"]),
swiftSettings: [
.swiftLanguageMode(.v5)
]),
] ]
) )

View File

@ -15,7 +15,7 @@ public protocol GalleryContentViewController: UIViewController {
var caption: String? { get } var caption: String? { get }
var contentOverlayAccessoryViewController: UIViewController? { get } var contentOverlayAccessoryViewController: UIViewController? { get }
var bottomControlsAccessoryViewController: UIViewController? { get } var bottomControlsAccessoryViewController: UIViewController? { get }
var presentationAnimation: GalleryContentPresentationAnimation { get } var canAnimateFromSourceView: Bool { get }
func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool)
func galleryContentDidAppear() func galleryContentDidAppear()
@ -31,8 +31,8 @@ public extension GalleryContentViewController {
nil nil
} }
var presentationAnimation: GalleryContentPresentationAnimation { var canAnimateFromSourceView: Bool {
.fromSourceView true
} }
func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) { func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
@ -44,9 +44,3 @@ public extension GalleryContentViewController {
func galleryContentWillDisappear() { func galleryContentWillDisappear() {
} }
} }
public enum GalleryContentPresentationAnimation {
case fade
case fromSourceView
case fromSourceViewWithoutSnapshot
}

View File

@ -30,37 +30,12 @@ class GalleryDismissAnimationController: NSObject, UIViewControllerAnimatedTrans
let itemViewController = from.currentItemViewController let itemViewController = from.currentItemViewController
if itemViewController.content.presentationAnimation == .fade || (UIAccessibility.prefersCrossFadeTransitions && interactiveVelocity == nil) { if !itemViewController.content.canAnimateFromSourceView || (UIAccessibility.prefersCrossFadeTransitions && interactiveVelocity == nil) {
animateCrossFadeTransition(using: transitionContext) animateCrossFadeTransition(using: transitionContext)
return return
} }
let container = transitionContext.containerView let container = transitionContext.containerView
// Moving `to.view` to the container is necessary when the presenting VC (i.e., `to`)
// is in the window's root presentation.
// But it breaks when the gallery is presented from a sheet-presented VC--in which case
// `to.view` is already in the view hierarchy at this point; and adding it to the
// container causees it to be removed when the transition completes.
if to.view.superview == nil {
to.view.frame = container.bounds
container.addSubview(to.view)
}
let sourceSnapshot: UIView? = if itemViewController.content.presentationAnimation == .fromSourceViewWithoutSnapshot {
nil
} else {
sourceView.snapshotView(afterScreenUpdates: false)
}
if let sourceSnapshot {
let snapshotContainer = sourceView.ancestorForInsertingSnapshot
snapshotContainer.addSubview(sourceSnapshot)
let sourceFrameInShapshotContainer = snapshotContainer.convert(sourceView.bounds, from: sourceView)
sourceSnapshot.frame = sourceFrameInShapshotContainer
sourceSnapshot.layer.opacity = 1
self.sourceView.layer.opacity = 0
}
let sourceFrameInContainer = container.convert(sourceView.bounds, from: sourceView) let sourceFrameInContainer = container.convert(sourceView.bounds, from: sourceView)
let destFrameInContainer = container.convert(itemViewController.content.view.bounds, from: itemViewController.content.view) let destFrameInContainer = container.convert(itemViewController.content.view.bounds, from: itemViewController.content.view)
@ -73,39 +48,38 @@ class GalleryDismissAnimationController: NSObject, UIViewControllerAnimatedTrans
.translatedBy(x: destFrameInContainer.midX - sourceFrameInContainer.midX, y: destFrameInContainer.midY - sourceFrameInContainer.midY) .translatedBy(x: destFrameInContainer.midX - sourceFrameInContainer.midX, y: destFrameInContainer.midY - sourceFrameInContainer.midY)
.scaledBy(x: scale, y: scale) .scaledBy(x: scale, y: scale)
sourceView.transform = sourceToDestTransform sourceView.transform = sourceToDestTransform
sourceSnapshot?.transform = sourceToDestTransform
} else { } else {
appliedSourceToDestTransform = false appliedSourceToDestTransform = false
} }
// Moving `to.view` to the container is necessary when the presenting VC (i.e., `to`)
// is in the window's root presentation.
// But it breaks when the gallery is presented from a sheet-presented VC--in which case
// `to.view` is already in the view hierarchy at this point; and adding it to the
// container causees it to be removed when the transition completes.
if to.view.superview == nil {
to.view.frame = container.bounds
container.addSubview(to.view)
}
from.view.frame = container.bounds from.view.frame = container.bounds
container.addSubview(from.view) container.addSubview(from.view)
let contentContainer = UIView()
contentContainer.layer.masksToBounds = true
contentContainer.frame = destFrameInContainer
container.addSubview(contentContainer)
let content = itemViewController.takeContent() let content = itemViewController.takeContent()
content.view.translatesAutoresizingMaskIntoConstraints = true content.view.translatesAutoresizingMaskIntoConstraints = true
content.view.transform = .identity content.view.layer.masksToBounds = true
container.addSubview(content.view)
content.view.frame = destFrameInContainer
content.view.layer.opacity = 1 content.view.layer.opacity = 1
content.view.frame = contentContainer.bounds
contentContainer.addSubview(content.view)
container.layoutIfNeeded() container.layoutIfNeeded()
// Hide overlaid controls immediately, to prevent the Live Text button's position
// getting caught up in the rest of the animation.
UIView.animate(withDuration: 0.1) {
content.setControlsVisible(false, animated: false, dueToUserInteraction: false)
}
let duration = self.transitionDuration(using: transitionContext) let duration = self.transitionDuration(using: transitionContext)
var initialVelocity: CGVector var initialVelocity: CGVector
if let interactiveVelocity, if let interactiveVelocity,
let interactiveTranslation, let interactiveTranslation,
// very short/fast flicks don't transfer their velocity, since it makes size change animation look wacky due to the spring's initial undershoot // very short/fast flicks don't transfer their velocity, since it makes size change animation look wacky due to the springs initial undershoot
sqrt(pow(interactiveTranslation.x, 2) + pow(interactiveTranslation.y, 2)) > 100, sqrt(pow(interactiveTranslation.x, 2) + pow(interactiveTranslation.y, 2)) > 100,
sqrt(pow(interactiveVelocity.x, 2) + pow(interactiveVelocity.y, 2)) < 2000 { sqrt(pow(interactiveVelocity.x, 2) + pow(interactiveVelocity.y, 2)) < 2000 {
let xDistance = sourceFrameInContainer.midX - destFrameInContainer.midX let xDistance = sourceFrameInContainer.midX - destFrameInContainer.midX
@ -128,34 +102,14 @@ class GalleryDismissAnimationController: NSObject, UIViewControllerAnimatedTrans
if appliedSourceToDestTransform { if appliedSourceToDestTransform {
self.sourceView.transform = origSourceTransform self.sourceView.transform = origSourceTransform
sourceSnapshot?.transform = origSourceTransform
} }
content.view.frame = sourceFrameInContainer
contentContainer.frame = sourceFrameInContainer content.view.layer.opacity = 0
// Using sourceSizeWithDestAspectRatioCenteredInContentContainer does not seem to be necessary here.
// I guess autoresizing takes care of it?
itemViewController.setControlsVisible(false, animated: false, dueToUserInteraction: false) itemViewController.setControlsVisible(false, animated: false, dueToUserInteraction: false)
} }
// Delay fading out the content because if it's still big while it's semi-transparent,
// seeing the stuff behind it looks odd.
animator.addAnimations({
content.view.layer.opacity = 0
}, delayFactor: 0.35)
if let sourceSnapshot {
animator.addAnimations({
self.sourceView.layer.opacity = 1
sourceSnapshot.layer.opacity = 0
}, delayFactor: 0.5)
}
animator.addCompletion { _ in animator.addCompletion { _ in
sourceSnapshot?.removeFromSuperview()
// Having dismissed, we don't need to undo any of the changes to the content VC.
transitionContext.completeTransition(true) transitionContext.completeTransition(true)
} }

View File

@ -20,8 +20,6 @@ class GalleryDismissInteraction: NSObject {
private(set) var dismissVelocity: CGPoint? private(set) var dismissVelocity: CGPoint?
private(set) var dismissTranslation: CGPoint? private(set) var dismissTranslation: CGPoint?
private var cancelAnimator: UIViewPropertyAnimator?
init(viewController: GalleryViewController) { init(viewController: GalleryViewController) {
self.viewController = viewController self.viewController = viewController
super.init() super.init()
@ -40,8 +38,6 @@ class GalleryDismissInteraction: NSObject {
content = viewController.currentItemViewController.takeContent() content = viewController.currentItemViewController.takeContent()
content!.view.translatesAutoresizingMaskIntoConstraints = true content!.view.translatesAutoresizingMaskIntoConstraints = true
content!.view.frame = origContentFrameInGallery! content!.view.frame = origContentFrameInGallery!
// Make sure the context remains behind the controls
content!.view.layer.zPosition = -1000
viewController.view.addSubview(content!.view) viewController.view.addSubview(content!.view)
origControlsVisible = viewController.currentItemViewController.controlsVisible origControlsVisible = viewController.currentItemViewController.controlsVisible
@ -57,42 +53,12 @@ class GalleryDismissInteraction: NSObject {
let translation = recognizer.translation(in: viewController.view) let translation = recognizer.translation(in: viewController.view)
let velocity = recognizer.velocity(in: viewController.view) let velocity = recognizer.velocity(in: viewController.view)
let translationMagnitude = sqrt(translation.x.magnitudeSquared + translation.y.magnitudeSquared) dismissVelocity = velocity
let velocityMagnitude = sqrt(velocity.x.magnitudeSquared + velocity.y.magnitudeSquared) dismissTranslation = translation
viewController.dismiss(animated: true)
if translationMagnitude < 150 && velocityMagnitude < 500 { // don't unset this until after dismiss is called, so that the dismiss animation controller can read it
isActive = false isActive = false
cancelAnimator?.stopAnimation(true)
let spring = UISpringTimingParameters(mass: 1, stiffness: 439, damping: 42, initialVelocity: .zero)
cancelAnimator = UIViewPropertyAnimator(duration: 0.2, timingParameters: spring)
cancelAnimator!.addAnimations {
self.content!.view.frame = self.origContentFrameInGallery!
self.viewController.currentItemViewController.setControlsVisible(self.origControlsVisible!, animated: false, dueToUserInteraction: false)
}
cancelAnimator!.addCompletion { _ in
guard !self.isActive else {
// bail in case the animation finishing raced with the user's interaction
return
}
self.content!.view.layer.zPosition = 0
self.content!.view.removeFromSuperview()
self.viewController.currentItemViewController.addContent()
self.content = nil
self.origContentFrameInGallery = nil
self.origControlsVisible = nil
}
cancelAnimator!.startAnimation()
} else {
dismissVelocity = velocity
dismissTranslation = translation
viewController.dismiss(animated: true)
// don't unset this until after dismiss is called, so that the dismiss animation controller can read it
isActive = false
}
default: default:
break break

View File

@ -11,7 +11,6 @@ import AVFoundation
@MainActor @MainActor
protocol GalleryItemViewControllerDelegate: AnyObject { protocol GalleryItemViewControllerDelegate: AnyObject {
func isGalleryBeingPresented() -> Bool func isGalleryBeingPresented() -> Bool
func isGalleryBeingDismissed() -> Bool
func addPresentationAnimationCompletion(_ block: @escaping () -> Void) func addPresentationAnimationCompletion(_ block: @escaping () -> Void)
func galleryItemClose(_ item: GalleryItemViewController) func galleryItemClose(_ item: GalleryItemViewController)
func galleryItemApplicationActivities(_ item: GalleryItemViewController) -> [UIActivity]? func galleryItemApplicationActivities(_ item: GalleryItemViewController) -> [UIActivity]?
@ -70,10 +69,6 @@ class GalleryItemViewController: UIViewController {
scrollView = UIScrollView() scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.delegate = self scrollView.delegate = self
// We calculate zoom/position ignoring the safe area, so content insets need to not incorporate it either.
// Otherwise, content that fills the screen (extending into the safe area) may still end up scrollable
// (this is readily observable with tall images on a landscape iPad).
scrollView.contentInsetAdjustmentBehavior = .never
view.addSubview(scrollView) view.addSubview(scrollView)
@ -377,27 +372,13 @@ class GalleryItemViewController: UIViewController {
} }
private func updateTopControlsInsets() { private func updateTopControlsInsets() {
guard delegate?.isGalleryBeingDismissed() != true else {
return
}
let notchedDeviceTopInsets: [CGFloat] = [ let notchedDeviceTopInsets: [CGFloat] = [
44, // iPhone X, Xs, Xs Max, 11 Pro, 11 Pro Max 44, // iPhone X, Xs, Xs Max, 11 Pro, 11 Pro Max
48, // iPhone XR, 11 48, // iPhone XR, 11
47, // iPhone 12, 12 Pro, 12 Pro Max, 13, 13 Pro, 13 Pro Max, 14, 14 Plus 47, // iPhone 12, 12 Pro, 12 Pro Max, 13, 13 Pro, 13 Pro Max, 14, 14 Plus
50, // iPhone 12 mini, 13 mini 50, // iPhone 12 mini, 13 mini
] ]
let topInset: CGFloat if notchedDeviceTopInsets.contains(view.safeAreaInsets.top) {
switch view.window?.windowScene?.interfaceOrientation {
case .portraitUpsideDown:
topInset = view.safeAreaInsets.bottom
case .landscapeLeft:
topInset = view.safeAreaInsets.right
case .landscapeRight:
topInset = view.safeAreaInsets.left
default:
topInset = view.safeAreaInsets.top
}
if notchedDeviceTopInsets.contains(topInset) {
// the notch width is not the same for the iPhones 13, // the notch width is not the same for the iPhones 13,
// but what we actually want is the same offset from the edges // but what we actually want is the same offset from the edges
// since the corner radius didn't change // since the corner radius didn't change
@ -406,7 +387,7 @@ class GalleryItemViewController: UIViewController {
let offset = (earWidth - (shareButton.imageView?.bounds.width ?? 0)) / 2 let offset = (earWidth - (shareButton.imageView?.bounds.width ?? 0)) / 2
shareButtonLeadingConstraint.constant = offset shareButtonLeadingConstraint.constant = offset
closeButtonTrailingConstraint.constant = offset closeButtonTrailingConstraint.constant = offset
} else if topInset == 0 { } else if view.safeAreaInsets.top == 0 {
// square corner devices // square corner devices
shareButtonLeadingConstraint.constant = 8 shareButtonLeadingConstraint.constant = 8
shareButtonTopConstraint.constant = 8 shareButtonTopConstraint.constant = 8

View File

@ -25,31 +25,11 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated
let itemViewController = to.currentItemViewController let itemViewController = to.currentItemViewController
if itemViewController.content.presentationAnimation == .fade || UIAccessibility.prefersCrossFadeTransitions { if !itemViewController.content.canAnimateFromSourceView || UIAccessibility.prefersCrossFadeTransitions {
animateCrossFadeTransition(using: transitionContext) animateCrossFadeTransition(using: transitionContext)
return return
} }
// Try to effectively "fade out" anything that's on top of the source view.
// The 0.1 duration makes this happen faster than the rest of the animation,
// and so less noticeable.
let sourceSnapshot: UIView? = if itemViewController.content.presentationAnimation == .fromSourceViewWithoutSnapshot {
nil
} else {
sourceView.snapshotView(afterScreenUpdates: false)
}
if let sourceSnapshot {
let snapshotContainer = sourceView.ancestorForInsertingSnapshot
snapshotContainer.addSubview(sourceSnapshot)
let sourceFrameInShapshotContainer = snapshotContainer.convert(sourceView.bounds, from: sourceView)
sourceSnapshot.frame = sourceFrameInShapshotContainer
sourceSnapshot.transform = sourceView.transform
sourceSnapshot.layer.opacity = 0
UIView.animate(withDuration: 0.1) {
sourceSnapshot.layer.opacity = 1
}
}
let container = transitionContext.containerView let container = transitionContext.containerView
to.view.frame = container.bounds to.view.frame = container.bounds
container.addSubview(to.view) container.addSubview(to.view)
@ -76,70 +56,21 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated
sourceToDestTransform = nil sourceToDestTransform = nil
} }
// Grab these before taking the content out and changing the transform.
let origContentTransform = itemViewController.content.view.transform
let origContentFrame = itemViewController.content.view.frame
// The content container provides the clipping for the content view,
// which, in case the source/dest aspect ratios don't match, makes
// it look like the content is expanding out from the source rect.
let contentContainer = UIView()
contentContainer.layer.masksToBounds = true
container.insertSubview(contentContainer, belowSubview: to.view)
let content = itemViewController.takeContent() let content = itemViewController.takeContent()
content.view.translatesAutoresizingMaskIntoConstraints = true content.view.translatesAutoresizingMaskIntoConstraints = true
content.view.transform = .identity container.insertSubview(content.view, belowSubview: to.view)
// The fade-in makes the aspect ratio handling look a little bit worse,
// but papers over the z-index change and potential corner radius change.
content.view.layer.opacity = 0
contentContainer.addSubview(content.view)
// Use a separate dimming view from to.view, so that the gallery controls can be in front of the moving content. // Use a separate dimming view from to.view, so that the gallery controls can be in front of the moving content.
let dimmingView = UIView() let dimmingView = UIView()
dimmingView.backgroundColor = .black dimmingView.backgroundColor = .black
dimmingView.frame = container.bounds dimmingView.frame = container.bounds
dimmingView.layer.opacity = 0 dimmingView.layer.opacity = 0
container.insertSubview(dimmingView, belowSubview: contentContainer) container.insertSubview(dimmingView, belowSubview: content.view)
to.view.backgroundColor = nil to.view.backgroundColor = nil
to.view.layer.opacity = 0 to.view.layer.opacity = 0
content.view.frame = sourceFrameInContainer
contentContainer.frame = sourceFrameInContainer content.view.layer.opacity = 0
let sourceAspectRatio: CGFloat = if sourceFrameInContainer.height > 0 {
sourceFrameInContainer.width / sourceFrameInContainer.height
} else {
0
}
let destAspectRatio: CGFloat = if destFrameInContainer.height > 0 {
destFrameInContainer.width / destFrameInContainer.height
} else {
0
}
let sourceSizeWithDestAspectRatioCenteredInContentContainer: CGRect
if 0.001 < abs(sourceAspectRatio - destAspectRatio) {
// asepct ratios are effectively equal
sourceSizeWithDestAspectRatioCenteredInContentContainer = CGRect(origin: .zero, size: sourceFrameInContainer.size)
} else if sourceAspectRatio < destAspectRatio {
// source aspect ratio is narrow/taller than dest
let width = sourceFrameInContainer.height * destAspectRatio
sourceSizeWithDestAspectRatioCenteredInContentContainer = CGRect(
x: -(width - sourceFrameInContainer.width) / 2,
y: 0,
width: width,
height: sourceFrameInContainer.height
)
} else {
// source aspect ratio is wider/shorter than dest
let height = sourceFrameInContainer.width / destAspectRatio
sourceSizeWithDestAspectRatioCenteredInContentContainer = CGRect(
x: 0,
y: -(height - sourceFrameInContainer.height) / 2,
width: sourceFrameInContainer.width,
height: height
)
}
content.view.frame = sourceSizeWithDestAspectRatioCenteredInContentContainer
container.layoutIfNeeded() container.layoutIfNeeded()
@ -147,14 +78,8 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated
itemViewController.setControlsVisible(false, animated: false, dueToUserInteraction: false) itemViewController.setControlsVisible(false, animated: false, dueToUserInteraction: false)
let duration = self.transitionDuration(using: transitionContext) let duration = self.transitionDuration(using: transitionContext)
// less bounce on bigger screens // rougly equivalent to duration: 0.35, bounce: 0.3
let spring = if UIDevice.current.userInterfaceIdiom == .pad { let spring = UISpringTimingParameters(mass: 1, stiffness: 322, damping: 25, initialVelocity: .zero)
// roughly equivalent to duration: 0.35, bounce: 0.2
UISpringTimingParameters(mass: 1, stiffness: 322, damping: 28, initialVelocity: .zero)
} else {
// roughly equivalent to duration: 0.35, bounce: 0.3
UISpringTimingParameters(mass: 1, stiffness: 322, damping: 25, initialVelocity: .zero)
}
let animator = UIViewPropertyAnimator(duration: duration, timingParameters: spring) let animator = UIViewPropertyAnimator(duration: duration, timingParameters: spring)
animator.addAnimations { animator.addAnimations {
@ -162,34 +87,24 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated
to.view.layer.opacity = 1 to.view.layer.opacity = 1
contentContainer.frame = destFrameInContainer content.view.frame = destFrameInContainer
content.view.frame = contentContainer.bounds
content.view.layer.opacity = 1 content.view.layer.opacity = 1
itemViewController.setControlsVisible(true, animated: false, dueToUserInteraction: false) itemViewController.setControlsVisible(true, animated: false, dueToUserInteraction: false)
if let sourceToDestTransform { if let sourceToDestTransform {
sourceSnapshot?.transform = sourceToDestTransform
self.sourceView.transform = sourceToDestTransform self.sourceView.transform = sourceToDestTransform
} }
} }
animator.addCompletion { _ in animator.addCompletion { _ in
sourceSnapshot?.removeFromSuperview()
self.sourceView.layer.opacity = 1
if sourceToDestTransform != nil {
self.sourceView.transform = origSourceTransform
}
contentContainer.removeFromSuperview()
dimmingView.removeFromSuperview() dimmingView.removeFromSuperview()
to.view.backgroundColor = .black to.view.backgroundColor = .black
// Reset the properties we changed before re-adding the content to the scroll view. if sourceToDestTransform != nil {
// (I would expect UIScrollView to effectively do this itself, but w/e.) self.sourceView.transform = origSourceTransform
content.view.transform = origContentTransform }
content.view.frame = origContentFrame
itemViewController.addContent() itemViewController.addContent()

View File

@ -139,10 +139,6 @@ extension GalleryViewController: GalleryItemViewControllerDelegate {
isBeingPresented isBeingPresented
} }
func isGalleryBeingDismissed() -> Bool {
isBeingDismissed
}
func addPresentationAnimationCompletion(_ block: @escaping () -> Void) { func addPresentationAnimationCompletion(_ block: @escaping () -> Void) {
presentationAnimationCompletionHandlers.append(block) presentationAnimationCompletionHandlers.append(block)
} }

View File

@ -1,26 +0,0 @@
//
// UIView+Utilities.swift
// GalleryVC
//
// Created by Shadowfacts on 11/24/24.
//
import UIKit
extension UIView {
var ancestorForInsertingSnapshot: UIView {
var view = self
while let superview = view.superview {
if superview.layer.masksToBounds {
return superview
} else if superview is UIScrollView {
return self
} else {
view = superview
}
}
return view
}
}

View File

@ -1,4 +1,4 @@
// swift-tools-version: 6.0 // swift-tools-version: 5.7
// The swift-tools-version declares the minimum version of Swift required to build this package. // The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription import PackageDescription
@ -23,15 +23,9 @@ let package = Package(
// Targets can depend on other targets in this package, and on products in packages this package depends on. // Targets can depend on other targets in this package, and on products in packages this package depends on.
.target( .target(
name: "InstanceFeatures", name: "InstanceFeatures",
dependencies: ["Pachyderm"], dependencies: ["Pachyderm"]),
swiftSettings: [
.swiftLanguageMode(.v5)
]),
.testTarget( .testTarget(
name: "InstanceFeaturesTests", name: "InstanceFeaturesTests",
dependencies: ["InstanceFeatures"], dependencies: ["InstanceFeatures"]),
swiftSettings: [
.swiftLanguageMode(.v5)
]),
] ]
) )

View File

@ -1,4 +1,4 @@
// swift-tools-version: 6.0 // swift-tools-version: 5.8
// The swift-tools-version declares the minimum version of Swift required to build this package. // The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription import PackageDescription
@ -18,10 +18,7 @@ let package = Package(
// Targets are the basic building blocks of a package, defining a module or a test suite. // Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies. // Targets can depend on other targets in this package and products from dependencies.
.target( .target(
name: "MatchedGeometryPresentation", name: "MatchedGeometryPresentation"),
swiftSettings: [
.swiftLanguageMode(.v5)
]),
// .testTarget( // .testTarget(
// name: "MatchedGeometryPresentationTests", // name: "MatchedGeometryPresentationTests",
// dependencies: ["MatchedGeometryPresentation"]), // dependencies: ["MatchedGeometryPresentation"]),

View File

@ -1,4 +1,4 @@
// swift-tools-version: 6.0 // swift-tools-version: 5.6
// The swift-tools-version declares the minimum version of Swift required to build this package. // The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription import PackageDescription
@ -26,15 +26,9 @@ let package = Package(
dependencies: [ dependencies: [
.product(name: "WebURL", package: "swift-url"), .product(name: "WebURL", package: "swift-url"),
.product(name: "WebURLFoundationExtras", package: "swift-url"), .product(name: "WebURLFoundationExtras", package: "swift-url"),
],
swiftSettings: [
.swiftLanguageMode(.v5)
]), ]),
.testTarget( .testTarget(
name: "PachydermTests", name: "PachydermTests",
dependencies: ["Pachyderm"], dependencies: ["Pachyderm"]),
swiftSettings: [
.swiftLanguageMode(.v5)
]),
] ]
) )

View File

@ -25,30 +25,27 @@ public struct Client: Sendable {
public var timeoutInterval: TimeInterval = 60 public var timeoutInterval: TimeInterval = 60
private static let dateFormatter: DateFormatter = { static let decoder: JSONDecoder = {
let decoder = JSONDecoder()
let formatter = DateFormatter() let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SZ" formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SZ"
formatter.timeZone = TimeZone(abbreviation: "UTC") formatter.timeZone = TimeZone(abbreviation: "UTC")
formatter.locale = Locale(identifier: "en_US_POSIX") formatter.locale = Locale(identifier: "en_US_POSIX")
return formatter let iso8601 = ISO8601DateFormatter()
}()
private static let iso8601Formatter = ISO8601DateFormatter()
private static func decodeDate(string: String) -> Date? {
// for the next time mastodon accidentally changes date formats >.>
return dateFormatter.date(from: string) ?? iso8601Formatter.date(from: string)
}
static let decoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom({ (decoder) in decoder.dateDecodingStrategy = .custom({ (decoder) in
let container = try decoder.singleValueContainer() let container = try decoder.singleValueContainer()
let str = try container.decode(String.self) let str = try container.decode(String.self)
if let date = Self.decodeDate(string: str) { // for the next time mastodon accidentally changes date formats >.>
if let date = formatter.date(from: str) {
return date
} else if let date = iso8601.date(from: str) {
return date return date
} else { } else {
throw DecodingError.typeMismatch(Date.self, .init(codingPath: container.codingPath, debugDescription: "unexpected date format: \(str)")) throw DecodingError.typeMismatch(Date.self, .init(codingPath: container.codingPath, debugDescription: "unexpected date format: \(str)"))
} }
}) })
return decoder return decoder
}() }()
@ -108,15 +105,6 @@ public struct Client: Sendable {
return task return task
} }
private func error(from response: HTTPURLResponse) -> ErrorType {
if response.statusCode == 429,
let date = response.value(forHTTPHeaderField: "X-RateLimit-Reset").flatMap(Self.decodeDate) {
return .rateLimited(date)
} else {
return .unexpectedStatus(response.statusCode)
}
}
@discardableResult @discardableResult
public func run<Result: Sendable>(_ request: Request<Result>) async throws -> (Result, Pagination?) { public func run<Result: Sendable>(_ request: Request<Result>) async throws -> (Result, Pagination?) {
return try await withCheckedThrowingContinuation { continuation in return try await withCheckedThrowingContinuation { continuation in
@ -587,8 +575,6 @@ extension Client {
return "Invalid Model" return "Invalid Model"
case .mastodonError(let code, let error): case .mastodonError(let code, let error):
return "Server Error (\(code)): \(error)" return "Server Error (\(code)): \(error)"
case .rateLimited(let reset):
return "Rate Limited Until \(reset.formatted(date: .omitted, time: .standard))"
} }
} }
} }
@ -599,7 +585,6 @@ extension Client {
case invalidResponse case invalidResponse
case invalidModel(Swift.Error) case invalidModel(Swift.Error)
case mastodonError(Int, String) case mastodonError(Int, String)
case rateLimited(Date)
} }
enum NodeInfoError: LocalizedError { enum NodeInfoError: LocalizedError {

View File

@ -22,12 +22,7 @@ public struct Emoji: Codable, Sendable {
let container = try decoder.container(keyedBy: CodingKeys.self) let container = try decoder.container(keyedBy: CodingKeys.self)
self.shortcode = try container.decode(String.self, forKey: .shortcode) self.shortcode = try container.decode(String.self, forKey: .shortcode)
do { self.url = try container.decode(WebURL.self, forKey: .url)
self.url = try container.decode(WebURL.self, forKey: .url)
} catch {
let s = try? container.decode(String.self, forKey: .url)
throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath + [CodingKeys.url], debugDescription: "Could not decode URL '\(s ?? "<failed to decode string>")'", underlyingError: error))
}
self.staticURL = try container.decode(WebURL.self, forKey: .staticURL) self.staticURL = try container.decode(WebURL.self, forKey: .staticURL)
self.visibleInPicker = try container.decode(Bool.self, forKey: .visibleInPicker) self.visibleInPicker = try container.decode(Bool.self, forKey: .visibleInPicker)
self.category = try container.decodeIfPresent(String.self, forKey: .category) self.category = try container.decodeIfPresent(String.self, forKey: .category)

View File

@ -197,72 +197,72 @@ class NotificationGroupTests: XCTestCase {
func testGroupSimple() { func testGroupSimple() {
let groups = NotificationGroup.createGroups(notifications: [likeA1, likeA2], only: [.favourite]) let groups = NotificationGroup.createGroups(notifications: [likeA1, likeA2], only: [.favourite])
XCTAssertEqual(groups, [NotificationGroup(notifications: [likeA1, likeA2], kind: .favourite)!]) XCTAssertEqual(groups, [NotificationGroup(notifications: [likeA1, likeA2])!])
} }
func testGroupWithOtherGroupableInBetween() { func testGroupWithOtherGroupableInBetween() {
let groups = NotificationGroup.createGroups(notifications: [likeA1, likeB, likeA2], only: [.favourite]) let groups = NotificationGroup.createGroups(notifications: [likeA1, likeB, likeA2], only: [.favourite])
XCTAssertEqual(groups, [ XCTAssertEqual(groups, [
NotificationGroup(notifications: [likeA1, likeA2], kind: .favourite)!, NotificationGroup(notifications: [likeA1, likeA2])!,
NotificationGroup(notifications: [likeB], kind: .favourite)!, NotificationGroup(notifications: [likeB])!,
]) ])
} }
func testDontGroupWithUngroupableInBetween() { func testDontGroupWithUngroupableInBetween() {
let groups = NotificationGroup.createGroups(notifications: [likeA1, mentionB, likeA2], only: [.favourite]) let groups = NotificationGroup.createGroups(notifications: [likeA1, mentionB, likeA2], only: [.favourite])
XCTAssertEqual(groups, [ XCTAssertEqual(groups, [
NotificationGroup(notifications: [likeA1], kind: .favourite)!, NotificationGroup(notifications: [likeA1])!,
NotificationGroup(notifications: [mentionB], kind: .mention)!, NotificationGroup(notifications: [mentionB])!,
NotificationGroup(notifications: [likeA2], kind: .favourite)!, NotificationGroup(notifications: [likeA2])!,
]) ])
} }
func testMergeSimpleGroups() { func testMergeSimpleGroups() {
let group1 = NotificationGroup(notifications: [likeA1], kind: .favourite)! let group1 = NotificationGroup(notifications: [likeA1])!
let group2 = NotificationGroup(notifications: [likeA2], kind: .favourite)! let group2 = NotificationGroup(notifications: [likeA2])!
let merged = NotificationGroup.mergeGroups(first: [group1], second: [group2], only: [.favourite]) let merged = NotificationGroup.mergeGroups(first: [group1], second: [group2], only: [.favourite])
XCTAssertEqual(merged, [ XCTAssertEqual(merged, [
NotificationGroup(notifications: [likeA1, likeA2], kind: .favourite)! NotificationGroup(notifications: [likeA1, likeA2])!
]) ])
} }
func testMergeGroupsWithOtherGroupableInBetween() { func testMergeGroupsWithOtherGroupableInBetween() {
let group1 = NotificationGroup(notifications: [likeA1], kind: .favourite)! let group1 = NotificationGroup(notifications: [likeA1])!
let group2 = NotificationGroup(notifications: [likeB], kind: .favourite)! let group2 = NotificationGroup(notifications: [likeB])!
let group3 = NotificationGroup(notifications: [likeA2], kind: .favourite)! let group3 = NotificationGroup(notifications: [likeA2])!
let merged = NotificationGroup.mergeGroups(first: [group1, group2], second: [group3], only: [.favourite]) let merged = NotificationGroup.mergeGroups(first: [group1, group2], second: [group3], only: [.favourite])
XCTAssertEqual(merged, [ XCTAssertEqual(merged, [
NotificationGroup(notifications: [likeA1, likeA2], kind: .favourite)!, NotificationGroup(notifications: [likeA1, likeA2])!,
NotificationGroup(notifications: [likeB], kind: .favourite)!, NotificationGroup(notifications: [likeB])!,
]) ])
let merged2 = NotificationGroup.mergeGroups(first: [group1], second: [group2, group3], only: [.favourite]) let merged2 = NotificationGroup.mergeGroups(first: [group1], second: [group2, group3], only: [.favourite])
XCTAssertEqual(merged2, [ XCTAssertEqual(merged2, [
NotificationGroup(notifications: [likeA1, likeA2], kind: .favourite)!, NotificationGroup(notifications: [likeA1, likeA2])!,
NotificationGroup(notifications: [likeB], kind: .favourite)!, NotificationGroup(notifications: [likeB])!,
]) ])
let group4 = NotificationGroup(notifications: [likeB2], kind: .favourite)! let group4 = NotificationGroup(notifications: [likeB2])!
let group5 = NotificationGroup(notifications: [mentionB], kind: .mention)! let group5 = NotificationGroup(notifications: [mentionB])!
let merged3 = NotificationGroup.mergeGroups(first: [group1, group5, group2], second: [group4, group3], only: [.favourite]) let merged3 = NotificationGroup.mergeGroups(first: [group1, group5, group2], second: [group4, group3], only: [.favourite])
print(merged3.count) print(merged3.count)
XCTAssertEqual(merged3, [ XCTAssertEqual(merged3, [
group1, group1,
group5, group5,
NotificationGroup(notifications: [likeB, likeB2], kind: .favourite), NotificationGroup(notifications: [likeB, likeB2]),
group3 group3
]) ])
} }
func testDontMergeWithUngroupableInBetween() { func testDontMergeWithUngroupableInBetween() {
let group1 = NotificationGroup(notifications: [likeA1], kind: .favourite)! let group1 = NotificationGroup(notifications: [likeA1])!
let group2 = NotificationGroup(notifications: [mentionB], kind: .mention)! let group2 = NotificationGroup(notifications: [mentionB])!
let group3 = NotificationGroup(notifications: [likeA2], kind: .favourite)! let group3 = NotificationGroup(notifications: [likeA2])!
let merged = NotificationGroup.mergeGroups(first: [group1, group2], second: [group3], only: [.favourite]) let merged = NotificationGroup.mergeGroups(first: [group1, group2], second: [group3], only: [.favourite])
XCTAssertEqual(merged, [ XCTAssertEqual(merged, [
NotificationGroup(notifications: [likeA1], kind: .favourite)!, NotificationGroup(notifications: [likeA1])!,
NotificationGroup(notifications: [mentionB], kind: .mention)!, NotificationGroup(notifications: [mentionB])!,
NotificationGroup(notifications: [likeA2], kind: .favourite)!, NotificationGroup(notifications: [likeA2])!,
]) ])
} }

View File

@ -1,4 +1,4 @@
// swift-tools-version: 6.0 // swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package. // The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription import PackageDescription
@ -23,17 +23,10 @@ let package = Package(
// Targets can depend on other targets in this package and products from dependencies. // Targets can depend on other targets in this package and products from dependencies.
.target( .target(
name: "PushNotifications", name: "PushNotifications",
dependencies: ["UserAccounts", "Pachyderm"], dependencies: ["UserAccounts", "Pachyderm"]
swiftSettings: [
.swiftLanguageMode(.v5)
]
), ),
.testTarget( .testTarget(
name: "PushNotificationsTests", name: "PushNotificationsTests",
dependencies: ["PushNotifications"], dependencies: ["PushNotifications"]),
swiftSettings: [
.swiftLanguageMode(.v5)
]
),
] ]
) )

View File

@ -1,4 +1,4 @@
// swift-tools-version: 6.0 // swift-tools-version: 5.7
// The swift-tools-version declares the minimum version of Swift required to build this package. // The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription import PackageDescription
@ -23,15 +23,9 @@ let package = Package(
// Targets can depend on other targets in this package, and on products in packages this package depends on. // Targets can depend on other targets in this package, and on products in packages this package depends on.
.target( .target(
name: "TTTKit", name: "TTTKit",
dependencies: [], dependencies: []),
swiftSettings: [
.swiftLanguageMode(.v5)
]),
.testTarget( .testTarget(
name: "TTTKitTests", name: "TTTKitTests",
dependencies: ["TTTKit"], dependencies: ["TTTKit"]),
swiftSettings: [
.swiftLanguageMode(.v5)
]),
] ]
) )

View File

@ -1,4 +1,4 @@
// swift-tools-version: 6.0 // swift-tools-version: 5.7
// The swift-tools-version declares the minimum version of Swift required to build this package. // The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription import PackageDescription
@ -23,10 +23,7 @@ let package = Package(
// Targets can depend on other targets in this package, and on products in packages this package depends on. // Targets can depend on other targets in this package, and on products in packages this package depends on.
.target( .target(
name: "TuskerComponents", name: "TuskerComponents",
dependencies: [], dependencies: []),
swiftSettings: [
.swiftLanguageMode(.v5)
]),
// .testTarget( // .testTarget(
// name: "TuskerComponentsTests", // name: "TuskerComponentsTests",
// dependencies: ["TuskerComponents"]), // dependencies: ["TuskerComponents"]),

View File

@ -1,4 +1,4 @@
// swift-tools-version: 6.0 // swift-tools-version: 5.8
// The swift-tools-version declares the minimum version of Swift required to build this package. // The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription import PackageDescription
@ -22,17 +22,11 @@ let package = Package(
// Targets can depend on other targets in this package and products from dependencies. // Targets can depend on other targets in this package and products from dependencies.
.target( .target(
name: "TuskerPreferences", name: "TuskerPreferences",
dependencies: ["Pachyderm"], dependencies: ["Pachyderm"]
swiftSettings: [
.swiftLanguageMode(.v5)
]
), ),
.testTarget( .testTarget(
name: "TuskerPreferencesTests", name: "TuskerPreferencesTests",
dependencies: ["TuskerPreferences"], dependencies: ["TuskerPreferences"]
swiftSettings: [
.swiftLanguageMode(.v5)
]
) )
] ]
) )

View File

@ -1,4 +1,4 @@
// swift-tools-version: 6.0 // swift-tools-version: 5.7
// The swift-tools-version declares the minimum version of Swift required to build this package. // The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription import PackageDescription
@ -23,10 +23,7 @@ let package = Package(
// Targets can depend on other targets in this package, and on products in packages this package depends on. // Targets can depend on other targets in this package, and on products in packages this package depends on.
.target( .target(
name: "UserAccounts", name: "UserAccounts",
dependencies: ["Pachyderm"], dependencies: ["Pachyderm"]),
swiftSettings: [
.swiftLanguageMode(.v5)
]),
// .testTarget( // .testTarget(
// name: "UserAccountsTests", // name: "UserAccountsTests",
// dependencies: ["UserAccounts"]), // dependencies: ["UserAccounts"]),

View File

@ -204,16 +204,20 @@
D691771729A710520054D7EF /* ProfileNoContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691771629A710520054D7EF /* ProfileNoContentCollectionViewCell.swift */; }; D691771729A710520054D7EF /* ProfileNoContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691771629A710520054D7EF /* ProfileNoContentCollectionViewCell.swift */; };
D691771929A7B8820054D7EF /* ProfileHeaderMovedOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691771829A7B8820054D7EF /* ProfileHeaderMovedOverlayView.swift */; }; D691771929A7B8820054D7EF /* ProfileHeaderMovedOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691771829A7B8820054D7EF /* ProfileHeaderMovedOverlayView.swift */; };
D691772E29AA5D420054D7EF /* UserActivityHandlingContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691772D29AA5D420054D7EF /* UserActivityHandlingContext.swift */; }; D691772E29AA5D420054D7EF /* UserActivityHandlingContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691772D29AA5D420054D7EF /* UserActivityHandlingContext.swift */; };
D69261232BB3AEFB0023152C /* VideoOverlayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69261222BB3AEFB0023152C /* VideoOverlayViewController.swift */; };
D69261272BB3BA610023152C /* Box.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69261262BB3BA610023152C /* Box.swift */; }; D69261272BB3BA610023152C /* Box.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69261262BB3BA610023152C /* Box.swift */; };
D6934F2C2BA7AD32002B1C8D /* GalleryVC in Frameworks */ = {isa = PBXBuildFile; productRef = D6934F2B2BA7AD32002B1C8D /* GalleryVC */; }; D6934F2C2BA7AD32002B1C8D /* GalleryVC in Frameworks */ = {isa = PBXBuildFile; productRef = D6934F2B2BA7AD32002B1C8D /* GalleryVC */; };
D6934F2E2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F2D2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift */; }; D6934F2E2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F2D2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift */; };
D6934F302BA7AF91002B1C8D /* GrayscalableImageGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F2F2BA7AF91002B1C8D /* GrayscalableImageGalleryContentViewController.swift */; }; D6934F302BA7AF91002B1C8D /* ImageGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F2F2BA7AF91002B1C8D /* ImageGalleryContentViewController.swift */; };
D6934F322BA7E43E002B1C8D /* LoadingGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F312BA7E43E002B1C8D /* LoadingGalleryContentViewController.swift */; };
D6934F342BA8D65A002B1C8D /* ImageGalleryDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F332BA8D65A002B1C8D /* ImageGalleryDataSource.swift */; }; D6934F342BA8D65A002B1C8D /* ImageGalleryDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F332BA8D65A002B1C8D /* ImageGalleryDataSource.swift */; };
D6934F362BA8E020002B1C8D /* GifvGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F352BA8E020002B1C8D /* GifvGalleryContentViewController.swift */; }; D6934F362BA8E020002B1C8D /* GifvGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F352BA8E020002B1C8D /* GifvGalleryContentViewController.swift */; };
D6934F382BA8E2B7002B1C8D /* GifvController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F372BA8E2B7002B1C8D /* GifvController.swift */; }; D6934F382BA8E2B7002B1C8D /* GifvController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F372BA8E2B7002B1C8D /* GifvController.swift */; };
D6934F3C2BAA0F80002B1C8D /* GrayscalableVideoGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F3B2BAA0F80002B1C8D /* GrayscalableVideoGalleryContentViewController.swift */; }; D6934F3A2BA8F3D7002B1C8D /* FallbackGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F392BA8F3D7002B1C8D /* FallbackGalleryContentViewController.swift */; };
D6934F3C2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F3B2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift */; };
D6934F3E2BAA19D5002B1C8D /* ImageActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F3D2BAA19D5002B1C8D /* ImageActivityItemSource.swift */; }; D6934F3E2BAA19D5002B1C8D /* ImageActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F3D2BAA19D5002B1C8D /* ImageActivityItemSource.swift */; };
D6934F402BAA19EC002B1C8D /* VideoActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F3F2BAA19EC002B1C8D /* VideoActivityItemSource.swift */; }; D6934F402BAA19EC002B1C8D /* VideoActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F3F2BAA19EC002B1C8D /* VideoActivityItemSource.swift */; };
D6934F422BAC7D6E002B1C8D /* VideoControlsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F412BAC7D6E002B1C8D /* VideoControlsViewController.swift */; };
D693A72825CF282E003A14E2 /* TrendingHashtagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */; }; D693A72825CF282E003A14E2 /* TrendingHashtagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */; };
D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */; }; D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */; };
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5823FE24300061E07D /* InteractivePushTransition.swift */; }; D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5823FE24300061E07D /* InteractivePushTransition.swift */; };
@ -635,15 +639,19 @@
D691771629A710520054D7EF /* ProfileNoContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileNoContentCollectionViewCell.swift; sourceTree = "<group>"; }; D691771629A710520054D7EF /* ProfileNoContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileNoContentCollectionViewCell.swift; sourceTree = "<group>"; };
D691771829A7B8820054D7EF /* ProfileHeaderMovedOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderMovedOverlayView.swift; sourceTree = "<group>"; }; D691771829A7B8820054D7EF /* ProfileHeaderMovedOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderMovedOverlayView.swift; sourceTree = "<group>"; };
D691772D29AA5D420054D7EF /* UserActivityHandlingContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityHandlingContext.swift; sourceTree = "<group>"; }; D691772D29AA5D420054D7EF /* UserActivityHandlingContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityHandlingContext.swift; sourceTree = "<group>"; };
D69261222BB3AEFB0023152C /* VideoOverlayViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoOverlayViewController.swift; sourceTree = "<group>"; };
D69261262BB3BA610023152C /* Box.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Box.swift; sourceTree = "<group>"; }; D69261262BB3BA610023152C /* Box.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Box.swift; sourceTree = "<group>"; };
D6934F2D2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusAttachmentsGalleryDataSource.swift; sourceTree = "<group>"; }; D6934F2D2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusAttachmentsGalleryDataSource.swift; sourceTree = "<group>"; };
D6934F2F2BA7AF91002B1C8D /* GrayscalableImageGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GrayscalableImageGalleryContentViewController.swift; sourceTree = "<group>"; }; D6934F2F2BA7AF91002B1C8D /* ImageGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGalleryContentViewController.swift; sourceTree = "<group>"; };
D6934F312BA7E43E002B1C8D /* LoadingGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingGalleryContentViewController.swift; sourceTree = "<group>"; };
D6934F332BA8D65A002B1C8D /* ImageGalleryDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGalleryDataSource.swift; sourceTree = "<group>"; }; D6934F332BA8D65A002B1C8D /* ImageGalleryDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGalleryDataSource.swift; sourceTree = "<group>"; };
D6934F352BA8E020002B1C8D /* GifvGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvGalleryContentViewController.swift; sourceTree = "<group>"; }; D6934F352BA8E020002B1C8D /* GifvGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvGalleryContentViewController.swift; sourceTree = "<group>"; };
D6934F372BA8E2B7002B1C8D /* GifvController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvController.swift; sourceTree = "<group>"; }; D6934F372BA8E2B7002B1C8D /* GifvController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvController.swift; sourceTree = "<group>"; };
D6934F3B2BAA0F80002B1C8D /* GrayscalableVideoGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GrayscalableVideoGalleryContentViewController.swift; sourceTree = "<group>"; }; D6934F392BA8F3D7002B1C8D /* FallbackGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FallbackGalleryContentViewController.swift; sourceTree = "<group>"; };
D6934F3B2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoGalleryContentViewController.swift; sourceTree = "<group>"; };
D6934F3D2BAA19D5002B1C8D /* ImageActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageActivityItemSource.swift; sourceTree = "<group>"; }; D6934F3D2BAA19D5002B1C8D /* ImageActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageActivityItemSource.swift; sourceTree = "<group>"; };
D6934F3F2BAA19EC002B1C8D /* VideoActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoActivityItemSource.swift; sourceTree = "<group>"; }; D6934F3F2BAA19EC002B1C8D /* VideoActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoActivityItemSource.swift; sourceTree = "<group>"; };
D6934F412BAC7D6E002B1C8D /* VideoControlsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoControlsViewController.swift; sourceTree = "<group>"; };
D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingHashtagsViewController.swift; sourceTree = "<group>"; }; D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingHashtagsViewController.swift; sourceTree = "<group>"; };
D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnhancedNavigationViewController.swift; sourceTree = "<group>"; }; D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnhancedNavigationViewController.swift; sourceTree = "<group>"; };
D693DE5823FE24300061E07D /* InteractivePushTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractivePushTransition.swift; sourceTree = "<group>"; }; D693DE5823FE24300061E07D /* InteractivePushTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractivePushTransition.swift; sourceTree = "<group>"; };
@ -893,9 +901,13 @@
children = ( children = (
D6934F2D2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift */, D6934F2D2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift */,
D6934F332BA8D65A002B1C8D /* ImageGalleryDataSource.swift */, D6934F332BA8D65A002B1C8D /* ImageGalleryDataSource.swift */,
D6934F2F2BA7AF91002B1C8D /* GrayscalableImageGalleryContentViewController.swift */, D6934F312BA7E43E002B1C8D /* LoadingGalleryContentViewController.swift */,
D6934F2F2BA7AF91002B1C8D /* ImageGalleryContentViewController.swift */,
D6934F352BA8E020002B1C8D /* GifvGalleryContentViewController.swift */, D6934F352BA8E020002B1C8D /* GifvGalleryContentViewController.swift */,
D6934F3B2BAA0F80002B1C8D /* GrayscalableVideoGalleryContentViewController.swift */, D6934F3B2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift */,
D69261222BB3AEFB0023152C /* VideoOverlayViewController.swift */,
D6934F412BAC7D6E002B1C8D /* VideoControlsViewController.swift */,
D6934F392BA8F3D7002B1C8D /* FallbackGalleryContentViewController.swift */,
); );
path = Gallery; path = Gallery;
sourceTree = "<group>"; sourceTree = "<group>";
@ -2113,6 +2125,7 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
D6934F422BAC7D6E002B1C8D /* VideoControlsViewController.swift in Sources */,
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */, 0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */,
D6C3F4F5298ED0890009FCFF /* LocalPredicateStatusesViewController.swift in Sources */, D6C3F4F5298ED0890009FCFF /* LocalPredicateStatusesViewController.swift in Sources */,
D691771729A710520054D7EF /* ProfileNoContentCollectionViewCell.swift in Sources */, D691771729A710520054D7EF /* ProfileNoContentCollectionViewCell.swift in Sources */,
@ -2159,7 +2172,8 @@
D6A4DCCC2553667800D9DE31 /* FastAccountSwitcherViewController.swift in Sources */, D6A4DCCC2553667800D9DE31 /* FastAccountSwitcherViewController.swift in Sources */,
D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */, D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */,
D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */, D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */,
D6934F3C2BAA0F80002B1C8D /* GrayscalableVideoGalleryContentViewController.swift in Sources */, D69261232BB3AEFB0023152C /* VideoOverlayViewController.swift in Sources */,
D6934F3C2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift in Sources */,
D6F6A552291F098700F496A8 /* RenameListService.swift in Sources */, D6F6A552291F098700F496A8 /* RenameListService.swift in Sources */,
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */, D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */,
D698F46B2BD079F00054DB14 /* AnnouncementListRow.swift in Sources */, D698F46B2BD079F00054DB14 /* AnnouncementListRow.swift in Sources */,
@ -2179,6 +2193,7 @@
D6D94955298963A900C59229 /* Colors.swift in Sources */, D6D94955298963A900C59229 /* Colors.swift in Sources */,
D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */, D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */,
D625E4822588262A0074BB2B /* DraggableTableViewCell.swift in Sources */, D625E4822588262A0074BB2B /* DraggableTableViewCell.swift in Sources */,
D6934F322BA7E43E002B1C8D /* LoadingGalleryContentViewController.swift in Sources */,
D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */, D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */,
D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */, D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */,
D64A50482C739DEA009D7193 /* BaseMainTabBarViewController.swift in Sources */, D64A50482C739DEA009D7193 /* BaseMainTabBarViewController.swift in Sources */,
@ -2206,6 +2221,7 @@
D630C3CC2BC5FD4600208903 /* GetAuthorizationTokenService.swift in Sources */, D630C3CC2BC5FD4600208903 /* GetAuthorizationTokenService.swift in Sources */,
D61DC84B28F4FD2000B82C6E /* ProfileHeaderCollectionViewCell.swift in Sources */, D61DC84B28F4FD2000B82C6E /* ProfileHeaderCollectionViewCell.swift in Sources */,
D61A45E828DF477D002BE511 /* LoadingCollectionViewCell.swift in Sources */, D61A45E828DF477D002BE511 /* LoadingCollectionViewCell.swift in Sources */,
D6934F3A2BA8F3D7002B1C8D /* FallbackGalleryContentViewController.swift in Sources */,
D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */, D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */,
D68A76EC295369A8001DA1B3 /* AboutView.swift in Sources */, D68A76EC295369A8001DA1B3 /* AboutView.swift in Sources */,
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */, 04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */,
@ -2328,7 +2344,7 @@
D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */, D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */,
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */, D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,
D64A50BE2C752247009D7193 /* AdaptableNavigationController.swift in Sources */, D64A50BE2C752247009D7193 /* AdaptableNavigationController.swift in Sources */,
D6934F302BA7AF91002B1C8D /* GrayscalableImageGalleryContentViewController.swift in Sources */, D6934F302BA7AF91002B1C8D /* ImageGalleryContentViewController.swift in Sources */,
D6934F3E2BAA19D5002B1C8D /* ImageActivityItemSource.swift in Sources */, D6934F3E2BAA19D5002B1C8D /* ImageActivityItemSource.swift in Sources */,
D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */, D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */,
D61F75962937037800C0B37F /* ToggleFollowHashtagService.swift in Sources */, D61F75962937037800C0B37F /* ToggleFollowHashtagService.swift in Sources */,

View File

@ -375,14 +375,13 @@ class MastodonCachePersistentStore: NSPersistentCloudKitContainer, @unchecked Se
} }
} }
func addAll(notifications: [Pachyderm.Notification], in context: NSManagedObjectContext? = nil, completion: (() -> Void)? = nil) { func addAll(notifications: [Pachyderm.Notification], completion: (() -> Void)? = nil) {
let context = context ?? backgroundContext backgroundContext.perform {
context.perform {
let statuses = notifications.compactMap { $0.status } let statuses = notifications.compactMap { $0.status }
let accounts = notifications.map { $0.account } let accounts = notifications.map { $0.account }
statuses.forEach { self.upsert(status: $0, context: context) } statuses.forEach { self.upsert(status: $0, context: self.backgroundContext) }
accounts.forEach { self.upsert(account: $0, in: context) } accounts.forEach { self.upsert(account: $0, in: self.backgroundContext) }
self.save(context: context) self.save(context: self.backgroundContext)
completion?() completion?()
statuses.forEach { self.statusSubject.send($0.id) } statuses.forEach { self.statusSubject.send($0.id) }
accounts.forEach { self.accountSubject.send($0.id) } accounts.forEach { self.accountSubject.send($0.id) }

View File

@ -484,11 +484,3 @@ extension ConversationViewController: StatusBarTappableViewController {
} }
} }
} }
extension ConversationViewController: RefreshableViewController {
func refresh() {
Task {
await refreshContext()
}
}
}

View File

@ -16,11 +16,15 @@ struct TrendingLinkCardView: View {
let card: Card let card: Card
private var imageURL: URL? { private var imageURL: URL? {
card.image.flatMap { URL($0) } if let image = card.image {
URL(image)
} else {
nil
}
} }
private var descriptionText: String { private var descriptionText: String {
let converter = TextConverter(configuration: .init(insertNewlines: false)) var converter = TextConverter(configuration: .init(insertNewlines: false))
return converter.convert(html: card.description) return converter.convert(html: card.description)
} }

View File

@ -1,13 +1,15 @@
// //
// FallbackGalleryContentViewController.swift // FallbackGalleryContentViewController.swift
// GalleryVC // Tusker
// //
// Created by Shadowfacts on 3/18/24. // Created by Shadowfacts on 3/18/24.
// Copyright © 2024 Shadowfacts. All rights reserved. // Copyright © 2024 Shadowfacts. All rights reserved.
// //
import UIKit import UIKit
import GalleryVC
import QuickLook import QuickLook
import Pachyderm
private class FallbackGalleryContentViewController: QLPreviewController { private class FallbackGalleryContentViewController: QLPreviewController {
private let previewItem = GalleryPreviewItem() private let previewItem = GalleryPreviewItem()
@ -50,40 +52,40 @@ extension FallbackGalleryContentViewController: QLPreviewControllerDataSource {
} }
} }
public class FallbackGalleryNavigationController: UINavigationController, GalleryContentViewController { class FallbackGalleryNavigationController: UINavigationController, GalleryContentViewController {
public init(url: URL) { init(url: URL) {
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
self.viewControllers = [FallbackGalleryContentViewController(url: url)] self.viewControllers = [FallbackGalleryContentViewController(url: url)]
} }
public override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
container?.disableGalleryScrollAndZoom() container?.disableGalleryScrollAndZoom()
} }
public required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
// MARK: GalleryContentViewController // MARK: GalleryContentViewController
public weak var container: (any GalleryContentViewControllerContainer)? weak var container: (any GalleryVC.GalleryContentViewControllerContainer)?
public var contentSize: CGSize { var contentSize: CGSize {
.zero .zero
} }
public var activityItemsForSharing: [Any] { var activityItemsForSharing: [Any] {
[] []
} }
public var caption: String? { var caption: String? {
nil nil
} }
public var presentationAnimation: GalleryContentPresentationAnimation { var canAnimateFromSourceView: Bool {
.fade false
} }
} }

View File

@ -69,8 +69,4 @@ class GifvGalleryContentViewController: UIViewController, GalleryContentViewCont
[VideoActivityItemSource(asset: controller.item.asset, url: url)] [VideoActivityItemSource(asset: controller.item.asset, url: url)]
} }
var presentationAnimation: GalleryContentPresentationAnimation {
.fromSourceViewWithoutSnapshot
}
} }

View File

@ -1,60 +0,0 @@
//
// GrayscalableImageGalleryContentViewController.swift
// Tusker
//
// Created by Shadowfacts on 11/21/24.
// Copyright © 2024 Shadowfacts. All rights reserved.
//
import UIKit
import TuskerComponents
import GalleryVC
class GrayscalableImageGalleryContentViewController: GalleryVC.ImageGalleryContentViewController {
private let url: URL
private let originalImage: UIImage
private let originalData: Data?
private var isGrayscale = false
init(url: URL, caption: String?, originalData: Data?, image: UIImage, gifController: GIFController?) {
self.url = url
self.originalImage = image
self.originalData = originalData
super.init(image: image, caption: caption, gifController: gifController)
isGrayscale = Preferences.shared.grayscaleImages
if isGrayscale {
self.image = ImageGrayscalifier.convert(url: url, image: image) ?? image
}
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
}
@MainActor required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func preferencesChanged() {
if isGrayscale != Preferences.shared.grayscaleImages {
isGrayscale = Preferences.shared.grayscaleImages
let image = if isGrayscale {
ImageGrayscalifier.convert(url: url, image: originalImage)
} else {
originalImage
}
if let image {
self.image = image
}
}
}
override var activityItemsForSharing: [Any] {
if let data = originalData ?? image.pngData() {
return [ImageActivityItemSource(data: data, url: url, image: image)]
} else {
return []
}
}
}

View File

@ -1,83 +0,0 @@
//
// GrayscalableVideoGalleryContentViewController.swift
// Tusker
//
// Created by Shadowfacts on 11/21/24.
// Copyright © 2024 Shadowfacts. All rights reserved.
//
import UIKit
import GalleryVC
import AVFoundation
class GrayscalableVideoGalleryContentViewController: GalleryVC.VideoGalleryContentViewController {
private var audioSessionToken: AudioSessionCoordinator.Token?
private var isGrayscale: Bool
private var isFirstAppearance = true
override init(url: URL, caption: String?) {
self.isGrayscale = Preferences.shared.grayscaleImages
super.init(url: url, caption: caption)
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
}
@MainActor required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override class func createItem(asset: AVAsset) -> AVPlayerItem {
let item = AVPlayerItem(asset: asset)
if Preferences.shared.grayscaleImages {
#if os(visionOS)
#warning("Use async AVVideoComposition CIFilter initializer")
#else
let filter = CIFilter(name: "CIColorMonochrome")!
filter.setValue(CIColor(red: 0.85, green: 0.85, blue: 0.85), forKey: "inputColor")
filter.setValue(1.0, forKey: "inputIntensity")
item.videoComposition = AVVideoComposition(asset: asset, applyingCIFiltersWithHandler: { request in
filter.setValue(request.sourceImage, forKey: "inputImage")
request.finish(with: filter.outputImage!, context: nil)
})
#endif
}
return item
}
@objc private func preferencesChanged() {
if isGrayscale != Preferences.shared.grayscaleImages {
let isPlaying = player.rate > 0
isGrayscale = Preferences.shared.grayscaleImages
replaceCurrentItem(with: Self.createItem(asset: item.asset))
if isPlaying {
player.play()
}
}
}
override func galleryContentDidAppear() {
super.galleryContentDidAppear()
let wasFirstAppearance = isFirstAppearance
isFirstAppearance = false
audioSessionToken = AudioSessionCoordinator.shared.beginPlayback(mode: .video) {
if wasFirstAppearance {
DispatchQueue.main.async {
self.player.play()
}
}
}
}
override func galleryContentWillDisappear() {
super.galleryContentWillDisappear()
if let audioSessionToken {
AudioSessionCoordinator.shared.endPlayback(token: audioSessionToken)
}
}
}

View File

@ -1,22 +1,22 @@
// //
// ImageGalleryContentViewController.swift // ImageGalleryContentViewController.swift
// GalleryVC // Tusker
// //
// Created by Shadowfacts on 3/17/24. // Created by Shadowfacts on 3/17/24.
// Copyright © 2024 Shadowfacts. All rights reserved. // Copyright © 2024 Shadowfacts. All rights reserved.
// //
import UIKit import UIKit
import GalleryVC
import Pachyderm
import TuskerComponents import TuskerComponents
@preconcurrency import VisionKit @preconcurrency import VisionKit
open class ImageGalleryContentViewController: UIViewController, GalleryContentViewController { class ImageGalleryContentViewController: UIViewController, GalleryContentViewController {
public let caption: String? let url: URL
public var image: UIImage { let caption: String?
didSet { let originalData: Data?
imageView?.image = image let image: UIImage
}
}
let gifController: GIFController? let gifController: GIFController?
private var imageView: GIFImageView! private var imageView: GIFImageView!
@ -27,8 +27,12 @@ open class ImageGalleryContentViewController: UIViewController, GalleryContentVi
@available(iOS 16.0, macCatalyst 17.0, *) @available(iOS 16.0, macCatalyst 17.0, *)
private var analysisInteraction: ImageAnalysisInteraction? { _analysisInteraction as? ImageAnalysisInteraction } private var analysisInteraction: ImageAnalysisInteraction? { _analysisInteraction as? ImageAnalysisInteraction }
public init(image: UIImage, caption: String?, gifController: GIFController?) { private var isGrayscale = false
init(url: URL, caption: String?, originalData: Data?, image: UIImage, gifController: GIFController?) {
self.url = url
self.caption = caption self.caption = caption
self.originalData = originalData
self.image = image self.image = image
self.gifController = gifController self.gifController = gifController
@ -37,14 +41,21 @@ open class ImageGalleryContentViewController: UIViewController, GalleryContentVi
preferredContentSize = image.size preferredContentSize = image.size
} }
public required init?(coder: NSCoder) { required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
public override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
imageView = GIFImageView(image: image) isGrayscale = Preferences.shared.grayscaleImages
let maybeGrayscaleImage = if isGrayscale {
ImageGrayscalifier.convert(url: url, image: image) ?? image
} else {
image
}
imageView = GIFImageView(image: maybeGrayscaleImage)
imageView.translatesAutoresizingMaskIntoConstraints = false imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFill imageView.contentMode = .scaleAspectFill
imageView.isUserInteractionEnabled = true imageView.isUserInteractionEnabled = true
@ -75,9 +86,11 @@ open class ImageGalleryContentViewController: UIViewController, GalleryContentVi
} }
} }
} }
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
} }
public override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated) super.viewWillAppear(animated)
if let gifController { if let gifController {
@ -85,23 +98,37 @@ open class ImageGalleryContentViewController: UIViewController, GalleryContentVi
} }
} }
@objc private func preferencesChanged() {
if isGrayscale != Preferences.shared.grayscaleImages {
isGrayscale = Preferences.shared.grayscaleImages
let image = if isGrayscale {
ImageGrayscalifier.convert(url: url, image: image)
} else {
image
}
if let image {
imageView.image = image
}
}
}
// MARK: GalleryContentViewController // MARK: GalleryContentViewController
public weak var container: (any GalleryContentViewControllerContainer)? weak var container: (any GalleryVC.GalleryContentViewControllerContainer)?
public var contentSize: CGSize { var contentSize: CGSize {
image.size image.size
} }
open var activityItemsForSharing: [Any] { var activityItemsForSharing: [Any] {
return [image] if let data = originalData ?? image.pngData() {
return [ImageActivityItemSource(data: data, url: url, image: image)]
} else {
return []
}
} }
public var presentationAnimation: GalleryContentPresentationAnimation { func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
gifController != nil ? .fromSourceViewWithoutSnapshot : .fromSourceView
}
public func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
if #available(iOS 16.0, macCatalyst 17.0, *), if #available(iOS 16.0, macCatalyst 17.0, *),
let analysisInteraction { let analysisInteraction {
analysisInteraction.setSupplementaryInterfaceHidden(!visible, animated: animated) analysisInteraction.setSupplementaryInterfaceHidden(!visible, animated: animated)
@ -111,7 +138,7 @@ open class ImageGalleryContentViewController: UIViewController, GalleryContentVi
@available(iOS 16.0, macCatalyst 17.0, *) @available(iOS 16.0, macCatalyst 17.0, *)
extension ImageGalleryContentViewController: ImageAnalysisInteractionDelegate { extension ImageGalleryContentViewController: ImageAnalysisInteractionDelegate {
public func interaction(_ interaction: ImageAnalysisInteraction, shouldBeginAt point: CGPoint, for interactionType: ImageAnalysisInteraction.InteractionTypes) -> Bool { func interaction(_ interaction: ImageAnalysisInteraction, shouldBeginAt point: CGPoint, for interactionType: ImageAnalysisInteraction.InteractionTypes) -> Bool {
return container?.galleryControlsVisible ?? true return container?.galleryControlsVisible ?? true
} }
} }

View File

@ -34,7 +34,7 @@ class ImageGalleryDataSource: GalleryDataSource {
} else { } else {
nil nil
} }
return GrayscalableImageGalleryContentViewController( return ImageGalleryContentViewController(
url: url, url: url,
caption: nil, caption: nil,
originalData: entry.data, originalData: entry.data,
@ -52,7 +52,7 @@ class ImageGalleryDataSource: GalleryDataSource {
} else { } else {
nil nil
} }
return GrayscalableImageGalleryContentViewController( return ImageGalleryContentViewController(
url: self.url, url: self.url,
caption: nil, caption: nil,
originalData: data, originalData: data,

View File

@ -7,42 +7,43 @@
// //
import UIKit import UIKit
import GalleryVC
public class LoadingGalleryContentViewController: UIViewController, GalleryContentViewController { class LoadingGalleryContentViewController: UIViewController, GalleryContentViewController {
private let fallbackCaption: String? private let fallbackCaption: String?
private let provider: () async -> (any GalleryContentViewController)? private let provider: () async -> (any GalleryContentViewController)?
private var wrapped: (any GalleryContentViewController)! private var wrapped: (any GalleryContentViewController)!
public weak var container: GalleryContentViewControllerContainer? weak var container: GalleryContentViewControllerContainer?
public var contentSize: CGSize { var contentSize: CGSize {
wrapped?.contentSize ?? .zero wrapped?.contentSize ?? .zero
} }
public var activityItemsForSharing: [Any] { var activityItemsForSharing: [Any] {
wrapped?.activityItemsForSharing ?? [] wrapped?.activityItemsForSharing ?? []
} }
public var caption: String? { var caption: String? {
wrapped?.caption ?? fallbackCaption wrapped?.caption ?? fallbackCaption
} }
public var presentationAnimation: GalleryContentPresentationAnimation { var canAnimateFromSourceView: Bool {
wrapped?.presentationAnimation ?? .fade wrapped?.canAnimateFromSourceView ?? true
} }
public init(caption: String?, provider: @escaping () async -> (any GalleryContentViewController)?) { init(caption: String?, provider: @escaping () async -> (any GalleryContentViewController)?) {
self.fallbackCaption = caption self.fallbackCaption = caption
self.provider = provider self.provider = provider
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
} }
public required init?(coder: NSCoder) { required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
public override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
container?.setGalleryContentLoading(true) container?.setGalleryContentLoading(true)
@ -80,7 +81,7 @@ public class LoadingGalleryContentViewController: UIViewController, GalleryConte
let label = UILabel() let label = UILabel()
label.text = "Error Loading" label.text = "Error Loading"
label.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .title1).withSymbolicTraits(.traitBold)!, size: 0) label.font = .preferredFont(forTextStyle: .title1).withTraits(.traitBold)!
label.textColor = .secondaryLabel label.textColor = .secondaryLabel
label.adjustsFontForContentSizeCategory = true label.adjustsFontForContentSizeCategory = true
@ -101,15 +102,15 @@ public class LoadingGalleryContentViewController: UIViewController, GalleryConte
]) ])
} }
public func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) { func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
wrapped?.setControlsVisible(visible, animated: animated, dueToUserInteraction: dueToUserInteraction) wrapped?.setControlsVisible(visible, animated: animated, dueToUserInteraction: dueToUserInteraction)
} }
public func galleryContentDidAppear() { func galleryContentDidAppear() {
wrapped?.galleryContentDidAppear() wrapped?.galleryContentDidAppear()
} }
public func galleryContentWillDisappear() { func galleryContentWillDisappear() {
wrapped?.galleryContentWillDisappear() wrapped?.galleryContentWillDisappear()
} }

View File

@ -33,7 +33,7 @@ class StatusAttachmentsGalleryDataSource: GalleryDataSource {
case .image: case .image:
if let view = attachmentView(for: attachment), if let view = attachmentView(for: attachment),
let image = view.attachmentImage { let image = view.attachmentImage {
return GrayscalableImageGalleryContentViewController( return ImageGalleryContentViewController(
url: attachment.url, url: attachment.url,
caption: attachment.description, caption: attachment.description,
originalData: view.originalData, originalData: view.originalData,
@ -49,7 +49,7 @@ class StatusAttachmentsGalleryDataSource: GalleryDataSource {
} else { } else {
nil nil
} }
return GrayscalableImageGalleryContentViewController( return ImageGalleryContentViewController(
url: attachment.url, url: attachment.url,
caption: attachment.description, caption: attachment.description,
originalData: entry.data, originalData: entry.data,
@ -68,7 +68,7 @@ class StatusAttachmentsGalleryDataSource: GalleryDataSource {
} else { } else {
nil nil
} }
return GrayscalableImageGalleryContentViewController( return ImageGalleryContentViewController(
url: attachment.url, url: attachment.url,
caption: attachment.description, caption: attachment.description,
originalData: data, originalData: data,
@ -91,10 +91,10 @@ class StatusAttachmentsGalleryDataSource: GalleryDataSource {
} }
return GifvGalleryContentViewController(controller: controller, url: attachment.url, caption: attachment.description) return GifvGalleryContentViewController(controller: controller, url: attachment.url, caption: attachment.description)
case .video: case .video:
return GrayscalableVideoGalleryContentViewController(url: attachment.url, caption: attachment.description) return VideoGalleryContentViewController(url: attachment.url, caption: attachment.description)
case .audio: case .audio:
// TODO: use separate content VC with audio visualization? // TODO: use separate content VC with audio visualization?
return GrayscalableVideoGalleryContentViewController(url: attachment.url, caption: attachment.description) return VideoGalleryContentViewController(url: attachment.url, caption: attachment.description)
case .unknown: case .unknown:
return LoadingGalleryContentViewController(caption: nil) { return LoadingGalleryContentViewController(caption: nil) {
do { do {

View File

@ -1,6 +1,6 @@
// //
// VideoControlsViewController.swift // VideoControlsViewController.swift
// GalleryVC // Tusker
// //
// Created by Shadowfacts on 3/21/24. // Created by Shadowfacts on 3/21/24.
// Copyright © 2024 Shadowfacts. All rights reserved. // Copyright © 2024 Shadowfacts. All rights reserved.
@ -9,15 +9,6 @@
import UIKit import UIKit
import AVFoundation import AVFoundation
@propertyWrapper
final class Box<T> {
var wrappedValue: T
init(wrappedValue: T) {
self.wrappedValue = wrappedValue
}
}
class VideoControlsViewController: UIViewController { class VideoControlsViewController: UIViewController {
private static let formatter: DateComponentsFormatter = { private static let formatter: DateComponentsFormatter = {
let f = DateComponentsFormatter() let f = DateComponentsFormatter()
@ -31,35 +22,27 @@ class VideoControlsViewController: UIViewController {
@Box private var playbackSpeed: Float @Box private var playbackSpeed: Float
#endif #endif
private lazy var muteButton: MuteButton = { private lazy var muteButton = MuteButton().configure {
let button = MuteButton() $0.addTarget(self, action: #selector(muteButtonPressed), for: .touchUpInside)
button.addTarget(self, action: #selector(muteButtonPressed), for: .touchUpInside) $0.setMuted(false, animated: false)
button.setMuted(false, animated: false) }
return button
}()
private let timestampLabel: UILabel = { private let timestampLabel = UILabel().configure {
let label = UILabel() $0.text = "0:00"
label.text = "0:00" $0.font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .monospacedDigitSystemFont(ofSize: 13, weight: .regular))
label.font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .monospacedDigitSystemFont(ofSize: 13, weight: .regular)) }
return label
}()
private lazy var scrubbingControl: VideoScrubbingControl = { private lazy var scrubbingControl = VideoScrubbingControl().configure {
let control = VideoScrubbingControl() $0.heightAnchor.constraint(equalToConstant: 44).isActive = true
control.heightAnchor.constraint(equalToConstant: 44).isActive = true $0.addTarget(self, action: #selector(scrubbingStarted), for: .editingDidBegin)
control.addTarget(self, action: #selector(scrubbingStarted), for: .editingDidBegin) $0.addTarget(self, action: #selector(scrubbingChanged), for: .editingChanged)
control.addTarget(self, action: #selector(scrubbingChanged), for: .editingChanged) $0.addTarget(self, action: #selector(scrubbingEnded), for: .editingDidEnd)
control.addTarget(self, action: #selector(scrubbingEnded), for: .editingDidEnd) }
return control
}()
private let timeRemainingLabel: UILabel = { private let timeRemainingLabel = UILabel().configure {
let label = UILabel() $0.text = "-0:00"
label.text = "-0:00" $0.font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .monospacedDigitSystemFont(ofSize: 13, weight: .regular))
label.font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .monospacedDigitSystemFont(ofSize: 13, weight: .regular)) }
return label
}()
private lazy var optionsButton = MenuButton { [unowned self] in private lazy var optionsButton = MenuButton { [unowned self] in
let imageName: String let imageName: String
@ -99,19 +82,17 @@ class VideoControlsViewController: UIViewController {
return UIMenu(children: [speedMenu]) return UIMenu(children: [speedMenu])
} }
private lazy var hStack: UIStackView = { private lazy var hStack = UIStackView(arrangedSubviews: [
let stack = UIStackView(arrangedSubviews: [ muteButton,
muteButton, timestampLabel,
timestampLabel, scrubbingControl,
scrubbingControl, timeRemainingLabel,
timeRemainingLabel, optionsButton,
optionsButton, ]).configure {
]) $0.axis = .horizontal
stack.axis = .horizontal $0.spacing = 8
stack.spacing = 8 $0.alignment = .center
stack.alignment = .center }
return stack
}()
private var timestampObserverToken: Any? private var timestampObserverToken: Any?
private var scrubberObserverToken: Any? private var scrubberObserverToken: Any?

View File

@ -1,58 +1,73 @@
// //
// VideoGalleryContentViewController.swift // VideoGalleryContentViewController.swift
// GalleryVC // Tusker
// //
// Created by Shadowfacts on 3/19/24. // Created by Shadowfacts on 3/19/24.
// Copyright © 2024 Shadowfacts. All rights reserved. // Copyright © 2024 Shadowfacts. All rights reserved.
// //
import UIKit import UIKit
import GalleryVC
import AVFoundation import AVFoundation
import CoreImage import CoreImage
open class VideoGalleryContentViewController: UIViewController, GalleryContentViewController { class VideoGalleryContentViewController: UIViewController, GalleryContentViewController {
public let url: URL private let url: URL
public let caption: String? let caption: String?
public private(set) var item: AVPlayerItem private var item: AVPlayerItem
public let player: AVPlayer let player: AVPlayer
#if !os(visionOS) #if !os(visionOS)
@available(iOS, obsoleted: 16.0, message: "Use AVPlayer.defaultRate") @available(iOS, obsoleted: 16.0, message: "Use AVPlayer.defaultRate")
@Box private var playbackSpeed: Float = 1 @Box private var playbackSpeed: Float = 1
#endif #endif
private var isGrayscale: Bool
private var presentationSizeObservation: NSKeyValueObservation? private var presentationSizeObservation: NSKeyValueObservation?
private var statusObservation: NSKeyValueObservation? private var statusObservation: NSKeyValueObservation?
private var rateObservation: NSKeyValueObservation? private var rateObservation: NSKeyValueObservation?
private var isFirstAppearance = true
private var hideControlsWorkItem: DispatchWorkItem? private var hideControlsWorkItem: DispatchWorkItem?
private var isShowingError = false private var audioSessionToken: AudioSessionCoordinator.Token?
public init(url: URL, caption: String?) { init(url: URL, caption: String?) {
self.url = url self.url = url
self.caption = caption self.caption = caption
self.isGrayscale = Preferences.shared.grayscaleImages
let asset = AVAsset(url: url) let asset = AVAsset(url: url)
self.item = Self.createItem(asset: asset) self.item = VideoGalleryContentViewController.createItem(asset: asset)
self.player = AVPlayer(playerItem: item) self.player = AVPlayer(playerItem: item)
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
} }
public required init?(coder: NSCoder) { required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
open class func createItem(asset: AVAsset) -> AVPlayerItem { private static func createItem(asset: AVAsset) -> AVPlayerItem {
return AVPlayerItem(asset: asset) let item = AVPlayerItem(asset: asset)
if Preferences.shared.grayscaleImages {
#if os(visionOS)
#warning("Use async AVVideoComposition CIFilter initializer")
#else
let filter = CIFilter(name: "CIColorMonochrome")!
filter.setValue(CIColor(red: 0.85, green: 0.85, blue: 0.85), forKey: "inputColor")
filter.setValue(1.0, forKey: "inputIntensity")
item.videoComposition = AVVideoComposition(asset: asset, applyingCIFiltersWithHandler: { request in
filter.setValue(request.sourceImage, forKey: "inputImage")
request.finish(with: filter.outputImage!, context: nil)
})
#endif
}
return item
} }
public func replaceCurrentItem(with item: AVPlayerItem) { override func viewDidLoad() {
self.item = item
player.replaceCurrentItem(with: item)
updateItemObservations()
}
public override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
container?.setGalleryContentLoading(true) container?.setGalleryContentLoading(true)
@ -77,17 +92,19 @@ open class VideoGalleryContentViewController: UIViewController, GalleryContentVi
scheduleControlsHide() scheduleControlsHide()
} }
}) })
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
} }
private func updateItemObservations() { private func updateItemObservations() {
presentationSizeObservation = item.observe(\.presentationSize, changeHandler: { [unowned self] item, _ in presentationSizeObservation = item.observe(\.presentationSize, changeHandler: { [unowned self] item, _ in
MainActor.assumeIsolated { MainActor.runUnsafely {
self.preferredContentSize = item.presentationSize self.preferredContentSize = item.presentationSize
self.container?.galleryContentChanged() self.container?.galleryContentChanged()
} }
}) })
statusObservation = item.observe(\.status, changeHandler: { [unowned self] item, _ in statusObservation = item.observe(\.status, changeHandler: { [unowned self] item, _ in
MainActor.assumeIsolated { MainActor.runUnsafely {
if item.status == .readyToPlay { if item.status == .readyToPlay {
self.container?.setGalleryContentLoading(false) self.container?.setGalleryContentLoading(false)
self.statusObservation = nil self.statusObservation = nil
@ -96,22 +113,19 @@ open class VideoGalleryContentViewController: UIViewController, GalleryContentVi
self.container?.setGalleryContentLoading(false) self.container?.setGalleryContentLoading(false)
self.showErrorView(error) self.showErrorView(error)
self.statusObservation = nil self.statusObservation = nil
self.overlayVC.setVisible(false)
} }
} }
}) })
} }
private func showErrorView(_ error: any Error) { private func showErrorView(_ error: any Error) {
isShowingError = true
let image = UIImageView(image: UIImage(systemName: "exclamationmark.triangle.fill")!) let image = UIImageView(image: UIImage(systemName: "exclamationmark.triangle.fill")!)
image.tintColor = .secondaryLabel image.tintColor = .secondaryLabel
image.contentMode = .scaleAspectFit image.contentMode = .scaleAspectFit
let label = UILabel() let label = UILabel()
label.text = "Error Loading" label.text = "Error Loading"
label.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .title1).withSymbolicTraits(.traitBold)!, size: 0) label.font = .preferredFont(forTextStyle: .title1).withTraits(.traitBold)!
label.textColor = .secondaryLabel label.textColor = .secondaryLabel
label.adjustsFontForContentSizeCategory = true label.adjustsFontForContentSizeCategory = true
@ -139,9 +153,26 @@ open class VideoGalleryContentViewController: UIViewController, GalleryContentVi
]) ])
} }
@objc private func preferencesChanged() {
if isGrayscale != Preferences.shared.grayscaleImages {
let isPlaying = player.rate > 0
isGrayscale = Preferences.shared.grayscaleImages
item = VideoGalleryContentViewController.createItem(asset: item.asset)
player.replaceCurrentItem(with: item)
updateItemObservations()
if isPlaying {
#if os(visionOS)
player.play()
#else
player.rate = playbackSpeed
#endif
}
}
}
private func scheduleControlsHide() { private func scheduleControlsHide() {
hideControlsWorkItem = DispatchWorkItem { [weak self] in hideControlsWorkItem = DispatchWorkItem { [weak self] in
MainActor.assumeIsolated { MainActor.runUnsafely {
guard let self, guard let self,
let container = self.container, let container = self.container,
container.galleryControlsVisible else { container.galleryControlsVisible else {
@ -155,19 +186,14 @@ open class VideoGalleryContentViewController: UIViewController, GalleryContentVi
// MARK: GalleryContentViewController // MARK: GalleryContentViewController
public weak var container: (any GalleryVC.GalleryContentViewControllerContainer)? weak var container: (any GalleryVC.GalleryContentViewControllerContainer)?
public var contentSize: CGSize { var contentSize: CGSize {
item.presentationSize item.presentationSize
} }
open var activityItemsForSharing: [Any] { var activityItemsForSharing: [Any] {
// [VideoActivityItemSource(asset: item.asset, url: url)] [VideoActivityItemSource(asset: item.asset, url: url)]
[]
}
public var presentationAnimation: GalleryContentPresentationAnimation {
isShowingError ? .fade : .fromSourceViewWithoutSnapshot
} }
#if os(visionOS) #if os(visionOS)
@ -175,20 +201,18 @@ open class VideoGalleryContentViewController: UIViewController, GalleryContentVi
#else #else
private lazy var overlayVC = VideoOverlayViewController(player: player, playbackSpeed: _playbackSpeed) private lazy var overlayVC = VideoOverlayViewController(player: player, playbackSpeed: _playbackSpeed)
#endif #endif
public var contentOverlayAccessoryViewController: UIViewController? { var contentOverlayAccessoryViewController: UIViewController? {
overlayVC overlayVC
} }
#if os(visionOS) #if os(visionOS)
public private(set) lazy var bottomControlsAccessoryViewController: UIViewController? = VideoControlsViewController(player: player) private(set) lazy var bottomControlsAccessoryViewController: UIViewController? = VideoControlsViewController(player: player)
#else #else
public private(set) lazy var bottomControlsAccessoryViewController: UIViewController? = VideoControlsViewController(player: player, playbackSpeed: _playbackSpeed) private(set) lazy var bottomControlsAccessoryViewController: UIViewController? = VideoControlsViewController(player: player, playbackSpeed: _playbackSpeed)
#endif #endif
public func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) { func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
if !isShowingError { overlayVC.setVisible(visible)
overlayVC.setVisible(visible)
}
if !visible { if !visible {
hideControlsWorkItem?.cancel() hideControlsWorkItem?.cancel()
@ -198,11 +222,25 @@ open class VideoGalleryContentViewController: UIViewController, GalleryContentVi
} }
} }
open func galleryContentDidAppear() { func galleryContentDidAppear() {
let wasFirstAppearance = isFirstAppearance
isFirstAppearance = false
audioSessionToken = AudioSessionCoordinator.shared.beginPlayback(mode: .video) {
if wasFirstAppearance {
DispatchQueue.main.async {
self.player.play()
}
}
}
} }
open func galleryContentWillDisappear() { func galleryContentWillDisappear() {
player.pause() player.pause()
if let audioSessionToken {
AudioSessionCoordinator.shared.endPlayback(token: audioSessionToken)
}
} }
} }
@ -231,9 +269,9 @@ private class PlayerView: UIView {
playerLayer.player = player playerLayer.player = player
playerLayer.videoGravity = .resizeAspect playerLayer.videoGravity = .resizeAspect
presentationSizeObservation = item.observe(\.presentationSize, changeHandler: { [weak self] _, _ in presentationSizeObservation = item.observe(\.presentationSize, changeHandler: { [unowned self] _, _ in
MainActor.assumeIsolated { MainActor.runUnsafely {
self?.invalidateIntrinsicContentSize() self.invalidateIntrinsicContentSize()
} }
}) })
} }

View File

@ -1,6 +1,6 @@
// //
// VideoOverlayViewController.swift // VideoOverlayViewController.swift
// GalleryVC // Tusker
// //
// Created by Shadowfacts on 3/26/24. // Created by Shadowfacts on 3/26/24.
// Copyright © 2024 Shadowfacts. All rights reserved. // Copyright © 2024 Shadowfacts. All rights reserved.
@ -90,7 +90,7 @@ class VideoOverlayViewController: UIViewController {
]) ])
rateObservation = player.observe(\.rate, changeHandler: { player, _ in rateObservation = player.observe(\.rate, changeHandler: { player, _ in
MainActor.assumeIsolated { MainActor.runUnsafely {
playPauseButton.image = player.rate > 0 ? VideoOverlayViewController.pauseImage : VideoOverlayViewController.playImage playPauseButton.image = player.rate > 0 ? VideoOverlayViewController.pauseImage : VideoOverlayViewController.playImage
} }
}) })

View File

@ -151,22 +151,6 @@ class BaseMainTabBarViewController: UITabBarController, FastAccountSwitcherViewC
return false return false
#endif // !os(visionOS) #endif // !os(visionOS)
} }
// MARK: Keyboard shortcuts
override func target(forAction action: Selector, withSender sender: Any?) -> Any? {
// This is a silly workaround for when the sidebar is focused (and therefore first responder), which,
// unfortunately, is almost always. Because the content view controller then isn't in the responder chain,
// we manually delegate to the top view controller if possible.
if action == #selector(RefreshableViewController.refresh),
let selected = selectedViewController as? NavigationControllerProtocol,
let top = selected.topViewController as? RefreshableViewController {
return top
} else {
return super.target(forAction: action, withSender: sender)
}
}
} }
extension BaseMainTabBarViewController: TuskerNavigationDelegate { extension BaseMainTabBarViewController: TuskerNavigationDelegate {

View File

@ -220,19 +220,6 @@ class MainSplitViewController: UISplitViewController {
compose(editing: nil) compose(editing: nil)
} }
override func target(forAction action: Selector, withSender sender: Any?) -> Any? {
// This is a silly workaround for when the sidebar is focused (and therefore first responder), which,
// unfortunately, is almost always. Because the content view controller then isn't in the responder chain,
// we manually delegate to the top view controller if possible.
if action == #selector(RefreshableViewController.refresh),
traitCollection.horizontalSizeClass == .regular,
let top = secondaryNavController.topViewController as? RefreshableViewController {
return top
} else {
return super.target(forAction: action, withSender: sender)
}
}
} }
extension MainSplitViewController: UISplitViewControllerDelegate { extension MainSplitViewController: UISplitViewControllerDelegate {

View File

@ -34,13 +34,6 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController {
@Box fileprivate var myProfileCell: UIView? @Box fileprivate var myProfileCell: UIView?
private var sidebarTapRecognizer: UITapGestureRecognizer? private var sidebarTapRecognizer: UITapGestureRecognizer?
private lazy var fastAccountSwitcherIndicator: UIView = {
let indicator = FastAccountSwitcherIndicatorView()
// need to explicitly set the frame to get it vertically centered
indicator.frame = CGRect(origin: .zero, size: indicator.intrinsicContentSize)
return indicator
}()
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
@ -461,20 +454,15 @@ extension NewMainTabBarViewController {
extension NewMainTabBarViewController: UITabBarControllerDelegate { extension NewMainTabBarViewController: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, shouldSelectTab tab: UITab) -> Bool { func tabBarController(_ tabBarController: UITabBarController, shouldSelectTab tab: UITab) -> Bool {
if tab.identifier == Tab.compose.rawValue { if tab.identifier == Tab.compose.rawValue {
if #unavailable(iOS 18.1) { let currentTab = selectedTab
let currentTab = selectedTab // returning false for shouldSelectTab doesn't prevent the UITabBar from being updated (FB14857254)
// returning false for shouldSelectTab doesn't prevent the UITabBar from being updated (FB14857254) // returning false and then setting selectedTab=tab and selectedTab=currentTab seems to leave things in a bad state (currentTab's VC is on screen but in the disappeared state)
// returning false and then setting selectedTab=tab and selectedTab=currentTab seems to leave things in a bad state (currentTab's VC is on screen but in the disappeared state) // so return true, and then after the tab bar VC has finished updating, go back to currentTab
// so return true, and then after the tab bar VC has finished updating, go back to currentTab DispatchQueue.main.async {
DispatchQueue.main.async { self.selectedTab = currentTab
self.selectedTab = currentTab
}
compose(editing: nil)
return true
} else {
compose(editing: nil)
return false
} }
compose(editing: nil)
return true
} else if let selectedTab, } else if let selectedTab,
selectedTab == tab, selectedTab == tab,
let nav = selectedViewController as? any NavigationControllerProtocol { let nav = selectedViewController as? any NavigationControllerProtocol {
@ -520,6 +508,13 @@ extension NewMainTabBarViewController: UITabBarControllerDelegate {
} }
} }
private var fastAccountSwitcherIndicator: UIView = {
let indicator = FastAccountSwitcherIndicatorView()
// need to explicitly set the frame to get it vertically centered
indicator.frame = CGRect(origin: .zero, size: indicator.intrinsicContentSize)
return indicator
}()
@available(iOS 18.0, *) @available(iOS 18.0, *)
extension NewMainTabBarViewController: UITabBarController.Sidebar.Delegate { extension NewMainTabBarViewController: UITabBarController.Sidebar.Delegate {
func tabBarController(_ tabBarController: UITabBarController, sidebarVisibilityWillChange sidebar: UITabBarController.Sidebar, animator: any UITabBarController.Sidebar.Animating) { func tabBarController(_ tabBarController: UITabBarController, sidebarVisibilityWillChange sidebar: UITabBarController.Sidebar, animator: any UITabBarController.Sidebar.Animating) {

View File

@ -48,9 +48,7 @@ class NotificationLoadingViewController: UIViewController {
do { do {
let (notification, _) = try await mastodonController.run(request) let (notification, _) = try await mastodonController.run(request)
await withCheckedContinuation { continuation in await withCheckedContinuation { continuation in
let container = mastodonController.persistentContainer mastodonController.persistentContainer.addAll(notifications: [notification]) {
let context = container.viewContext
container.addAll(notifications: [notification], in: context) {
continuation.resume() continuation.resume()
} }
} }

View File

@ -180,9 +180,3 @@ extension NotificationsPageViewController: StateRestorableViewController {
return currentPage.userActivity(accountID: mastodonController.accountInfo!.id) return currentPage.userActivity(accountID: mastodonController.accountInfo!.id)
} }
} }
extension NotificationsPageViewController: RefreshableViewController {
func refresh() {
(currentViewController as? RefreshableViewController)?.refresh()
}
}

View File

@ -393,9 +393,3 @@ extension ProfileViewController: StatusBarTappableViewController {
return currentViewController.handleStatusBarTapped(xPosition: xPosition) return currentViewController.handleStatusBarTapped(xPosition: xPosition)
} }
} }
extension ProfileViewController: RefreshableViewController {
func refresh() {
currentViewController.refresh()
}
}

View File

@ -77,25 +77,11 @@ class InstanceTimelineViewController: TimelineViewController {
cell.updateUI(statusID: id, state: state, filterResult: filterResult, precomputedContent: precomputedContent) cell.updateUI(statusID: id, state: state, filterResult: filterResult, precomputedContent: precomputedContent)
} }
override func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
guard browsingEnabled else {
return false
}
return super.collectionView(collectionView, shouldSelectItemAt: indexPath)
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard browsingEnabled else { return } guard browsingEnabled else { return }
super.collectionView(collectionView, didSelectItemAt: indexPath) super.collectionView(collectionView, didSelectItemAt: indexPath)
} }
override func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
guard browsingEnabled else {
return nil
}
return super.collectionView(collectionView, contextMenuConfigurationForItemAt: indexPath, point: point)
}
// MARK: Timeline // MARK: Timeline
override func handleLoadAllError(_ error: Swift.Error) async { override func handleLoadAllError(_ error: Swift.Error) async {

View File

@ -212,9 +212,3 @@ extension TimelinesPageViewController: StateRestorableViewController {
return (currentViewController as? TimelineViewController)?.stateRestorationActivity() return (currentViewController as? TimelineViewController)?.stateRestorationActivity()
} }
} }
extension TimelinesPageViewController: RefreshableViewController {
func refresh() {
(currentViewController as? RefreshableViewController)?.refresh()
}
}

View File

@ -30,9 +30,7 @@ class AccountDisplayAndUserNameLabel: EmojiLabel {
private func makeAttributedText(state: State) -> NSAttributedString { private func makeAttributedText(state: State) -> NSAttributedString {
let s = NSMutableAttributedString() let s = NSMutableAttributedString()
// U+2068 FIRST-STRONG ISOLATE and U+2069 POP DIRECTIONAL ISOLATE s.append(NSAttributedString(string: state.displayName, attributes: [
// to prevent bidi text in the display name influencing the username
s.append(NSAttributedString(string: "\u{2068}\(state.displayName)\u{2069}", attributes: [
.font: UIFont(descriptor: baseFont.addingAttributes([ .font: UIFont(descriptor: baseFont.addingAttributes([
.traits: [ .traits: [
UIFontDescriptor.TraitKey.weight: UIFont.Weight.semibold.rawValue, UIFontDescriptor.TraitKey.weight: UIFont.Weight.semibold.rawValue,

View File

@ -412,7 +412,7 @@ class AttachmentView: GIFImageView {
makeBadgeView(text: "ALT") makeBadgeView(text: "ALT")
} }
if badges.contains(.noAlt) { if badges.contains(.noAlt) {
makeBadgeView(text: "NO ALT") makeBadgeView(text: "No ALT")
} }
let first = stack.arrangedSubviews.first! let first = stack.arrangedSubviews.first!
@ -477,12 +477,12 @@ extension AttachmentView: UIContextMenuInteractionDelegate {
return UIContextMenuConfiguration { [unowned self] () -> UIViewController? in return UIContextMenuConfiguration { [unowned self] () -> UIViewController? in
if self.attachment.kind == .image, if self.attachment.kind == .image,
let image { let image {
return GrayscalableImageGalleryContentViewController(url: self.attachment.url, caption: nil, originalData: nil, image: image, gifController: self.gifController) return ImageGalleryContentViewController(url: self.attachment.url, caption: nil, originalData: nil, image: image, gifController: self.gifController)
} else if self.attachment.kind == .gifv, } else if self.attachment.kind == .gifv,
let gifvView { let gifvView {
return GifvGalleryContentViewController(controller: gifvView.controller, url: self.attachment.url, caption: nil) return GifvGalleryContentViewController(controller: gifvView.controller, url: self.attachment.url, caption: nil)
} else if self.attachment.kind == .video || self.attachment.kind == .audio { } else if self.attachment.kind == .video || self.attachment.kind == .audio {
let vc = GrayscalableVideoGalleryContentViewController(url: self.attachment.url, caption: nil) let vc = VideoGalleryContentViewController(url: self.attachment.url, caption: nil)
vc.player.isMuted = true vc.player.isMuted = true
return vc return vc
} else { } else {

View File

@ -295,11 +295,7 @@ class AttachmentsContainerView: UIView {
accessibilityElements.append(moreView) accessibilityElements.append(moreView)
} }
self.aspectRatio = if aspectRatio.isNaN || aspectRatio.isInfinite { self.aspectRatio = aspectRatio
16/9
} else {
aspectRatio
}
} else { } else {
self.isHidden = true self.isHidden = true
} }

View File

@ -60,9 +60,9 @@ class GifvController {
} }
private func updatePresentationSizeObservation() { private func updatePresentationSizeObservation() {
presentationSizeObservation = item.observe(\.presentationSize, changeHandler: { [weak self] item, _ in presentationSizeObservation = item.observe(\.presentationSize, changeHandler: { [unowned self] item, _ in
DispatchQueue.main.async { DispatchQueue.main.async {
self?.presentationSizeSubject.send(item.presentationSize) self.presentationSizeSubject.send(item.presentationSize)
} }
}) })
} }

View File

@ -12,7 +12,11 @@ import SwiftUI
import SafariServices import SafariServices
class ProfileFieldValueView: UIView { class ProfileFieldValueView: UIView {
weak var navigationDelegate: TuskerNavigationDelegate? weak var navigationDelegate: TuskerNavigationDelegate? {
didSet {
textView.navigationDelegate = navigationDelegate
}
}
private static let converter = HTMLConverter( private static let converter = HTMLConverter(
font: .preferredFont(forTextStyle: .body), font: .preferredFont(forTextStyle: .body),
@ -24,9 +28,8 @@ class ProfileFieldValueView: UIView {
private let account: AccountMO private let account: AccountMO
private let field: Account.Field private let field: Account.Field
private var link: (String, URL)?
private let label = EmojiLabel() private let textView = ContentTextView()
private var iconView: UIView? private var iconView: UIView?
private var currentTargetedPreview: UITargetedPreview? private var currentTargetedPreview: UITargetedPreview?
@ -39,34 +42,28 @@ class ProfileFieldValueView: UIView {
let converted = NSMutableAttributedString(attributedString: ProfileFieldValueView.converter.convert(field.value)) let converted = NSMutableAttributedString(attributedString: ProfileFieldValueView.converter.convert(field.value))
converted.enumerateAttribute(.link, in: converted.fullRange) { value, range, stop in #if os(visionOS)
guard value != nil else { return } textView.linkTextAttributes = [
if self.link == nil { .foregroundColor: UIColor.link
self.link = (converted.attributedSubstring(from: range).string, value as! URL) ]
} #else
#if os(visionOS) textView.linkTextAttributes = [
converted.addAttribute(.foregroundColor, value: UIColor.link, range: range) .foregroundColor: UIColor.tintColor
#else ]
converted.addAttribute(.foregroundColor, value: UIColor.tintColor, range: range) #endif
#endif textView.backgroundColor = nil
// the .link attribute in a UILabel always makes the color blue >.> textView.isScrollEnabled = false
converted.removeAttribute(.link, range: range) textView.isSelectable = false
} textView.isEditable = false
textView.font = .preferredFont(forTextStyle: .body)
if link != nil { updateTextContainerInset()
label.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(linkTapped))) textView.adjustsFontForContentSizeCategory = true
label.addInteraction(UIContextMenuInteraction(delegate: self)) textView.attributedText = converted
label.isUserInteractionEnabled = true textView.setEmojis(account.emojis, identifier: account.id)
} textView.isUserInteractionEnabled = true
textView.setContentCompressionResistancePriority(.required, for: .vertical)
label.numberOfLines = 0 textView.translatesAutoresizingMaskIntoConstraints = false
label.font = .preferredFont(forTextStyle: .body) addSubview(textView)
label.adjustsFontForContentSizeCategory = true
label.attributedText = converted
label.setEmojis(account.emojis, identifier: account.id)
label.setContentCompressionResistancePriority(.required, for: .vertical)
label.translatesAutoresizingMaskIntoConstraints = false
addSubview(label)
let labelTrailingConstraint: NSLayoutConstraint let labelTrailingConstraint: NSLayoutConstraint
@ -83,20 +80,20 @@ class ProfileFieldValueView: UIView {
icon.isPointerInteractionEnabled = true icon.isPointerInteractionEnabled = true
icon.accessibilityLabel = "Verified link" icon.accessibilityLabel = "Verified link"
addSubview(icon) addSubview(icon)
labelTrailingConstraint = label.trailingAnchor.constraint(equalTo: icon.leadingAnchor) labelTrailingConstraint = textView.trailingAnchor.constraint(equalTo: icon.leadingAnchor)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
icon.centerYAnchor.constraint(equalTo: label.centerYAnchor), icon.centerYAnchor.constraint(equalTo: textView.centerYAnchor),
icon.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor), icon.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor),
]) ])
} else { } else {
labelTrailingConstraint = label.trailingAnchor.constraint(equalTo: trailingAnchor) labelTrailingConstraint = textView.trailingAnchor.constraint(equalTo: trailingAnchor)
} }
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
label.leadingAnchor.constraint(equalTo: leadingAnchor), textView.leadingAnchor.constraint(equalTo: leadingAnchor),
labelTrailingConstraint, labelTrailingConstraint,
label.topAnchor.constraint(equalTo: topAnchor), textView.topAnchor.constraint(equalTo: topAnchor),
label.bottomAnchor.constraint(equalTo: bottomAnchor), textView.bottomAnchor.constraint(equalTo: bottomAnchor),
]) ])
} }
@ -105,37 +102,36 @@ class ProfileFieldValueView: UIView {
} }
override func sizeThatFits(_ size: CGSize) -> CGSize { override func sizeThatFits(_ size: CGSize) -> CGSize {
var size = label.sizeThatFits(size) var size = textView.sizeThatFits(size)
if let iconView { if let iconView {
size.width += iconView.sizeThatFits(UIView.layoutFittingCompressedSize).width size.width += iconView.sizeThatFits(UIView.layoutFittingCompressedSize).width
} }
return size return size
} }
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
if traitCollection.preferredContentSizeCategory != previousTraitCollection?.preferredContentSizeCategory {
updateTextContainerInset()
}
}
private func updateTextContainerInset() {
// blergh
switch traitCollection.preferredContentSizeCategory {
case .extraSmall:
textView.textContainerInset = UIEdgeInsets(top: 4, left: 0, bottom: 0, right: 0)
case .small:
textView.textContainerInset = UIEdgeInsets(top: 3, left: 0, bottom: 0, right: 0)
case .medium, .large:
textView.textContainerInset = UIEdgeInsets(top: 2, left: 0, bottom: 0, right: 0)
default:
textView.textContainerInset = .zero
}
}
func setTextAlignment(_ alignment: NSTextAlignment) { func setTextAlignment(_ alignment: NSTextAlignment) {
label.textAlignment = alignment textView.textAlignment = alignment
}
func getHashtagOrURL() -> (Hashtag?, URL)? {
guard let (text, url) = link else {
return nil
}
if text.starts(with: "#") {
return (Hashtag(name: String(text.dropFirst()), url: url), url)
} else {
return (nil, url)
}
}
@objc private func linkTapped() {
guard let (hashtag, url) = getHashtagOrURL() else {
return
}
if let hashtag {
navigationDelegate?.selected(tag: hashtag)
} else {
navigationDelegate?.selected(url: url)
}
} }
@objc private func verifiedIconTapped() { @objc private func verifiedIconTapped() {
@ -145,7 +141,7 @@ class ProfileFieldValueView: UIView {
let view = ProfileFieldVerificationView( let view = ProfileFieldVerificationView(
acct: account.acct, acct: account.acct,
verifiedAt: field.verifiedAt!, verifiedAt: field.verifiedAt!,
linkText: label.text ?? "", linkText: textView.text ?? "",
navigationDelegate: navigationDelegate navigationDelegate: navigationDelegate
) )
let host = UIHostingController(rootView: view) let host = UIHostingController(rootView: view)
@ -169,49 +165,3 @@ class ProfileFieldValueView: UIView {
navigationDelegate.present(toPresent, animated: true) navigationDelegate.present(toPresent, animated: true)
} }
} }
extension ProfileFieldValueView: UIContextMenuInteractionDelegate, MenuActionProvider {
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
guard let (hashtag, url) = getHashtagOrURL(),
let navigationDelegate else {
return nil
}
if let hashtag {
return UIContextMenuConfiguration {
HashtagTimelineViewController(for: hashtag, mastodonController: navigationDelegate.apiController)
} actionProvider: { _ in
UIMenu(children: self.actionsForHashtag(hashtag, source: .view(self)))
}
} else {
return UIContextMenuConfiguration {
let vc = SFSafariViewController(url: url)
#if !os(visionOS)
vc.preferredControlTintColor = Preferences.shared.accentColor.color
#endif
return vc
} actionProvider: { _ in
UIMenu(children: self.actionsForURL(url, source: .view(self)))
}
}
}
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configuration: UIContextMenuConfiguration, highlightPreviewForItemWithIdentifier identifier: NSCopying) -> UITargetedPreview? {
var rect = label.textRect(forBounds: label.bounds, limitedToNumberOfLines: 0)
// the rect should be vertically centered, but textRect doesn't seem to take the label's vertical alignment into account
rect.origin.x = 0
rect.origin.y = (bounds.height - rect.height) / 2
let parameters = UIPreviewParameters(textLineRects: [rect as NSValue])
let preview = UITargetedPreview(view: label, parameters: parameters)
currentTargetedPreview = preview
return preview
}
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configuration: UIContextMenuConfiguration, dismissalPreviewForItemWithIdentifier identifier: NSCopying) -> UITargetedPreview? {
return currentTargetedPreview
}
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
MenuPreviewHelper.willPerformPreviewAction(animator: animator, presenter: navigationDelegate!)
}
}

View File

@ -9,8 +9,8 @@
// Configuration settings file format documentation can be found at: // Configuration settings file format documentation can be found at:
// https://help.apple.com/xcode/#/dev745c5c974 // https://help.apple.com/xcode/#/dev745c5c974
MARKETING_VERSION = 2024.5 MARKETING_VERSION = 2024.4
CURRENT_PROJECT_VERSION = 141 CURRENT_PROJECT_VERSION = 136
CURRENT_PROJECT_VERSION = $(inherited)$(CURRENT_PROJECT_VERSION_BUILD_SUFFIX_$(CONFIGURATION)) CURRENT_PROJECT_VERSION = $(inherited)$(CURRENT_PROJECT_VERSION_BUILD_SUFFIX_$(CONFIGURATION))
CURRENT_PROJECT_VERSION_BUILD_SUFFIX_Debug=-dev CURRENT_PROJECT_VERSION_BUILD_SUFFIX_Debug=-dev