Viewing polls
This commit is contained in:
parent
ad4b83d82b
commit
990b377ccf
|
@ -23,6 +23,7 @@
|
|||
B95ED2332B8707D60055F5BD /* RevenueCat in Frameworks */ = {isa = PBXBuildFile; productRef = B95ED2322B8707D60055F5BD /* RevenueCat */; };
|
||||
B95ED2352B8707D60055F5BD /* RevenueCatUI in Frameworks */ = {isa = PBXBuildFile; productRef = B95ED2342B8707D60055F5BD /* RevenueCatUI */; };
|
||||
B95ED2372B87C9550055F5BD /* StoreKitTestCertificate.cer in Resources */ = {isa = PBXBuildFile; fileRef = B95ED2362B87C9550055F5BD /* StoreKitTestCertificate.cer */; };
|
||||
B964F8062B9B78F4005C193D /* PostPoll.swift in Sources */ = {isa = PBXBuildFile; fileRef = B964F8052B9B78F4005C193D /* PostPoll.swift */; };
|
||||
B97491E32B6E96700098BC48 /* SymbolWidth.swift in Sources */ = {isa = PBXBuildFile; fileRef = B97491E22B6E96700098BC48 /* SymbolWidth.swift */; };
|
||||
B97798892B853E6600DC869F /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B97798882B853E6600DC869F /* UpdateView.swift */; };
|
||||
B97BCE242B3DD8400044756D /* HapticManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B97BCE232B3DD8400044756D /* HapticManager.swift */; };
|
||||
|
@ -195,6 +196,7 @@
|
|||
B93B67792B42EC51000892E9 /* MetaPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetaPicker.swift; sourceTree = "<group>"; };
|
||||
B93B677B2B433A6E000892E9 /* PostingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostingView.swift; sourceTree = "<group>"; };
|
||||
B95ED2362B87C9550055F5BD /* StoreKitTestCertificate.cer */ = {isa = PBXFileReference; lastKnownFileType = file; name = StoreKitTestCertificate.cer; path = ../../../../../Downloads/StoreKitTestCertificate.cer; sourceTree = "<group>"; };
|
||||
B964F8052B9B78F4005C193D /* PostPoll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostPoll.swift; sourceTree = "<group>"; };
|
||||
B97491E22B6E96700098BC48 /* SymbolWidth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SymbolWidth.swift; sourceTree = "<group>"; };
|
||||
B97798882B853E6600DC869F /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = "<group>"; };
|
||||
B97BCE232B3DD8400044756D /* HapticManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticManager.swift; sourceTree = "<group>"; };
|
||||
|
@ -397,6 +399,7 @@
|
|||
B9B63B242B44997400BBC82D /* QuotePostView.swift */,
|
||||
B9BED5152B5D5E6500C9B715 /* PostInteractor.swift */,
|
||||
B9BED5172B5D649C00C9B715 /* PostMenu.swift */,
|
||||
B964F8052B9B78F4005C193D /* PostPoll.swift */,
|
||||
B98F47952B645DF40092000F /* EmojiSelector.swift */,
|
||||
);
|
||||
path = Post;
|
||||
|
@ -875,6 +878,7 @@
|
|||
B9FB94BC2B2F035500D81C07 /* Tag.swift in Sources */,
|
||||
B9029FC42B8125CE00AA9B68 /* HuggingFace.swift in Sources */,
|
||||
B9BED51A2B5D662D00C9B715 /* ShareSheetController.swift in Sources */,
|
||||
B964F8062B9B78F4005C193D /* PostPoll.swift in Sources */,
|
||||
B93757112B7FB8D400652F91 /* AltClients.swift in Sources */,
|
||||
B9BED5162B5D5E6500C9B715 /* PostInteractor.swift in Sources */,
|
||||
B98BC74B2B46CF0400595441 /* ThreadedStyle.swift in Sources */,
|
||||
|
@ -954,7 +958,7 @@
|
|||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = "ThreadedWatch Watch App.entitlements";
|
||||
CODE_SIGN_ENTITLEMENTS = "ThreadedWatch/ThreadedWatch Watch App.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"ThreadedWatch/Preview Content\"";
|
||||
|
@ -984,7 +988,7 @@
|
|||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = "ThreadedWatch Watch App.entitlements";
|
||||
CODE_SIGN_ENTITLEMENTS = "ThreadedWatch/ThreadedWatch Watch App.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"ThreadedWatch/Preview Content\"";
|
||||
|
|
|
@ -127,6 +127,10 @@ struct CompactPostView: View {
|
|||
}
|
||||
}
|
||||
|
||||
if status.poll != nil {
|
||||
PostPoll(poll: status.poll!)
|
||||
}
|
||||
|
||||
if status.card != nil && status.mediaAttachments.isEmpty && !hasQuote {
|
||||
PostCardView(card: status.card!)
|
||||
}
|
||||
|
@ -270,3 +274,11 @@ struct CompactPostView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
CompactPostView(status: Status.placeholder(forSettings: true, language: "fr"))
|
||||
.environment(AccountManager())
|
||||
.environment(UniversalNavigator())
|
||||
.environmentObject(UserPreferences.defaultPreferences)
|
||||
.environmentObject(Navigator())
|
||||
}
|
||||
|
|
|
@ -0,0 +1,205 @@
|
|||
//Made by Lumaa
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct PostPoll: View {
|
||||
@Environment(AccountManager.self) private var accountManager: AccountManager
|
||||
|
||||
@State var poll: Poll
|
||||
@State private var selectedOption: [Int] = []
|
||||
|
||||
@State private var submitted: Bool = false
|
||||
@State private var showResults: Bool = false
|
||||
|
||||
init(poll: Poll) {
|
||||
self.poll = poll
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
ForEach(poll.options) { option in
|
||||
let index = poll.options.firstIndex(where: { $0.id == option.id })
|
||||
let isMostVoted: Bool = self.isMostVoted(option: option)
|
||||
|
||||
let clamped: Double = Double(option.votesCount ?? 0) / Double(poll.safeVotersCount)
|
||||
|
||||
Button {
|
||||
if !submitted && !poll.expired {
|
||||
withAnimation(.spring(duration: 0.25)) {
|
||||
selectVote(index ?? 0)
|
||||
|
||||
if !poll.multiple {
|
||||
Task {
|
||||
await vote()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 15)
|
||||
.stroke(Color.gray.opacity(0.4), lineWidth: 2.5)
|
||||
.frame(width: 300, height: 50)
|
||||
.overlay {
|
||||
HStack {
|
||||
Text(option.title)
|
||||
.font(.subheadline)
|
||||
.padding(.leading, 25)
|
||||
.foregroundStyle(Color(uiColor: UIColor.label))
|
||||
|
||||
if selectedOption.contains(index ?? 0) {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Color(uiColor: UIColor.label))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if showResults {
|
||||
Text(String("\(Int(clamped * 100))%"))
|
||||
.font(.subheadline)
|
||||
.padding(.horizontal, 25)
|
||||
.foregroundStyle(Color(uiColor: UIColor.label))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RoundedRectangle(cornerRadius: 15)
|
||||
.fill(Color(uiColor: UIColor.label))
|
||||
.frame(width: 300, height: 50)
|
||||
.overlay {
|
||||
HStack {
|
||||
Text(option.title)
|
||||
.foregroundStyle(Color(uiColor: UIColor.systemBackground))
|
||||
.font(.subheadline)
|
||||
.padding(.leading, 25)
|
||||
|
||||
if selectedOption.contains(index ?? 0) {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(Color(uiColor: UIColor.systemBackground))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if showResults {
|
||||
Text(String("\(Int(clamped * 100))%"))
|
||||
.font(.subheadline)
|
||||
.padding(.horizontal, 25)
|
||||
.foregroundStyle(Color(uiColor: UIColor.systemBackground))
|
||||
}
|
||||
}
|
||||
}
|
||||
.mask(alignment: .leading) {
|
||||
RoundedRectangle(cornerRadius: 15)
|
||||
.frame(width: showResults ? CGFloat(clamped * 300) : 0, height: 50)
|
||||
}
|
||||
}
|
||||
.opacity((poll.expired || submitted) && !isMostVoted ? 0.5 : 1.0)
|
||||
}
|
||||
.buttonStyle(NoTapAnimationStyle())
|
||||
}
|
||||
|
||||
if !poll.expired && !submitted {
|
||||
HStack {
|
||||
if poll.multiple {
|
||||
Button {
|
||||
Task {
|
||||
await vote()
|
||||
}
|
||||
} label: {
|
||||
Text("status.poll.submit")
|
||||
}
|
||||
.buttonStyle(LargeButton(filled: true, height: 7.5, disabled: selectedOption.count == 0))
|
||||
.disabled(selectedOption.count == 0)
|
||||
}
|
||||
|
||||
Button {
|
||||
withAnimation(.spring) {
|
||||
showResults.toggle()
|
||||
}
|
||||
} label: {
|
||||
Text(showResults ? LocalizedStringKey("status.poll.hide-results") : LocalizedStringKey("status.poll.show-results"))
|
||||
}
|
||||
.buttonStyle(LargeButton(filled: false, height: 7.5))
|
||||
}
|
||||
}
|
||||
|
||||
HStack {
|
||||
if !poll.expired {
|
||||
Text("status.poll.expires-in.\(poll.expiresAt.value?.relativeFormatted ?? "unknown")")
|
||||
.contentTransition(.numericText())
|
||||
.font(.caption)
|
||||
.multilineTextAlignment(.leading)
|
||||
.foregroundStyle(Color.gray)
|
||||
} else {
|
||||
Text("status.poll.expired")
|
||||
.font(.caption)
|
||||
.multilineTextAlignment(.leading)
|
||||
.foregroundStyle(Color.gray)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("status.poll.voters.\(poll.safeVotersCount)")
|
||||
.font(.caption)
|
||||
.multilineTextAlignment(.trailing)
|
||||
.foregroundStyle(Color.gray)
|
||||
}
|
||||
.frame(width: 300)
|
||||
}
|
||||
.task {
|
||||
await getPoll()
|
||||
}
|
||||
}
|
||||
|
||||
private func isMostVoted(option: Poll.Option) -> Bool {
|
||||
let sortedOptions: [Poll.Option] = poll.options.sorted(by: { $0.votesCount ?? 0 > $1.votesCount ?? 0 })
|
||||
let checkEquality: [Poll.Option] = sortedOptions.filter({ ($0.votesCount ?? 0) == (sortedOptions[0].votesCount ?? 0) })
|
||||
let isMostVoted: Bool = checkEquality.contains(where: { $0.id == option.id })
|
||||
return isMostVoted
|
||||
}
|
||||
|
||||
private func getPoll() async {
|
||||
guard let client = accountManager.getClient() else { return }
|
||||
if let p: Poll = try? await client.get(endpoint: Polls.poll(id: poll.id)) {
|
||||
poll = p
|
||||
selectedOption = p.ownVotes ?? []
|
||||
submitted = p.voted ?? true
|
||||
showResults = submitted || p.expired
|
||||
}
|
||||
}
|
||||
|
||||
private func selectVote(_ index: Int) {
|
||||
guard !poll.expired else { return }
|
||||
|
||||
if poll.multiple {
|
||||
let remove: Bool = selectedOption.contains(index)
|
||||
if remove {
|
||||
selectedOption.removeAll(where: { $0 == index })
|
||||
} else {
|
||||
selectedOption.append(index)
|
||||
}
|
||||
} else {
|
||||
selectedOption = [index]
|
||||
}
|
||||
}
|
||||
|
||||
private func vote() async {
|
||||
guard let client = accountManager.getClient(), !poll.expired else { return }
|
||||
|
||||
_ = try? await client.post(endpoint: Polls.vote(id: poll.id, votes: selectedOption))
|
||||
withAnimation(.spring) {
|
||||
showResults = true
|
||||
submitted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
CompactPostView(status: Status.placeholder(forSettings: true, language: "fr"))
|
||||
.environment(AccountManager())
|
||||
.environment(UniversalNavigator())
|
||||
.environmentObject(UserPreferences.defaultPreferences)
|
||||
.environmentObject(Navigator())
|
||||
}
|
|
@ -71,7 +71,7 @@ public final class Status: AnyStatus, Codable, Identifiable, Equatable, Hashable
|
|||
|
||||
public static func placeholder(forSettings: Bool = false, language: String? = nil) -> Status {
|
||||
.init(id: UUID().uuidString,
|
||||
content: .init(stringValue: "Here's to the [#crazy](#) ones",
|
||||
content: .init(stringValue: "Have you ever tried [#Threaded](#)?",
|
||||
parseMarkdown: forSettings),
|
||||
|
||||
account: .placeholder(),
|
||||
|
@ -94,7 +94,7 @@ public final class Status: AnyStatus, Codable, Identifiable, Equatable, Hashable
|
|||
inReplyToId: nil,
|
||||
inReplyToAccountId: nil,
|
||||
visibility: .pub,
|
||||
poll: nil,
|
||||
poll: Poll.placeholder,
|
||||
spoilerText: .init(stringValue: ""),
|
||||
filtered: [],
|
||||
sensitive: false,
|
||||
|
@ -412,11 +412,20 @@ public struct Poll: Codable, Equatable, Hashable {
|
|||
public var safeVotersCount: Int {
|
||||
votersCount ?? votesCount
|
||||
}
|
||||
|
||||
static let placeholder: Poll = Poll(id: "ABC", expiresAt: NullableString(), expired: false, multiple: true, votesCount: 3, votersCount: 3, voted: false, ownVotes: nil, options: [
|
||||
Poll.Option.init(id: "ABC", title: "Option 1", votesCount: 1),
|
||||
Poll.Option.init(id: "DEF", title: "Option 2", votesCount: 2)
|
||||
])
|
||||
}
|
||||
|
||||
public struct NullableString: Codable, Equatable, Hashable {
|
||||
public let value: ServerDate?
|
||||
|
||||
init() {
|
||||
self.value = ServerDate()
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
do {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
|
|
@ -680,3 +680,31 @@ public enum Conversations: Endpoint {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum Polls: Endpoint {
|
||||
case poll(id: String)
|
||||
case vote(id: String, votes: [Int])
|
||||
|
||||
public func path() -> String {
|
||||
switch self {
|
||||
case let .poll(id):
|
||||
"polls/\(id)"
|
||||
case let .vote(id, _):
|
||||
"polls/\(id)/votes"
|
||||
}
|
||||
}
|
||||
|
||||
public func queryItems() -> [URLQueryItem]? {
|
||||
switch self {
|
||||
case let .vote(_, votes):
|
||||
var params: [URLQueryItem] = []
|
||||
for vote in votes {
|
||||
params.append(.init(name: "choices[]", value: "\(vote)"))
|
||||
}
|
||||
return params
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2335,6 +2335,84 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"status.poll.expired" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Expired"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"status.poll.expires-in.%@" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Expires %@"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"status.poll.hide-results" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Hide Results"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"status.poll.show-results" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Show Results"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"status.poll.submit" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Submit"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"status.poll.voters.%lld" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"variations" : {
|
||||
"plural" : {
|
||||
"one" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%lld vote"
|
||||
}
|
||||
},
|
||||
"other" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%lld votes"
|
||||
}
|
||||
},
|
||||
"zero" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "No votes"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"status.posting" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
|
|
|
@ -90,6 +90,10 @@ struct PostDetailsView: View {
|
|||
.id("\(detailedStatus.id)@\(detailedStatus.account.id)")
|
||||
}
|
||||
|
||||
if status.poll != nil {
|
||||
PostPoll(poll: status.poll!)
|
||||
}
|
||||
|
||||
if status.card != nil && status.mediaAttachments.isEmpty {
|
||||
PostCardView(card: status.card!)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue