Skip to content

Commit dcfed91

Browse files
authored
Merge pull request #49 from SubvertDev/feature/topic-poll
Topic Polls
2 parents a2f1920 + 78c8079 commit dcfed91

File tree

9 files changed

+500
-33
lines changed

9 files changed

+500
-33
lines changed

Modules/Sources/APIClient/APIClient.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ public struct APIClient: Sendable {
6363
public var editPost: @Sendable (_ request: PostEditRequest) async throws -> PostSendResponse
6464
public var deletePosts: @Sendable (_ postIds: [Int]) async throws -> Bool
6565
public var postKarma: @Sendable (_ postId: Int, _ isUp: Bool) async throws -> Bool
66+
public var voteInTopicPoll: @Sendable (_ topicId: Int, _ selections: [[Int]]) async throws -> Bool
6667

6768
// Favorites
6869
public var getFavorites: @Sendable (_ request: FavoritesRequest, _ policy: CachePolicy) async throws -> AsyncThrowingStream<Favorite, any Error>
@@ -357,6 +358,13 @@ extension APIClient: DependencyKey {
357358
return status == 0
358359
},
359360

361+
voteInTopicPoll: { topicId, selections in
362+
let command = ForumCommand.Topic.Poll.vote(topicId: topicId, selections: selections)
363+
let response = try await api.send(command)
364+
let status = Int(response.getResponseStatus())!
365+
return status == 0
366+
},
367+
360368
// MARK: - Favorites
361369

362370
getFavorites: { request, policy in
@@ -570,6 +578,9 @@ extension APIClient: DependencyKey {
570578
postKarma: { _, _ in
571579
return true
572580
},
581+
voteInTopicPoll: { _, _ in
582+
return true
583+
},
573584
getFavorites: { _, _ in
574585
let (stream, continuation) = AsyncThrowingStream.makeStream(of: Favorite.self)
575586
continuation.yield(with: .success(.mock))

Modules/Sources/AnalyticsClient/Events/TopicEvent.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Foundation
1010
public enum TopicEvent: Event {
1111
case onRefresh
1212
case topicHatOpenButtonTapped
13+
case topicPollOpenButtonTapped
1314
case userTapped(Int)
1415
case urlTapped(URL)
1516
case imageTapped(URL)

Modules/Sources/Models/Forum/Topic.swift

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public struct Topic: Codable, Sendable, Identifiable, Hashable {
4141
self.options = options
4242
}
4343

44-
public struct Choice: Sendable, Codable, Hashable {
44+
public struct Choice: Sendable, Codable, Hashable, Identifiable {
4545
public let id: Int
4646
public let votes: Int
4747
public let name: String
@@ -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
@@ -110,20 +112,7 @@ public extension Topic {
110112
authorName: "4spander",
111113
curatorId: 6176341,
112114
curatorName: "AirFlare",
113-
poll: Poll(
114-
name: "Some simple poll...",
115-
voted: false,
116-
totalVotes: 2134,
117-
options: [
118-
Poll.Option(
119-
name: "Select this choise...",
120-
several: false,
121-
choices: [
122-
Poll.Choice(id: 2, name: "First choice", votes: 2)
123-
]
124-
)
125-
]
126-
),
115+
poll: .mock,
127116
postsCount: 5005,
128117
posts: [
129118
.mock(id: 0), .mock(id: 1), .mock(id: 2)
@@ -133,3 +122,31 @@ public extension Topic {
133122
]
134123
)
135124
}
125+
126+
public extension Topic.Poll {
127+
static let mock = Topic.Poll(
128+
name: "Some simple poll...",
129+
voted: false,
130+
totalVotes: 12,
131+
options: [
132+
.init(
133+
id: 0,
134+
name: "Select not several...",
135+
several: false,
136+
choices: [
137+
.init(id: 2, name: "First choice", votes: 2),
138+
.init(id: 3, name: "Second choice", votes: 4)
139+
]
140+
),
141+
.init(
142+
id: 1,
143+
name: "Select several...",
144+
several: true,
145+
choices: [
146+
.init(id: 4, name: "First choice", votes: 4),
147+
.init(id: 5, name: "Second choice", votes: 2)
148+
]
149+
),
150+
]
151+
)
152+
}

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 (optionId, 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: optionId,
154155
name: name,
155156
several: several == 1 ? true : false,
156157
choices: choices

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

Lines changed: 5 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,
@@ -41,6 +43,9 @@ extension TopicFeature {
4143
case .view(.topicHatOpenButtonTapped):
4244
analytics.log(TopicEvent.topicHatOpenButtonTapped)
4345

46+
case .view(.topicPollOpenButtonTapped):
47+
analytics.log(TopicEvent.topicPollOpenButtonTapped)
48+
4449
case let .view(.userTapped(userId: userId)):
4550
analytics.log(TopicEvent.userTapped(userId))
4651

Modules/Sources/TopicFeature/Resources/Localizable.xcstrings

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
{
22
"sourceLanguage" : "en",
33
"strings" : {
4+
"%lld people voted" : {
5+
"localizations" : {
6+
"ru" : {
7+
"stringUnit" : {
8+
"state" : "translated",
9+
"value" : "Проголосовало %lld чел."
10+
}
11+
}
12+
}
13+
},
414
"Add to favorites" : {
515
"localizations" : {
616
"ru" : {
@@ -137,6 +147,16 @@
137147
}
138148
}
139149
},
150+
"Poll" : {
151+
"localizations" : {
152+
"ru" : {
153+
"stringUnit" : {
154+
"state" : "translated",
155+
"value" : "Опрос"
156+
}
157+
}
158+
}
159+
},
140160
"Post deleted" : {
141161
"localizations" : {
142162
"ru" : {
@@ -227,6 +247,16 @@
227247
}
228248
}
229249
},
250+
"Show results" : {
251+
"localizations" : {
252+
"ru" : {
253+
"stringUnit" : {
254+
"state" : "translated",
255+
"value" : "Результаты"
256+
}
257+
}
258+
}
259+
},
230260
"Today, %@" : {
231261
"localizations" : {
232262
"ru" : {
@@ -267,6 +297,26 @@
267297
}
268298
}
269299
},
300+
"Vote" : {
301+
"localizations" : {
302+
"ru" : {
303+
"stringUnit" : {
304+
"state" : "translated",
305+
"value" : "Голосовать"
306+
}
307+
}
308+
}
309+
},
310+
"Vote approved" : {
311+
"localizations" : {
312+
"ru" : {
313+
"stringUnit" : {
314+
"state" : "translated",
315+
"value" : "Голос засчитан"
316+
}
317+
}
318+
}
319+
},
270320
"Write Post" : {
271321
"localizations" : {
272322
"ru" : {

Modules/Sources/TopicFeature/TopicFeature.swift

Lines changed: 30 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
@@ -81,6 +82,7 @@ public struct TopicFeature: Reducer, Sendable {
8182
}
8283

8384
var shouldShowTopicHatButton = false
85+
var shouldShowTopicPollButton = true
8486

8587
public init(
8688
topicId: Int,
@@ -117,6 +119,8 @@ public struct TopicFeature: Reducer, Sendable {
117119
case onRefresh
118120
case finishedPostAnimation
119121
case topicHatOpenButtonTapped
122+
case topicPollOpenButtonTapped
123+
case topicPollVoteButtonTapped([Int: Set<Int>])
120124
case changeKarmaTapped(Int, Bool)
121125
case userTapped(Int)
122126
case urlTapped(URL)
@@ -132,6 +136,7 @@ public struct TopicFeature: Reducer, Sendable {
132136
case refresh
133137
case goToPost(postId: Int, offset: Int, forceRefresh: Bool)
134138
case changeKarma(postId: Int, isUp: Bool)
139+
case voteInPoll(selections: [[Int]])
135140
case loadTopic(Int)
136141
case loadTypes([[TopicTypeUI]])
137142
case topicResponse(Result<Topic, any Error>)
@@ -228,6 +233,16 @@ public struct TopicFeature: Reducer, Sendable {
228233
state.shouldShowTopicHatButton = false
229234
return .none
230235

236+
case .view(.topicPollOpenButtonTapped):
237+
state.shouldShowTopicPollButton = false
238+
return .none
239+
240+
case let .view(.topicPollVoteButtonTapped(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+
231246
case let .view(.userTapped(id)):
232247
return .send(.delegate(.openUser(id: id)))
233248

@@ -397,6 +412,20 @@ public struct TopicFeature: Reducer, Sendable {
397412
jumpTo(.post(id: postId), true, &state)
398413
)
399414

415+
case let .internal(.voteInPoll(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+
400429
case let .internal(.loadTopic(offset)):
401430
if !state.isRefreshing {
402431
state.isLoadingTopic = true
@@ -465,6 +494,7 @@ public struct TopicFeature: Reducer, Sendable {
465494

466495
state.isLoadingTopic = false
467496
state.isRefreshing = false
497+
state.shouldShowTopicPollButton = true
468498
state.shouldShowTopicHatButton = !state.pageNavigation.isFirstPage
469499

470500
reportFullyDisplayed(&state)

0 commit comments

Comments
 (0)