Finish poll editor

This commit is contained in:
Shadowfacts 2025-01-30 13:21:01 -05:00
parent d87e9e6d92
commit 221ea05629
8 changed files with 71 additions and 18 deletions

View File

@ -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
) )

View File

@ -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

View File

@ -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"/>

View File

@ -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
} }

View File

@ -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)
}
}

View File

@ -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 {
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") 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
}
}
} }

View File

@ -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))

View File

@ -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)