Re-add compose ducking
This commit is contained in:
parent
99a12c58de
commit
fb39a93569
@ -11,11 +11,12 @@ import PhotosUI
|
||||
import PencilKit
|
||||
import TuskerComponents
|
||||
|
||||
// Configuration/data injected from outside the compose UI.
|
||||
public struct ComposeUIConfig {
|
||||
// Config
|
||||
public var allowSwitchingDrafts = true
|
||||
public var textSelectionStartsAtBeginning = false
|
||||
public var deleteDraftOnDisappear = true
|
||||
public var showToolbar = true
|
||||
|
||||
// Style
|
||||
public var backgroundColor = Color(uiColor: .systemBackground)
|
||||
|
@ -9,33 +9,53 @@ import SwiftUI
|
||||
import CoreData
|
||||
import Pachyderm
|
||||
|
||||
// State owned by the compose UI but that needs to be accessible from outside.
|
||||
public final class ComposeViewState: ObservableObject {
|
||||
@Published var poster: PostService?
|
||||
@Published public internal(set) var draft: Draft
|
||||
@Published public internal(set) var didPostSuccessfully = false
|
||||
|
||||
public var isPosting: Bool {
|
||||
poster != nil
|
||||
}
|
||||
|
||||
public init(draft: Draft) {
|
||||
self.draft = draft
|
||||
}
|
||||
}
|
||||
|
||||
public struct ComposeView: View {
|
||||
@State var draft: Draft
|
||||
@ObservedObject var state: ComposeViewState
|
||||
let mastodonController: any ComposeMastodonContext
|
||||
let currentAccount: (any AccountProtocol)?
|
||||
let config: ComposeUIConfig
|
||||
|
||||
public init(
|
||||
initialDraft: Draft,
|
||||
state: ComposeViewState,
|
||||
mastodonController: any ComposeMastodonContext,
|
||||
currentAccount: (any AccountProtocol)?,
|
||||
config: ComposeUIConfig
|
||||
) {
|
||||
self.draft = initialDraft
|
||||
self.state = state
|
||||
self.mastodonController = mastodonController
|
||||
self.currentAccount = currentAccount
|
||||
self.config = config
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
ComposeViewBody(draft: draft, mastodonController: mastodonController, setDraft: self.setDraft)
|
||||
ComposeViewBody(
|
||||
draft: state.draft,
|
||||
mastodonController: mastodonController,
|
||||
state: state,
|
||||
setDraft: self.setDraft
|
||||
)
|
||||
.environment(\.composeUIConfig, config)
|
||||
.environment(\.currentAccount, currentAccount)
|
||||
}
|
||||
|
||||
private func setDraft(_ draft: Draft) {
|
||||
let oldDraft = self.draft
|
||||
self.draft = draft
|
||||
let oldDraft = state.draft
|
||||
state.draft = draft
|
||||
|
||||
if oldDraft.hasContent {
|
||||
oldDraft.lastModified = Date()
|
||||
@ -50,10 +70,9 @@ public struct ComposeView: View {
|
||||
private struct ComposeViewBody: View {
|
||||
@ObservedObject var draft: Draft
|
||||
let mastodonController: any ComposeMastodonContext
|
||||
@ObservedObject var state: ComposeViewState
|
||||
let setDraft: (Draft) -> Void
|
||||
@State private var poster: PostService?
|
||||
@State private var postError: PostService.Error?
|
||||
@State private var didPostSuccessfully = false
|
||||
@FocusState private var focusedField: FocusableField?
|
||||
@State private var isShowingDrafts = false
|
||||
@State private var isDismissing = false
|
||||
@ -108,19 +127,22 @@ private struct ComposeViewBody: View {
|
||||
#endif
|
||||
}
|
||||
.overlay(alignment: .top) {
|
||||
if let poster {
|
||||
if let poster = state.poster {
|
||||
PostProgressView(poster: poster)
|
||||
.frame(alignment: .top)
|
||||
}
|
||||
}
|
||||
#if !os(visionOS)
|
||||
.overlay(alignment: .bottom, content: {
|
||||
// TODO: during ducking animation, toolbar should move off the botto edge
|
||||
// This needs to be in an overlay, ignoring the keyboard safe area
|
||||
// doesn't work with the safeAreaInset modifier.
|
||||
if config.showToolbar {
|
||||
toolbarView
|
||||
.frame(maxHeight: .infinity, alignment: .bottom)
|
||||
.ignoresSafeArea(.keyboard)
|
||||
.transition(.move(edge: .bottom))
|
||||
.animation(.snappy, value: config.showToolbar)
|
||||
}
|
||||
})
|
||||
#endif
|
||||
// Have these after the overlays so they barely work instead of not working at all. FB11790805
|
||||
@ -129,7 +151,7 @@ private struct ComposeViewBody: View {
|
||||
.modifier(NavigationTitleModifier(draft: draft, mastodonController: mastodonController))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ComposeNavigationBarActions(draft: draft, isShowingDrafts: $isShowingDrafts, isPosting: poster != nil, cancel: self.cancel(deleteDraft:), postStatus: self.postStatus)
|
||||
ComposeNavigationBarActions(draft: draft, isShowingDrafts: $isShowingDrafts, isPosting: state.isPosting, cancel: self.cancel(deleteDraft:), postStatus: self.postStatus)
|
||||
#if os(visionOS)
|
||||
ToolbarItem(placement: .bottomOrnament) {
|
||||
toolbarView
|
||||
@ -158,7 +180,7 @@ private struct ComposeViewBody: View {
|
||||
|
||||
private func deleteOrSaveDraft() {
|
||||
if isDismissing,
|
||||
!draft.hasContent || didPostSuccessfully || userConfirmedDelete {
|
||||
!draft.hasContent || state.didPostSuccessfully || userConfirmedDelete {
|
||||
DraftsPersistentContainer.shared.viewContext.delete(draft)
|
||||
} else {
|
||||
draft.lastModified = Date()
|
||||
@ -173,19 +195,19 @@ private struct ComposeViewBody: View {
|
||||
}
|
||||
|
||||
private func postStatus() async {
|
||||
guard poster == nil,
|
||||
guard !state.isPosting,
|
||||
draft.editedStatusID != nil || draft.hasContent else {
|
||||
return
|
||||
}
|
||||
|
||||
let poster = PostService(mastodonController: mastodonController, config: config, draft: draft)
|
||||
self.poster = poster
|
||||
state.poster = poster
|
||||
|
||||
do {
|
||||
try await poster.post()
|
||||
|
||||
isDismissing = true
|
||||
didPostSuccessfully = true
|
||||
state.didPostSuccessfully = true
|
||||
|
||||
// wait .25 seconds so the user can see the progress bar has completed
|
||||
try? await Task.sleep(nanoseconds: 250_000_000)
|
||||
@ -195,16 +217,13 @@ private struct ComposeViewBody: View {
|
||||
config.dismiss(.post)
|
||||
} catch {
|
||||
self.postError = error
|
||||
self.poster = nil
|
||||
state.poster = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct NavigationTitleModifier: ViewModifier {
|
||||
let draft: Draft
|
||||
let mastodonController: any ComposeMastodonContext
|
||||
|
||||
private var navigationTitle: String {
|
||||
extension ComposeView {
|
||||
public static func navigationTitle(for draft: Draft, mastodonController: any ComposeMastodonContext) -> String {
|
||||
if let id = draft.inReplyToID,
|
||||
let status = mastodonController.fetchStatus(id: id) {
|
||||
return "Reply to @\(status.account.acct)"
|
||||
@ -214,20 +233,16 @@ private struct NavigationTitleModifier: ViewModifier {
|
||||
return "New Post"
|
||||
}
|
||||
}
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
let title = navigationTitle
|
||||
content
|
||||
.navigationTitle(title)
|
||||
.preference(key: NavigationTitlePreferenceKey.self, value: title)
|
||||
}
|
||||
}
|
||||
|
||||
// Public preference so that the host can read the title.
|
||||
public struct NavigationTitlePreferenceKey: PreferenceKey {
|
||||
public static var defaultValue: String? { nil }
|
||||
public static func reduce(value: inout String?, nextValue: () -> String?) {
|
||||
value = value ?? nextValue()
|
||||
private struct NavigationTitleModifier: ViewModifier {
|
||||
let draft: Draft
|
||||
let mastodonController: any ComposeMastodonContext
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
let title = ComposeView.navigationTitle(for: draft, mastodonController: mastodonController)
|
||||
content
|
||||
.navigationTitle(title)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,8 @@ class DuckedPlaceholderViewController: UIViewController {
|
||||
|
||||
var topConstraint: NSLayoutConstraint!
|
||||
|
||||
private var titleObservation: NSKeyValueObservation?
|
||||
|
||||
init(for duckableViewController: DuckableViewController, owner: DuckableContainerViewController) {
|
||||
self.owner = owner
|
||||
|
||||
@ -21,8 +23,12 @@ class DuckedPlaceholderViewController: UIViewController {
|
||||
|
||||
let item = UINavigationItem()
|
||||
item.title = duckableViewController.navigationItem.title
|
||||
item.titleView = duckableViewController.navigationItem.titleView
|
||||
assert(duckableViewController.navigationItem.titleView == nil)
|
||||
navBar.setItems([item], animated: false)
|
||||
|
||||
titleObservation = duckableViewController.navigationItem.observe(\.title, changeHandler: { _, _ in
|
||||
item.title = duckableViewController.navigationItem.title
|
||||
})
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
|
@ -68,11 +68,10 @@ class ComposeSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDeleg
|
||||
window!.rootViewController = composeVC
|
||||
window!.makeKeyAndVisible()
|
||||
|
||||
// TODO: get the title out here somehow
|
||||
// updateTitle(draft: composeVC.controller.draft)
|
||||
// composeVC.controller.$draft
|
||||
// .sink { [unowned self] in self.updateTitle(draft: $0) }
|
||||
// .store(in: &cancellables)
|
||||
updateTitle(draft: composeVC.state.draft)
|
||||
composeVC.state.$draft
|
||||
.sink { [unowned self] in self.updateTitle(draft: $0) }
|
||||
.store(in: &cancellables)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(themePrefChanged), name: .themePreferenceChanged, object: nil)
|
||||
themePrefChanged()
|
||||
@ -81,13 +80,12 @@ class ComposeSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDeleg
|
||||
func sceneWillResignActive(_ scene: UIScene) {
|
||||
DraftsPersistentContainer.shared.save()
|
||||
|
||||
// TODO: update user activity
|
||||
// if let window = window,
|
||||
// let nav = window.rootViewController as? UINavigationController,
|
||||
// let compose = nav.topViewController as? ComposeHostingController,
|
||||
// !compose.controller.didPostSuccessfully {
|
||||
// scene.userActivity = UserActivityManager.editDraftActivity(id: compose.controller.draft.id, accountID: scene.session.mastodonController!.accountInfo!.id)
|
||||
// }
|
||||
if let window = window,
|
||||
let nav = window.rootViewController as? UINavigationController,
|
||||
let composeVC = nav.topViewController as? ComposeHostingController,
|
||||
!composeVC.state.didPostSuccessfully {
|
||||
scene.userActivity = UserActivityManager.editDraftActivity(id: composeVC.state.draft.id, accountID: scene.session.mastodonController!.accountInfo!.id)
|
||||
}
|
||||
}
|
||||
|
||||
func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
|
||||
|
@ -36,6 +36,8 @@ class ComposeHostingController: UIHostingController<ComposeHostingController.Vie
|
||||
|
||||
@ObservableObjectBox private var config: ComposeUIConfig
|
||||
@ObservableObjectBox private var currentAccount: AccountMO?
|
||||
// Internal visibility so it can be accessed from ComposeSceneDelegate for the window title
|
||||
let state: ComposeViewState
|
||||
|
||||
init(draft: Draft?, mastodonController: MastodonController) {
|
||||
let draft = draft ?? mastodonController.createDraft()
|
||||
@ -43,8 +45,16 @@ class ComposeHostingController: UIHostingController<ComposeHostingController.Vie
|
||||
self.mastodonController = mastodonController
|
||||
self.config = ComposeUIConfig()
|
||||
self.currentAccount = mastodonController.account
|
||||
let state = ComposeViewState(draft: draft)
|
||||
self.state = state
|
||||
|
||||
super.init(rootView: View(initialDraft: draft, mastodonController: mastodonController, config: _config, currentAccount: _currentAccount))
|
||||
let rootView = View(
|
||||
state: state,
|
||||
mastodonController: mastodonController,
|
||||
config: _config,
|
||||
currentAccount: _currentAccount
|
||||
)
|
||||
super.init(rootView: rootView)
|
||||
|
||||
self.updateConfig()
|
||||
|
||||
@ -53,8 +63,7 @@ class ComposeHostingController: UIHostingController<ComposeHostingController.Vie
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(updateConfig), name: .preferencesChanged, object: nil)
|
||||
|
||||
// set an initial title immediately, in case we're starting ducked
|
||||
// self.navigationItem.title = self.controller.navigationTitle
|
||||
// TODO: get the navigation title from somewhere
|
||||
self.navigationItem.title = ComposeView.navigationTitle(for: draft, mastodonController: mastodonController)
|
||||
|
||||
mastodonController.$account
|
||||
.sink { [unowned self] in
|
||||
@ -142,13 +151,18 @@ class ComposeHostingController: UIHostingController<ComposeHostingController.Vie
|
||||
}
|
||||
|
||||
struct View: SwiftUI.View {
|
||||
let initialDraft: Draft
|
||||
let mastodonController: MastodonController
|
||||
@ObservedObject @ObservableObjectBox private var config: ComposeUIConfig
|
||||
@ObservedObject @ObservableObjectBox private var currentAccount: AccountMO?
|
||||
let state: ComposeViewState
|
||||
|
||||
fileprivate init(initialDraft: Draft, mastodonController: MastodonController, config: ObservableObjectBox<ComposeUIConfig>, currentAccount: ObservableObjectBox<AccountMO?>) {
|
||||
self.initialDraft = initialDraft
|
||||
fileprivate init(
|
||||
state: ComposeViewState,
|
||||
mastodonController: MastodonController,
|
||||
config: ObservableObjectBox<ComposeUIConfig>,
|
||||
currentAccount: ObservableObjectBox<AccountMO?>
|
||||
) {
|
||||
self.state = state
|
||||
self.mastodonController = mastodonController
|
||||
self._config = ObservedObject(wrappedValue: config)
|
||||
self._currentAccount = ObservedObject(wrappedValue: currentAccount)
|
||||
@ -156,7 +170,7 @@ class ComposeHostingController: UIHostingController<ComposeHostingController.Vie
|
||||
|
||||
var body: some SwiftUI.View {
|
||||
ComposeView(
|
||||
initialDraft: initialDraft,
|
||||
state: state,
|
||||
mastodonController: mastodonController,
|
||||
currentAccount: currentAccount,
|
||||
config: config
|
||||
@ -183,28 +197,22 @@ private final class ObservableObjectBox<T>: ObservableObject {
|
||||
#if canImport(Duckable)
|
||||
extension ComposeHostingController: DuckableViewController {
|
||||
func duckableViewControllerShouldDuck() -> DuckAttemptAction {
|
||||
// if controller.isPosting {
|
||||
// return .block
|
||||
// } else if controller.draft.hasContent {
|
||||
// return .duck
|
||||
// } else {
|
||||
// return .dismiss
|
||||
// }
|
||||
// TODO: ducking
|
||||
if state.isPosting {
|
||||
return .block
|
||||
} else if state.draft.hasContent {
|
||||
return .duck
|
||||
} else {
|
||||
return .dismiss
|
||||
}
|
||||
}
|
||||
|
||||
func duckableViewControllerWillAnimateDuck(withDuration duration: CGFloat, afterDelay delay: CGFloat) {
|
||||
config.deleteDraftOnDisappear = false
|
||||
|
||||
// TODO: figure out if this is still necessary
|
||||
// withAnimation(.linear(duration: duration).delay(delay)) {
|
||||
// controller.showToolbar = false
|
||||
// }
|
||||
navigationItem.title = ComposeView.navigationTitle(for: state.draft, mastodonController: mastodonController)
|
||||
config.showToolbar = false
|
||||
}
|
||||
|
||||
func duckableViewControllerDidFinishAnimatingDuck() {
|
||||
// controller.showToolbar = true
|
||||
config.showToolbar = true
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
Loading…
x
Reference in New Issue
Block a user