Compare commits

..

6 Commits

Author SHA1 Message Date
Shadowfacts cca2a03b2f When routing the SplitNav responder chain through the root VC, go as deep into it as possible
Makes keyboard shortcuts from, e.g., TimelineVC accessible when the root is TimelinesPageVC

See #302
2023-01-20 11:34:44 -05:00
Shadowfacts 1a64bfcef8 Disallow keyboard focus in sidebar
Makes keyboard shortcuts from the split VC's primary content available

See #302
2023-01-20 11:33:28 -05:00
Shadowfacts 907810d98a Make link preview cards larger 2023-01-20 11:22:28 -05:00
Shadowfacts 23a4999196 Complete asynchronous swipe actions immediately
Fixes crash when the user things the action has failed and taps it
again, which results in an invalid completion handler later being called
2023-01-20 10:53:30 -05:00
Shadowfacts 3e0feba273 Fix collapse button disappearing when navigating away 2023-01-20 10:51:56 -05:00
Shadowfacts 468a559127 Fix crash when TimelinePosition's center status ID isn't in the list of IDs 2023-01-19 21:46:57 -05:00
10 changed files with 125 additions and 41 deletions

View File

@ -99,9 +99,9 @@ private func createFavoriteAction(status: StatusMO, container: StatusSwipeAction
}
let title = status.favourited ? "Unfavorite" : "Favorite"
let action = UIContextualAction(style: .normal, title: title) { [unowned container] _, _, completion in
completion(true)
Task { @MainActor in
await FavoriteService(status: status, mastodonController: container.mastodonController, presenter: container.navigationDelegate).toggleFavorite()
completion(true)
}
}
action.image = UIImage(systemName: "star.fill")
@ -116,9 +116,9 @@ private func createReblogAction(status: StatusMO, container: StatusSwipeActionCo
}
let title = status.reblogged ? "Unreblog" : "Reblog"
let action = UIContextualAction(style: .normal, title: title) { [unowned container] _, _, completion in
completion(true)
Task { @MainActor in
await ReblogService(status: status, mastodonController: container.mastodonController, presenter: container.navigationDelegate).toggleReblog()
completion(true)
}
}
action.image = UIImage(systemName: "repeat")
@ -145,6 +145,7 @@ private func createBookmarkAction(status: StatusMO, container: StatusSwipeAction
let bookmarked = status.bookmarked ?? false
let title = bookmarked ? "Unbookmark" : "Bookmark"
let action = UIContextualAction(style: .normal, title: title) { [unowned container] _, _, completion in
completion(true)
Task { @MainActor in
let request = (bookmarked ? Status.unbookmark : Status.bookmark)(status.id)
do {
@ -156,7 +157,6 @@ private func createBookmarkAction(status: StatusMO, container: StatusSwipeAction
toastable.showToast(configuration: config, animated: true)
}
}
completion(true)
}
}
action.image = UIImage(systemName: "bookmark.fill")

View File

@ -89,6 +89,8 @@ class MainSidebarViewController: UIViewController {
collectionView.delegate = self
collectionView.dragDelegate = self
collectionView.isSpringLoaded = true
// TODO: allow focusing sidebar once there's a workaround for keyboard shortcuts from main split content not being accessible when not in the responder chain
collectionView.allowsFocus = false
view.addSubview(collectionView)
dataSource = createDataSource()

View File

@ -432,8 +432,9 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
}
// update the timeline position in case some statuses couldn't be loaded
if let center = position.centerStatusID {
let nearestLoadedStatusToCenter = position.statusIDs[position.statusIDs.firstIndex(of: center)!...].first(where: { id in
if let center = position.centerStatusID,
let centerIndex = position.statusIDs.firstIndex(of: center) {
let nearestLoadedStatusToCenter = position.statusIDs[centerIndex...].first(where: { id in
// was already loaded or was just now loaded
!unloaded.contains(id) || statuses.contains(where: { $0.id == id })
})

View File

@ -198,3 +198,9 @@ extension SegmentedPageViewController: StatusBarTappableViewController {
return .continue
}
}
extension SegmentedPageViewController: NestedResponderProvider {
var innerResponder: UIResponder? {
currentViewController
}
}

View File

@ -283,7 +283,11 @@ private class SplitSecondaryNavigationController: EnhancedNavigationViewControll
// ordinarily, the next responder in the chain would be the SplitNavigationController's view
// but that would bypass the VC in the root nav, so we reroute the repsonder chain to include it
// first seems to be nil when using the view debugger for some reason, so in that case, defer to super
owner.viewControllers.first?.view ?? super.next
if let root = owner.viewControllers.first {
return root.innermostResponder() ?? super.next
} else {
return super.next
}
}
private func configureSecondarySplitCloseButton(for viewController: UIViewController) {
@ -300,3 +304,17 @@ private class SplitSecondaryNavigationController: EnhancedNavigationViewControll
}
}
protocol NestedResponderProvider {
var innerResponder: UIResponder? { get }
}
extension UIResponder {
func innermostResponder() -> UIResponder? {
if let nestedProvider = self as? NestedResponderProvider {
return nestedProvider.innerResponder?.innermostResponder() ?? self
} else {
return self
}
}
}

View File

@ -361,23 +361,31 @@ class BaseStatusTableViewCell: UITableViewCell {
let buttonImage = UIImage(systemName: collapsed ? "chevron.down" : "chevron.up")!
if animated, let buttonImageView = collapseButton.imageView {
// we need to use a keyframe animation for this, because we want to control the direction the chevron rotates
// when rotating ±π, UIKit will always rotate in the same direction
// using a keyframe to set an intermediate point in the animation allows us to force a specific direction
UIView.animateKeyframes(withDuration: 0.2, delay: 0, options: .calculationModeLinear, animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5) {
buttonImageView.transform = CGAffineTransform(rotationAngle: collapsed ? .pi / 2 : -.pi / 2)
}
UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5) {
buttonImageView.transform = CGAffineTransform(rotationAngle: .pi)
}
}, completion: { (finished) in
buttonImageView.transform = .identity
self.collapseButton.setImage(buttonImage, for: .normal)
})
} else {
if let buttonImageView = collapseButton.imageView {
collapseButton.setImage(buttonImage, for: .normal)
if animated {
buttonImageView.layer.opacity = 0
// this whole hack is necessary because when just rotating buttonImageView, it moves to the left of the button and then animates back to the center
let imageView = UIImageView(image: buttonImageView.image)
imageView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(imageView)
NSLayoutConstraint.activate([
imageView.widthAnchor.constraint(equalTo: buttonImageView.widthAnchor),
imageView.heightAnchor.constraint(equalTo: buttonImageView.heightAnchor),
imageView.centerXAnchor.constraint(equalTo: collapseButton.centerXAnchor),
imageView.centerYAnchor.constraint(equalTo: collapseButton.centerYAnchor),
])
imageView.tintColor = .white
UIView.animate(withDuration: 0.3, delay: 0) {
imageView.transform = CGAffineTransform(rotationAngle: .pi)
} completion: { _ in
imageView.removeFromSuperview()
buttonImageView.layer.opacity = 1
}
}
}
if collapsed {

View File

@ -81,10 +81,14 @@
<constraints>
<constraint firstAttribute="height" constant="30" id="icD-3q-uJ6"/>
</constraints>
<color key="tintColor" systemColor="systemBackgroundColor"/>
<color key="tintColor" systemColor="tintColor"/>
<state key="normal" image="chevron.down" catalog="system">
<preferredSymbolConfiguration key="preferredSymbolConfiguration" scale="large"/>
</state>
<buttonConfiguration key="configuration" style="filled" image="chevron.down" catalog="system">
<preferredSymbolConfiguration key="preferredSymbolConfigurationForImage" scale="large"/>
<color key="baseForegroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</buttonConfiguration>
<connections>
<action selector="collapseButtonPressed" destination="IDI-ur-8pa" eventType="touchUpInside" id="00b-nM-U5g"/>
</connections>
@ -98,9 +102,9 @@
</textView>
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="QqC-GR-TLC" customClass="StatusCardView" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="131" width="361" height="0.0"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" priority="999" constant="65" id="Tdo-Hv-ITE"/>
<constraint firstAttribute="height" priority="999" constant="90" id="Tdo-Hv-ITE"/>
</constraints>
</view>
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="IF9-9U-Gk0" customClass="AttachmentsContainerView" customModule="Tusker" customModuleProvider="target">

View File

@ -25,8 +25,10 @@ class StatusCardView: UIView {
private var imageRequest: ImageCache.Request?
private var isGrayscale = false
private var hStack: UIStackView!
private var titleLabel: UILabel!
private var descriptionLabel: UILabel!
private var domainLabel: UILabel!
private var imageView: UIImageView!
private var placeholderImageView: UIImageView!
@ -41,11 +43,13 @@ class StatusCardView: UIView {
}
private func commonInit() {
self.clipsToBounds = true
self.layer.cornerRadius = 6.5
self.layer.borderWidth = 1
self.layer.borderColor = UIColor.lightGray.cgColor
self.backgroundColor = inactiveBackgroundColor
// self.clipsToBounds = true
// self.layer.borderWidth = 0.5
// self.layer.borderColor = UIColor.lightGray.cgColor
self.layer.shadowColor = UIColor.black.cgColor
self.layer.shadowRadius = 5
self.layer.shadowOpacity = 0.2
self.layer.shadowOffset = .zero
self.addInteraction(UIContextMenuInteraction(delegate: self))
@ -60,9 +64,16 @@ class StatusCardView: UIView {
descriptionLabel.numberOfLines = 2
descriptionLabel.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
domainLabel = UILabel()
domainLabel.font = .preferredFont(forTextStyle: .caption2)
domainLabel.adjustsFontForContentSizeCategory = true
domainLabel.numberOfLines = 1
domainLabel.textColor = .tintColor
let vStack = UIStackView(arrangedSubviews: [
titleLabel,
descriptionLabel
descriptionLabel,
domainLabel,
])
vStack.axis = .vertical
vStack.alignment = .leading
@ -73,15 +84,23 @@ class StatusCardView: UIView {
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
let hStack = UIStackView(arrangedSubviews: [
let spacer = UIView()
spacer.backgroundColor = .clear
hStack = UIStackView(arrangedSubviews: [
imageView,
vStack
vStack,
spacer,
])
hStack.translatesAutoresizingMaskIntoConstraints = false
hStack.axis = .horizontal
hStack.alignment = .center
hStack.distribution = .fill
hStack.spacing = 4
hStack.clipsToBounds = true
hStack.layer.borderWidth = 0.5
hStack.layer.borderColor = UIColor.lightGray.cgColor
hStack.backgroundColor = inactiveBackgroundColor
addSubview(hStack)
@ -98,8 +117,10 @@ class StatusCardView: UIView {
vStack.heightAnchor.constraint(equalTo: heightAnchor, constant: -8),
spacer.widthAnchor.constraint(equalToConstant: 4),
hStack.leadingAnchor.constraint(equalTo: leadingAnchor),
hStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -4),
hStack.trailingAnchor.constraint(equalTo: trailingAnchor),
hStack.topAnchor.constraint(equalTo: topAnchor),
hStack.bottomAnchor.constraint(equalTo: bottomAnchor),
@ -112,6 +133,11 @@ class StatusCardView: UIView {
NotificationCenter.default.addObserver(self, selector: #selector(updateUIForPreferences), name: .preferencesChanged, object: nil)
}
override func layoutSubviews() {
super.layoutSubviews()
hStack.layer.cornerRadius = 0.1 * bounds.height
}
func updateUI(status: StatusMO) {
guard status.id != statusID else {
return
@ -135,6 +161,13 @@ class StatusCardView: UIView {
let description = card.description.trimmingCharacters(in: .whitespacesAndNewlines)
descriptionLabel.text = description
descriptionLabel.isHidden = description.isEmpty
if let host = card.url.host {
domainLabel.text = host.serialized
domainLabel.isHidden = false
} else {
domainLabel.isHidden = true
}
}
@objc private func updateUIForPreferences() {
@ -201,7 +234,7 @@ class StatusCardView: UIView {
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
backgroundColor = activeBackgroundColor
hStack.backgroundColor = activeBackgroundColor
setNeedsDisplay()
}
@ -209,7 +242,7 @@ class StatusCardView: UIView {
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
backgroundColor = inactiveBackgroundColor
hStack.backgroundColor = inactiveBackgroundColor
setNeedsDisplay()
if let card = card, let delegate = navigationDelegate {
@ -218,7 +251,7 @@ class StatusCardView: UIView {
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
backgroundColor = inactiveBackgroundColor
hStack.backgroundColor = inactiveBackgroundColor
setNeedsDisplay()
}
@ -238,6 +271,12 @@ extension StatusCardView: UIContextMenuInteractionDelegate {
}
}
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, previewForHighlightingMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
let params = UIPreviewParameters()
params.visiblePath = UIBezierPath(roundedRect: hStack.bounds, cornerRadius: hStack.layer.cornerRadius)
return UITargetedPreview(view: hStack, parameters: params)
}
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
if let viewController = animator.previewViewController,
let delegate = navigationDelegate {

View File

@ -20,7 +20,7 @@ class StatusContentContainer: UIView {
let cardView = StatusCardView().configure {
NSLayoutConstraint.activate([
$0.heightAnchor.constraint(equalToConstant: 65),
$0.heightAnchor.constraint(equalToConstant: 90),
])
}

View File

@ -109,10 +109,16 @@
<constraints>
<constraint firstAttribute="height" constant="30" id="z84-XW-gP3"/>
</constraints>
<color key="tintColor" systemColor="systemBackgroundColor"/>
<color key="tintColor" systemColor="tintColor"/>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
<state key="normal" image="chevron.down" catalog="system">
<color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<preferredSymbolConfiguration key="preferredSymbolConfiguration" scale="large"/>
</state>
<buttonConfiguration key="configuration" style="filled" image="chevron.down" catalog="system">
<preferredSymbolConfiguration key="preferredSymbolConfigurationForImage" scale="large"/>
<color key="baseForegroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</buttonConfiguration>
<connections>
<action selector="collapseButtonPressed" destination="BR5-ZS-LIo" eventType="touchUpInside" id="twO-rE-1pQ"/>
</connections>
@ -126,9 +132,9 @@
</textView>
<view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="LKo-VB-XWl" customClass="StatusCardView" customModule="Tusker" customModuleProvider="target">
<rect key="frame" x="0.0" y="200.5" width="295" height="0.0"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" priority="999" constant="65" id="khY-jm-CPn"/>
<constraint firstAttribute="height" priority="999" constant="90" id="khY-jm-CPn"/>
</constraints>
</view>
<view hidden="YES" contentMode="scaleToFill" verticalCompressionResistancePriority="1" translatesAutoresizingMaskIntoConstraints="NO" id="nbq-yr-2mA" customClass="AttachmentsContainerView" customModule="Tusker" customModuleProvider="target">