Compare commits
7 Commits
a5506aeab6
...
cf870916c9
Author | SHA1 | Date |
---|---|---|
Shadowfacts | cf870916c9 | |
Shadowfacts | 7297566060 | |
Shadowfacts | 4f28fec62a | |
Shadowfacts | c01bc4d840 | |
Shadowfacts | ea6698a2d8 | |
Shadowfacts | 1e950b5ccb | |
Shadowfacts | 3e5a3c81b5 |
|
@ -29,11 +29,11 @@ public protocol DuckableViewControllerDelegate: AnyObject {
|
||||||
|
|
||||||
extension UIViewController {
|
extension UIViewController {
|
||||||
@available(iOS 16.0, *)
|
@available(iOS 16.0, *)
|
||||||
public func presentDuckable(_ viewController: DuckableViewController) -> Bool {
|
public func presentDuckable(_ viewController: DuckableViewController, animated: Bool, isDucked: Bool = false) -> Bool {
|
||||||
var cur: UIViewController? = self
|
var cur: UIViewController? = self
|
||||||
while let vc = cur {
|
while let vc = cur {
|
||||||
if let container = vc as? DuckableContainerViewController {
|
if let container = vc as? DuckableContainerViewController {
|
||||||
container.presentDuckable(viewController, animated: true, completion: nil)
|
container.presentDuckable(viewController, animated: animated, isDucked: isDucked, completion: nil)
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
cur = vc.parent
|
cur = vc.parent
|
||||||
|
|
|
@ -17,6 +17,14 @@ public class DuckableContainerViewController: UIViewController, DuckableViewCont
|
||||||
private var bottomConstraint: NSLayoutConstraint!
|
private var bottomConstraint: NSLayoutConstraint!
|
||||||
private(set) var state = State.idle
|
private(set) var state = State.idle
|
||||||
|
|
||||||
|
public var duckedViewController: DuckableViewController? {
|
||||||
|
if case .ducked(let vc, placeholder: _) = state {
|
||||||
|
return vc
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public init(child: UIViewController) {
|
public init(child: UIViewController) {
|
||||||
self.child = child
|
self.child = child
|
||||||
|
|
||||||
|
@ -50,7 +58,7 @@ public class DuckableContainerViewController: UIViewController, DuckableViewCont
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
func presentDuckable(_ viewController: DuckableViewController, animated: Bool, completion: (() -> Void)?) {
|
func presentDuckable(_ viewController: DuckableViewController, animated: Bool, isDucked: Bool, completion: (() -> Void)?) {
|
||||||
guard case .idle = state else {
|
guard case .idle = state else {
|
||||||
if animated,
|
if animated,
|
||||||
case .ducked(_, placeholder: let placeholder) = state {
|
case .ducked(_, placeholder: let placeholder) = state {
|
||||||
|
@ -69,8 +77,13 @@ public class DuckableContainerViewController: UIViewController, DuckableViewCont
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
state = .presentingDucked(viewController, isFirstPresentation: true)
|
if isDucked {
|
||||||
doPresentDuckable(viewController, animated: animated, completion: completion)
|
state = .ducked(viewController, placeholder: createPlaceholderForDuckedViewController(viewController))
|
||||||
|
configureChildForDuckedPlaceholder()
|
||||||
|
} else {
|
||||||
|
state = .presentingDucked(viewController, isFirstPresentation: true)
|
||||||
|
doPresentDuckable(viewController, animated: animated, completion: completion)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func doPresentDuckable(_ viewController: DuckableViewController, animated: Bool, completion: (() -> Void)?) {
|
private func doPresentDuckable(_ viewController: DuckableViewController, animated: Bool, completion: (() -> Void)?) {
|
||||||
|
@ -79,9 +92,7 @@ public class DuckableContainerViewController: UIViewController, DuckableViewCont
|
||||||
nav.modalPresentationStyle = .custom
|
nav.modalPresentationStyle = .custom
|
||||||
nav.transitioningDelegate = self
|
nav.transitioningDelegate = self
|
||||||
present(nav, animated: animated) {
|
present(nav, animated: animated) {
|
||||||
self.bottomConstraint.isActive = false
|
self.configureChildForDuckedPlaceholder()
|
||||||
self.bottomConstraint = self.child.view.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: -detentHeight - 4)
|
|
||||||
self.bottomConstraint.isActive = true
|
|
||||||
completion?()
|
completion?()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -127,10 +138,18 @@ public class DuckableContainerViewController: UIViewController, DuckableViewCont
|
||||||
}
|
}
|
||||||
let placeholder = createPlaceholderForDuckedViewController(viewController)
|
let placeholder = createPlaceholderForDuckedViewController(viewController)
|
||||||
state = .ducked(viewController, placeholder: placeholder)
|
state = .ducked(viewController, placeholder: placeholder)
|
||||||
|
configureChildForDuckedPlaceholder()
|
||||||
|
dismiss(animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureChildForDuckedPlaceholder() {
|
||||||
|
bottomConstraint.isActive = false
|
||||||
|
bottomConstraint = child.view.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -detentHeight - 4)
|
||||||
|
bottomConstraint.isActive = true
|
||||||
|
|
||||||
child.view.layer.cornerRadius = duckedCornerRadius
|
child.view.layer.cornerRadius = duckedCornerRadius
|
||||||
child.view.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
child.view.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
||||||
child.view.layer.masksToBounds = true
|
child.view.layer.masksToBounds = true
|
||||||
dismiss(animated: true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func unduckViewController() {
|
@objc func unduckViewController() {
|
||||||
|
@ -191,7 +210,10 @@ extension DuckableContainerViewController: UIViewControllerTransitioningDelegate
|
||||||
@available(iOS 16.0, *)
|
@available(iOS 16.0, *)
|
||||||
extension DuckableContainerViewController: UISheetPresentationControllerDelegate {
|
extension DuckableContainerViewController: UISheetPresentationControllerDelegate {
|
||||||
public func presentationController(_ presentationController: UIPresentationController, willPresentWithAdaptiveStyle style: UIModalPresentationStyle, transitionCoordinator: UIViewControllerTransitionCoordinator?) {
|
public func presentationController(_ presentationController: UIPresentationController, willPresentWithAdaptiveStyle style: UIModalPresentationStyle, transitionCoordinator: UIViewControllerTransitionCoordinator?) {
|
||||||
let snapshot = child.view.snapshotView(afterScreenUpdates: false)!
|
guard let snapshot = child.view.snapshotView(afterScreenUpdates: false) else {
|
||||||
|
setOverrideTraitCollection(UITraitCollection(userInterfaceLevel: .elevated), forChild: child)
|
||||||
|
return
|
||||||
|
}
|
||||||
snapshot.translatesAutoresizingMaskIntoConstraints = false
|
snapshot.translatesAutoresizingMaskIntoConstraints = false
|
||||||
self.view.addSubview(snapshot)
|
self.view.addSubview(snapshot)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
|
|
|
@ -48,6 +48,7 @@
|
||||||
D61F758A2932E1FC00C0B37F /* SwipeActionsPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75892932E1FC00C0B37F /* SwipeActionsPrefsView.swift */; };
|
D61F758A2932E1FC00C0B37F /* SwipeActionsPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75892932E1FC00C0B37F /* SwipeActionsPrefsView.swift */; };
|
||||||
D61F758D2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F758B2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.swift */; };
|
D61F758D2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F758B2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.swift */; };
|
||||||
D61F758E2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D61F758C2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.xib */; };
|
D61F758E2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D61F758C2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.xib */; };
|
||||||
|
D61F759029353B4300C0B37F /* FileManager+Size.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F758F29353B4300C0B37F /* FileManager+Size.swift */; };
|
||||||
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483323D3801D008A63EF /* LinkTextView.swift */; };
|
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483323D3801D008A63EF /* LinkTextView.swift */; };
|
||||||
D620483623D38075008A63EF /* ContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483523D38075008A63EF /* ContentTextView.swift */; };
|
D620483623D38075008A63EF /* ContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483523D38075008A63EF /* ContentTextView.swift */; };
|
||||||
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483723D38190008A63EF /* StatusContentTextView.swift */; };
|
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483723D38190008A63EF /* StatusContentTextView.swift */; };
|
||||||
|
@ -412,6 +413,7 @@
|
||||||
D61F75892932E1FC00C0B37F /* SwipeActionsPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeActionsPrefsView.swift; sourceTree = "<group>"; };
|
D61F75892932E1FC00C0B37F /* SwipeActionsPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeActionsPrefsView.swift; sourceTree = "<group>"; };
|
||||||
D61F758B2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusUpdatedNotificationTableViewCell.swift; sourceTree = "<group>"; };
|
D61F758B2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusUpdatedNotificationTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
D61F758C2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = StatusUpdatedNotificationTableViewCell.xib; sourceTree = "<group>"; };
|
D61F758C2933C69C00C0B37F /* StatusUpdatedNotificationTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = StatusUpdatedNotificationTableViewCell.xib; sourceTree = "<group>"; };
|
||||||
|
D61F758F29353B4300C0B37F /* FileManager+Size.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Size.swift"; sourceTree = "<group>"; };
|
||||||
D620483323D3801D008A63EF /* LinkTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkTextView.swift; sourceTree = "<group>"; };
|
D620483323D3801D008A63EF /* LinkTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkTextView.swift; sourceTree = "<group>"; };
|
||||||
D620483523D38075008A63EF /* ContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentTextView.swift; sourceTree = "<group>"; };
|
D620483523D38075008A63EF /* ContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentTextView.swift; sourceTree = "<group>"; };
|
||||||
D620483723D38190008A63EF /* StatusContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentTextView.swift; sourceTree = "<group>"; };
|
D620483723D38190008A63EF /* StatusContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentTextView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1166,6 +1168,7 @@
|
||||||
D62E9984279CA23900C26176 /* URLSession+Development.swift */,
|
D62E9984279CA23900C26176 /* URLSession+Development.swift */,
|
||||||
D6ADB6ED28EA74E8009924AB /* UIView+Configure.swift */,
|
D6ADB6ED28EA74E8009924AB /* UIView+Configure.swift */,
|
||||||
D63CC70F2911F1E4000E19DE /* UIScrollView+Top.swift */,
|
D63CC70F2911F1E4000E19DE /* UIScrollView+Top.swift */,
|
||||||
|
D61F758F29353B4300C0B37F /* FileManager+Size.swift */,
|
||||||
);
|
);
|
||||||
path = Extensions;
|
path = Extensions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1935,6 +1938,7 @@
|
||||||
D6C143DA253510F4007DC240 /* ComposeEmojiTextField.swift in Sources */,
|
D6C143DA253510F4007DC240 /* ComposeEmojiTextField.swift in Sources */,
|
||||||
D6DD2A3F273C1F4900386A6C /* ComposeAttachmentImage.swift in Sources */,
|
D6DD2A3F273C1F4900386A6C /* ComposeAttachmentImage.swift in Sources */,
|
||||||
D6DD2A45273D6C5700386A6C /* GIFImageView.swift in Sources */,
|
D6DD2A45273D6C5700386A6C /* GIFImageView.swift in Sources */,
|
||||||
|
D61F759029353B4300C0B37F /* FileManager+Size.swift in Sources */,
|
||||||
0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */,
|
0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */,
|
||||||
D627943923A553B600D38C68 /* UnbookmarkStatusActivity.swift in Sources */,
|
D627943923A553B600D38C68 /* UnbookmarkStatusActivity.swift in Sources */,
|
||||||
D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */,
|
D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */,
|
||||||
|
|
|
@ -122,6 +122,10 @@ class DiskCache<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getSizeInBytes() -> Int64? {
|
||||||
|
return fileManager.recursiveSize(url: URL(fileURLWithPath: path, isDirectory: true))
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension DiskCache {
|
extension DiskCache {
|
||||||
|
|
|
@ -110,6 +110,10 @@ class ImageCache {
|
||||||
try cache.removeAll()
|
try cache.removeAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getDiskSizeInBytes() -> Int64? {
|
||||||
|
return cache.disk?.getSizeInBytes()
|
||||||
|
}
|
||||||
|
|
||||||
typealias Request = URLSessionDataTask
|
typealias Request = URLSessionDataTask
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import UIKit
|
||||||
class ImageDataCache {
|
class ImageDataCache {
|
||||||
|
|
||||||
private let memory: MemoryCache<Entry>
|
private let memory: MemoryCache<Entry>
|
||||||
private let disk: DiskCache<Data>?
|
let disk: DiskCache<Data>?
|
||||||
|
|
||||||
private let storeOriginalDataInMemory: Bool
|
private let storeOriginalDataInMemory: Bool
|
||||||
private let desiredPixelSize: CGSize?
|
private let desiredPixelSize: CGSize?
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
//
|
||||||
|
// FileManager+Size.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 11/28/22.
|
||||||
|
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension FileManager {
|
||||||
|
func recursiveSize(url: URL) -> Int64? {
|
||||||
|
if (try? url.resourceValues(forKeys: [.isRegularFileKey]).isRegularFile) == true {
|
||||||
|
return size(url: url)
|
||||||
|
} else {
|
||||||
|
guard let enumerator = enumerator(at: url, includingPropertiesForKeys: [.isRegularFileKey, .fileSizeKey, .totalFileAllocatedSizeKey]) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var total: Int64 = 0
|
||||||
|
for case let url as URL in enumerator {
|
||||||
|
total += size(url: url) ?? 0
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func size(url: URL) -> Int64? {
|
||||||
|
guard let resourceValues = try? url.resourceValues(forKeys: [.isRegularFileKey, .fileSizeKey, .totalFileAllocatedSizeKey]),
|
||||||
|
resourceValues.isRegularFile ?? false,
|
||||||
|
let size = resourceValues.fileSize ?? resourceValues.totalFileAllocatedSize else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return Int64(size)
|
||||||
|
}
|
|
@ -97,7 +97,7 @@ private func createFavoriteAction(status: StatusMO, container: StatusSwipeAction
|
||||||
}
|
}
|
||||||
let title = status.favourited ? "Unfavorite" : "Favorite"
|
let title = status.favourited ? "Unfavorite" : "Favorite"
|
||||||
let action = UIContextualAction(style: .normal, title: title) { [unowned container] _, _, completion in
|
let action = UIContextualAction(style: .normal, title: title) { [unowned container] _, _, completion in
|
||||||
Task {
|
Task { @MainActor in
|
||||||
await FavoriteService(status: status, mastodonController: container.mastodonController, presenter: container.navigationDelegate).toggleFavorite()
|
await FavoriteService(status: status, mastodonController: container.mastodonController, presenter: container.navigationDelegate).toggleFavorite()
|
||||||
completion(true)
|
completion(true)
|
||||||
}
|
}
|
||||||
|
@ -113,7 +113,7 @@ private func createReblogAction(status: StatusMO, container: StatusSwipeActionCo
|
||||||
}
|
}
|
||||||
let title = status.reblogged ? "Unreblog" : "Reblog"
|
let title = status.reblogged ? "Unreblog" : "Reblog"
|
||||||
let action = UIContextualAction(style: .normal, title: title) { [unowned container] _, _, completion in
|
let action = UIContextualAction(style: .normal, title: title) { [unowned container] _, _, completion in
|
||||||
Task {
|
Task { @MainActor in
|
||||||
await ReblogService(status: status, mastodonController: container.mastodonController, presenter: container.navigationDelegate).toggleReblog()
|
await ReblogService(status: status, mastodonController: container.mastodonController, presenter: container.navigationDelegate).toggleReblog()
|
||||||
completion(true)
|
completion(true)
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ struct ComposeCurrentAccount: View {
|
||||||
ComposeAvatarImageView(url: account?.avatar)
|
ComposeAvatarImageView(url: account?.avatar)
|
||||||
.frame(width: 50, height: 50)
|
.frame(width: 50, height: 50)
|
||||||
.cornerRadius(preferences.avatarStyle.cornerRadiusFraction * 50)
|
.cornerRadius(preferences.avatarStyle.cornerRadiusFraction * 50)
|
||||||
.accessibility(label: Text(account != nil ? "\(account!.displayName) avatar" : "Avatar"))
|
.accessibilityHidden(true)
|
||||||
|
|
||||||
if let id = account?.id,
|
if let id = account?.id,
|
||||||
let account = mastodonController.persistentContainer.account(for: id) {
|
let account = mastodonController.persistentContainer.account(for: id) {
|
||||||
|
|
|
@ -43,11 +43,11 @@ class ComposeHostingController: UIHostingController<ComposeHostingController.Wra
|
||||||
super.init(rootView: wrapper)
|
super.init(rootView: wrapper)
|
||||||
|
|
||||||
self.uiState.delegate = self
|
self.uiState.delegate = self
|
||||||
|
|
||||||
pasteConfiguration = UIPasteConfiguration(forAccepting: CompositionAttachment.self)
|
pasteConfiguration = UIPasteConfiguration(forAccepting: CompositionAttachment.self)
|
||||||
|
|
||||||
userActivity = UserActivityManager.newPostActivity(accountID: mastodonController.accountInfo!.id)
|
userActivity = UserActivityManager.newPostActivity(accountID: mastodonController.accountInfo!.id)
|
||||||
|
|
||||||
|
updateNavigationTitle(draft: uiState.draft)
|
||||||
|
|
||||||
self.uiState.$draft
|
self.uiState.$draft
|
||||||
.flatMap(\.objectWillChange)
|
.flatMap(\.objectWillChange)
|
||||||
.debounce(for: .milliseconds(250), scheduler: DispatchQueue.global(qos: .utility))
|
.debounce(for: .milliseconds(250), scheduler: DispatchQueue.global(qos: .utility))
|
||||||
|
@ -55,12 +55,27 @@ class ComposeHostingController: UIHostingController<ComposeHostingController.Wra
|
||||||
DraftsManager.save()
|
DraftsManager.save()
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
self.uiState.$draft
|
||||||
|
.sink { [unowned self] draft in
|
||||||
|
self.updateNavigationTitle(draft: draft)
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder aDecoder: NSCoder) {
|
required init?(coder aDecoder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func updateNavigationTitle(draft: Draft) {
|
||||||
|
if let id = draft.inReplyToID,
|
||||||
|
let status = mastodonController.persistentContainer.status(for: id) {
|
||||||
|
navigationItem.title = "Reply to @\(status.account.acct)"
|
||||||
|
} else {
|
||||||
|
navigationItem.title = "New Post"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override func viewWillDisappear(_ animated: Bool) {
|
override func viewWillDisappear(_ animated: Bool) {
|
||||||
super.viewWillDisappear(animated)
|
super.viewWillDisappear(animated)
|
||||||
|
|
||||||
|
@ -92,6 +107,11 @@ class ComposeHostingController: UIHostingController<ComposeHostingController.Wra
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func accessibilityPerformEscape() -> Bool {
|
||||||
|
dismissCompose(mode: .cancel)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Duckable
|
// MARK: Duckable
|
||||||
|
|
||||||
func duckableViewControllerWillAnimateDuck(withDuration duration: CGFloat, afterDelay delay: CGFloat) {
|
func duckableViewControllerWillAnimateDuck(withDuration duration: CGFloat, afterDelay delay: CGFloat) {
|
||||||
|
|
|
@ -80,6 +80,7 @@ struct ComposeReplyView: View {
|
||||||
.frame(width: 50, height: 50)
|
.frame(width: 50, height: 50)
|
||||||
.cornerRadius(preferences.avatarStyle.cornerRadiusFraction * 50)
|
.cornerRadius(preferences.avatarStyle.cornerRadiusFraction * 50)
|
||||||
.offset(x: 0, y: offset)
|
.offset(x: 0, y: offset)
|
||||||
|
.accessibilityHidden(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,7 +107,6 @@ struct ComposeView: View {
|
||||||
globalFrameOutsideList = frame
|
globalFrameOutsideList = frame
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.navigationTitle(navTitle)
|
|
||||||
.sheet(isPresented: $uiState.isShowingDraftsList) {
|
.sheet(isPresented: $uiState.isShowingDraftsList) {
|
||||||
DraftsView(currentDraft: draft, mastodonController: mastodonController)
|
DraftsView(currentDraft: draft, mastodonController: mastodonController)
|
||||||
}
|
}
|
||||||
|
@ -203,23 +202,19 @@ struct ComposeView: View {
|
||||||
private var header: some View {
|
private var header: some View {
|
||||||
HStack(alignment: .top) {
|
HStack(alignment: .top) {
|
||||||
ComposeCurrentAccount()
|
ComposeCurrentAccount()
|
||||||
|
.accessibilitySortPriority(1)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Text(verbatim: charactersRemaining.description)
|
Text(verbatim: charactersRemaining.description)
|
||||||
.foregroundColor(charactersRemaining < 0 ? .red : .secondary)
|
.foregroundColor(charactersRemaining < 0 ? .red : .secondary)
|
||||||
.font(Font.body.monospacedDigit())
|
.font(Font.body.monospacedDigit())
|
||||||
.accessibility(label: Text(charactersRemaining < 0 ? "\(-charactersRemaining) characters too many" : "\(charactersRemaining) characters remaining"))
|
.accessibility(label: Text(charactersRemaining < 0 ? "\(-charactersRemaining) characters too many" : "\(charactersRemaining) characters remaining"))
|
||||||
|
// this should come first, so VO users can back to it from the main compose text view
|
||||||
|
.accessibilitySortPriority(0)
|
||||||
}.frame(height: 50)
|
}.frame(height: 50)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var navTitle: Text {
|
|
||||||
if let id = draft.inReplyToID,
|
|
||||||
let status = mastodonController.persistentContainer.status(for: id) {
|
|
||||||
return Text("Reply to @\(status.account.acct)")
|
|
||||||
} else {
|
|
||||||
return Text("New Post")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var cancelButton: some View {
|
private var cancelButton: some View {
|
||||||
Button(action: self.cancel) {
|
Button(action: self.cancel) {
|
||||||
Text("Cancel")
|
Text("Cancel")
|
||||||
|
|
|
@ -12,10 +12,21 @@ import Duckable
|
||||||
@available(iOS 16.0, *)
|
@available(iOS 16.0, *)
|
||||||
extension DuckableContainerViewController: TuskerRootViewController {
|
extension DuckableContainerViewController: TuskerRootViewController {
|
||||||
func stateRestorationActivity() -> NSUserActivity? {
|
func stateRestorationActivity() -> NSUserActivity? {
|
||||||
(child as? TuskerRootViewController)?.stateRestorationActivity()
|
var activity = (child as? TuskerRootViewController)?.stateRestorationActivity()
|
||||||
|
if let compose = duckedViewController as? ComposeHostingController,
|
||||||
|
compose.draft.hasContent {
|
||||||
|
activity = UserActivityManager.addDuckedDraft(to: activity, draft: compose.draft)
|
||||||
|
}
|
||||||
|
return activity
|
||||||
}
|
}
|
||||||
|
|
||||||
func restoreActivity(_ activity: NSUserActivity) {
|
func restoreActivity(_ activity: NSUserActivity) {
|
||||||
|
if let draft = UserActivityManager.getDraft(from: activity),
|
||||||
|
let account = UserActivityManager.getAccount(from: activity) {
|
||||||
|
let mastodonController = MastodonController.getForAccount(account)
|
||||||
|
let compose = ComposeHostingController(draft: draft, mastodonController: mastodonController)
|
||||||
|
_ = presentDuckable(compose, animated: false, isDucked: true)
|
||||||
|
}
|
||||||
(child as? TuskerRootViewController)?.restoreActivity(activity)
|
(child as? TuskerRootViewController)?.restoreActivity(activity)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -391,8 +391,7 @@ extension MainSplitViewController: TuskerRootViewController {
|
||||||
return tabBarViewController.stateRestorationActivity()
|
return tabBarViewController.stateRestorationActivity()
|
||||||
} else {
|
} else {
|
||||||
if let timelinePages = navigationStackFor(item: .tab(.timelines))?.first as? TimelinesPageViewController {
|
if let timelinePages = navigationStackFor(item: .tab(.timelines))?.first as? TimelinesPageViewController {
|
||||||
let timeline = timelinePages.pageControllers[timelinePages.currentIndex] as! TimelineViewController
|
return timelinePages.stateRestorationActivity()
|
||||||
return timeline.stateRestorationActivity()
|
|
||||||
} else {
|
} else {
|
||||||
stateRestorationLogger.fault("MainSplitViewController: Unable to create state restoration activity")
|
stateRestorationLogger.fault("MainSplitViewController: Unable to create state restoration activity")
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -13,11 +13,14 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
private var composePlaceholder: UIViewController!
|
private var composePlaceholder: UIViewController!
|
||||||
private var fastAccountSwitcher: FastAccountSwitcherViewController!
|
|
||||||
|
|
||||||
|
private var fastAccountSwitcher: FastAccountSwitcherViewController!
|
||||||
private var fastSwitcherIndicator: FastAccountSwitcherIndicatorView!
|
private var fastSwitcherIndicator: FastAccountSwitcherIndicatorView!
|
||||||
private var fastSwitcherConstraints: [NSLayoutConstraint] = []
|
private var fastSwitcherConstraints: [NSLayoutConstraint] = []
|
||||||
|
|
||||||
|
@available(iOS, obsoleted: 16.0)
|
||||||
|
private var draftToPresentOnAppear: Draft?
|
||||||
|
|
||||||
var selectedTab: Tab {
|
var selectedTab: Tab {
|
||||||
return Tab(rawValue: selectedIndex)!
|
return Tab(rawValue: selectedIndex)!
|
||||||
}
|
}
|
||||||
|
@ -85,6 +88,11 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
super.viewDidAppear(animated)
|
super.viewDidAppear(animated)
|
||||||
stateRestorationLogger.info("MainTabBarViewController: viewDidAppear, selectedIndex=\(self.selectedIndex, privacy: .public)")
|
stateRestorationLogger.info("MainTabBarViewController: viewDidAppear, selectedIndex=\(self.selectedIndex, privacy: .public)")
|
||||||
|
|
||||||
|
if let draftToPresentOnAppear {
|
||||||
|
self.draftToPresentOnAppear = nil
|
||||||
|
compose(editing: draftToPresentOnAppear, animated: true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidLayoutSubviews() {
|
override func viewDidLayoutSubviews() {
|
||||||
|
@ -235,23 +243,39 @@ extension MainTabBarViewController: TuskerNavigationDelegate {
|
||||||
extension MainTabBarViewController: TuskerRootViewController {
|
extension MainTabBarViewController: TuskerRootViewController {
|
||||||
func stateRestorationActivity() -> NSUserActivity? {
|
func stateRestorationActivity() -> NSUserActivity? {
|
||||||
let nav = viewController(for: .timelines) as! UINavigationController
|
let nav = viewController(for: .timelines) as! UINavigationController
|
||||||
guard let timelinePages = nav.viewControllers.first as? TimelinesPageViewController,
|
var activity: NSUserActivity?
|
||||||
let timelineVC = timelinePages.pageControllers[timelinePages.currentIndex] as? TimelineViewController else {
|
if let timelinePages = nav.viewControllers.first as? TimelinesPageViewController {
|
||||||
stateRestorationLogger.fault("MainTabBarViewController: Unable to create state restoration actiivty, couldn't find timeline/page VC")
|
activity = timelinePages.stateRestorationActivity()
|
||||||
return nil
|
} else {
|
||||||
|
stateRestorationLogger.fault("MainTabBarViewController: Unable to create state restoration activity, couldn't find timeline/page VC")
|
||||||
}
|
}
|
||||||
return timelineVC.stateRestorationActivity()
|
if let presentedNav = presentedViewController as? UINavigationController,
|
||||||
|
let compose = presentedNav.viewControllers.first as? ComposeHostingController {
|
||||||
|
activity = UserActivityManager.addEditedDraft(to: activity, draft: compose.draft)
|
||||||
|
}
|
||||||
|
return activity
|
||||||
}
|
}
|
||||||
|
|
||||||
func restoreActivity(_ activity: NSUserActivity) {
|
func restoreActivity(_ activity: NSUserActivity) {
|
||||||
|
func restoreEditedDraft() {
|
||||||
|
// on iOS 16+, this is handled by the duckable container
|
||||||
|
if #unavailable(iOS 16.0),
|
||||||
|
let draft = UserActivityManager.getDraft(from: activity) {
|
||||||
|
draftToPresentOnAppear = draft
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if activity.activityType == UserActivityType.showTimeline.rawValue {
|
if activity.activityType == UserActivityType.showTimeline.rawValue {
|
||||||
let nav = viewController(for: .timelines) as! UINavigationController
|
let nav = viewController(for: .timelines) as! UINavigationController
|
||||||
guard let timelinePages = nav.viewControllers.first as? TimelinesPageViewController,
|
guard let timelinePages = nav.viewControllers.first as? TimelinesPageViewController else {
|
||||||
let timelineVC = timelinePages.pageControllers[timelinePages.currentIndex] as? TimelineViewController else {
|
|
||||||
stateRestorationLogger.fault("MainTabBarViewController: Unable to restore timeline activity, couldn't find VC")
|
stateRestorationLogger.fault("MainTabBarViewController: Unable to restore timeline activity, couldn't find VC")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
timelineVC.restoreActivity(activity)
|
timelinePages.restoreActivity(activity)
|
||||||
|
restoreEditedDraft()
|
||||||
|
} else if activity.activityType == UserActivityType.newPost.rawValue {
|
||||||
|
restoreEditedDraft()
|
||||||
|
return
|
||||||
} else {
|
} else {
|
||||||
stateRestorationLogger.fault("MainTabBarViewController: Unable to restore activity of unexpected type \(activity.activityType, privacy: .public)")
|
stateRestorationLogger.fault("MainTabBarViewController: Unable to restore activity of unexpected type \(activity.activityType, privacy: .public)")
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,8 @@ import CoreData
|
||||||
|
|
||||||
struct AdvancedPrefsView : View {
|
struct AdvancedPrefsView : View {
|
||||||
@ObservedObject var preferences = Preferences.shared
|
@ObservedObject var preferences = Preferences.shared
|
||||||
|
@State private var imageCacheSize: Int64 = 0
|
||||||
|
@State private var mastodonCacheSize: Int64 = 0
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
|
@ -64,13 +66,42 @@ struct AdvancedPrefsView : View {
|
||||||
}
|
}
|
||||||
|
|
||||||
var cachingSection: some View {
|
var cachingSection: some View {
|
||||||
Section(header: Text("Caching"), footer: Text("Clearing caches will restart the app.")) {
|
Section {
|
||||||
Button(action: clearCache) {
|
Button(action: clearCache) {
|
||||||
Text("Clear Mastodon Cache")
|
Text("Clear Mastodon Cache")
|
||||||
}.foregroundColor(.red)
|
}.foregroundColor(.red)
|
||||||
Button(action: clearImageCaches) {
|
Button(action: clearImageCaches) {
|
||||||
Text("Clear Image Caches")
|
Text("Clear Image Caches")
|
||||||
}.foregroundColor(.red)
|
}.foregroundColor(.red)
|
||||||
|
} header: {
|
||||||
|
Text("Caching")
|
||||||
|
} footer: {
|
||||||
|
var s: AttributedString = "Clearing caches will restart the app."
|
||||||
|
if imageCacheSize != 0 {
|
||||||
|
s += AttributedString("\nImage cache size: \(ByteCountFormatter().string(fromByteCount: imageCacheSize))")
|
||||||
|
}
|
||||||
|
if mastodonCacheSize != 0 {
|
||||||
|
s += AttributedString("\nMastodon cache size: \(ByteCountFormatter().string(fromByteCount: mastodonCacheSize))")
|
||||||
|
}
|
||||||
|
return Text(s)
|
||||||
|
}.task {
|
||||||
|
imageCacheSize = [
|
||||||
|
ImageCache.avatars,
|
||||||
|
.headers,
|
||||||
|
.attachments,
|
||||||
|
.emojis,
|
||||||
|
].map {
|
||||||
|
$0.getDiskSizeInBytes() ?? 0
|
||||||
|
}.reduce(0, +)
|
||||||
|
mastodonCacheSize = LocalData.shared.accounts.map {
|
||||||
|
let descriptions = MastodonController.getForAccount($0).persistentContainer.persistentStoreDescriptions
|
||||||
|
return descriptions.map {
|
||||||
|
guard let url = $0.url else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return FileManager.default.recursiveSize(url: url) ?? 0
|
||||||
|
}.reduce(0, +)
|
||||||
|
}.reduce(0, +)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -252,8 +252,10 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro
|
||||||
}
|
}
|
||||||
|
|
||||||
private func doRestore() -> Bool {
|
private func doRestore() -> Bool {
|
||||||
guard let activity = activityToRestore,
|
guard let activity = activityToRestore else {
|
||||||
let statusIDs = activity.userInfo?["statusIDs"] as? [String] else {
|
return false
|
||||||
|
}
|
||||||
|
guard let statusIDs = activity.userInfo?["statusIDs"] as? [String] else {
|
||||||
stateRestorationLogger.fault("TimelineViewController: activity missing statusIDs")
|
stateRestorationLogger.fault("TimelineViewController: activity missing statusIDs")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,21 +46,27 @@ class TimelinesPageViewController: SegmentedPageViewController {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func stateRestorationActivity() -> NSUserActivity? {
|
||||||
|
return (pageControllers[currentIndex] as! TimelineViewController).stateRestorationActivity()
|
||||||
|
}
|
||||||
|
|
||||||
func restoreActivity(_ activity: NSUserActivity) {
|
func restoreActivity(_ activity: NSUserActivity) {
|
||||||
guard let timeline = UserActivityManager.getTimeline(from: activity) else {
|
guard let timeline = UserActivityManager.getTimeline(from: activity) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
let index: Int
|
||||||
switch timeline {
|
switch timeline {
|
||||||
case .home:
|
case .home:
|
||||||
selectPage(at: 0, animated: false)
|
index = 0
|
||||||
case .public(local: false):
|
case .public(local: false):
|
||||||
selectPage(at: 1, animated: false)
|
index = 1
|
||||||
case .public(local: true):
|
case .public(local: true):
|
||||||
selectPage(at: 2, animated: false)
|
index = 2
|
||||||
default:
|
default:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let timelineVC = pageControllers[currentIndex] as! TimelineViewController
|
selectPage(at: index, animated: false)
|
||||||
|
let timelineVC = pageControllers[index] as! TimelineViewController
|
||||||
timelineVC.restoreActivity(activity)
|
timelineVC.restoreActivity(activity)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ class SegmentedPageViewController: UIPageViewController, UIPageViewControllerDel
|
||||||
let titles: [String]
|
let titles: [String]
|
||||||
let pageControllers: [UIViewController]
|
let pageControllers: [UIViewController]
|
||||||
|
|
||||||
|
private var initialIndex = 0
|
||||||
private(set) var currentIndex = 0
|
private(set) var currentIndex = 0
|
||||||
|
|
||||||
var segmentedControl: UISegmentedControl!
|
var segmentedControl: UISegmentedControl!
|
||||||
|
@ -43,7 +44,7 @@ class SegmentedPageViewController: UIPageViewController, UIPageViewControllerDel
|
||||||
|
|
||||||
view.backgroundColor = .systemBackground
|
view.backgroundColor = .systemBackground
|
||||||
|
|
||||||
selectPage(at: 0, animated: false)
|
selectPage(at: initialIndex, animated: false)
|
||||||
|
|
||||||
addKeyCommand(MenuController.prevSubTabCommand)
|
addKeyCommand(MenuController.prevSubTabCommand)
|
||||||
addKeyCommand(MenuController.nextSubTabCommand)
|
addKeyCommand(MenuController.nextSubTabCommand)
|
||||||
|
@ -57,6 +58,10 @@ class SegmentedPageViewController: UIPageViewController, UIPageViewControllerDel
|
||||||
}
|
}
|
||||||
|
|
||||||
func selectPage(at index: Int, animated: Bool) {
|
func selectPage(at index: Int, animated: Bool) {
|
||||||
|
guard isViewLoaded else {
|
||||||
|
initialIndex = index
|
||||||
|
return
|
||||||
|
}
|
||||||
let direction: UIPageViewController.NavigationDirection = index - currentIndex > 0 ? .forward : .reverse
|
let direction: UIPageViewController.NavigationDirection = index - currentIndex > 0 ? .forward : .reverse
|
||||||
setViewControllers([pageControllers[index]], direction: direction, animated: animated)
|
setViewControllers([pageControllers[index]], direction: direction, animated: animated)
|
||||||
navigationItem.title = pageControllers[index].title
|
navigationItem.title = pageControllers[index].title
|
||||||
|
|
|
@ -91,10 +91,37 @@ class UserActivityManager {
|
||||||
return activity
|
return activity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func addDuckedDraft(to activity: NSUserActivity?, draft: Draft) -> NSUserActivity {
|
||||||
|
if let activity {
|
||||||
|
activity.addUserInfoEntries(from: [
|
||||||
|
"duckedDraftID": draft.id.uuidString
|
||||||
|
])
|
||||||
|
return activity
|
||||||
|
} else {
|
||||||
|
return editDraftActivity(id: draft.id, accountID: draft.accountID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func addEditedDraft(to activity: NSUserActivity?, draft: Draft) -> NSUserActivity {
|
||||||
|
if let activity {
|
||||||
|
activity.addUserInfoEntries(from: [
|
||||||
|
"editedDraftID": draft.id.uuidString
|
||||||
|
])
|
||||||
|
return activity
|
||||||
|
} else {
|
||||||
|
return editDraftActivity(id: draft.id, accountID: draft.accountID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static func getDraft(from activity: NSUserActivity) -> Draft? {
|
static func getDraft(from activity: NSUserActivity) -> Draft? {
|
||||||
guard activity.activityType == UserActivityType.newPost.rawValue,
|
let idStr: String?
|
||||||
let str = activity.userInfo?["draftID"] as? String,
|
if activity.activityType == UserActivityType.newPost.rawValue {
|
||||||
let uuid = UUID(uuidString: str) else {
|
idStr = activity.userInfo?["draftID"] as? String
|
||||||
|
} else {
|
||||||
|
idStr = activity.userInfo?["duckedDraftID"] as? String ?? activity.userInfo?["editedDraftID"] as? String
|
||||||
|
}
|
||||||
|
guard let idStr,
|
||||||
|
let uuid = UUID(uuidString: idStr) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return DraftsManager.shared.getBy(id: uuid)
|
return DraftsManager.shared.getBy(id: uuid)
|
||||||
|
|
|
@ -88,7 +88,7 @@ extension TuskerNavigationDelegate {
|
||||||
show(conversation(mainStatusID: statusID, state: state), sender: self)
|
show(conversation(mainStatusID: statusID, state: state), sender: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
func compose(editing draft: Draft) {
|
func compose(editing draft: Draft, animated: Bool = true) {
|
||||||
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
let compose = UserActivityManager.editDraftActivity(id: draft.id, accountID: apiController.accountInfo!.id)
|
let compose = UserActivityManager.editDraftActivity(id: draft.id, accountID: apiController.accountInfo!.id)
|
||||||
let options = UIWindowScene.ActivationRequestOptions()
|
let options = UIWindowScene.ActivationRequestOptions()
|
||||||
|
@ -97,20 +97,20 @@ extension TuskerNavigationDelegate {
|
||||||
} else {
|
} else {
|
||||||
let compose = ComposeHostingController(draft: draft, mastodonController: apiController)
|
let compose = ComposeHostingController(draft: draft, mastodonController: apiController)
|
||||||
if #available(iOS 16.0, *),
|
if #available(iOS 16.0, *),
|
||||||
presentDuckable(compose) {
|
presentDuckable(compose, animated: animated) {
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
let compose = ComposeHostingController(draft: draft, mastodonController: apiController)
|
let compose = ComposeHostingController(draft: draft, mastodonController: apiController)
|
||||||
let nav = UINavigationController(rootViewController: compose)
|
let nav = UINavigationController(rootViewController: compose)
|
||||||
nav.presentationController?.delegate = compose
|
nav.presentationController?.delegate = compose
|
||||||
present(nav, animated: true)
|
present(nav, animated: animated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func compose(inReplyToID: String? = nil, mentioningAcct: String? = nil) {
|
func compose(inReplyToID: String? = nil, mentioningAcct: String? = nil, animated: Bool = true) {
|
||||||
let draft = apiController.createDraft(inReplyToID: inReplyToID, mentioningAcct: mentioningAcct)
|
let draft = apiController.createDraft(inReplyToID: inReplyToID, mentioningAcct: mentioningAcct)
|
||||||
compose(editing: draft)
|
compose(editing: draft, animated: animated)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView) -> LoadingLargeImageViewController {
|
func loadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView) -> LoadingLargeImageViewController {
|
||||||
|
|
|
@ -14,6 +14,7 @@ import WebURL
|
||||||
import WebURLFoundationExtras
|
import WebURLFoundationExtras
|
||||||
|
|
||||||
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
|
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
|
||||||
|
private let dataDetectorsScheme = "x-apple-data-detectors"
|
||||||
|
|
||||||
class ContentTextView: LinkTextView, BaseEmojiLabel {
|
class ContentTextView: LinkTextView, BaseEmojiLabel {
|
||||||
|
|
||||||
|
@ -198,7 +199,8 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
|
||||||
}
|
}
|
||||||
|
|
||||||
let location = recognizer.location(in: self)
|
let location = recognizer.location(in: self)
|
||||||
if let (link, range) = getLinkAtPoint(location) {
|
if let (link, range) = getLinkAtPoint(location),
|
||||||
|
link.scheme != dataDetectorsScheme {
|
||||||
let text = (self.text as NSString).substring(with: range)
|
let text = (self.text as NSString).substring(with: range)
|
||||||
handleLinkTapped(url: link, text: text)
|
handleLinkTapped(url: link, text: text)
|
||||||
}
|
}
|
||||||
|
@ -287,9 +289,15 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
|
||||||
|
|
||||||
extension ContentTextView: UITextViewDelegate {
|
extension ContentTextView: UITextViewDelegate {
|
||||||
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
|
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
|
||||||
// generally disable the text view's link interactions, we handle tapping links ourself with a gesture recognizer
|
|
||||||
// the builtin data detectors use the x-apple-data-detectors scheme, and we allow the text view to handle those itself
|
// the builtin data detectors use the x-apple-data-detectors scheme, and we allow the text view to handle those itself
|
||||||
return URL.scheme == "x-apple-data-detectors"
|
if URL.scheme == dataDetectorsScheme {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
// otherwise, regular taps are handled by the gesture recognizer, but the accessibility interaction to select links with the rotor goes through here
|
||||||
|
// and this seems to be the only way of overriding what it does
|
||||||
|
handleLinkTapped(url: URL, text: (text as NSString).substring(with: characterRange))
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -395,6 +395,27 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override var accessibilityCustomActions: [UIAccessibilityCustomAction]? {
|
||||||
|
get {
|
||||||
|
guard let text = contentTextView.attributedText else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var actions: [UIAccessibilityCustomAction] = []
|
||||||
|
text.enumerateAttribute(.link, in: NSRange(location: 0, length: text.length)) { value, range, stop in
|
||||||
|
guard let value = value as? URL else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let text = text.attributedSubstring(from: range).string
|
||||||
|
actions.append(UIAccessibilityCustomAction(name: text) { [unowned self] _ in
|
||||||
|
self.contentTextView.handleLinkTapped(url: value, text: text)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return actions
|
||||||
|
}
|
||||||
|
set {}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Configure UI
|
// MARK: Configure UI
|
||||||
|
|
||||||
func updateUI(statusID: String, state: StatusState) {
|
func updateUI(statusID: String, state: StatusState) {
|
||||||
|
|
|
@ -308,6 +308,27 @@ class TimelineStatusTableViewCell: BaseStatusTableViewCell {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override var accessibilityCustomActions: [UIAccessibilityCustomAction]? {
|
||||||
|
get {
|
||||||
|
guard let text = contentTextView.attributedText else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var actions: [UIAccessibilityCustomAction] = []
|
||||||
|
text.enumerateAttribute(.link, in: NSRange(location: 0, length: text.length)) { value, range, stop in
|
||||||
|
guard let value = value as? URL else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let text = text.attributedSubstring(from: range).string
|
||||||
|
actions.append(UIAccessibilityCustomAction(name: text) { [unowned self] _ in
|
||||||
|
self.contentTextView.handleLinkTapped(url: value, text: text)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return actions
|
||||||
|
}
|
||||||
|
set {}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TimelineStatusTableViewCell: SelectableTableViewCell {
|
extension TimelineStatusTableViewCell: SelectableTableViewCell {
|
||||||
|
|
Loading…
Reference in New Issue