* Add poll options

* Add the poll view

* Disable adding attachments when showing polls

* Update to post poll info

* Wire up poll view

* Remove debug code

* Use VM for showing poll

* Rename PollView to something better!

* Move file location

* Disable poll button if media is attached.

* Don't refocus on delete option to avoid index out of range crash

Co-authored-by: Thomas Ricouard <ricouard77@gmail.com>
This commit is contained in:
Sean Goldin 2023-01-13 00:30:15 -06:00 committed by GitHub
parent 7d053592c9
commit ba64015f18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 213 additions and 4 deletions

View File

@ -0,0 +1,36 @@
import Foundation
public enum PollDuration: Int, CaseIterable {
// rawValue == time in seconds; used for sending to the API
case fiveMinutes = 300
case halfAnHour = 1800
case oneHour = 3600
case sixHours = 21600
case oneDay = 86400
case threeDays = 259_200
case sevenDays = 604_800
public var displayString: String {
switch self {
case .fiveMinutes: return "5 minutes"
case .halfAnHour: return "30 minutes"
case .oneHour: return "1 hour"
case .sixHours: return "6 hours"
case .oneDay: return "1 day"
case .threeDays: return "3 days"
case .sevenDays: return "7 days"
}
}
}
public enum PollVotingFrequency: String, CaseIterable {
case oneVote = "One Vote"
case multipleVotes = "Multiple Votes"
public var canVoteMultipleTimes: Bool {
switch self {
case .multipleVotes: return true
case .oneVote: return false
}
}
}

View File

@ -6,7 +6,10 @@ public enum Statuses: Endpoint {
inReplyTo: String?,
mediaIds: [String]?,
spoilerText: String?,
visibility: Visibility)
visibility: Visibility,
pollOptions: [String],
pollVotingFrequency: Bool?,
pollDuration: Int?)
case editStatus(id: String,
status: String,
mediaIds: [String]?,
@ -60,7 +63,7 @@ public enum Statuses: Endpoint {
public func queryItems() -> [URLQueryItem]? {
switch self {
case let .postStatus(status, inReplyTo, mediaIds, spoilerText, visibility):
case let .postStatus(status, inReplyTo, mediaIds, spoilerText, visibility, pollOptions, pollVotingFrequency, pollDuration):
var params: [URLQueryItem] = [.init(name: "status", value: status),
.init(name: "visibility", value: visibility.rawValue)]
if let inReplyTo {
@ -74,6 +77,14 @@ public enum Statuses: Endpoint {
if let spoilerText {
params.append(.init(name: "spoiler_text", value: spoilerText))
}
if !pollOptions.isEmpty, let pollVotingFrequency, let pollDuration {
for option in pollOptions {
params.append(.init(name: "poll[options][]", value: option))
}
params.append(.init(name: "poll[multiple]", value: pollVotingFrequency ? "true" : "false"))
params.append(.init(name: "poll[expires_in]", value: "\(pollDuration)"))
}
return params
case let .editStatus(_, status, mediaIds, spoilerText, visibility):
var params: [URLQueryItem] = [.init(name: "status", value: status),

View File

@ -23,6 +23,7 @@ struct StatusEditorAccessoryView: View {
matching: .images) {
Image(systemName: "photo.fill.on.rectangle.fill")
}
.disabled(viewModel.showPoll)
Button {
viewModel.insertStatusText(text: " @")
@ -35,6 +36,15 @@ struct StatusEditorAccessoryView: View {
} label: {
Image(systemName: "number")
}
Button {
withAnimation {
viewModel.showPoll.toggle()
}
} label: {
Image(systemName: "chart.bar")
}
.disabled(viewModel.shouldDisablePollButton)
Button {
withAnimation {

View File

@ -0,0 +1,125 @@
import SwiftUI
import DesignSystem
import Env
struct StatusEditorPollView: View {
enum FocusField: Hashable {
case option(Int)
}
@FocusState var focused: FocusField?
@State private var currentFocusIndex: Int = 0
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var currentInstance: CurrentInstance
@ObservedObject var viewModel: StatusEditorViewModel
@Binding var showPoll: Bool
var body: some View {
let count = viewModel.pollOptions.count
VStack {
ForEach(0..<count, id: \.self) { index in
VStack {
HStack(spacing: 16) {
TextField("Option \(index + 1)", text: $viewModel.pollOptions[index])
.textFieldStyle(.roundedBorder)
.focused($focused, equals: .option(index))
.onTapGesture {
if canAddMoreAt(index) {
currentFocusIndex = index
}
}
.onSubmit {
if canAddMoreAt(index) {
addChoice(at: index)
}
}
.onChange(of: viewModel.pollOptions[index]) {
let maxCharacters: Int = currentInstance.instance?.configuration.polls.maxCharactersPerOption ?? 50
viewModel.pollOptions[index] = String($0.prefix(maxCharacters))
}
if canAddMoreAt(index) {
Button {
addChoice(at: index)
} label: {
Image(systemName: "plus.circle.fill")
}
} else {
Button {
removeChoice(at: index)
} label: {
Image(systemName: "minus.circle.fill")
}
}
}
.padding(.horizontal)
.padding(.top)
}
}
.onAppear {
focused = .option(0)
}
HStack {
Picker("Polling Frequency", selection: $viewModel.pollVotingFrequency) {
ForEach(PollVotingFrequency.allCases, id: \.rawValue) {
Text($0.rawValue)
.tag($0)
}
}
.layoutPriority(1.0)
Spacer()
Picker("Poll Duration", selection: $viewModel.pollDuration) {
ForEach(PollDuration.allCases, id: \.rawValue) {
Text($0.displayString)
.tag($0)
}
}
}
.padding(.horizontal)
}
.background(
RoundedRectangle(cornerRadius: 6.0)
.stroke(theme.secondaryBackgroundColor.opacity(0.6), lineWidth: 1)
.background(theme.primaryBackgroundColor.opacity(0.3))
)
}
private func addChoice(at index: Int) {
viewModel.pollOptions.append("")
currentFocusIndex = index + 1
moveFocus()
}
private func removeChoice(at index: Int) {
viewModel.pollOptions.remove(at: index)
if viewModel.pollOptions.count == 1 {
viewModel.resetPollDefaults()
withAnimation {
showPoll = false
}
}
}
private func moveFocus() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
focused = .option(currentFocusIndex)
}
}
private func canAddMoreAt(_ index: Int) -> Bool {
let count = viewModel.pollOptions.count
let maxEntries: Int = currentInstance.instance?.configuration.polls.maxOptions ?? 4
return index == count - 1 && count < maxEntries
}
}

View File

@ -49,6 +49,10 @@ public struct StatusEditorView: View {
.padding(.horizontal, .layoutPadding)
.disabled(true)
}
if viewModel.showPoll {
StatusEditorPollView(viewModel: viewModel, showPoll: $viewModel.showPoll)
.padding(.horizontal)
}
Spacer()
}
.padding(.top, 8)

View File

@ -1,5 +1,6 @@
import SwiftUI
import DesignSystem
import Env
import Models
import Network
import PhotosUI
@ -26,7 +27,12 @@ public class StatusEditorViewModel: ObservableObject {
checkEmbed()
}
}
@Published var showPoll: Bool = false
@Published var pollVotingFrequency = PollVotingFrequency.oneVote
@Published var pollDuration = PollDuration.oneDay
@Published var pollOptions: [String] = ["", ""]
@Published var spoilerOn: Bool = false
@Published var spoilerText: String = ""
@ -47,6 +53,10 @@ public class StatusEditorViewModel: ObservableObject {
var canPost: Bool {
statusText.length > 0 || !selectedMedias.isEmpty
}
var shouldDisablePollButton: Bool {
showPoll || !selectedMedias.isEmpty
}
@Published var visibility: Models.Visibility = .pub
@ -78,6 +88,10 @@ public class StatusEditorViewModel: ObservableObject {
statusText = string
selectedRange = NSRange(location: inRange.location + text.utf16.count, length: 0)
}
private func getPollOptionsForAPI() -> [String] {
pollOptions.filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty }
}
func postStatus() async -> Status? {
guard let client else { return nil }
@ -90,7 +104,10 @@ public class StatusEditorViewModel: ObservableObject {
inReplyTo: mode.replyToStatus?.id,
mediaIds: mediasImages.compactMap{ $0.mediaAttachement?.id },
spoilerText: spoilerOn ? spoilerText : nil,
visibility: visibility))
visibility: visibility,
pollOptions: getPollOptionsForAPI(),
pollVotingFrequency: pollVotingFrequency.canVoteMultipleTimes,
pollDuration: pollDuration.rawValue))
case let .edit(status):
postStatus = try await client.put(endpoint: Statuses.editStatus(id: status.id,
status: statusText.string,
@ -194,6 +211,12 @@ public class StatusEditorViewModel: ObservableObject {
}
}
func resetPollDefaults() {
pollOptions = ["", ""]
pollDuration = .oneDay
pollVotingFrequency = .oneVote
}
private func checkEmbed() {
if let url = embededStatusURL,