added poll detail scene with mock data

This commit is contained in:
Flavio Alescio
2023-01-19 15:42:45 +01:00
parent da17249200
commit 84e8d879bd
14 changed files with 804 additions and 5 deletions
@@ -15,17 +15,21 @@
//
import CommonKit
import MatrixSDK
import SwiftUI
struct PollHistoryCoordinatorParameters {
let mode: PollHistoryMode
let session: MXSession
let room: MXRoom
let navigationRouter: NavigationRouterType
}
final class PollHistoryCoordinator: Coordinator, Presentable {
final class PollHistoryCoordinator: NSObject, Coordinator, Presentable {
private let parameters: PollHistoryCoordinatorParameters
private let pollHistoryHostingController: UIViewController
private var pollHistoryViewModel: PollHistoryViewModelProtocol
private let navigationRouter: NavigationRouterType
// Must be used only internally
var childCoordinators: [Coordinator] = []
@@ -37,6 +41,7 @@ final class PollHistoryCoordinator: Coordinator, Presentable {
let view = PollHistory(viewModel: viewModel.context)
pollHistoryViewModel = viewModel
pollHistoryHostingController = VectorHostingController(rootView: view)
navigationRouter = parameters.navigationRouter
}
// MARK: - Public
@@ -44,11 +49,44 @@ final class PollHistoryCoordinator: Coordinator, Presentable {
func start() {
MXLog.debug("[PollHistoryCoordinator] did start.")
pollHistoryViewModel.completion = { [weak self] result in
self?.completion?()
switch result {
case .showPollDetail(let poll):
self?.showPollDetail(poll)
}
}
}
func showPollDetail(_ poll: PollListData) {
let detailCoordinator: PollHistoryDetailCoordinator = .init(parameters: .init(pollHistoryDetails: .dummy, session: parameters.session, room: parameters.room))
detailCoordinator.toPresentable().presentationController?.delegate = self
detailCoordinator.completion = { [weak self, weak detailCoordinator] result in
guard let self = self, let coordinator = detailCoordinator else { return }
switch result {
case .dismiss:
self.toPresentable().dismiss(animated: true)
self.remove(childCoordinator: coordinator)
default:
break
}
}
add(childCoordinator: detailCoordinator)
detailCoordinator.start()
toPresentable().present(detailCoordinator.toPresentable(), animated: true)
}
func toPresentable() -> UIViewController {
pollHistoryHostingController
}
}
// MARK: UIAdaptivePresentationControllerDelegate
extension PollHistoryCoordinator: UIAdaptivePresentationControllerDelegate {
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
guard let coordinator = childCoordinators.last else {
return
}
remove(childCoordinator: coordinator)
}
}
@@ -0,0 +1,105 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import CommonKit
import SwiftUI
import Combine
struct PollHistoryDetailCoordinatorParameters {
let pollHistoryDetails: PollHistoryDetails
let session: MXSession
let room: MXRoom
}
final class PollHistoryDetailCoordinator: Coordinator, Presentable {
private let parameters: PollHistoryDetailCoordinatorParameters
private let pollHistoryDetailHostingController: UIViewController
private var pollHistoryDetailViewModel: PollHistoryDetailViewModelProtocol
private let selectedAnswerIdentifiersSubject = PassthroughSubject<[String], Never>()
private var cancellables = Set<AnyCancellable>()
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
private var loadingIndicator: UserIndicator?
// Must be used only internally
var childCoordinators: [Coordinator] = []
var completion: ((PollHistoryDetailViewModelResult) -> Void)?
init(parameters: PollHistoryDetailCoordinatorParameters) {
self.parameters = parameters
let viewModel = PollHistoryDetailViewModel(pollHistoryDetails: parameters.pollHistoryDetails)
let view = PollHistoryDetail(viewModel: viewModel.context)
pollHistoryDetailViewModel = viewModel
pollHistoryDetailHostingController = VectorHostingController(rootView: view)
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: pollHistoryDetailHostingController)
viewModel.completion = { [weak self] result in
guard let self = self else { return }
switch result {
case .selectedAnswerOptionsWithIdentifiers(let identifiers):
self.selectedAnswerIdentifiersSubject.send(identifiers)
case .dismiss:
self.completion?(.dismiss)
}
}
selectedAnswerIdentifiersSubject
.debounce(for: 2.0, scheduler: RunLoop.main)
.removeDuplicates()
.sink { [weak self] identifiers in
guard let self = self else { return }
// self.parameters.room.sendPollResponse(for: parameters.pollEvent,
// withAnswerIdentifiers: identifiers,
// threadId: nil,
// localEcho: nil, success: nil) { [weak self] error in
// guard let self = self else { return }
//
// MXLog.error("[TimelinePollCoordinator]] Failed submitting response", context: error)
//
// self.viewModel.showAnsweringFailure()
// }
}
.store(in: &cancellables)
}
// MARK: - Public
func start() {
MXLog.debug("[PollHistoryDetailCoordinator] did start.")
}
func toPresentable() -> UIViewController {
pollHistoryDetailHostingController
}
// MARK: - Private
/// Show an activity indicator whilst loading.
/// - Parameters:
/// - label: The label to show on the indicator.
/// - isInteractionBlocking: Whether the indicator should block any user interaction.
private func startLoading(label: String = VectorL10n.loading, isInteractionBlocking: Bool = true) {
loadingIndicator = indicatorPresenter.present(.loading(label: label, isInteractionBlocking: isInteractionBlocking))
}
/// Hide the currently displayed activity indicator.
private func stopLoading() {
loadingIndicator = nil
}
}
@@ -0,0 +1,55 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
import SwiftUI
enum MockPollHistoryDetailScreenState: MockScreenState, CaseIterable {
case openDisclosed
case closedDisclosed
case openUndisclosed
case closedUndisclosed
case closedPollEnded
var screenType: Any.Type {
PollHistoryDetails.self
}
var poll: PollHistoryDetails {
let answerOptions = [TimelinePollAnswerOption(id: "1", text: "First", count: 10, winner: false, selected: false),
TimelinePollAnswerOption(id: "2", text: "Second", count: 5, winner: false, selected: true),
TimelinePollAnswerOption(id: "3", text: "Third", count: 15, winner: true, selected: false)]
let poll = PollHistoryDetails(question: "Question",
answerOptions: answerOptions,
closed: self == .closedDisclosed || self == .closedUndisclosed ? true : false,
totalAnswerCount: 20,
type: self == .closedDisclosed || self == .openDisclosed ? .disclosed : .undisclosed,
eventType: self == .closedPollEnded ? .ended : .started,
maxAllowedSelections: 1,
hasBeenEdited: false,
hasDecryptionError: false)
return poll
}
var screenView: ([Any], AnyView) {
let viewModel = PollHistoryDetailViewModel(pollHistoryDetails: poll)
return ([viewModel], AnyView(PollHistoryDetail(viewModel: viewModel.context)))
}
}
@@ -0,0 +1,88 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
// MARK: - Coordinator
typealias PollHistoryDetailViewModelCallback = (PollHistoryDetailViewModelResult) -> Void
enum PollHistoryDetailViewModelResult {
case selectedAnswerOptionsWithIdentifiers([String])
case dismiss
}
// MARK: View model
struct PollHistoryDetails {
public static let dummy: PollHistoryDetails = MockPollHistoryDetailScreenState.openUndisclosed.poll
var question: String
var answerOptions: [TimelinePollAnswerOption]
var closed: Bool
var totalAnswerCount: UInt
var type: TimelinePollType
var eventType: TimelinePollEventType
var maxAllowedSelections: UInt
var hasBeenEdited = true
var hasDecryptionError: Bool
init(question: String, answerOptions: [TimelinePollAnswerOption],
closed: Bool,
totalAnswerCount: UInt,
type: TimelinePollType,
eventType: TimelinePollEventType,
maxAllowedSelections: UInt,
hasBeenEdited: Bool,
hasDecryptionError: Bool) {
self.question = question
self.answerOptions = answerOptions
self.closed = closed
self.totalAnswerCount = totalAnswerCount
self.type = type
self.eventType = eventType
self.maxAllowedSelections = maxAllowedSelections
self.hasBeenEdited = hasBeenEdited
self.hasDecryptionError = hasDecryptionError
}
var hasCurrentUserVoted: Bool {
answerOptions.filter { $0.selected == true }.count > 0
}
var shouldDiscloseResults: Bool {
if closed {
return totalAnswerCount > 0
} else {
return type == .disclosed && totalAnswerCount > 0 && hasCurrentUserVoted
}
}
var representsPollEndedEvent: Bool {
eventType == .ended
}
}
// MARK: View
struct PollHistoryDetailViewState: BindableState {
var poll: PollHistoryDetails
}
enum PollHistoryDetailViewAction {
case selectAnswerOptionWithIdentifier(String)
}
@@ -0,0 +1,115 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Combine
import SwiftUI
typealias PollHistoryDetailViewModelType = StateStoreViewModel<PollHistoryDetailViewState, PollHistoryDetailViewAction>
class PollHistoryDetailViewModel: PollHistoryDetailViewModelType, PollHistoryDetailViewModelProtocol {
// MARK: - Properties
// MARK: Private
// MARK: Public
var completion: PollHistoryDetailViewModelCallback?
// MARK: - Setup
init(pollHistoryDetails: PollHistoryDetails) {
super.init(initialViewState: PollHistoryDetailViewState(poll: pollHistoryDetails))
}
// MARK: - Public
override func process(viewAction: PollHistoryDetailViewAction) {
switch viewAction {
case .selectAnswerOptionWithIdentifier(let identifier):
guard !state.poll.closed else {
return
}
if state.poll.maxAllowedSelections == 1 {
updateSingleSelectPollLocalState(selectedAnswerIdentifier: identifier, callback: completion)
} else {
updateMultiSelectPollLocalState(&state, selectedAnswerIdentifier: identifier, callback: completion)
}
}
}
// MARK: - TimelinePollViewModelProtocol
func updateWithPollDetails(_ pollDetails: PollHistoryDetails) {
state.poll = pollDetails
}
func updateSingleSelectPollLocalState(selectedAnswerIdentifier: String, callback: PollHistoryDetailViewModelCallback?) {
state.poll.answerOptions.updateEach { answerOption in
if answerOption.selected {
answerOption.selected = false
answerOption.count = UInt(max(0, Int(answerOption.count) - 1))
state.poll.totalAnswerCount = UInt(max(0, Int(state.poll.totalAnswerCount) - 1))
}
if answerOption.id == selectedAnswerIdentifier {
answerOption.selected = true
answerOption.count += 1
state.poll.totalAnswerCount += 1
}
}
informCoordinatorOfSelectionUpdate(state: state, callback: callback)
}
func updateMultiSelectPollLocalState(_ state: inout PollHistoryDetailViewState, selectedAnswerIdentifier: String, callback: PollHistoryDetailViewModelCallback?) {
let selectedAnswerOptions = state.poll.answerOptions.filter { $0.selected == true }
let isDeselecting = selectedAnswerOptions.filter { $0.id == selectedAnswerIdentifier }.count > 0
if !isDeselecting, selectedAnswerOptions.count >= state.poll.maxAllowedSelections {
return
}
state.poll.answerOptions.updateEach { answerOption in
if answerOption.id != selectedAnswerIdentifier {
return
}
if answerOption.selected {
answerOption.selected = false
answerOption.count = UInt(max(0, Int(answerOption.count) - 1))
state.poll.totalAnswerCount = UInt(max(0, Int(state.poll.totalAnswerCount) - 1))
} else {
answerOption.selected = true
answerOption.count += 1
state.poll.totalAnswerCount += 1
}
}
informCoordinatorOfSelectionUpdate(state: state, callback: callback)
}
func informCoordinatorOfSelectionUpdate(state: PollHistoryDetailViewState, callback: PollHistoryDetailViewModelCallback?) {
let selectedIdentifiers = state.poll.answerOptions.compactMap { answerOption in
answerOption.selected ? answerOption.id : nil
}
callback?(.selectedAnswerOptionsWithIdentifiers(selectedIdentifiers))
}
}
@@ -0,0 +1,21 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
protocol PollHistoryDetailViewModelProtocol {
var context: PollHistoryDetailViewModelType.Context { get }
}
@@ -0,0 +1,38 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import RiotSwiftUI
import XCTest
class PollHistoryDetailUITests: MockScreenTestCase {
func testPollHistoryDetailPromptRegular() {
let promptType = PollHistoryDetailPromptType.regular
app.goToScreenWithIdentifier(MockPollHistoryDetailScreenState.promptType(promptType).title)
let title = app.staticTexts["title"]
XCTAssert(title.exists)
XCTAssertEqual(title.label, promptType.title)
}
func testPollHistoryDetailPromptUpgrade() {
let promptType = PollHistoryDetailPromptType.upgrade
app.goToScreenWithIdentifier(MockPollHistoryDetailScreenState.promptType(promptType).title)
let title = app.staticTexts["title"]
XCTAssert(title.exists)
XCTAssertEqual(title.label, promptType.title)
}
}
@@ -0,0 +1,48 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import XCTest
@testable import RiotSwiftUI
class PollHistoryDetailViewModelTests: XCTestCase {
private enum Constants {
static let counterInitialValue = 0
}
var viewModel: PollHistoryDetailViewModelProtocol!
var context: PollHistoryDetailViewModelType.Context!
override func setUpWithError() throws {
viewModel = PollHistoryDetailViewModel(promptType: .regular, initialCount: Constants.counterInitialValue)
context = viewModel.context
}
func testInitialState() {
XCTAssertEqual(context.viewState.count, Constants.counterInitialValue)
}
func testCounter() throws {
context.send(viewAction: .incrementCount)
XCTAssertEqual(context.viewState.count, 1)
context.send(viewAction: .incrementCount)
XCTAssertEqual(context.viewState.count, 2)
context.send(viewAction: .decrementCount)
XCTAssertEqual(context.viewState.count, 1)
}
}
@@ -0,0 +1,122 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
struct PollHistoryDetail: View {
// MARK: - Properties
// MARK: Private
@Environment(\.theme) private var theme: ThemeSwiftUI
// MARK: Public
@ObservedObject var viewModel: PollHistoryDetailViewModel.Context
var body: some View {
let poll = viewModel.viewState.poll
VStack(alignment: .leading, spacing: 16.0) {
if poll.representsPollEndedEvent {
Text(VectorL10n.pollTimelineEndedText)
.font(theme.fonts.footnote)
.foregroundColor(theme.colors.tertiaryContent)
}
Text(poll.question)
.font(theme.fonts.bodySB)
.foregroundColor(theme.colors.primaryContent) +
Text(editedText)
.font(theme.fonts.footnote)
.foregroundColor(theme.colors.secondaryContent)
VStack(spacing: 24.0) {
ForEach(poll.answerOptions) { answerOption in
PollHistoryDetailAnswerOptionButton(poll: poll, answerOption: answerOption) {
viewModel.send(viewAction: .selectAnswerOptionWithIdentifier(answerOption.id))
}
}
}
.disabled(poll.closed)
.fixedSize(horizontal: false, vertical: true)
Text(totalVotesString)
.lineLimit(2)
.font(theme.fonts.footnote)
.foregroundColor(theme.colors.tertiaryContent)
}
.padding([.horizontal], 16)
.padding([.top, .bottom])
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(theme.colors.background.ignoresSafeArea())
.navigationTitle(navigationTitle)
// .alert(item: $viewModel.alertInfo) { info in
// info.alert
// }
}
private var navigationTitle: String {
let poll = viewModel.viewState.poll
if poll.closed {
return VectorL10n.pollHistoryPastSegmentTitle
} else {
return VectorL10n.pollHistoryActiveSegmentTitle
}
}
private var totalVotesString: String {
let poll = viewModel.viewState.poll
if poll.hasDecryptionError, poll.totalAnswerCount > 0 {
return VectorL10n.pollTimelineDecryptionError
}
if poll.closed {
if poll.totalAnswerCount == 1 {
return VectorL10n.pollTimelineTotalFinalResultsOneVote
} else {
return VectorL10n.pollTimelineTotalFinalResults(Int(poll.totalAnswerCount))
}
}
switch poll.totalAnswerCount {
case 0:
return VectorL10n.pollTimelineTotalNoVotes
case 1:
return (poll.hasCurrentUserVoted || poll.type == .undisclosed ?
VectorL10n.pollTimelineTotalOneVote :
VectorL10n.pollTimelineTotalOneVoteNotVoted)
default:
return (poll.hasCurrentUserVoted || poll.type == .undisclosed ?
VectorL10n.pollTimelineTotalVotes(Int(poll.totalAnswerCount)) :
VectorL10n.pollTimelineTotalVotesNotVoted(Int(poll.totalAnswerCount)))
}
}
private var editedText: String {
viewModel.viewState.poll.hasBeenEdited ? " \(VectorL10n.eventFormatterMessageEditedMention)" : ""
}
}
// MARK: - Previews
struct PollHistoryDetail_Previews: PreviewProvider {
static let stateRenderer = MockPollHistoryDetailScreenState.stateRenderer
static var previews: some View {
stateRenderer.screenGroup()
}
}
@@ -0,0 +1,163 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
struct PollHistoryDetailAnswerOptionButton: View {
// MARK: - Properties
// MARK: Private
@Environment(\.theme) private var theme: ThemeSwiftUI
let poll: PollHistoryDetails
let answerOption: TimelinePollAnswerOption
let action: () -> Void
// MARK: Public
var body: some View {
Button(action: action) {
let rect = RoundedRectangle(cornerRadius: 4.0)
answerOptionLabel
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 8.0)
.padding(.top, 12.0)
.padding(.bottom, 12.0)
.clipShape(rect)
.overlay(rect.stroke(borderAccentColor, lineWidth: 1.0))
.accentColor(progressViewAccentColor)
}
.accessibilityIdentifier("PollAnswerOption\(optionIndex)")
}
var answerOptionLabel: some View {
VStack(alignment: .leading, spacing: 12.0) {
HStack(alignment: .top, spacing: 8.0) {
if !poll.closed {
Image(uiImage: answerOption.selected ? Asset.Images.pollCheckboxSelected.image : Asset.Images.pollCheckboxDefault.image)
}
Text(answerOption.text)
.font(theme.fonts.body)
.foregroundColor(theme.colors.primaryContent)
.accessibilityIdentifier("PollAnswerOption\(optionIndex)Label")
if poll.closed, answerOption.winner {
Spacer()
Image(uiImage: Asset.Images.pollWinnerIcon.image)
}
}
if poll.type == .disclosed || poll.closed {
HStack {
ProgressView(value: Double(poll.shouldDiscloseResults ? answerOption.count : 0),
total: Double(poll.totalAnswerCount))
.progressViewStyle(LinearProgressViewStyle())
.scaleEffect(x: 1.0, y: 1.2, anchor: .center)
.accessibilityIdentifier("PollAnswerOption\(optionIndex)Progress")
if poll.shouldDiscloseResults {
Text(answerOption.count == 1 ? VectorL10n.pollTimelineOneVote : VectorL10n.pollTimelineVotesCount(Int(answerOption.count)))
.font(theme.fonts.footnote)
.foregroundColor(poll.closed && answerOption.winner ? theme.colors.accent : theme.colors.secondaryContent)
.accessibilityIdentifier("PollAnswerOption\(optionIndex)Count")
}
}
}
}
}
var borderAccentColor: Color {
guard !poll.closed else {
return (answerOption.winner ? theme.colors.accent : theme.colors.quinaryContent)
}
return answerOption.selected ? theme.colors.accent : theme.colors.quinaryContent
}
var progressViewAccentColor: Color {
guard !poll.closed else {
return (answerOption.winner ? theme.colors.accent : theme.colors.quarterlyContent)
}
return answerOption.selected ? theme.colors.accent : theme.colors.quarterlyContent
}
var optionIndex: Int {
poll.answerOptions.firstIndex { $0.id == answerOption.id } ?? Int.max
}
}
struct PollHistoryDetailAnswerOptionButton_Previews: PreviewProvider {
static let stateRenderer = MockPollHistoryDetailScreenState.stateRenderer
static var previews: some View {
Group {
let pollTypes: [TimelinePollType] = [.disclosed, .undisclosed]
ForEach(pollTypes, id: \.self) { type in
VStack {
TimelinePollAnswerOptionButton(poll: buildPoll(closed: false, type: type),
answerOption: buildAnswerOption(selected: false),
action: { })
TimelinePollAnswerOptionButton(poll: buildPoll(closed: false, type: type),
answerOption: buildAnswerOption(selected: true),
action: { })
TimelinePollAnswerOptionButton(poll: buildPoll(closed: true, type: type),
answerOption: buildAnswerOption(selected: false, winner: false),
action: { })
TimelinePollAnswerOptionButton(poll: buildPoll(closed: true, type: type),
answerOption: buildAnswerOption(selected: false, winner: true),
action: { })
TimelinePollAnswerOptionButton(poll: buildPoll(closed: true, type: type),
answerOption: buildAnswerOption(selected: true, winner: false),
action: { })
TimelinePollAnswerOptionButton(poll: buildPoll(closed: true, type: type),
answerOption: buildAnswerOption(selected: true, winner: true),
action: { })
let longText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
TimelinePollAnswerOptionButton(poll: buildPoll(closed: true, type: type),
answerOption: buildAnswerOption(text: longText, selected: true, winner: true),
action: { })
}
}
}
}
static func buildPoll(closed: Bool, type: TimelinePollType) -> TimelinePollDetails {
TimelinePollDetails(question: "",
answerOptions: [],
closed: closed,
totalAnswerCount: 100,
type: type,
eventType: .started,
maxAllowedSelections: 1,
hasBeenEdited: false,
hasDecryptionError: false)
}
static func buildAnswerOption(text: String = "Test", selected: Bool, winner: Bool = false) -> TimelinePollAnswerOption {
TimelinePollAnswerOption(id: "1", text: text, count: 5, winner: winner, selected: selected)
}
}
@@ -20,8 +20,8 @@ enum PollHistoryConstants {
static let chunkSizeInDays: UInt = 30
}
enum PollHistoryViewModelResult: Equatable {
#warning("e.g. show poll detail")
enum PollHistoryViewModelResult {
case showPollDetail(poll: PollListData)
}
// MARK: View
@@ -49,4 +49,5 @@ struct PollHistoryViewState: BindableState {
enum PollHistoryViewAction {
case viewAppeared
case segmentDidChange
case showPollDetail(poll: PollListData)
}
@@ -40,6 +40,8 @@ final class PollHistoryViewModel: PollHistoryViewModelType, PollHistoryViewModel
fetchFirstBatch()
case .segmentDidChange:
updateViewState()
case .showPollDetail(let poll):
completion?(.showPollDetail(poll: poll))
}
}
}
@@ -62,6 +62,9 @@ struct PollHistory: View {
LazyVStack(spacing: 32) {
ForEach(viewModel.viewState.polls ?? []) { pollData in
PollListItem(pollData: pollData)
.onTapGesture {
viewModel.send(viewAction: .showPollDetail(poll: pollData))
}
}
.frame(maxWidth: .infinity, alignment: .leading)