Posting posts & locales

This commit is contained in:
Lumaa 2024-03-09 19:01:29 +01:00
parent c167717825
commit 64a71fc5ea
3 changed files with 351 additions and 24 deletions

View File

@ -2,6 +2,7 @@
import Foundation
import RegexBuilder
import SwiftUI
public protocol Endpoint: Sendable {
func path() -> String
@ -501,6 +502,38 @@ public struct StatusData: Encodable, Sendable {
self.multiple = multiple
self.expires_in = expires_in
}
enum DefaultExpiry: Int, CaseIterable {
case fiveMinutes = 300
case thirtyMinutes = 1800
case oneHour = 3600
case sixHours = 21600
case twelveHours = 43200
case oneDay = 86400
case threeDays = 259_200
case sevenDays = 604_800
public var description: LocalizedStringKey {
switch self {
case .fiveMinutes:
"poll.expiry.fiveMinutes"
case .thirtyMinutes:
"poll.expiry.thirtyMinutes"
case .oneHour:
"poll.expiry.oneHour"
case .sixHours:
"poll.expiry.sixHours"
case .twelveHours:
"poll.expiry.twelveHours"
case .oneDay:
"poll.expiry.oneDay"
case .threeDays:
"poll.expiry.threeDays"
case .sevenDays:
"poll.expiry.sevenDays"
}
}
}
}
public struct MediaAttribute: Encodable, Sendable {

View File

@ -463,6 +463,22 @@
}
}
},
"activity.poll.%@" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "**%@**'s poll ended"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Sondage de **%@** terminé"
}
}
}
},
"activity.reblogged.%@" : {
"localizations" : {
"en" : {
@ -1175,6 +1191,86 @@
}
}
},
"poll.expiry.fiveMinutes" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "5 minutes"
}
}
}
},
"poll.expiry.oneDay" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "1 day"
}
}
}
},
"poll.expiry.oneHour" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "1 hour"
}
}
}
},
"poll.expiry.sevenDays" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "7 days"
}
}
}
},
"poll.expiry.sixHours" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "6 hours"
}
}
}
},
"poll.expiry.thirtyMinutes" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "30 minutes"
}
}
}
},
"poll.expiry.threeDays" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "3 days"
}
}
}
},
"poll.expiry.twelveHours" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "12 hours"
}
}
}
},
"posting.alt.apply" : {
"localizations" : {
"en" : {
@ -2342,6 +2438,12 @@
"state" : "translated",
"value" : "Expired"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Expiré"
}
}
}
},
@ -2352,6 +2454,12 @@
"state" : "translated",
"value" : "Expires %@"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Expire %@"
}
}
}
},
@ -2362,6 +2470,12 @@
"state" : "translated",
"value" : "Hide Results"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Cacher les votes"
}
}
}
},
@ -2372,6 +2486,12 @@
"state" : "translated",
"value" : "Show Results"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Voir les votes"
}
}
}
},
@ -2382,6 +2502,12 @@
"state" : "translated",
"value" : "Submit"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Envoyer"
}
}
}
},
@ -2410,6 +2536,30 @@
}
}
}
},
"fr" : {
"variations" : {
"plural" : {
"one" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld votant"
}
},
"other" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld votants"
}
},
"zero" : {
"stringUnit" : {
"state" : "translated",
"value" : "Aucun votants"
}
}
}
}
}
}
},
@ -2477,6 +2627,52 @@
}
}
},
"status.posting.poll.disable-multi" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Multiple votes"
}
}
}
},
"status.posting.poll.enable-multi" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "One vote"
}
}
}
},
"status.posting.poll.expiry" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Poll Duration"
}
}
}
},
"status.posting.poll.option-%lld" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Option %lld"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Option %lld"
}
}
}
},
"status.posting.post" : {
"localizations" : {
"en" : {

View File

@ -25,6 +25,12 @@ struct PostingView: View {
@State private var selectedPhotos: [PhotosPickerItem] = []
@State private var player: AVPlayer?
@State private var hasPoll: Bool = false
@State private var pollOptions: [String] = ["", ""]
@State private var pollExpiry: StatusData.PollData.DefaultExpiry = .oneDay
@State private var multiSelect: Bool = false
@State private var selectingEmoji: Bool = false
@State private var makingAlt: MediaContainer? = nil
@ -87,6 +93,10 @@ struct PostingView: View {
mediasView(containers: mediaContainers)
}
if hasPoll {
editPollView
}
editorButtons
.padding(.vertical)
}
@ -182,7 +192,12 @@ struct PostingView: View {
// await upload(container: container)
// }
let json: StatusData = .init(status: viewModel.postText.string, visibility: visibility, inReplyToId: replyId, mediaIds: mediaContainers.compactMap { $0.mediaAttachment?.id }, mediaAttributes: mediaAttributes)
var pollData: StatusData.PollData? = nil
if self.hasPoll {
pollData = StatusData.PollData(options: self.pollOptions, multiple: self.multiSelect, expires_in: pollExpiry.rawValue)
}
let json: StatusData = .init(status: viewModel.postText.string, visibility: visibility, inReplyToId: replyId, mediaIds: mediaContainers.compactMap { $0.mediaAttachment?.id }, poll: pollData, mediaAttributes: mediaAttributes)
let isEdit: Bool = editId != nil
let endp: Endpoint = isEdit ? Statuses.editStatus(id: editId!, json: json) : Statuses.postStatus(json: json)
@ -200,6 +215,77 @@ struct PostingView: View {
}
}
var editPollView: some View {
VStack {
ForEach(0 ..< pollOptions.count, id: \.self) { i in
let isLast: Bool = pollOptions.count - 1 == i;
RoundedRectangle(cornerRadius: 15)
.stroke(Color.gray.opacity(0.4), lineWidth: 2.5)
.frame(width: 300, height: 50)
.transition(.opacity.combined(with: .move(edge: .top)))
.overlay {
HStack {
TextField("status.posting.poll.option-\(i + 1)", text: $pollOptions[i], axis: .horizontal)
.font(.subheadline)
.padding(.leading, 25)
.foregroundStyle(Color(uiColor: UIColor.label))
Spacer()
HStack(spacing: 3.5) {
Button {
withAnimation(.spring) {
self.pollOptions.append("")
}
} label: {
Image(systemName: "plus.circle.fill")
.font(.callout)
.foregroundStyle(pollOptions.count >= 4 || !isLast ? Color.gray : Color(uiColor: UIColor.label))
}
.disabled(pollOptions.count >= 4 || !isLast)
Button {
withAnimation(.spring) {
if pollOptions.count == 2 {
self.hasPoll = false
} else {
let index: Int = i
self.pollOptions.remove(at: index)
}
}
} label: {
Image(systemName: "minus.circle.fill")
.font(.callout)
.foregroundStyle(Color(uiColor: UIColor.label))
}
}
.padding(.trailing, 25)
}
}
}
HStack {
Button {
withAnimation(.spring) {
multiSelect.toggle()
}
} label: {
Text(multiSelect ? LocalizedStringKey("status.posting.poll.disable-multi") : LocalizedStringKey("status.posting.poll.enable-multi"))
}
.buttonStyle(LargeButton(filled: false, height: 7.5))
Spacer()
Picker("status.posting.poll.expiry", selection: $pollExpiry) {
ForEach(StatusData.PollData.DefaultExpiry.allCases, id: \.self) { expiry in
Text(expiry.description)
}
}
}
.frame(width: 300)
}
}
var loading: some View {
ProgressView()
.foregroundStyle(.white)
@ -385,39 +471,51 @@ struct PostingView: View {
}
var editorButtons: some View {
//MARK: Action buttons
HStack(spacing: 18) {
actionButton("photo.badge.plus") {
selectingPhotos.toggle()
}
.photosPicker(isPresented: $selectingPhotos, selection: $selectedPhotos, maxSelectionCount: 4, matching: .any(of: [.images, .videos]), photoLibrary: .shared())
.onChange(of: selectedPhotos) { oldValue, _ in
if selectedPhotos.count > 4 {
selectedPhotos = selectedPhotos.prefix(4).map { $0 }
if !self.hasPoll {
actionButton("photo.badge.plus") {
selectingPhotos.toggle()
}
let removedIDs = oldValue
.filter { !selectedPhotos.contains($0) }
.compactMap(\.itemIdentifier)
mediaContainers.removeAll { removedIDs.contains($0.id) }
let newPickerItems = selectedPhotos.filter { !oldValue.contains($0) }
if !newPickerItems.isEmpty {
loadingContent = true
Task {
for item in newPickerItems {
initImage(for: item)
.transition(.opacity.combined(with: .move(edge: .leading)))
.photosPicker(isPresented: $selectingPhotos, selection: $selectedPhotos, maxSelectionCount: 4, matching: .any(of: [.images, .videos]), photoLibrary: .shared())
.onChange(of: selectedPhotos) { oldValue, _ in
if selectedPhotos.count > 4 {
selectedPhotos = selectedPhotos.prefix(4).map { $0 }
}
let removedIDs = oldValue
.filter { !selectedPhotos.contains($0) }
.compactMap(\.itemIdentifier)
mediaContainers.removeAll { removedIDs.contains($0.id) }
let newPickerItems = selectedPhotos.filter { !oldValue.contains($0) }
if !newPickerItems.isEmpty {
loadingContent = true
Task {
for item in newPickerItems {
initImage(for: item)
}
}
}
}
.tint(Color.blue)
}
.tint(Color.blue)
actionButton("number") {
DispatchQueue.main.async {
viewModel.append(text: "#")
if mediaContainers.isEmpty || selectedPhotos.isEmpty {
actionButton("checklist") {
withAnimation(.spring) {
self.hasPoll.toggle()
}
}
}
// actionButton("number") {
// DispatchQueue.main.async {
// viewModel.append(text: "#")
// }
// }
let smileSf = colorScheme == .light ? "face.smiling" : "face.smiling.inverse"
actionButton(smileSf) {
viewModel.textView?.resignFirstResponder()