mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-17 15:09:31 +02:00
Feature/4383 poll participants details
This commit is contained in:
committed by
Arnfried Griesert
parent
00286183f6
commit
9bd569dac0
@@ -125,6 +125,7 @@ class BWIBuildSettings: NSObject {
|
||||
|
||||
var bwiShowClosedPolls = true
|
||||
var bwiPollShowParticipantsToggle = true
|
||||
var bwiPollVisibleVotes = 5
|
||||
var bwiShowThreads = false
|
||||
|
||||
var bwiShowRoomCreationSectionFooter = false
|
||||
|
||||
2
Podfile
2
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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)?) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -50,7 +50,7 @@ class TimelinePollViewModel: TimelinePollViewModelType, TimelinePollViewModelPro
|
||||
updateMultiSelectPollLocalState(&state, selectedAnswerIdentifier: identifier, callback: completion)
|
||||
}
|
||||
case .showParticipants:
|
||||
break
|
||||
completion?(.showParticipants)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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:[])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
121
bwi/PollParticipantDetails/PollParticipantDetailsView.swift
Normal file
121
bwi/PollParticipantDetails/PollParticipantDetailsView.swift
Normal file
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<PollParticipantDetailsViewState, PollParticipantDetailsViewAction>
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user