Compare commits
2 Commits
d87e9e6d92
...
502e9e3b2a
Author | SHA1 | Date | |
---|---|---|---|
502e9e3b2a | |||
221ea05629 |
@ -45,6 +45,14 @@ final class PostService: ObservableObject {
|
|||||||
await updateEditedAttachments()
|
await updateEditedAttachments()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let pollParams: EditPollParameters?
|
||||||
|
if draft.pollEnabled,
|
||||||
|
let poll = draft.poll {
|
||||||
|
pollParams = EditPollParameters(options: poll.pollOptions.map(\.text), expiresIn: Int(poll.duration), multiple: poll.multiple)
|
||||||
|
} else {
|
||||||
|
pollParams = nil
|
||||||
|
}
|
||||||
|
|
||||||
request = Client.editStatus(
|
request = Client.editStatus(
|
||||||
id: editedStatusID,
|
id: editedStatusID,
|
||||||
text: textForPosting(),
|
text: textForPosting(),
|
||||||
@ -60,11 +68,23 @@ final class PostService: ObservableObject {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
poll: draft.poll.map {
|
poll: pollParams
|
||||||
EditPollParameters(options: $0.pollOptions.map(\.text), expiresIn: Int($0.duration), multiple: $0.multiple)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
let pollOptions: [String]?
|
||||||
|
let pollExpiresIn: Int?
|
||||||
|
let pollMultiple: Bool?
|
||||||
|
if draft.pollEnabled,
|
||||||
|
let poll = draft.poll {
|
||||||
|
pollOptions = poll.pollOptions.map(\.text)
|
||||||
|
pollExpiresIn = Int(poll.duration)
|
||||||
|
pollMultiple = poll.multiple
|
||||||
|
} else {
|
||||||
|
pollOptions = nil
|
||||||
|
pollExpiresIn = nil
|
||||||
|
pollMultiple = nil
|
||||||
|
}
|
||||||
|
|
||||||
request = Client.createStatus(
|
request = Client.createStatus(
|
||||||
text: textForPosting(),
|
text: textForPosting(),
|
||||||
contentType: config.contentType,
|
contentType: config.contentType,
|
||||||
@ -74,9 +94,9 @@ final class PostService: ObservableObject {
|
|||||||
spoilerText: contentWarning,
|
spoilerText: contentWarning,
|
||||||
visibility: draft.localOnly && mastodonController.instanceFeatures.localOnlyPostsVisibility ? Status.localPostVisibility : draft.visibility.rawValue,
|
visibility: draft.localOnly && mastodonController.instanceFeatures.localOnlyPostsVisibility ? Status.localPostVisibility : draft.visibility.rawValue,
|
||||||
language: mastodonController.instanceFeatures.createStatusWithLanguage ? draft.language : nil,
|
language: mastodonController.instanceFeatures.createStatusWithLanguage ? draft.language : nil,
|
||||||
pollOptions: draft.poll?.pollOptions.map(\.text),
|
pollOptions: pollOptions,
|
||||||
pollExpiresIn: draft.poll == nil ? nil : Int(draft.poll!.duration),
|
pollExpiresIn: pollExpiresIn,
|
||||||
pollMultiple: draft.poll?.multiple,
|
pollMultiple: pollMultiple,
|
||||||
localOnly: mastodonController.instanceFeatures.localOnlyPosts && !mastodonController.instanceFeatures.localOnlyPostsVisibility ? draft.localOnly : nil,
|
localOnly: mastodonController.instanceFeatures.localOnlyPosts && !mastodonController.instanceFeatures.localOnlyPostsVisibility ? draft.localOnly : nil,
|
||||||
idempotencyKey: draft.id.uuidString
|
idempotencyKey: draft.id.uuidString
|
||||||
)
|
)
|
||||||
|
@ -83,7 +83,7 @@ public final class ComposeController: ViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var isPollValid: Bool {
|
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 {
|
public var navigationTitle: String {
|
||||||
|
@ -31,12 +31,18 @@ public class Draft: NSManagedObject, Identifiable {
|
|||||||
@NSManaged public var language: String? // ISO 639 language code
|
@NSManaged public var language: String? // ISO 639 language code
|
||||||
@NSManaged public var lastModified: Date!
|
@NSManaged public var lastModified: Date!
|
||||||
@NSManaged public var localOnly: Bool
|
@NSManaged public var localOnly: Bool
|
||||||
|
@NSManaged private var pollEnabledInternal: NSNumber?
|
||||||
@NSManaged public var text: String
|
@NSManaged public var text: String
|
||||||
@NSManaged private var visibilityStr: String
|
@NSManaged private var visibilityStr: String
|
||||||
|
|
||||||
@NSManaged internal var attachments: NSMutableOrderedSet
|
@NSManaged internal var attachments: NSMutableOrderedSet
|
||||||
@NSManaged public var poll: Poll?
|
@NSManaged public var poll: Poll?
|
||||||
|
|
||||||
|
public var pollEnabled: Bool {
|
||||||
|
get { pollEnabledInternal.map(\.boolValue) ?? (poll != nil) }
|
||||||
|
set { pollEnabledInternal = NSNumber(booleanLiteral: newValue) }
|
||||||
|
}
|
||||||
|
|
||||||
public var visibility: Visibility {
|
public var visibility: Visibility {
|
||||||
get {
|
get {
|
||||||
Visibility(rawValue: visibilityStr) ?? .public
|
Visibility(rawValue: visibilityStr) ?? .public
|
||||||
@ -65,9 +71,6 @@ public class Draft: NSManagedObject, Identifiable {
|
|||||||
|
|
||||||
extension Draft {
|
extension Draft {
|
||||||
public var hasContent: Bool {
|
public var hasContent: Bool {
|
||||||
(!text.isEmpty && text != initialText) ||
|
!text.isEmpty && text != initialText
|
||||||
(contentWarningEnabled && !contentWarning.isEmpty && contentWarning != initialContentWarning) ||
|
|
||||||
attachments.count > 0 ||
|
|
||||||
poll?.hasContent == true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22222" systemVersion="22G91" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23605" systemVersion="24A335" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||||
<entity name="Draft" representedClassName="ComposeUI.Draft" syncable="YES">
|
<entity name="Draft" representedClassName="ComposeUI.Draft" syncable="YES">
|
||||||
<attribute name="accountID" attributeType="String"/>
|
<attribute name="accountID" attributeType="String"/>
|
||||||
<attribute name="contentWarning" attributeType="String" defaultValueString=""/>
|
<attribute name="contentWarning" attributeType="String" defaultValueString=""/>
|
||||||
@ -12,6 +12,7 @@
|
|||||||
<attribute name="language" optional="YES" attributeType="String"/>
|
<attribute name="language" optional="YES" attributeType="String"/>
|
||||||
<attribute name="lastModified" attributeType="Date" usesScalarValueType="NO"/>
|
<attribute name="lastModified" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
<attribute name="localOnly" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
<attribute name="localOnly" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="pollEnabledInternal" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||||
<attribute name="text" attributeType="String" defaultValueString=""/>
|
<attribute name="text" attributeType="String" defaultValueString=""/>
|
||||||
<attribute name="visibilityStr" optional="YES" attributeType="String"/>
|
<attribute name="visibilityStr" optional="YES" attributeType="String"/>
|
||||||
<relationship name="attachments" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="DraftAttachment" inverseName="draft" inverseEntity="DraftAttachment"/>
|
<relationship name="attachments" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="DraftAttachment" inverseName="draft" inverseEntity="DraftAttachment"/>
|
||||||
|
@ -41,6 +41,6 @@ public class Poll: NSManagedObject {
|
|||||||
|
|
||||||
extension Poll {
|
extension Poll {
|
||||||
public var hasContent: Bool {
|
public var hasContent: Bool {
|
||||||
pollOptions.allSatisfy { !$0.text.isEmpty }
|
pollOptions.contains { !$0.text.isEmpty }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,6 @@ struct OptionalObservedObject<T: ObservableObject>: DynamicProperty {
|
|||||||
didSet {
|
didSet {
|
||||||
cancellable?.cancel()
|
cancellable?.cancel()
|
||||||
cancellable = wrapped?.objectWillChange
|
cancellable = wrapped?.objectWillChange
|
||||||
.receive(on: RunLoop.main)
|
|
||||||
.sink { [unowned self] _ in
|
.sink { [unowned self] _ in
|
||||||
self.objectWillChange.send()
|
self.objectWillChange.send()
|
||||||
}
|
}
|
||||||
@ -27,6 +26,10 @@ struct OptionalObservedObject<T: ObservableObject>: DynamicProperty {
|
|||||||
@StateObject private var republisher = Republisher()
|
@StateObject private var republisher = Republisher()
|
||||||
var wrappedValue: T?
|
var wrappedValue: T?
|
||||||
|
|
||||||
|
init(wrappedValue: T?) {
|
||||||
|
self.wrappedValue = wrappedValue
|
||||||
|
}
|
||||||
|
|
||||||
func update() {
|
func update() {
|
||||||
republisher.wrapped = wrappedValue
|
republisher.wrapped = wrappedValue
|
||||||
}
|
}
|
||||||
|
@ -482,7 +482,7 @@ struct AddAttachmentConditionsModifier: ViewModifier {
|
|||||||
if instanceFeatures.mastodonAttachmentRestrictions {
|
if instanceFeatures.mastodonAttachmentRestrictions {
|
||||||
return draft.attachments.count < 4
|
return draft.attachments.count < 4
|
||||||
&& draft.draftAttachments.allSatisfy { $0.type == .image }
|
&& draft.draftAttachments.allSatisfy { $0.type == .image }
|
||||||
&& draft.poll == nil
|
&& !draft.pollEnabled
|
||||||
} else {
|
} else {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,8 @@ struct ComposeDraftView: View {
|
|||||||
|
|
||||||
DraftContentEditor(draft: draft, focusedField: $focusedField)
|
DraftContentEditor(draft: draft, focusedField: $focusedField)
|
||||||
|
|
||||||
if let poll = draft.poll {
|
if let poll = draft.poll,
|
||||||
|
draft.pollEnabled {
|
||||||
PollEditor(poll: poll)
|
PollEditor(poll: poll)
|
||||||
.padding(.bottom, 4)
|
.padding(.bottom, 4)
|
||||||
// So that during the appearance transition, it's behind the text view.
|
// So that during the appearance transition, it's behind the text view.
|
||||||
@ -41,7 +42,9 @@ struct ComposeDraftView: View {
|
|||||||
// Otherwise, when the poll is added, its bottom edge is aligned with the top edge of the attachments
|
// Otherwise, when the poll is added, its bottom edge is aligned with the top edge of the attachments
|
||||||
.padding(.top, draft.poll == nil ? 0 : -4)
|
.padding(.top, draft.poll == nil ? 0 : -4)
|
||||||
}
|
}
|
||||||
.animation(.snappy, value: draft.poll == nil)
|
// These animations are here, because the height of the VStack and the positions of the lower views needs to animate too.
|
||||||
|
.animation(.snappy, value: draft.pollEnabled)
|
||||||
|
.modifier(PollAnimatingModifier(poll: draft.poll))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -78,3 +81,12 @@ private struct AccountNameView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Separate modifier because we need to observe the Poll itself, not the draft
|
||||||
|
private struct PollAnimatingModifier: ViewModifier {
|
||||||
|
@OptionalObservedObject var poll: Poll?
|
||||||
|
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
content.animation(.snappy, value: poll?.pollOptions.count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -0,0 +1,221 @@
|
|||||||
|
//
|
||||||
|
// 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(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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#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 {
|
struct ComposeView: View {
|
||||||
@ObservedObject var draft: Draft
|
@ObservedObject var draft: Draft
|
||||||
let mastodonController: any ComposeMastodonContext
|
let mastodonController: any ComposeMastodonContext
|
||||||
@State private var poster: PostService? = nil
|
// @State private var poster: PostService? = nil
|
||||||
@FocusState private var focusedField: FocusableField?
|
@FocusState private var focusedField: FocusableField?
|
||||||
@EnvironmentObject private var controller: ComposeController
|
@EnvironmentObject private var controller: ComposeController
|
||||||
@State private var isShowingDrafts = false
|
@State private var isShowingDrafts = false
|
||||||
|
|
||||||
|
// TODO: replace this with an @State owned by this view
|
||||||
|
var poster: PostService? {
|
||||||
|
controller.poster
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
navigation
|
navigation
|
||||||
.environmentObject(mastodonController.instanceFeatures)
|
.environmentObject(mastodonController.instanceFeatures)
|
||||||
@ -80,7 +85,7 @@ struct ComposeView: View {
|
|||||||
.modifier(NavigationTitleModifier(draft: draft, mastodonController: mastodonController))
|
.modifier(NavigationTitleModifier(draft: draft, mastodonController: mastodonController))
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarActions(draft: draft, controller: controller, isShowingDrafts: $isShowingDrafts)
|
ComposeNavigationBarActions(draft: draft, controller: controller, isShowingDrafts: $isShowingDrafts, isPosting: poster != nil)
|
||||||
#if os(visionOS)
|
#if os(visionOS)
|
||||||
ToolbarItem(placement: .bottomOrnament) {
|
ToolbarItem(placement: .bottomOrnament) {
|
||||||
toolbarView
|
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 {
|
enum FocusableField: Hashable {
|
||||||
case contentWarning
|
case contentWarning
|
||||||
case body
|
case body
|
||||||
|
@ -96,22 +96,27 @@ private struct TogglePollButton: View {
|
|||||||
@EnvironmentObject private var instanceFeatures: InstanceFeatures
|
@EnvironmentObject private var instanceFeatures: InstanceFeatures
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Button {
|
Button(action: togglePoll) {
|
||||||
if draft.poll == nil {
|
Image(systemName: draft.pollEnabled ? "chart.bar.doc.horizontal.fill" : "chart.bar.horizontal.page")
|
||||||
draft.poll = Poll(context: DraftsPersistentContainer.shared.viewContext)
|
|
||||||
} else {
|
|
||||||
draft.poll = nil
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Image(systemName: draft.poll == nil ? "chart.bar.doc.horizontal" : "chart.bar.horizontal.page.fill")
|
|
||||||
}
|
}
|
||||||
.buttonStyle(LanguageButtonStyle())
|
.buttonStyle(LanguageButtonStyle())
|
||||||
.disabled(disabled)
|
.disabled(disabled)
|
||||||
.animation(.linear(duration: 0.2), value: disabled)
|
.animation(.linear(duration: 0.2), value: disabled)
|
||||||
.animation(.linear(duration: 0.2), value: draft.poll == nil)
|
.animation(.linear(duration: 0.2), value: draft.pollEnabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var disabled: Bool {
|
private var disabled: Bool {
|
||||||
instanceFeatures.mastodonAttachmentRestrictions && draft.attachments.count > 0
|
instanceFeatures.mastodonAttachmentRestrictions && draft.attachments.count > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func togglePoll() {
|
||||||
|
if draft.pollEnabled {
|
||||||
|
draft.pollEnabled = false
|
||||||
|
} else {
|
||||||
|
if draft.poll == nil {
|
||||||
|
draft.poll = Poll(context: DraftsPersistentContainer.shared.viewContext)
|
||||||
|
}
|
||||||
|
draft.pollEnabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -136,6 +136,11 @@ private struct DraftRowView: View {
|
|||||||
Text(draft.text)
|
Text(draft.text)
|
||||||
.font(.body)
|
.font(.body)
|
||||||
|
|
||||||
|
if draft.pollEnabled {
|
||||||
|
Text("Poll")
|
||||||
|
.font(.body.bold())
|
||||||
|
}
|
||||||
|
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
ForEach(draft.draftAttachments) { attachment in
|
ForEach(draft.draftAttachments) { attachment in
|
||||||
AttachmentThumbnailView(attachment: attachment, thumbnailSize: CGSize(width: 50, height: 50))
|
AttachmentThumbnailView(attachment: attachment, thumbnailSize: CGSize(width: 50, height: 50))
|
||||||
|
@ -21,6 +21,7 @@ struct PollEditor: View {
|
|||||||
PollOptionEditor(option: option, index: poll.options.index(of: option)) {
|
PollOptionEditor(option: option, index: poll.options.index(of: option)) {
|
||||||
self.removeOption(option)
|
self.removeOption(option)
|
||||||
}
|
}
|
||||||
|
.transition(.opacity)
|
||||||
}
|
}
|
||||||
|
|
||||||
AddOptionButton(poll: poll)
|
AddOptionButton(poll: poll)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user