diff --git a/Config/BWIBuildSettings.swift b/Config/BWIBuildSettings.swift index 8e2bc079a..63d82dc57 100644 --- a/Config/BWIBuildSettings.swift +++ b/Config/BWIBuildSettings.swift @@ -125,6 +125,7 @@ class BWIBuildSettings: NSObject { var bwiShowClosedPolls = true var bwiPollShowParticipantsToggle = true + var bwiPollVisibleVotes = 5 var bwiShowThreads = false var bwiShowRoomCreationSectionFooter = false diff --git a/Podfile b/Podfile index 969eed1b1..f64282478 100644 --- a/Podfile +++ b/Podfile @@ -43,7 +43,7 @@ when String # specific MatrixSDK released version $matrixSDKVersionSpec = $matrixSDKVersion end -$matrixSDKVersionSpec = { :git => 'https://dl-gitlab.example.com/bwmessenger/bundesmessenger/bundesmessenger-ios-sdk', :tag => 'v0.26.9_bwi_beta_2' } +$matrixSDKVersionSpec = { :git => 'https://dl-gitlab.example.com/bwmessenger/bundesmessenger/bundesmessenger-ios-sdk', :tag => 'v0.26.9_bwi_4383' } # Method to import the MatrixSDK def import_MatrixSDK diff --git a/Riot/Assets/de.lproj/Bwi.strings b/Riot/Assets/de.lproj/Bwi.strings index 4b0801314..05bdbf973 100644 --- a/Riot/Assets/de.lproj/Bwi.strings +++ b/Riot/Assets/de.lproj/Bwi.strings @@ -541,6 +541,9 @@ "poll_edit_form_poll_type_closed" = "Versteckte Umfrage"; "poll_edit_form_poll_type_open" = "Offene Umfrage"; "poll_edit_form_participant_toggle" = "Anzeigen, wer für welche Option gestimmt hat."; +"poll_timeline_show_participants_button" = "Stimmen anzeigen"; +"poll_participant_details_show_more" = "Alle ansehen (%lu weitere)"; +"poll_participant_details_title" = "Umfragedetails"; // MARK: - Welcome Experience "welcome_experience_title1" = "Willkommen beim BundesMessenger"; diff --git a/Riot/Assets/en.lproj/Bwi.strings b/Riot/Assets/en.lproj/Bwi.strings index 33da52014..c885cd0b7 100644 --- a/Riot/Assets/en.lproj/Bwi.strings +++ b/Riot/Assets/en.lproj/Bwi.strings @@ -446,6 +446,9 @@ "poll_edit_form_poll_type_closed" = "Hidden Poll"; "poll_edit_form_poll_type_open" = "Open poll"; "poll_edit_form_participant_toggle" = "Show who voted for which option"; +"poll_timeline_show_participants_button" = "Show votes"; +"poll_participant_details_show_more" = "Show all (%lu more)"; +"poll_participant_details_title" = "Poll details"; // MARK: - Welcome Experience "welcome_experience_title1" = "Welcome to BundesMessenger"; diff --git a/Riot/Generated/BWIStrings.swift b/Riot/Generated/BWIStrings.swift index 206257ed6..d99fc819e 100644 --- a/Riot/Generated/BWIStrings.swift +++ b/Riot/Generated/BWIStrings.swift @@ -983,6 +983,18 @@ public class BWIL10n: NSObject { public static var pollEditFormPollTypeOpen: String { return BWIL10n.tr("Bwi", "poll_edit_form_poll_type_open") } + /// Alle ansehen (%lu weitere) + public static func pollParticipantDetailsShowMore(_ p1: Int) -> String { + return BWIL10n.tr("Bwi", "poll_participant_details_show_more", p1) + } + /// Umfragedetails + public static var pollParticipantDetailsTitle: String { + return BWIL10n.tr("Bwi", "poll_participant_details_title") + } + /// Stimmen anzeigen + public static var pollTimelineShowParticipantsButton: String { + return BWIL10n.tr("Bwi", "poll_timeline_show_participants_button") + } /// Wiederholen public static var retry: String { return BWIL10n.tr("Bwi", "retry") diff --git a/Riot/Modules/Room/RoomCoordinator.swift b/Riot/Modules/Room/RoomCoordinator.swift index f249b863c..e018bfbe8 100644 --- a/Riot/Modules/Room/RoomCoordinator.swift +++ b/Riot/Modules/Room/RoomCoordinator.swift @@ -143,6 +143,12 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { navigationRouter.setRootModule(self, popCompletion: nil) } } + + // FRROT its like sleep() again -> hopefully there is a better solution. When directly calling self.navigation router here its still nil and it needs to be called this early because soon afterwards the pollCells get build + DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { + TimelinePollProvider.shared.navigationRouter = self.navigationRouter + } + } func start(withEventId eventId: String, completion: (() -> Void)?) { diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift b/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift index f312bd1b3..3c0db09b2 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift @@ -42,7 +42,7 @@ final class PollEditFormCoordinator: Coordinator, Presentable { init(parameters: PollEditFormCoordinatorParameters) { self.parameters = parameters - + var viewModel: PollEditFormViewModel if let startEvent = parameters.pollStartEvent, let pollContent = MXEventContentPollStart(fromJSON: startEvent.content) { diff --git a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift index ea209bc75..2fa3ca831 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/PollHistoryDetail/MockPollHistoryDetailScreenState.swift @@ -29,9 +29,9 @@ enum MockPollHistoryDetailScreenState: MockScreenState, CaseIterable { } var poll: TimelinePollDetails { - 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 answerOptions = [TimelinePollAnswerOption(id: "1", text: "First", count: 10, winner: false, selected: false, voters:[]), + TimelinePollAnswerOption(id: "2", text: "Second", count: 5, winner: false, selected: true, voters:[]), + TimelinePollAnswerOption(id: "3", text: "Third", count: 15, winner: true, selected: false, voters:[])] let poll = TimelinePollDetails(id: "id", question: "Question", diff --git a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift index 715c37d44..e97a8d3f7 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/Service/Mock/MockPollHistoryService.swift @@ -70,7 +70,7 @@ private extension MockPollHistoryService { .map { index in TimelinePollDetails(id: "p\(index)", question: "Do you like the active poll number \(index)?", - answerOptions: [.init(id: "id", text: "Yes, of course!", count: 20, winner: true, selected: true)], + answerOptions: [.init(id: "id", text: "Yes, of course!", count: 20, winner: true, selected: true, voters: [])], closed: true, startDate: .init().addingTimeInterval(TimeInterval(-index) * 3600 * 24), totalAnswerCount: 30, diff --git a/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift b/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift index 18ad338a4..96b0278ed 100644 --- a/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift +++ b/RiotSwiftUI/Modules/Room/PollHistory/View/PollListItem.swift @@ -85,7 +85,7 @@ struct PollListItem_Previews: PreviewProvider { Group { let pollData1 = TimelinePollDetails(id: UUID().uuidString, question: "Do you like polls?", - answerOptions: [.init(id: "id", text: "Yes, of course!", count: 18, winner: true, selected: true)], + answerOptions: [.init(id: "id", text: "Yes, of course!", count: 18, winner: true, selected: true, voters:[])], closed: true, startDate: .init(), totalAnswerCount: 30, @@ -98,7 +98,7 @@ struct PollListItem_Previews: PreviewProvider { let pollData2 = TimelinePollDetails(id: UUID().uuidString, question: "Do you like polls?", - answerOptions: [.init(id: "id", text: "Yes, of course!", count: 18, winner: true, selected: true)], + answerOptions: [.init(id: "id", text: "Yes, of course!", count: 18, winner: true, selected: true, voters:[])], closed: false, startDate: .init(), totalAnswerCount: 30, @@ -112,8 +112,8 @@ struct PollListItem_Previews: PreviewProvider { let pollData3 = TimelinePollDetails(id: UUID().uuidString, question: "Do you like polls?", answerOptions: [ - .init(id: "id1", text: "Yes, of course!", count: 15, winner: true, selected: true), - .init(id: "id2", text: "No, I don't :-(", count: 15, winner: true, selected: true) + .init(id: "id1", text: "Yes, of course!", count: 15, winner: true, selected: true, voters:[]), + .init(id: "id2", text: "No, I don't :-(", count: 15, winner: true, selected: true, voters:[]) ], closed: true, startDate: .init(), diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift index f4f3a8a5b..94fe5b28a 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift @@ -29,6 +29,7 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel // MARK: Private + private let navigationRouter: NavigationRouterType? private let parameters: TimelinePollCoordinatorParameters private let selectedAnswerIdentifiersSubject = PassthroughSubject<[String], Never>() @@ -43,9 +44,12 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel // MARK: - Setup - init(parameters: TimelinePollCoordinatorParameters) throws { + // FRROT show participants needs a navigation router as it is a button click that creates a new View + init(parameters: TimelinePollCoordinatorParameters, navigationRouter: NavigationRouterType? = nil) throws { self.parameters = parameters + self.navigationRouter = navigationRouter + try pollAggregator = PollAggregator(session: parameters.session, room: parameters.room, pollEvent: parameters.pollEvent) pollAggregator.delegate = self @@ -56,6 +60,9 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel switch result { case .selectedAnswerOptionsWithIdentifiers(let identifiers): self.selectedAnswerIdentifiersSubject.send(identifiers) + case .showParticipants: + self.showParticipantsView() + } } @@ -105,6 +112,23 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel } } + func showParticipantsView() { + if let navigationRouter = navigationRouter { + let parameters = PollParticipantDetailsCoordinatorParameters(room: parameters.room, poll: pollAggregator.poll) + let coordinator = PollParticipantDetailsCoordinator(parameters: parameters) + + add(childCoordinator: coordinator) + + if navigationRouter.modules.isEmpty == false { + navigationRouter.push(coordinator, animated: true, popCompletion: nil) + } else { + navigationRouter.setRootModule(coordinator, popCompletion: nil) + } + + coordinator.start() + } + } + // MARK: - PollAggregatorDelegate func pollAggregatorDidUpdateData(_ aggregator: PollAggregator) { @@ -134,7 +158,8 @@ extension TimelinePollDetails { text: pollAnswerOption.text, count: pollAnswerOption.count, winner: pollAnswerOption.isWinner, - selected: pollAnswerOption.isCurrentUserSelection) + selected: pollAnswerOption.isCurrentUserSelection, + voters:pollAnswerOption.voters) } self.init(id: poll.id, diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift index 0c7233298..43db0c9e6 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift @@ -31,11 +31,15 @@ class TimelinePollProvider: NSObject { } } } + + var navigationRouter: NavigationRouterType? = nil + var coordinatorsForEventIdentifiers = [String: TimelinePollCoordinator]() /// Create or retrieve the poll timeline coordinator for this event and return /// a view to be displayed in the timeline func buildTimelinePollVCForEvent(_ event: MXEvent) -> UIViewController? { + guard let session = session, let room = session.room(withRoomId: event.roomId) else { return nil } @@ -45,7 +49,7 @@ class TimelinePollProvider: NSObject { } let parameters = TimelinePollCoordinatorParameters(session: session, room: room, pollEvent: event) - guard let coordinator = try? TimelinePollCoordinator(parameters: parameters) else { + guard let coordinator = try? TimelinePollCoordinator(parameters: parameters, navigationRouter: navigationRouter ) else { return messageViewController(for: event) } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift index 9d846e190..aaf3e5d67 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift @@ -26,6 +26,7 @@ enum TimelinePollViewAction { enum TimelinePollViewModelResult { case selectedAnswerOptionsWithIdentifiers([String]) + case showParticipants } enum TimelinePollType { @@ -44,13 +45,15 @@ struct TimelinePollAnswerOption: Identifiable { var count: UInt var winner: Bool var selected: Bool + var voters: [MXEvent] - init(id: String, text: String, count: UInt, winner: Bool, selected: Bool) { + init(id: String, text: String, count: UInt, winner: Bool, selected: Bool, voters: [MXEvent]) { self.id = id self.text = text self.count = count self.winner = winner self.selected = selected + self.voters = voters } } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift index 9c010fe5a..f5910edf8 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift @@ -29,9 +29,9 @@ enum MockTimelinePollScreenState: MockScreenState, CaseIterable { } var screenView: ([Any], AnyView) { - 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 answerOptions = [TimelinePollAnswerOption(id: "1", text: "First", count: 10, winner: false, selected: false, voters:[]), + TimelinePollAnswerOption(id: "2", text: "Second", count: 5, winner: false, selected: true, voters:[]), + TimelinePollAnswerOption(id: "3", text: "Third", count: 15, winner: true, selected: false, voters:[])] let poll = TimelinePollDetails(id: "id", question: "Question", diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift index bbe304565..0a0aa6a6d 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift @@ -50,7 +50,7 @@ class TimelinePollViewModel: TimelinePollViewModelType, TimelinePollViewModelPro updateMultiSelectPollLocalState(&state, selectedAnswerIdentifier: identifier, callback: completion) } case .showParticipants: - break + completion?(.showParticipants) } } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift index e16fee5af..958df2099 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift @@ -169,6 +169,6 @@ struct TimelinePollAnswerOptionButton_Previews: PreviewProvider { } static func buildAnswerOption(text: String = "Test", selected: Bool, winner: Bool = false) -> TimelinePollAnswerOption { - TimelinePollAnswerOption(id: "1", text: text, count: 5, winner: winner, selected: selected) + TimelinePollAnswerOption(id: "1", text: text, count: 5, winner: winner, selected: selected, voters:[]) } } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift index 2109a0e8a..358e5752d 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift @@ -58,6 +58,19 @@ struct TimelinePollView: View { .lineLimit(2) .font(theme.fonts.footnote) .foregroundColor(theme.colors.tertiaryContent) + + if poll.showParticipants && (poll.type == .undisclosed || poll.closed) { + Button(action: { + viewModel.send(viewAction:.showParticipants) + }) + { + Text(BWIL10n.pollTimelineShowParticipantsButton) + .font(theme.fonts.body) + .bold() + .foregroundColor(theme.colors.accent) + } + } + } .padding([.horizontal, .top], 2.0) .padding([.bottom]) diff --git a/bwi/PollParticipantDetails/PollParticipantDetailsCoordinator.swift b/bwi/PollParticipantDetails/PollParticipantDetailsCoordinator.swift new file mode 100644 index 000000000..efe8aa1d6 --- /dev/null +++ b/bwi/PollParticipantDetails/PollParticipantDetailsCoordinator.swift @@ -0,0 +1,68 @@ +// +/* + * Copyright (c) 2022 BWI GmbH + * + * 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 +import UIKit + +struct PollParticipantDetailsCoordinatorParameters { + let room: MXRoom + let poll: PollProtocol +} + +final class PollParticipantDetailsCoordinator: Coordinator, Presentable { + + // MARK: Private + + private let parameters: PollParticipantDetailsCoordinatorParameters + private let pollParticipantDetailsHostingController: UIViewController + private var pollParticipantDetailsViewModel: PollParticipantDetailsViewModelProtocol + + // MARK: Public + + var childCoordinators: [Coordinator] = [] + + var completion: (() -> Void)? + + // MARK: - Setup + + init(parameters: PollParticipantDetailsCoordinatorParameters) { + self.parameters = parameters + + var viewModel: PollParticipantDetailsViewModel + + viewModel = PollParticipantDetailsViewModel.init(parameters: PollParticipantDetailsViewModelParameters(poll: parameters.poll, room: parameters.room)) + + let view = PollParticipantDetailsView(viewModel: viewModel.context) + .environmentObject(AvatarViewModel(avatarService: AvatarService(mediaManager: parameters.room.mxSession.mediaManager))) + + pollParticipantDetailsViewModel = viewModel + pollParticipantDetailsHostingController = VectorHostingController(rootView: view) + } + + // MARK: - Public + + func start() { + + } + + // MARK: - Presentable + + func toPresentable() -> UIViewController { + pollParticipantDetailsHostingController + } +} diff --git a/bwi/PollParticipantDetails/PollParticipantDetailsModels.swift b/bwi/PollParticipantDetails/PollParticipantDetailsModels.swift new file mode 100644 index 000000000..2a8f11599 --- /dev/null +++ b/bwi/PollParticipantDetails/PollParticipantDetailsModels.swift @@ -0,0 +1,96 @@ +// +/* + * Copyright (c) 2022 BWI GmbH + * + * 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 + +struct PollParticipantDetailsViewState: BindableState { + var answers: [PollParticipantAnswer] = [] + var poll: PollParticipantPoll +} + +enum PollParticipantDetailsMode { + case someParticipants + case allParticipants +} + +enum PollParticipantDetailsViewAction { + case openAllParticipants(index: Int) + case closeAllParticipants(index: Int) +} + +struct PollParticipantVoter: Identifiable, BindableState { + var id: String { + displayName + } + + var displayName: String + var userAvatarData: AvatarInputProtocol + var formattedVotingTime: String + + static func buildPollParticipantVoter( event: MXEvent, room: MXRoom) -> PollParticipantVoter? { + if let user = room.mxSession.user(withUserId: event.sender) { + let avatarData = AvatarInput(mxContentUri: user.avatarUrl, matrixItemId: event.sender, displayName: user.displayname) + + let votingTime = Date(timeIntervalSince1970: TimeInterval(event.originServerTs / 1000)) + let dateFormatter = DateFormatter() + dateFormatter.timeZone = TimeZone.current + dateFormatter.calendar = Calendar.current + dateFormatter.dateFormat = "dd. MMMM, yyyy HH:mm" + let strDate = dateFormatter.string(from: votingTime) + + return PollParticipantVoter(displayName: user.displayname, userAvatarData: avatarData, formattedVotingTime: strDate) + } else { + return nil + } + } +} + +struct PollParticipantAnswer: Identifiable, BindableState { + var id: String { + originalId + } + + var name: String + var votes: Int + var visibleVotes: Int + var votesText: String + var originalId: String + var voters: [PollParticipantVoter] + var expanded: Bool = false + + static func buildPollParticipantAnswer( answerOption: PollAnswerOptionProtocol, parameters: PollParticipantDetailsViewModelParameters) -> PollParticipantAnswer { + var voters: [PollParticipantVoter] = [] + for participantEvent in answerOption.voters { + if let voter = PollParticipantVoter.buildPollParticipantVoter(event: participantEvent, room: parameters.room) { + voters.append(voter) + } + } + + let votesText = answerOption.count == 1 ? VectorL10n.pollTimelineOneVote : VectorL10n.pollTimelineVotesCount(Int(answerOption.count)) + return PollParticipantAnswer.init( name: answerOption.text, + votes: Int(answerOption.count), + visibleVotes: min(Int(answerOption.count), BWIBuildSettings.shared.bwiPollVisibleVotes), + votesText: votesText, + originalId: answerOption.id, + voters: voters) + } +} + +struct PollParticipantPoll : BindableState { + var name: String + let voterRows: Int = 2 +} diff --git a/bwi/PollParticipantDetails/PollParticipantDetailsView.swift b/bwi/PollParticipantDetails/PollParticipantDetailsView.swift new file mode 100644 index 000000000..70bc5372f --- /dev/null +++ b/bwi/PollParticipantDetails/PollParticipantDetailsView.swift @@ -0,0 +1,121 @@ +// +/* + * Copyright (c) 2022 BWI GmbH + * + * 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 + +struct PollParticipantDetailsView: View { + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + // MARK: Public + + @ObservedObject var viewModel: PollParticipantDetailsViewModel.Context + + var body: some View { + NavigationView { + VStack { + PollParticipantPollHeaderView(poll: viewModel.viewState.poll) + + List { + ForEach(Array(viewModel.viewState.answers.enumerated()), id: \.offset) { index, answer in + SwiftUI.Section(header: PollParticipantSectionHeaderView(answer: answer)) { + if answer.votes > 0 { + VStack { + ForEach(answer.voters.prefix(upTo: answer.visibleVotes)) { voter in + PollParticipantVoterView(voter: voter) + } + + if answer.votes > answer.visibleVotes && !answer.expanded { + Button(action: { onExpandButton(index: index) }) { + Text(BWIL10n.pollParticipantDetailsShowMore(Int(answer.votes-answer.visibleVotes))) + } + } + } + } + } + } + } + .listStyle(.grouped) + } + } + .accentColor(theme.colors.accent) + .navigationViewStyle(StackNavigationViewStyle()) + .navigationTitle(BWIL10n.pollParticipantDetailsTitle) + .navigationBarTitleDisplayMode(.inline) + } + + private func onExpandButton(index: Int) { + viewModel.send(viewAction: .openAllParticipants(index: index)) + } +} + +struct PollParticipantVoterView: View { + @Environment(\.theme) private var theme: ThemeSwiftUI + + var voter: PollParticipantVoter + + var body: some View { + HStack(alignment: .center, spacing: 10) { + AvatarImage(avatarData: voter.userAvatarData, size: .medium) + .border() + + VStack(alignment: .leading, spacing: 3) { + Text(voter.displayName) + .font(theme.fonts.body) + .foregroundColor(theme.colors.primaryContent) + Text(voter.formattedVotingTime) + .font(theme.fonts.footnote) + .foregroundColor(theme.colors.secondaryContent) + } + } + } +} + +struct PollParticipantPollHeaderView: View { + @Environment(\.theme) private var theme: ThemeSwiftUI + + var poll: PollParticipantPoll + + var body: some View { + Text(poll.name) + .font(theme.fonts.headline) + .foregroundColor(theme.colors.primaryContent) + } +} + +struct PollParticipantSectionHeaderView: View { + @Environment(\.theme) private var theme: ThemeSwiftUI + + var answer: PollParticipantAnswer + + var body: some View { + HStack(alignment: .center) { + Text(answer.name) + .font(theme.fonts.headline) + .foregroundColor(theme.colors.primaryContent) + Spacer() + Text(answer.votesText) + .font(theme.fonts.footnote) + .foregroundColor(theme.colors.secondaryContent) + .accessibilityIdentifier("PollAnswerOptionCount") + } + } +} diff --git a/bwi/PollParticipantDetails/PollParticipantDetailsViewModel.swift b/bwi/PollParticipantDetails/PollParticipantDetailsViewModel.swift new file mode 100644 index 000000000..e9b56e62f --- /dev/null +++ b/bwi/PollParticipantDetails/PollParticipantDetailsViewModel.swift @@ -0,0 +1,66 @@ +// +/* + * Copyright (c) 2022 BWI GmbH + * + * 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 Combine +import SwiftUI + +struct PollParticipantDetailsViewModelParameters { + let poll: PollProtocol + let room: MXRoom +} + +typealias PollParticipantDetailsViewModelType = StateStoreViewModel + +class PollParticipantDetailsViewModel: PollParticipantDetailsViewModelType, PollParticipantDetailsViewModelProtocol { + + init(parameters: PollParticipantDetailsViewModelParameters) { + var state = PollParticipantDetailsViewState(poll: PollParticipantPoll(name: parameters.poll.text)) + + var answers: [PollParticipantAnswer] = [] + + let room = parameters.room + + for answerOption in parameters.poll.answerOptions { + + let answer = PollParticipantAnswer.buildPollParticipantAnswer(answerOption: answerOption, parameters: parameters) + answers.append(answer) + } + answers.sort { + $0.votes > $1.votes + } + + state.answers = answers + + super.init(initialViewState: state) + } + + // MARK: - Public + + override func process(viewAction: PollParticipantDetailsViewAction) { + switch viewAction { + + case .openAllParticipants(index: let index): + state.answers[index].expanded = true + state.answers[index].visibleVotes = state.answers[index].votes + case .closeAllParticipants(index: let index): + state.answers[index].expanded = false + state.answers[index].visibleVotes = min(state.answers[index].votes, state.poll.voterRows) + } + } +} diff --git a/bwi/PollParticipantDetails/PollParticipantDetailsViewModelProtocol.swift b/bwi/PollParticipantDetails/PollParticipantDetailsViewModelProtocol.swift new file mode 100644 index 000000000..4511d979b --- /dev/null +++ b/bwi/PollParticipantDetails/PollParticipantDetailsViewModelProtocol.swift @@ -0,0 +1,22 @@ +// +/* + * Copyright (c) 2022 BWI GmbH + * + * 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 PollParticipantDetailsViewModelProtocol { + +}