From ba64015f18bd42434dfbca909feaa0640d4cf69d Mon Sep 17 00:00:00 2001 From: Sean Goldin Date: Fri, 13 Jan 2023 00:30:15 -0600 Subject: [PATCH] Polls (#70) * 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 --- .../Env/Sources/Env/PollPreferences.swift | 36 +++++ .../Sources/Network/Endpoint/Statuses.swift | 15 ++- .../StatusEditorAccessoryView.swift | 10 ++ .../Components/StatusEditorPollView.swift | 125 ++++++++++++++++++ .../Status/Editor/StatusEditorView.swift | 4 + .../Status/Editor/StatusEditorViewModel.swift | 27 +++- 6 files changed, 213 insertions(+), 4 deletions(-) create mode 100644 Packages/Env/Sources/Env/PollPreferences.swift create mode 100644 Packages/Status/Sources/Status/Editor/Components/StatusEditorPollView.swift diff --git a/Packages/Env/Sources/Env/PollPreferences.swift b/Packages/Env/Sources/Env/PollPreferences.swift new file mode 100644 index 00000000..0d27b343 --- /dev/null +++ b/Packages/Env/Sources/Env/PollPreferences.swift @@ -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 + } + } +} diff --git a/Packages/Network/Sources/Network/Endpoint/Statuses.swift b/Packages/Network/Sources/Network/Endpoint/Statuses.swift index fe6092e2..200856ca 100644 --- a/Packages/Network/Sources/Network/Endpoint/Statuses.swift +++ b/Packages/Network/Sources/Network/Endpoint/Statuses.swift @@ -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), diff --git a/Packages/Status/Sources/Status/Editor/Components/StatusEditorAccessoryView.swift b/Packages/Status/Sources/Status/Editor/Components/StatusEditorAccessoryView.swift index c7cf9031..cefd8de3 100644 --- a/Packages/Status/Sources/Status/Editor/Components/StatusEditorAccessoryView.swift +++ b/Packages/Status/Sources/Status/Editor/Components/StatusEditorAccessoryView.swift @@ -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 { diff --git a/Packages/Status/Sources/Status/Editor/Components/StatusEditorPollView.swift b/Packages/Status/Sources/Status/Editor/Components/StatusEditorPollView.swift new file mode 100644 index 00000000..13a2c826 --- /dev/null +++ b/Packages/Status/Sources/Status/Editor/Components/StatusEditorPollView.swift @@ -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.. Bool { + let count = viewModel.pollOptions.count + let maxEntries: Int = currentInstance.instance?.configuration.polls.maxOptions ?? 4 + + return index == count - 1 && count < maxEntries + } +} diff --git a/Packages/Status/Sources/Status/Editor/StatusEditorView.swift b/Packages/Status/Sources/Status/Editor/StatusEditorView.swift index f5bbc699..71dbe166 100644 --- a/Packages/Status/Sources/Status/Editor/StatusEditorView.swift +++ b/Packages/Status/Sources/Status/Editor/StatusEditorView.swift @@ -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) diff --git a/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift b/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift index 74f0baea..2de00707 100644 --- a/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift +++ b/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift @@ -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,