mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2025-02-20 11:36:17 +00:00
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 <ricouard77@gmail.com>
This commit is contained in:
parent
7d053592c9
commit
ba64015f18
6 changed files with 213 additions and 4 deletions
36
Packages/Env/Sources/Env/PollPreferences.swift
Normal file
36
Packages/Env/Sources/Env/PollPreferences.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,7 +6,10 @@ public enum Statuses: Endpoint {
|
||||||
inReplyTo: String?,
|
inReplyTo: String?,
|
||||||
mediaIds: [String]?,
|
mediaIds: [String]?,
|
||||||
spoilerText: String?,
|
spoilerText: String?,
|
||||||
visibility: Visibility)
|
visibility: Visibility,
|
||||||
|
pollOptions: [String],
|
||||||
|
pollVotingFrequency: Bool?,
|
||||||
|
pollDuration: Int?)
|
||||||
case editStatus(id: String,
|
case editStatus(id: String,
|
||||||
status: String,
|
status: String,
|
||||||
mediaIds: [String]?,
|
mediaIds: [String]?,
|
||||||
|
@ -60,7 +63,7 @@ public enum Statuses: Endpoint {
|
||||||
|
|
||||||
public func queryItems() -> [URLQueryItem]? {
|
public func queryItems() -> [URLQueryItem]? {
|
||||||
switch self {
|
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),
|
var params: [URLQueryItem] = [.init(name: "status", value: status),
|
||||||
.init(name: "visibility", value: visibility.rawValue)]
|
.init(name: "visibility", value: visibility.rawValue)]
|
||||||
if let inReplyTo {
|
if let inReplyTo {
|
||||||
|
@ -74,6 +77,14 @@ public enum Statuses: Endpoint {
|
||||||
if let spoilerText {
|
if let spoilerText {
|
||||||
params.append(.init(name: "spoiler_text", value: 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
|
return params
|
||||||
case let .editStatus(_, status, mediaIds, spoilerText, visibility):
|
case let .editStatus(_, status, mediaIds, spoilerText, visibility):
|
||||||
var params: [URLQueryItem] = [.init(name: "status", value: status),
|
var params: [URLQueryItem] = [.init(name: "status", value: status),
|
||||||
|
|
|
@ -23,6 +23,7 @@ struct StatusEditorAccessoryView: View {
|
||||||
matching: .images) {
|
matching: .images) {
|
||||||
Image(systemName: "photo.fill.on.rectangle.fill")
|
Image(systemName: "photo.fill.on.rectangle.fill")
|
||||||
}
|
}
|
||||||
|
.disabled(viewModel.showPoll)
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
viewModel.insertStatusText(text: " @")
|
viewModel.insertStatusText(text: " @")
|
||||||
|
@ -36,6 +37,15 @@ struct StatusEditorAccessoryView: View {
|
||||||
Image(systemName: "number")
|
Image(systemName: "number")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
withAnimation {
|
||||||
|
viewModel.showPoll.toggle()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "chart.bar")
|
||||||
|
}
|
||||||
|
.disabled(viewModel.shouldDisablePollButton)
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
viewModel.spoilerOn.toggle()
|
viewModel.spoilerOn.toggle()
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -49,6 +49,10 @@ public struct StatusEditorView: View {
|
||||||
.padding(.horizontal, .layoutPadding)
|
.padding(.horizontal, .layoutPadding)
|
||||||
.disabled(true)
|
.disabled(true)
|
||||||
}
|
}
|
||||||
|
if viewModel.showPoll {
|
||||||
|
StatusEditorPollView(viewModel: viewModel, showPoll: $viewModel.showPoll)
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.padding(.top, 8)
|
.padding(.top, 8)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import DesignSystem
|
import DesignSystem
|
||||||
|
import Env
|
||||||
import Models
|
import Models
|
||||||
import Network
|
import Network
|
||||||
import PhotosUI
|
import PhotosUI
|
||||||
|
@ -27,6 +28,11 @@ public class StatusEditorViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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 spoilerOn: Bool = false
|
||||||
@Published var spoilerText: String = ""
|
@Published var spoilerText: String = ""
|
||||||
|
|
||||||
|
@ -48,6 +54,10 @@ public class StatusEditorViewModel: ObservableObject {
|
||||||
statusText.length > 0 || !selectedMedias.isEmpty
|
statusText.length > 0 || !selectedMedias.isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var shouldDisablePollButton: Bool {
|
||||||
|
showPoll || !selectedMedias.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
@Published var visibility: Models.Visibility = .pub
|
@Published var visibility: Models.Visibility = .pub
|
||||||
|
|
||||||
@Published var mentionsSuggestions: [Account] = []
|
@Published var mentionsSuggestions: [Account] = []
|
||||||
|
@ -79,6 +89,10 @@ public class StatusEditorViewModel: ObservableObject {
|
||||||
selectedRange = NSRange(location: inRange.location + text.utf16.count, length: 0)
|
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? {
|
func postStatus() async -> Status? {
|
||||||
guard let client else { return nil }
|
guard let client else { return nil }
|
||||||
do {
|
do {
|
||||||
|
@ -90,7 +104,10 @@ public class StatusEditorViewModel: ObservableObject {
|
||||||
inReplyTo: mode.replyToStatus?.id,
|
inReplyTo: mode.replyToStatus?.id,
|
||||||
mediaIds: mediasImages.compactMap{ $0.mediaAttachement?.id },
|
mediaIds: mediasImages.compactMap{ $0.mediaAttachement?.id },
|
||||||
spoilerText: spoilerOn ? spoilerText : nil,
|
spoilerText: spoilerOn ? spoilerText : nil,
|
||||||
visibility: visibility))
|
visibility: visibility,
|
||||||
|
pollOptions: getPollOptionsForAPI(),
|
||||||
|
pollVotingFrequency: pollVotingFrequency.canVoteMultipleTimes,
|
||||||
|
pollDuration: pollDuration.rawValue))
|
||||||
case let .edit(status):
|
case let .edit(status):
|
||||||
postStatus = try await client.put(endpoint: Statuses.editStatus(id: status.id,
|
postStatus = try await client.put(endpoint: Statuses.editStatus(id: status.id,
|
||||||
status: statusText.string,
|
status: statusText.string,
|
||||||
|
@ -195,6 +212,12 @@ public class StatusEditorViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resetPollDefaults() {
|
||||||
|
pollOptions = ["", ""]
|
||||||
|
pollDuration = .oneDay
|
||||||
|
pollVotingFrequency = .oneVote
|
||||||
|
}
|
||||||
|
|
||||||
private func checkEmbed() {
|
private func checkEmbed() {
|
||||||
if let url = embededStatusURL,
|
if let url = embededStatusURL,
|
||||||
!statusText.string.contains(url.absoluteString) {
|
!statusText.string.contains(url.absoluteString) {
|
||||||
|
|
Loading…
Reference in a new issue