Merge branch 'develop' into compose-redesign

This commit is contained in:
Shadowfacts 2024-12-03 13:55:23 -05:00
commit c564bb4112
20 changed files with 323 additions and 67 deletions

View File

@ -1,5 +1,16 @@
# Changelog # Changelog
## 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

@ -82,8 +82,8 @@ public class FallbackGalleryNavigationController: UINavigationController, Galler
nil nil
} }
public var canAnimateFromSourceView: Bool { public var presentationAnimation: GalleryContentPresentationAnimation {
false .fade
} }
} }

View File

@ -97,6 +97,10 @@ open class ImageGalleryContentViewController: UIViewController, GalleryContentVi
return [image] return [image]
} }
public var presentationAnimation: GalleryContentPresentationAnimation {
gifController != nil ? .fromSourceViewWithoutSnapshot : .fromSourceView
}
public func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) { 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 {

View File

@ -27,8 +27,8 @@ public class LoadingGalleryContentViewController: UIViewController, GalleryConte
wrapped?.caption ?? fallbackCaption wrapped?.caption ?? fallbackCaption
} }
public var canAnimateFromSourceView: Bool { public var presentationAnimation: GalleryContentPresentationAnimation {
wrapped?.canAnimateFromSourceView ?? true wrapped?.presentationAnimation ?? .fade
} }
public init(caption: String?, provider: @escaping () async -> (any GalleryContentViewController)?) { public init(caption: String?, provider: @escaping () async -> (any GalleryContentViewController)?) {

View File

@ -20,6 +20,7 @@ open class VideoGalleryContentViewController: UIViewController, GalleryContentVi
private var statusObservation: NSKeyValueObservation? private var statusObservation: NSKeyValueObservation?
private var rateObservation: NSKeyValueObservation? private var rateObservation: NSKeyValueObservation?
private var hideControlsWorkItem: DispatchWorkItem? private var hideControlsWorkItem: DispatchWorkItem?
private var isShowingError = false
public init(url: URL, caption: String?) { public init(url: URL, caption: String?) {
self.url = url self.url = url
@ -90,12 +91,15 @@ 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
@ -156,6 +160,10 @@ open class VideoGalleryContentViewController: UIViewController, GalleryContentVi
[] []
} }
public var presentationAnimation: GalleryContentPresentationAnimation {
isShowingError ? .fade : .fromSourceViewWithoutSnapshot
}
private lazy var overlayVC = VideoOverlayViewController(player: player) private lazy var overlayVC = VideoOverlayViewController(player: player)
public var contentOverlayAccessoryViewController: UIViewController? { public var contentOverlayAccessoryViewController: UIViewController? {
overlayVC overlayVC
@ -164,7 +172,9 @@ open class VideoGalleryContentViewController: UIViewController, GalleryContentVi
public private(set) lazy var bottomControlsAccessoryViewController: UIViewController? = VideoControlsViewController(player: player) public private(set) lazy var bottomControlsAccessoryViewController: UIViewController? = VideoControlsViewController(player: player)
public func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) { public func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
overlayVC.setVisible(visible) if !isShowingError {
overlayVC.setVisible(visible)
}
if !visible { if !visible {
hideControlsWorkItem?.cancel() hideControlsWorkItem?.cancel()
@ -207,9 +217,9 @@ private class PlayerView: UIView {
playerLayer.player = player playerLayer.player = player
playerLayer.videoGravity = .resizeAspect playerLayer.videoGravity = .resizeAspect
presentationSizeObservation = item.observe(\.presentationSize, changeHandler: { [unowned self] _, _ in presentationSizeObservation = item.observe(\.presentationSize, changeHandler: { [weak self] _, _ in
MainActor.assumeIsolated { MainActor.assumeIsolated {
self.invalidateIntrinsicContentSize() self?.invalidateIntrinsicContentSize()
} }
}) })
} }

View File

@ -16,7 +16,7 @@ public protocol GalleryContentViewController: UIViewController {
var contentOverlayAccessoryViewController: UIViewController? { get } var contentOverlayAccessoryViewController: UIViewController? { get }
var bottomControlsAccessoryViewController: UIViewController? { get } var bottomControlsAccessoryViewController: UIViewController? { get }
var insetBottomControlsAccessoryViewControllerToSafeArea: Bool { get } var insetBottomControlsAccessoryViewControllerToSafeArea: Bool { get }
var canAnimateFromSourceView: Bool { get } var presentationAnimation: GalleryContentPresentationAnimation { get }
var hideControlsOnZoom: Bool { get } var hideControlsOnZoom: Bool { get }
func shouldHideControls() -> Bool func shouldHideControls() -> Bool
@ -38,8 +38,8 @@ public extension GalleryContentViewController {
true true
} }
var canAnimateFromSourceView: Bool { var presentationAnimation: GalleryContentPresentationAnimation {
true .fromSourceView
} }
var hideControlsOnZoom: Bool { var hideControlsOnZoom: Bool {
@ -59,3 +59,9 @@ public extension GalleryContentViewController {
func galleryContentWillDisappear() { func galleryContentWillDisappear() {
} }
} }
public enum GalleryContentPresentationAnimation {
case fade
case fromSourceView
case fromSourceViewWithoutSnapshot
}

View File

@ -30,12 +30,37 @@ class GalleryDismissAnimationController: NSObject, UIViewControllerAnimatedTrans
let itemViewController = from.currentItemViewController let itemViewController = from.currentItemViewController
if !itemViewController.content.canAnimateFromSourceView || (UIAccessibility.prefersCrossFadeTransitions && interactiveVelocity == nil) { if itemViewController.content.presentationAnimation == .fade || (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)
@ -48,38 +73,39 @@ 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.layer.masksToBounds = true content.view.transform = .identity
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 springs initial undershoot // very short/fast flicks don't transfer their velocity, since it makes size change animation look wacky due to the spring's 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
@ -102,14 +128,34 @@ class GalleryDismissAnimationController: NSObject, UIViewControllerAnimatedTrans
if appliedSourceToDestTransform { if appliedSourceToDestTransform {
self.sourceView.transform = origSourceTransform self.sourceView.transform = origSourceTransform
sourceSnapshot?.transform = origSourceTransform
} }
content.view.frame = sourceFrameInContainer
content.view.layer.opacity = 0 contentContainer.frame = sourceFrameInContainer
// 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,6 +20,8 @@ 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()
@ -38,6 +40,8 @@ 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
@ -53,12 +57,42 @@ 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)
dismissVelocity = velocity let translationMagnitude = sqrt(translation.x.magnitudeSquared + translation.y.magnitudeSquared)
dismissTranslation = translation let velocityMagnitude = sqrt(velocity.x.magnitudeSquared + velocity.y.magnitudeSquared)
viewController.dismiss(animated: true)
if translationMagnitude < 150 && velocityMagnitude < 500 {
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()
// don't unset this until after dismiss is called, so that the dismiss animation controller can read it } else {
isActive = false 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

@ -79,6 +79,10 @@ 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)

View File

@ -25,11 +25,31 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated
let itemViewController = to.currentItemViewController let itemViewController = to.currentItemViewController
if !itemViewController.content.canAnimateFromSourceView || UIAccessibility.prefersCrossFadeTransitions { if itemViewController.content.presentationAnimation == .fade || 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)
@ -56,21 +76,70 @@ 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
container.insertSubview(content.view, belowSubview: to.view) content.view.transform = .identity
// 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: content.view) container.insertSubview(dimmingView, belowSubview: contentContainer)
to.view.backgroundColor = nil to.view.backgroundColor = nil
to.view.layer.opacity = 0 to.view.layer.opacity = 0
content.view.frame = sourceFrameInContainer
content.view.layer.opacity = 0 contentContainer.frame = sourceFrameInContainer
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()
@ -78,8 +147,14 @@ 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)
// rougly equivalent to duration: 0.35, bounce: 0.3 // less bounce on bigger screens
let spring = UISpringTimingParameters(mass: 1, stiffness: 322, damping: 25, initialVelocity: .zero) let spring = if UIDevice.current.userInterfaceIdiom == .pad {
// 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 {
@ -87,25 +162,35 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated
to.view.layer.opacity = 1 to.view.layer.opacity = 1
content.view.frame = destFrameInContainer contentContainer.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
if sourceToDestTransform != nil { // Reset the properties we changed before re-adding the content to the scroll view.
self.sourceView.transform = origSourceTransform // (I would expect UIScrollView to effectively do this itself, but w/e.)
} content.view.transform = origContentTransform
content.view.frame = origContentFrame
itemViewController.addContent() itemViewController.addContent()
transitionContext.completeTransition(true) transitionContext.completeTransition(true)

View File

@ -0,0 +1,26 @@
//
// 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

@ -22,7 +22,12 @@ 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)
self.url = try container.decode(WebURL.self, forKey: .url) do {
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

@ -375,13 +375,14 @@ class MastodonCachePersistentStore: NSPersistentCloudKitContainer, @unchecked Se
} }
} }
func addAll(notifications: [Pachyderm.Notification], completion: (() -> Void)? = nil) { func addAll(notifications: [Pachyderm.Notification], in context: NSManagedObjectContext? = nil, completion: (() -> Void)? = nil) {
backgroundContext.perform { let context = context ?? backgroundContext
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: self.backgroundContext) } statuses.forEach { self.upsert(status: $0, context: context) }
accounts.forEach { self.upsert(account: $0, in: self.backgroundContext) } accounts.forEach { self.upsert(account: $0, in: context) }
self.save(context: self.backgroundContext) self.save(context: context)
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

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

View File

@ -33,6 +33,13 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController {
private var isCompact: Bool? private var isCompact: Bool?
@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()
@ -513,13 +520,6 @@ 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,7 +48,9 @@ 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
mastodonController.persistentContainer.addAll(notifications: [notification]) { let container = mastodonController.persistentContainer
let context = container.viewContext
container.addAll(notifications: [notification], in: context) {
continuation.resume() continuation.resume()
} }
} }

View File

@ -77,11 +77,25 @@ 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

@ -295,7 +295,11 @@ class AttachmentsContainerView: UIView {
accessibilityElements.append(moreView) accessibilityElements.append(moreView)
} }
self.aspectRatio = aspectRatio self.aspectRatio = if aspectRatio.isNaN || aspectRatio.isInfinite {
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: { [unowned self] item, _ in presentationSizeObservation = item.observe(\.presentationSize, changeHandler: { [weak self] item, _ in
DispatchQueue.main.async { DispatchQueue.main.async {
self.presentationSizeSubject.send(item.presentationSize) self?.presentationSizeSubject.send(item.presentationSize)
} }
}) })
} }

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.4 MARKETING_VERSION = 2024.5
CURRENT_PROJECT_VERSION = 136 CURRENT_PROJECT_VERSION = 137
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