Skip to content

Commit a674971

Browse files
committed
Add topic polls full support
1 parent 1a51f24 commit a674971

File tree

7 files changed

+93
-26
lines changed

7 files changed

+93
-26
lines changed

Modules/Sources/Models/Forum/Topic.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,14 @@ public struct Topic: Codable, Sendable, Identifiable, Hashable {
5353
}
5454
}
5555

56-
public struct Option: Sendable, Codable, Hashable {
56+
public struct Option: Sendable, Codable, Hashable, Identifiable {
57+
public let id: Int
5758
public let name: String
5859
public let several: Bool
5960
public let choices: [Choice]
6061

61-
public init(name: String, several: Bool, choices: [Choice]) {
62+
public init(id: Int, name: String, several: Bool, choices: [Choice]) {
63+
self.id = id
6264
self.name = name
6365
self.several = several
6466
self.choices = choices
@@ -128,6 +130,7 @@ public extension Topic.Poll {
128130
totalVotes: 12,
129131
options: [
130132
.init(
133+
id: 0,
131134
name: "Select not several...",
132135
several: false,
133136
choices: [
@@ -136,6 +139,7 @@ public extension Topic.Poll {
136139
]
137140
),
138141
.init(
142+
id: 1,
139143
name: "Select several...",
140144
several: true,
141145
choices: [

Modules/Sources/ParsingClient/Parsers/TopicParser.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ public struct TopicParser {
131131

132132
private static func parsePollOptions(_ optionsRaw: [[Any]]) throws(ParsingError) -> [Topic.Poll.Option] {
133133
var options: [Topic.Poll.Option] = []
134-
for option in optionsRaw {
134+
for (idx, option) in optionsRaw.enumerated() {
135135
guard let name = option[safe: 0] as? String,
136136
let several = option[safe: 1] as? Int,
137137
let names = option[safe: 2] as? [String],
@@ -151,6 +151,7 @@ public struct TopicParser {
151151
}
152152

153153
let option = Topic.Poll.Option(
154+
id: idx,
154155
name: name,
155156
several: several == 1 ? true : false,
156157
choices: choices

Modules/Sources/TopicFeature/Analytics/TopicFeature+Analytics.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@ extension TopicFeature {
2323
.view(.onNextAppear),
2424
.view(.finishedPostAnimation),
2525
.view(.changeKarmaTapped),
26+
.view(.topicPollVoteButtonTapped),
2627
.internal(.loadTypes),
2728
.internal(.goToPost),
2829
.internal(.jumpRequestFailed),
2930
.internal(.changeKarma),
31+
.internal(.voteInPoll),
3032
.internal(.load),
3133
.internal(.refresh),
3234
.pageNavigation,

Modules/Sources/TopicFeature/Resources/Localizable.xcstrings

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,16 @@
307307
}
308308
}
309309
},
310+
"Vote approved" : {
311+
"localizations" : {
312+
"ru" : {
313+
"stringUnit" : {
314+
"state" : "translated",
315+
"value" : "Голос засчитан"
316+
}
317+
}
318+
}
319+
},
310320
"Write Post" : {
311321
"localizations" : {
312322
"ru" : {

Modules/Sources/TopicFeature/TopicFeature.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public struct TopicFeature: Reducer, Sendable {
3434
static let favoriteRemoved = LocalizedStringResource("Removed from favorites", bundle: .module)
3535
static let postDeleted = LocalizedStringResource("Post deleted", bundle: .module)
3636
static let postKarmaChanged = LocalizedStringResource("Post karma changed", bundle: .module)
37+
static let topicVoteApproved = LocalizedStringResource("Vote approved", bundle: .module)
3738
}
3839

3940
// MARK: - Destinations
@@ -119,6 +120,7 @@ public struct TopicFeature: Reducer, Sendable {
119120
case finishedPostAnimation
120121
case topicHatOpenButtonTapped
121122
case topicPollOpenButtonTapped
123+
case topicPollVoteButtonTapped([Int: Set<Int>])
122124
case changeKarmaTapped(Int, Bool)
123125
case userTapped(Int)
124126
case urlTapped(URL)
@@ -134,6 +136,7 @@ public struct TopicFeature: Reducer, Sendable {
134136
case refresh
135137
case goToPost(postId: Int, offset: Int, forceRefresh: Bool)
136138
case changeKarma(postId: Int, isUp: Bool)
139+
case voteInPoll(selections: [[Int]])
137140
case loadTopic(Int)
138141
case loadTypes([[TopicTypeUI]])
139142
case topicResponse(Result<Topic, any Error>)
@@ -234,6 +237,12 @@ public struct TopicFeature: Reducer, Sendable {
234237
state.shouldShowTopicPollButton = false
235238
return .none
236239

240+
case .view(.topicPollVoteButtonTapped(let selections)):
241+
let values = selections.sorted(by: { $0.key < $1.key }).map {
242+
Array($0.value)
243+
}
244+
return .send(.internal(.voteInPoll(selections: values)))
245+
237246
case let .view(.userTapped(id)):
238247
return .send(.delegate(.openUser(id: id)))
239248

@@ -403,6 +412,20 @@ public struct TopicFeature: Reducer, Sendable {
403412
jumpTo(.post(id: postId), true, &state)
404413
)
405414

415+
case .internal(.voteInPoll(let selections)):
416+
return .concatenate(
417+
.run { [topicId = state.topicId] _ in
418+
let status = try await apiClient.voteInTopicPoll(
419+
topicId: topicId,
420+
selections: selections
421+
)
422+
let voteApproved = ToastMessage(text: Localization.topicVoteApproved, haptic: .success)
423+
await toastClient.showToast(status ? voteApproved : .whoopsSomethingWentWrong)
424+
}.cancellable(id: CancelID.loading),
425+
426+
.send(.internal(.refresh))
427+
)
428+
406429
case let .internal(.loadTopic(offset)):
407430
if !state.isRefreshing {
408431
state.isLoadingTopic = true

Modules/Sources/TopicFeature/TopicScreen.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ public struct TopicScreen: View {
203203
}
204204
} else {
205205
PollView(poll: poll, onVoteButtonTapped: { selections in
206-
// TODO: Implement...
206+
send(.topicPollVoteButtonTapped(selections))
207207
})
208208
}
209209

Modules/Sources/TopicFeature/Views/PollView.swift

Lines changed: 49 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,28 @@ struct PollView: View {
1414
@Environment(\.tintColor) private var tintColor
1515

1616
let poll: Topic.Poll
17-
let onVoteButtonTapped: ([String: [Int]]) -> Void
17+
let onVoteButtonTapped: ([Int: Set<Int>]) -> Void
1818

19+
@State private var isSending = false
1920
@State private var showVoteResultsButtonTapped = false
20-
@State private var selections: [String: Set<Int>] = [:]
21+
@State private var selections: [Int: Set<Int>] = [:]
22+
23+
private var isVotable: Bool {
24+
for option in poll.options {
25+
if selections[option.id] == nil {
26+
return false
27+
}
28+
if selections[option.id] != nil,
29+
selections[option.id]!.isEmpty {
30+
return false
31+
}
32+
}
33+
return true
34+
}
2135

2236
init(
2337
poll: Topic.Poll,
24-
onVoteButtonTapped: @escaping ([String: [Int]]) -> Void
38+
onVoteButtonTapped: @escaping ([Int: Set<Int>]) -> Void
2539
) {
2640
self.poll = poll
2741
self.onVoteButtonTapped = onVoteButtonTapped
@@ -44,7 +58,7 @@ struct PollView: View {
4458
.foregroundStyle(Color(.Labels.primary))
4559
.frame(maxWidth: .infinity, alignment: .leading)
4660

47-
if showVoteResultsButtonTapped {
61+
if showVoteResultsButtonTapped || poll.voted {
4862
OptionChoices(choices: option.choices)
4963
} else {
5064
OptionChoicesSelect(option: option)
@@ -58,7 +72,9 @@ struct PollView: View {
5872
.foregroundStyle(Color(.Labels.teritary))
5973
.frame(maxWidth: .infinity, alignment: .leading)
6074

61-
PollActionButtons()
75+
if !poll.voted {
76+
PollActionButtons()
77+
}
6278
}
6379
.padding(16)
6480
}
@@ -72,16 +88,18 @@ struct PollView: View {
7288
if showVoteResultsButtonTapped {
7389
showVoteResultsButtonTapped = false
7490
} else {
75-
// TODO: Implement selection data sending...
91+
isSending = true
92+
onVoteButtonTapped(selections)
7693
}
7794
} label: {
7895
Text("Vote", bundle: .module)
7996
.padding(.horizontal, 18)
8097
.padding(.vertical, 9)
8198
}
82-
.foregroundStyle(tintColor)
83-
.background(tintColor.opacity(0.12))
99+
.foregroundStyle(voteButtonForegroundColor())
100+
.background(voteButtonBackgroundColor())
84101
.clipShape(RoundedRectangle(cornerRadius: 10))
102+
.disabled(!showVoteResultsButtonTapped && !isVotable)
85103

86104
Spacer()
87105

@@ -93,9 +111,10 @@ struct PollView: View {
93111
.padding(.horizontal, 18)
94112
.padding(.vertical, 9)
95113
}
96-
.foregroundStyle(tintColor)
97-
.background(tintColor.opacity(0.12))
114+
.foregroundStyle(isSending ? Color(.Labels.quintuple) : tintColor)
115+
.background(isSending ? Color(.Main.greyAlpha) : tintColor.opacity(0.12))
98116
.clipShape(RoundedRectangle(cornerRadius: 10))
117+
.disabled(isSending)
99118
}
100119
}
101120
}
@@ -109,21 +128,21 @@ struct PollView: View {
109128
HStack(alignment: .top, spacing: 11) {
110129
if option.several {
111130
Toggle(isOn: Binding(
112-
get: { isSelected(option.name, choice.id) },
131+
get: { isSelected(option.id, choice.id) },
113132
set: { isSelected in
114133
withAnimation {
115-
updateMultiSelections(option.name, choice.id, isSelected)
134+
updateMultiSelections(option.id, choice.id, isSelected)
116135
}
117136
}
118137
)) {}
119138
.toggleStyle(CheckBoxToggleStyle())
120139
} else {
121140
Button {
122141
withAnimation {
123-
selections[option.name] = Set([choice.id])
142+
selections[option.id] = Set([choice.id])
124143
}
125144
} label: {
126-
if isSelected(option.name, choice.id) {
145+
if isSelected(option.id, choice.id) {
127146
ZStack {
128147
Circle()
129148
.strokeBorder(Color(.Labels.quintuple))
@@ -195,21 +214,29 @@ struct PollView: View {
195214
return CGFloat(choice.votes) / CGFloat(totalVotes)
196215
}
197216

198-
private func isSelected(_ option: String, _ choiceId: Int) -> Bool {
199-
return if selections[option] != nil {
200-
selections[option]!.contains(choiceId)
217+
private func voteButtonForegroundColor() -> Color {
218+
return (!isVotable && !showVoteResultsButtonTapped || isSending) ? Color(.Labels.quintuple) : tintColor
219+
}
220+
221+
private func voteButtonBackgroundColor() -> Color {
222+
return (!isVotable && !showVoteResultsButtonTapped || isSending) ? Color(.Main.greyAlpha) : tintColor.opacity(0.12)
223+
}
224+
225+
private func isSelected(_ optionId: Int, _ choiceId: Int) -> Bool {
226+
return if selections[optionId] != nil {
227+
selections[optionId]!.contains(choiceId)
201228
} else { false }
202229
}
203230

204-
private func updateMultiSelections(_ option: String, _ choiceId: Int, _ isSelected: Bool) {
205-
if selections[option] != nil {
231+
private func updateMultiSelections(_ optionId: Int, _ choiceId: Int, _ isSelected: Bool) {
232+
if selections[optionId] != nil {
206233
if isSelected {
207-
selections[option]!.insert(choiceId)
234+
selections[optionId]!.insert(choiceId)
208235
} else {
209-
selections[option]!.remove(choiceId)
236+
selections[optionId]!.remove(choiceId)
210237
}
211238
} else {
212-
selections[option] = Set([choiceId])
239+
selections[optionId] = Set([choiceId])
213240
}
214241
}
215242
}

0 commit comments

Comments
 (0)