Files
andros e4fdde15aa Full edit fidelity, poll field parity, mention warning, reaction remove
- EditPostView now edits Schedule / Visibility / Language / Mood / Tags; form
  state lives in an @Observable model to prevent init-time @State resets that
  were silently dropping selections. Visibility/Mood rows switched away from
  Menu/confirmationDialog (which refused to propagate selection inside the
  sheet) to an inline segmented Picker and a horizontal emoji strip + free
  text field. Shared across Compose and Edit via PostOptionRows.
- Polls now accept lang/tags/mood/visibility (spec-compliant); ComposeView no
  longer hides those rows when Poll is on. NewPostOptions.poll signature
  extended with the new optional params.
- Compose warns before publishing a mention-only post that has no
  [[org-social:URL][nick]] links in the body (would otherwise be invisible).
- Reactions can be removed: ReactionViewModel gains loadExistingReaction
  (scans own feed on row appear so the toggled state survives relaunches) and
  unreact (deletes the reaction post). Tapping the React button when already
  reacted now removes the reaction.
- PostRowView strips raw `- [ ] Option` checkbox lines from poll body render
  so the poll card is the sole UI for options.
- PostWriter.editPost learns MOOD; OrgSocialPost.mood promoted to var so
  timeline/profile applyEdit can update it.
2026-04-22 11:27:30 +02:00

121 lines
4.2 KiB
Swift

import Foundation
/// Parameters for creating a new post.
public struct NewPostOptions: Sendable {
public enum Visibility: String, Sendable {
case `public`
case mention
}
public var text: String
public var lang: String?
public var tags: String?
/// Full post URL to reply to: `"https://feed.url/social.org#TIMESTAMP"`.
public var replyTo: String?
/// Full post URL to boost: `"https://feed.url/social.org#TIMESTAMP"`.
public var include: String?
/// Emoji or short text reaction. Combined with `replyTo` and empty `text` = reaction post.
public var mood: String?
/// Group in `"Name https://relay.url"` format.
public var group: String?
public var visibility: Visibility?
public var client: String
/// Poll close date. Body must contain `- [ ] Option` lines. Written as `:POLL_END:`.
public var pollEnd: Date?
/// The option text chosen when voting on a poll. Combined with `replyTo`. Written as `:POLL_OPTION:`.
public var pollOption: String?
/// Account migration in `"old-url new-url"` format. Written as `:MIGRATION:`.
public var migration: String?
public init(
text: String,
lang: String? = nil,
tags: String? = nil,
replyTo: String? = nil,
include: String? = nil,
mood: String? = nil,
group: String? = nil,
visibility: Visibility? = nil,
client: String = "iOS",
pollEnd: Date? = nil,
pollOption: String? = nil,
migration: String? = nil
) {
self.text = text
self.lang = lang
self.tags = tags
self.replyTo = replyTo
self.include = include
self.mood = mood
self.group = group
self.visibility = visibility
self.client = client
self.pollEnd = pollEnd
self.pollOption = pollOption
self.migration = migration
}
// MARK: - Factories
/// Reaction post: `REPLY_TO` + `MOOD`, no body text.
public static func reaction(to postURL: String, mood: String) -> NewPostOptions {
NewPostOptions(text: "", replyTo: postURL, mood: mood)
}
/// Boost post: `INCLUDE` pointing to the boosted post URL.
/// `commentary` is optional body text (quote-boost).
public static func boost(of postURL: String, commentary: String = "") -> NewPostOptions {
NewPostOptions(text: commentary, include: postURL)
}
/// Poll post: checkboxes in body, `POLL_END` property.
///
/// - Parameters:
/// - question: Optional introductory text above the options.
/// - options: Non-empty list of poll option strings.
/// - end: Poll close date.
/// - lang: Optional `:LANG:`.
/// - tags: Optional `:TAGS:`.
/// - mood: Optional `:MOOD:`.
/// - visibility: Optional `:VISIBILITY:` (`.public` is implicit).
public static func poll(
question: String = "",
options: [String],
end: Date,
lang: String? = nil,
tags: String? = nil,
mood: String? = nil,
visibility: Visibility? = nil
) -> NewPostOptions {
let checkboxes = options.map { "- [ ] \($0)" }.joined(separator: "\n")
// Org Mode requires a blank line between a paragraph and a list
// for the list to be recognised; without it Emacs renders the
// checkboxes as a single wrapped line.
let body = question.isEmpty ? checkboxes : "\(question)\n\n\(checkboxes)"
return NewPostOptions(
text: body,
lang: lang,
tags: tags,
mood: mood,
visibility: visibility,
pollEnd: end
)
}
/// Poll vote: `REPLY_TO` + `POLL_OPTION`.
///
/// - Parameters:
/// - option: The exact option text being voted for.
/// - pollURL: Canonical URL of the poll post (`feed#timestamp`).
public static func vote(for option: String, on pollURL: String) -> NewPostOptions {
NewPostOptions(text: "", replyTo: pollURL, pollOption: option)
}
/// Account migration post: `MIGRATION: old-url new-url`.
public static func migration(from oldURL: String, to newURL: String) -> NewPostOptions {
NewPostOptions(text: "", migration: "\(oldURL) \(newURL)")
}
}