diff --git a/Packages/ComposeUI/Sources/ComposeUI/API/PostService.swift b/Packages/ComposeUI/Sources/ComposeUI/API/PostService.swift index 762a5053..5084b502 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/API/PostService.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/API/PostService.swift @@ -45,6 +45,14 @@ final class PostService: ObservableObject { 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( id: editedStatusID, text: textForPosting(), @@ -60,11 +68,23 @@ final class PostService: ObservableObject { return nil } }, - poll: draft.poll.map { - EditPollParameters(options: $0.pollOptions.map(\.text), expiresIn: Int($0.duration), multiple: $0.multiple) - } + poll: pollParams ) } 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( text: textForPosting(), contentType: config.contentType, @@ -74,9 +94,9 @@ final class PostService: ObservableObject { spoilerText: contentWarning, visibility: draft.localOnly && mastodonController.instanceFeatures.localOnlyPostsVisibility ? Status.localPostVisibility : draft.visibility.rawValue, language: mastodonController.instanceFeatures.createStatusWithLanguage ? draft.language : nil, - pollOptions: draft.poll?.pollOptions.map(\.text), - pollExpiresIn: draft.poll == nil ? nil : Int(draft.poll!.duration), - pollMultiple: draft.poll?.multiple, + pollOptions: pollOptions, + pollExpiresIn: pollExpiresIn, + pollMultiple: pollMultiple, localOnly: mastodonController.instanceFeatures.localOnlyPosts && !mastodonController.instanceFeatures.localOnlyPostsVisibility ? draft.localOnly : nil, idempotencyKey: draft.id.uuidString ) diff --git a/Packages/ComposeUI/Sources/ComposeUI/CoreData/Draft.swift b/Packages/ComposeUI/Sources/ComposeUI/CoreData/Draft.swift index 2f37284a..3b49b3c8 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/CoreData/Draft.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/CoreData/Draft.swift @@ -31,12 +31,18 @@ public class Draft: NSManagedObject, Identifiable { @NSManaged public var language: String? // ISO 639 language code @NSManaged public var lastModified: Date! @NSManaged public var localOnly: Bool + @NSManaged private var pollEnabledInternal: NSNumber? @NSManaged public var text: String @NSManaged private var visibilityStr: String @NSManaged internal var attachments: NSMutableOrderedSet @NSManaged public var poll: Poll? + public var pollEnabled: Bool { + get { pollEnabledInternal.map(\.boolValue) ?? (poll != nil) } + set { pollEnabledInternal = NSNumber(booleanLiteral: newValue) } + } + public var visibility: Visibility { get { Visibility(rawValue: visibilityStr) ?? .public diff --git a/Packages/ComposeUI/Sources/ComposeUI/CoreData/Drafts.xcdatamodeld/Drafts.xcdatamodel/contents b/Packages/ComposeUI/Sources/ComposeUI/CoreData/Drafts.xcdatamodeld/Drafts.xcdatamodel/contents index f7048141..0a28ee6c 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/CoreData/Drafts.xcdatamodeld/Drafts.xcdatamodel/contents +++ b/Packages/ComposeUI/Sources/ComposeUI/CoreData/Drafts.xcdatamodeld/Drafts.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -12,6 +12,7 @@ + diff --git a/Packages/ComposeUI/Sources/ComposeUI/OptionalObservedObject.swift b/Packages/ComposeUI/Sources/ComposeUI/OptionalObservedObject.swift index fe2e9316..daeed240 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/OptionalObservedObject.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/OptionalObservedObject.swift @@ -16,7 +16,6 @@ struct OptionalObservedObject: DynamicProperty { didSet { cancellable?.cancel() cancellable = wrapped?.objectWillChange - .receive(on: RunLoop.main) .sink { [unowned self] _ in self.objectWillChange.send() } @@ -27,6 +26,10 @@ struct OptionalObservedObject: DynamicProperty { @StateObject private var republisher = Republisher() var wrappedValue: T? + init(wrappedValue: T?) { + self.wrappedValue = wrappedValue + } + func update() { republisher.wrapped = wrappedValue } diff --git a/Packages/ComposeUI/Sources/ComposeUI/Views/ComposeDraftView.swift b/Packages/ComposeUI/Sources/ComposeUI/Views/ComposeDraftView.swift index 032a3265..5262a489 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Views/ComposeDraftView.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Views/ComposeDraftView.swift @@ -28,7 +28,8 @@ struct ComposeDraftView: View { DraftContentEditor(draft: draft, focusedField: $focusedField) - if let poll = draft.poll { + if let poll = draft.poll, + draft.pollEnabled { PollEditor(poll: poll) .padding(.bottom, 4) // 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 .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) + } +} diff --git a/Packages/ComposeUI/Sources/ComposeUI/Views/DraftContentEditor.swift b/Packages/ComposeUI/Sources/ComposeUI/Views/DraftContentEditor.swift index f9801c5e..604f7122 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Views/DraftContentEditor.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Views/DraftContentEditor.swift @@ -96,22 +96,27 @@ private struct TogglePollButton: View { @EnvironmentObject private var instanceFeatures: InstanceFeatures var body: some View { - Button { - if draft.poll == nil { - draft.poll = Poll(context: DraftsPersistentContainer.shared.viewContext) - } else { - draft.poll = nil - } - } label: { + Button(action: togglePoll) { Image(systemName: draft.poll == nil ? "chart.bar.doc.horizontal" : "chart.bar.horizontal.page.fill") } .buttonStyle(LanguageButtonStyle()) .disabled(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 { 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 + } + } } diff --git a/Packages/ComposeUI/Sources/ComposeUI/Views/DraftsView.swift b/Packages/ComposeUI/Sources/ComposeUI/Views/DraftsView.swift index 503fc416..f3f4b4b5 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Views/DraftsView.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Views/DraftsView.swift @@ -136,6 +136,11 @@ private struct DraftRowView: View { Text(draft.text) .font(.body) + if draft.pollEnabled { + Text("Poll") + .font(.body.bold()) + } + HStack(spacing: 8) { ForEach(draft.draftAttachments) { attachment in AttachmentThumbnailView(attachment: attachment, thumbnailSize: CGSize(width: 50, height: 50)) diff --git a/Packages/ComposeUI/Sources/ComposeUI/Views/PollEditor.swift b/Packages/ComposeUI/Sources/ComposeUI/Views/PollEditor.swift index 4036609f..a06649b9 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Views/PollEditor.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Views/PollEditor.swift @@ -21,6 +21,7 @@ struct PollEditor: View { PollOptionEditor(option: option, index: poll.options.index(of: option)) { self.removeOption(option) } + .transition(.opacity) } AddOptionButton(poll: poll)