Fix Post button enabling/disabling
This commit is contained in:
parent
221ea05629
commit
f46150422a
@ -83,7 +83,7 @@ public final class ComposeController: ViewController {
|
||||
}
|
||||
|
||||
private var isPollValid: Bool {
|
||||
draft.poll == nil || draft.poll!.pollOptions.allSatisfy { !$0.text.isEmpty }
|
||||
!draft.pollEnabled || draft.poll!.pollOptions.allSatisfy { !$0.text.isEmpty }
|
||||
}
|
||||
|
||||
public var navigationTitle: String {
|
||||
|
@ -71,9 +71,6 @@ public class Draft: NSManagedObject, Identifiable {
|
||||
|
||||
extension Draft {
|
||||
public var hasContent: Bool {
|
||||
(!text.isEmpty && text != initialText) ||
|
||||
(contentWarningEnabled && !contentWarning.isEmpty && contentWarning != initialContentWarning) ||
|
||||
attachments.count > 0 ||
|
||||
poll?.hasContent == true
|
||||
!text.isEmpty && text != initialText
|
||||
}
|
||||
}
|
||||
|
@ -41,6 +41,6 @@ public class Poll: NSManagedObject {
|
||||
|
||||
extension Poll {
|
||||
public var hasContent: Bool {
|
||||
pollOptions.allSatisfy { !$0.text.isEmpty }
|
||||
pollOptions.contains { !$0.text.isEmpty }
|
||||
}
|
||||
}
|
||||
|
@ -482,7 +482,7 @@ struct AddAttachmentConditionsModifier: ViewModifier {
|
||||
if instanceFeatures.mastodonAttachmentRestrictions {
|
||||
return draft.attachments.count < 4
|
||||
&& draft.draftAttachments.allSatisfy { $0.type == .image }
|
||||
&& draft.poll == nil
|
||||
&& !draft.pollEnabled
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
|
@ -0,0 +1,219 @@
|
||||
//
|
||||
// ComposeNavigationBarActions.swift
|
||||
// ComposeUI
|
||||
//
|
||||
// Created by Shadowfacts on 1/30/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
import InstanceFeatures
|
||||
|
||||
struct ComposeNavigationBarActions: ToolbarContent {
|
||||
@ObservedObject var draft: Draft
|
||||
// Prior to iOS 16, the toolbar content doesn't seem to have access
|
||||
// to the environment from the containing view.
|
||||
let controller: ComposeController
|
||||
@Binding var isShowingDrafts: Bool
|
||||
let isPosting: Bool
|
||||
|
||||
var body: some ToolbarContent {
|
||||
ToolbarItem(placement: .cancellationAction) { ToolbarCancelButton(draft: draft) }
|
||||
|
||||
#if targetEnvironment(macCatalyst)
|
||||
ToolbarItem(placement: .topBarTrailing) { DraftsButton(isShowingDrafts: $isShowingDrafts) }
|
||||
ToolbarItem(placement: .confirmationAction) { PostButton(draft: draft, isPosting: isPosting) }
|
||||
#else
|
||||
ToolbarItem(placement: .confirmationAction) { PostOrDraftsButton(draft: draft, isShowingDrafts: $isShowingDrafts, isPosting: isPosting) }
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private struct ToolbarCancelButton: View {
|
||||
let draft: Draft
|
||||
@EnvironmentObject private var controller: ComposeController
|
||||
|
||||
var body: some View {
|
||||
Button(role: .cancel, action: controller.cancel) {
|
||||
Text("Cancel")
|
||||
}
|
||||
.disabled(controller.isPosting)
|
||||
.confirmationDialog("Are you sure?", isPresented: $controller.isShowingSaveDraftSheet) {
|
||||
// edit drafts can't be saved
|
||||
if draft.editedStatusID == nil {
|
||||
Button(action: { controller.cancel(deleteDraft: false) }) {
|
||||
Text("Save Draft")
|
||||
}
|
||||
Button(role: .destructive, action: { controller.cancel(deleteDraft: true) }) {
|
||||
Text("Delete Draft")
|
||||
}
|
||||
} else {
|
||||
Button(role: .destructive, action: { controller.cancel(deleteDraft: true) }) {
|
||||
Text("Cancel Edit")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
private struct PostOrDraftsButton: View {
|
||||
@DraftObserving var draft: Draft
|
||||
@Binding var isShowingDrafts: Bool
|
||||
let isPosting: Bool
|
||||
@Environment(\.composeUIConfig.allowSwitchingDrafts) private var allowSwitchingDrafts
|
||||
|
||||
var body: some View {
|
||||
if !draftIsEmpty || draft.editedStatusID != nil || !allowSwitchingDrafts {
|
||||
PostButton(draft: draft, isPosting: isPosting)
|
||||
} else {
|
||||
DraftsButton(isShowingDrafts: $isShowingDrafts)
|
||||
}
|
||||
}
|
||||
|
||||
private var draftIsEmpty: Bool {
|
||||
draft.text == draft.initialText && (!draft.contentWarningEnabled || draft.contentWarning == draft.initialContentWarning) && draft.attachments.count == 0 && !draft.pollEnabled
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
private struct PostButton: View {
|
||||
@DraftObserving var draft: Draft
|
||||
let isPosting: Bool
|
||||
@EnvironmentObject private var instanceFeatures: InstanceFeatures
|
||||
@Environment(\.composeUIConfig.requireAttachmentDescriptions) private var requireAttachmentDescriptions
|
||||
@EnvironmentObject private var controller: ComposeController
|
||||
|
||||
var body: some View {
|
||||
Button(action: controller.postStatus) {
|
||||
Text(draft.editedStatusID == nil ? "Post" : "Edit")
|
||||
}
|
||||
.keyboardShortcut(.return, modifiers: .command)
|
||||
.disabled(!draftValid)
|
||||
.disabled(isPosting)
|
||||
}
|
||||
|
||||
private var hasCharactersRemaining: Bool {
|
||||
let limit = instanceFeatures.maxStatusChars
|
||||
let cwCount = draft.contentWarningEnabled ? draft.contentWarning.count : 0
|
||||
let bodyCount = CharacterCounter.count(text: draft.text, for: instanceFeatures)
|
||||
let remaining = limit - (cwCount + bodyCount)
|
||||
return remaining >= 0
|
||||
}
|
||||
|
||||
private var attachmentsCombinationValid: Bool {
|
||||
if !instanceFeatures.mastodonAttachmentRestrictions {
|
||||
true
|
||||
} else if draft.attachments.count > 1,
|
||||
draft.draftAttachments.contains(where: { $0.type == .video }) {
|
||||
false
|
||||
} else if draft.attachments.count > 4 {
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private var attachmentsValid: Bool {
|
||||
(!requireAttachmentDescriptions || draft.draftAttachments.allSatisfy { !$0.attachmentDescription.isEmpty })
|
||||
&& attachmentsCombinationValid
|
||||
}
|
||||
|
||||
private var pollValid: Bool {
|
||||
!draft.pollEnabled || draft.poll!.pollOptions.allSatisfy { !$0.text.isEmpty }
|
||||
}
|
||||
|
||||
private var draftValid: Bool {
|
||||
draft.editedStatusID != nil ||
|
||||
((draft.hasContent || draft.attachments.count > 0)
|
||||
&& hasCharactersRemaining
|
||||
&& attachmentsValid
|
||||
&& pollValid)
|
||||
}
|
||||
}
|
||||
|
||||
private struct DraftsButton: View {
|
||||
@Binding var isShowingDrafts: Bool
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
isShowingDrafts = true
|
||||
} label: {
|
||||
Text("Drafts")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This property wrapper lets a View observe all of the following:
|
||||
// 1. The Draft itself
|
||||
// 2. The Draft's Poll (if it has one)
|
||||
// 3. Each of the Poll's PollOptions (if there is a Poll)
|
||||
// 4. Each of the Draft's DraftAttachments
|
||||
@propertyWrapper
|
||||
private struct DraftObserving: DynamicProperty {
|
||||
let wrappedValue: Draft
|
||||
@StateObject private var observer = Observer()
|
||||
|
||||
init(wrappedValue: Draft) {
|
||||
self.wrappedValue = wrappedValue
|
||||
}
|
||||
|
||||
func update() {
|
||||
observer.update(draft: wrappedValue)
|
||||
}
|
||||
|
||||
private class Observer: ObservableObject {
|
||||
private var draft: Draft?
|
||||
|
||||
private var cancellable: AnyCancellable?
|
||||
private var draftPollObservation: NSKeyValueObservation?
|
||||
private var pollOptionsObservation: NSKeyValueObservation?
|
||||
private var pollOptionsCancellables: [AnyCancellable] = []
|
||||
private var draftAttachmentsObservation: NSKeyValueObservation?
|
||||
private var draftAttachmentsCancellables: [AnyCancellable] = []
|
||||
|
||||
func update(draft: Draft) {
|
||||
guard draft !== self.draft else {
|
||||
return
|
||||
}
|
||||
self.draft = draft
|
||||
cancellable = draft.objectWillChange
|
||||
.sink { [unowned self] _ in self.objectWillChange.send() }
|
||||
draftPollObservation = draft.observe(\.poll) { [unowned self] _, _ in
|
||||
objectWillChange.send()
|
||||
self.pollChanged()
|
||||
}
|
||||
pollChanged()
|
||||
draftAttachmentsObservation = draft.observe(\.attachments) { [unowned self] _, _ in
|
||||
objectWillChange.send()
|
||||
self.draftAttachmentsChanged()
|
||||
}
|
||||
draftAttachmentsChanged()
|
||||
}
|
||||
|
||||
private func pollChanged() {
|
||||
pollOptionsObservation = (draft?.poll).map {
|
||||
$0.observe(\.options) { [unowned self] _, _ in
|
||||
objectWillChange.send()
|
||||
self.pollOptionsChanged()
|
||||
}
|
||||
}
|
||||
pollOptionsChanged()
|
||||
}
|
||||
|
||||
private func pollOptionsChanged() {
|
||||
pollOptionsCancellables = draft?.poll?.pollOptions.map {
|
||||
$0.objectWillChange
|
||||
.sink { [unowned self] _ in self.objectWillChange.send() }
|
||||
} ?? []
|
||||
}
|
||||
|
||||
private func draftAttachmentsChanged() {
|
||||
draftAttachmentsCancellables = draft?.draftAttachments.map {
|
||||
$0.objectWillChange
|
||||
.sink { [unowned self] _ in self.objectWillChange.send() }
|
||||
} ?? []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,11 +10,16 @@ import SwiftUI
|
||||
struct ComposeView: View {
|
||||
@ObservedObject var draft: Draft
|
||||
let mastodonController: any ComposeMastodonContext
|
||||
@State private var poster: PostService? = nil
|
||||
// @State private var poster: PostService? = nil
|
||||
@FocusState private var focusedField: FocusableField?
|
||||
@EnvironmentObject private var controller: ComposeController
|
||||
@State private var isShowingDrafts = false
|
||||
|
||||
// TODO: replace this with an @State owned by this view
|
||||
var poster: PostService? {
|
||||
controller.poster
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
navigation
|
||||
.environmentObject(mastodonController.instanceFeatures)
|
||||
@ -80,7 +85,7 @@ struct ComposeView: View {
|
||||
.modifier(NavigationTitleModifier(draft: draft, mastodonController: mastodonController))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarActions(draft: draft, controller: controller, isShowingDrafts: $isShowingDrafts)
|
||||
ComposeNavigationBarActions(draft: draft, controller: controller, isShowingDrafts: $isShowingDrafts, isPosting: poster != nil)
|
||||
#if os(visionOS)
|
||||
ToolbarItem(placement: .bottomOrnament) {
|
||||
toolbarView
|
||||
@ -143,82 +148,6 @@ public struct NavigationTitlePreferenceKey: PreferenceKey {
|
||||
}
|
||||
}
|
||||
|
||||
private struct ToolbarActions: ToolbarContent {
|
||||
@ObservedObject var draft: Draft
|
||||
// Prior to iOS 16, the toolbar content doesn't seem to have access
|
||||
// to the environment from the containing view.
|
||||
let controller: ComposeController
|
||||
@Binding var isShowingDrafts: Bool
|
||||
|
||||
var body: some ToolbarContent {
|
||||
ToolbarItem(placement: .cancellationAction) { ToolbarCancelButton(draft: draft) }
|
||||
|
||||
#if targetEnvironment(macCatalyst)
|
||||
ToolbarItem(placement: .topBarTrailing) { draftsButton }
|
||||
ToolbarItem(placement: .confirmationAction) { postButton }
|
||||
#else
|
||||
ToolbarItem(placement: .confirmationAction) { postOrDraftsButton }
|
||||
#endif
|
||||
}
|
||||
|
||||
private var draftsButton: some View {
|
||||
Button {
|
||||
isShowingDrafts = true
|
||||
} label: {
|
||||
Text("Drafts")
|
||||
}
|
||||
}
|
||||
|
||||
private var postButton: some View {
|
||||
// TODO: don't use the controller for this
|
||||
Button(action: controller.postStatus) {
|
||||
Text(draft.editedStatusID == nil ? "Post" : "Edit")
|
||||
}
|
||||
.keyboardShortcut(.return, modifiers: .command)
|
||||
.disabled(!controller.postButtonEnabled)
|
||||
}
|
||||
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
@ViewBuilder
|
||||
private var postOrDraftsButton: some View {
|
||||
if draft.hasContent || draft.editedStatusID != nil || !controller.config.allowSwitchingDrafts {
|
||||
postButton
|
||||
} else {
|
||||
draftsButton
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private struct ToolbarCancelButton: View {
|
||||
let draft: Draft
|
||||
@EnvironmentObject private var controller: ComposeController
|
||||
|
||||
var body: some View {
|
||||
Button(action: controller.cancel) {
|
||||
Text("Cancel")
|
||||
// otherwise all Buttons in the nav bar are made semibold
|
||||
.font(.system(size: 17, weight: .regular))
|
||||
}
|
||||
.disabled(controller.isPosting)
|
||||
.confirmationDialog("Are you sure?", isPresented: $controller.isShowingSaveDraftSheet) {
|
||||
// edit drafts can't be saved
|
||||
if draft.editedStatusID == nil {
|
||||
Button(action: { controller.cancel(deleteDraft: false) }) {
|
||||
Text("Save Draft")
|
||||
}
|
||||
Button(role: .destructive, action: { controller.cancel(deleteDraft: true) }) {
|
||||
Text("Delete Draft")
|
||||
}
|
||||
} else {
|
||||
Button(role: .destructive, action: { controller.cancel(deleteDraft: true) }) {
|
||||
Text("Cancel Edit")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum FocusableField: Hashable {
|
||||
case contentWarning
|
||||
case body
|
||||
|
@ -97,7 +97,7 @@ private struct TogglePollButton: View {
|
||||
|
||||
var body: some View {
|
||||
Button(action: togglePoll) {
|
||||
Image(systemName: draft.poll == nil ? "chart.bar.doc.horizontal" : "chart.bar.horizontal.page.fill")
|
||||
Image(systemName: draft.pollEnabled ? "chart.bar.doc.horizontal.fill" : "chart.bar.horizontal.page")
|
||||
}
|
||||
.buttonStyle(LanguageButtonStyle())
|
||||
.disabled(disabled)
|
||||
|
Loading…
x
Reference in New Issue
Block a user