From 5a070f5e1bf5c320f33322c4f2ec944aeebb8f1b Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Tue, 14 Jun 2022 11:32:19 +0200 Subject: [PATCH] wip: view poll results Signed-off-by: Marcel Hibbe --- .../talk/dagger/modules/ViewModelModule.kt | 6 + .../talk/polls/adapters/PollResultItem.kt | 8 + .../adapters/PollResultItemClickListener.kt | 5 + .../polls/adapters/PollResultViewHolder.kt | 18 ++ .../talk/polls/adapters/PollResultsAdapter.kt | 25 +++ .../com/nextcloud/talk/polls/model/Poll.kt | 2 +- .../polls/repositories/PollRepositoryImpl.kt | 1 - .../polls/repositories/model/PollResponse.kt | 2 +- .../talk/polls/ui/PollMainDialogFragment.kt | 48 ++++-- .../talk/polls/ui/PollResultsFragment.kt | 159 ++++++++++++++++++ .../talk/polls/ui/PollVoteFragment.kt | 6 +- .../polls/viewmodels/PollResultsViewModel.kt | 50 ++++++ .../talk/polls/viewmodels/PollViewModel.kt | 22 ++- app/src/main/res/layout/dialog_poll_main.xml | 4 +- .../main/res/layout/dialog_poll_results.xml | 62 +++++++ app/src/main/res/layout/dialog_poll_vote.xml | 6 +- app/src/main/res/layout/poll_result_item.xml | 33 ++++ app/src/main/res/values/strings.xml | 3 + 18 files changed, 434 insertions(+), 26 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultItem.kt create mode 100644 app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultItemClickListener.kt create mode 100644 app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultViewHolder.kt create mode 100644 app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultsAdapter.kt create mode 100644 app/src/main/java/com/nextcloud/talk/polls/ui/PollResultsFragment.kt create mode 100644 app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollResultsViewModel.kt create mode 100644 app/src/main/res/layout/dialog_poll_results.xml create mode 100644 app/src/main/res/layout/poll_result_item.xml diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt b/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt index cfccd7a23..338c0d340 100644 --- a/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt @@ -25,6 +25,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.nextcloud.talk.remotefilebrowser.viewmodels.RemoteFileBrowserItemsViewModel import com.nextcloud.talk.messagesearch.MessageSearchViewModel +import com.nextcloud.talk.polls.viewmodels.PollResultsViewModel import com.nextcloud.talk.polls.viewmodels.PollViewModel import com.nextcloud.talk.polls.viewmodels.PollVoteViewModel import com.nextcloud.talk.shareditems.viewmodels.SharedItemsViewModel @@ -73,6 +74,11 @@ abstract class ViewModelModule { @ViewModelKey(PollVoteViewModel::class) abstract fun pollVoteViewModel(viewModel: PollVoteViewModel): ViewModel + @Binds + @IntoMap + @ViewModelKey(PollResultsViewModel::class) + abstract fun pollResultsViewModel(viewModel: PollResultsViewModel): ViewModel + @Binds @IntoMap @ViewModelKey(RemoteFileBrowserItemsViewModel::class) diff --git a/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultItem.kt b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultItem.kt new file mode 100644 index 000000000..324253b6d --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultItem.kt @@ -0,0 +1,8 @@ +package com.nextcloud.talk.polls.adapters + +class PollResultItem( + val pollOption: String, + val pollPercent: Int + + // val voters.... +) diff --git a/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultItemClickListener.kt b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultItemClickListener.kt new file mode 100644 index 000000000..0f73a37a2 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultItemClickListener.kt @@ -0,0 +1,5 @@ +package com.nextcloud.talk.polls.adapters + +interface PollResultItemClickListener { + fun onClick(pollResultItem: PollResultItem) +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultViewHolder.kt b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultViewHolder.kt new file mode 100644 index 000000000..ce928340d --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultViewHolder.kt @@ -0,0 +1,18 @@ +package com.nextcloud.talk.polls.adapters + +import android.annotation.SuppressLint +import androidx.recyclerview.widget.RecyclerView +import com.nextcloud.talk.databinding.PollResultItemBinding + +class PollResultViewHolder( + private val binding: PollResultItemBinding +) : RecyclerView.ViewHolder(binding.root) { + + @SuppressLint("SetTextI18n") + fun bind(pollResultItem: PollResultItem, clickListener: PollResultItemClickListener) { + binding.root.setOnClickListener { clickListener.onClick(pollResultItem) } + binding.pollOptionText.text = pollResultItem.pollOption + binding.pollOptionPercentText.text = pollResultItem.pollPercent.toString() + "%" + binding.pollOptionBar.progress = pollResultItem.pollPercent + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultsAdapter.kt b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultsAdapter.kt new file mode 100644 index 000000000..8c2471f94 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/adapters/PollResultsAdapter.kt @@ -0,0 +1,25 @@ +package com.nextcloud.talk.polls.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.nextcloud.talk.databinding.PollResultItemBinding + +class PollResultsAdapter( + private val clickListener: PollResultItemClickListener +) : RecyclerView.Adapter() { + internal var list: MutableList = ArrayList() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PollResultViewHolder { + val itemBinding = PollResultItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return PollResultViewHolder(itemBinding) + } + + override fun onBindViewHolder(holder: PollResultViewHolder, position: Int) { + holder.bind(list[position], clickListener) + } + + override fun getItemCount(): Int { + return list.size + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/model/Poll.kt b/app/src/main/java/com/nextcloud/talk/polls/model/Poll.kt index 4433ace25..f02829bfb 100644 --- a/app/src/main/java/com/nextcloud/talk/polls/model/Poll.kt +++ b/app/src/main/java/com/nextcloud/talk/polls/model/Poll.kt @@ -4,7 +4,7 @@ data class Poll( val id: String, val question: String?, val options: List?, - val votes: List?, + val votes: Map?, val actorType: String?, val actorId: String?, val actorDisplayName: String?, diff --git a/app/src/main/java/com/nextcloud/talk/polls/repositories/PollRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/polls/repositories/PollRepositoryImpl.kt index c02f7bd22..9604baabc 100644 --- a/app/src/main/java/com/nextcloud/talk/polls/repositories/PollRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/polls/repositories/PollRepositoryImpl.kt @@ -49,7 +49,6 @@ class PollRepositoryImpl(private val ncApi: NcApi, private val currentUserProvid ), ).map { mapToPoll(it.ocs?.data!!) } - // // // TODO actual api call // return Observable.just( // Poll( // id = "aaa", diff --git a/app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollResponse.kt b/app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollResponse.kt index 0bd86c56f..6215fb89b 100644 --- a/app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollResponse.kt +++ b/app/src/main/java/com/nextcloud/talk/polls/repositories/model/PollResponse.kt @@ -37,7 +37,7 @@ data class PollResponse( var options: ArrayList? = null, @JsonField(name = ["votes"]) - var votes: ArrayList? = null, + var votes: Map? = null, @JsonField(name = ["actorType"]) var actorType: String? = null, diff --git a/app/src/main/java/com/nextcloud/talk/polls/ui/PollMainDialogFragment.kt b/app/src/main/java/com/nextcloud/talk/polls/ui/PollMainDialogFragment.kt index 8e90e2275..942e24b7d 100644 --- a/app/src/main/java/com/nextcloud/talk/polls/ui/PollMainDialogFragment.kt +++ b/app/src/main/java/com/nextcloud/talk/polls/ui/PollMainDialogFragment.kt @@ -12,6 +12,7 @@ import androidx.lifecycle.ViewModelProvider import autodagger.AutoInjector import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.databinding.DialogPollMainBinding +import com.nextcloud.talk.polls.model.Poll import com.nextcloud.talk.polls.viewmodels.PollViewModel import javax.inject.Inject @@ -58,16 +59,21 @@ class PollMainDialogFragment( viewModel.viewState.observe(viewLifecycleOwner) { state -> when (state) { PollViewModel.InitialState -> {} - is PollViewModel.PollClosedState -> TODO() - is PollViewModel.PollOpenState -> { - val contentFragment = PollVoteFragment( - viewModel, - roomToken, - pollId - ) - val transaction = childFragmentManager.beginTransaction() - transaction.replace(binding.messagePollContentFragment.id, contentFragment) - transaction.commit() + + is PollViewModel.PollVotedState -> { + if (state.poll.resultMode == Poll.RESULT_MODE_HIDDEN) { + showVoteFragment() + } else { + showResultsFragment() + } + } + + is PollViewModel.PollUnvotedState -> { + if (state.poll.status == Poll.STATUS_CLOSED) { + showResultsFragment() + } else { + showVoteFragment() + } } } } @@ -75,6 +81,28 @@ class PollMainDialogFragment( viewModel.initialize(roomToken, pollId) } + private fun showVoteFragment() { + val contentFragment = PollVoteFragment( + viewModel, + roomToken, + pollId + ) + val transaction = childFragmentManager.beginTransaction() + transaction.replace(binding.messagePollContentFragment.id, contentFragment) + transaction.commit() + } + + private fun showResultsFragment() { + val contentFragment = PollResultsFragment( + viewModel, + roomToken, + pollId + ) + val transaction = childFragmentManager.beginTransaction() + transaction.replace(binding.messagePollContentFragment.id, contentFragment) + transaction.commit() + } + /** * Fragment creator */ diff --git a/app/src/main/java/com/nextcloud/talk/polls/ui/PollResultsFragment.kt b/app/src/main/java/com/nextcloud/talk/polls/ui/PollResultsFragment.kt new file mode 100644 index 000000000..16da389ec --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/ui/PollResultsFragment.kt @@ -0,0 +1,159 @@ +/* + * Nextcloud Talk application + * + * @author Álvaro Brey + * Copyright (C) 2022 Álvaro Brey + * Copyright (C) 2022 Nextcloud GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.polls.ui + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.LinearLayoutManager +import autodagger.AutoInjector +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.databinding.DialogPollResultsBinding +import com.nextcloud.talk.polls.adapters.PollResultItem +import com.nextcloud.talk.polls.adapters.PollResultItemClickListener +import com.nextcloud.talk.polls.adapters.PollResultsAdapter +import com.nextcloud.talk.polls.model.Poll +import com.nextcloud.talk.polls.viewmodels.PollResultsViewModel +import com.nextcloud.talk.polls.viewmodels.PollViewModel +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class PollResultsFragment( + private val parentViewModel: PollViewModel, + private val roomToken: String, + private val pollId: String +) : Fragment(), PollResultItemClickListener { + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + + lateinit var viewModel: PollResultsViewModel + + var _binding: DialogPollResultsBinding? = null + val binding: DialogPollResultsBinding + get() = _binding!! + + private var adapter: PollResultsAdapter? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + viewModel = ViewModelProvider(this, viewModelFactory)[PollResultsViewModel::class.java] + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = DialogPollResultsBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + adapter = PollResultsAdapter(this) + _binding?.pollResultsList?.adapter = adapter + _binding?.pollResultsList?.layoutManager = LinearLayoutManager(context) + + parentViewModel.viewState.observe(viewLifecycleOwner) { state -> + if (state is PollViewModel.PollVotedState && + state.poll.resultMode == Poll.RESULT_MODE_PUBLIC + ) { + + initPollResults(state.poll) + initAmountVotersInfo(state) + initEditButton(state) + } else if (state is PollViewModel.PollUnvotedState && + state.poll.status == Poll.STATUS_CLOSED + ) { + Log.d(TAG, "show results also if self never voted") + } + + } + } + + private fun initPollResults(poll: Poll) { + if (poll.details != null) { + val votersAmount = poll.details.size + val oneVoteInPercent = 100 / votersAmount // TODO: poll.numVoters when fixed on api + + poll.options?.forEachIndexed { index, option -> + val votersForThisOption = poll.details.filter { it.optionId == index }.size + val optionsPercent = oneVoteInPercent * votersForThisOption + + val pollResultItem = PollResultItem(option, optionsPercent) // TODO add participants to PollResultItem + adapter?.list?.add(pollResultItem) + } + } else if (poll.votes != null) { + val votersAmount = poll.numVoters + val oneVoteInPercent = 100 / votersAmount + + poll.options?.forEachIndexed { index, option -> + val votersForThisOption = poll.votes?.filter { it.value == index }?.size!! + val optionsPercent = oneVoteInPercent * votersForThisOption + + val pollResultItem = PollResultItem(option, optionsPercent) + adapter?.list?.add(pollResultItem) + } + } else { + Log.e(TAG, "failed to get data to show poll results") + } + } + + private fun initAmountVotersInfo(state: PollViewModel.PollVotedState) { + _binding?.pollAmountVoters?.text = String.format( + resources.getString(R.string.polls_amount_voters), + state.poll.numVoters + ) + } + + private fun initEditButton(state: PollViewModel.PollVotedState) { + if (state.poll.status == Poll.STATUS_OPEN && state.poll.resultMode == Poll.RESULT_MODE_PUBLIC) { + _binding?.editVoteButton?.visibility = View.VISIBLE + _binding?.editVoteButton?.setOnClickListener { + parentViewModel.edit() + } + } else { + _binding?.editVoteButton?.visibility = View.GONE + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + companion object { + private val TAG = PollResultsFragment::class.java.simpleName + } + + override fun onClick(pollResultItem: PollResultItem) { + Log.d(TAG, "click..") + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/ui/PollVoteFragment.kt b/app/src/main/java/com/nextcloud/talk/polls/ui/PollVoteFragment.kt index 20a895eaf..c908053d8 100644 --- a/app/src/main/java/com/nextcloud/talk/polls/ui/PollVoteFragment.kt +++ b/app/src/main/java/com/nextcloud/talk/polls/ui/PollVoteFragment.kt @@ -32,6 +32,7 @@ import androidx.lifecycle.ViewModelProvider import autodagger.AutoInjector import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.databinding.DialogPollVoteBinding +import com.nextcloud.talk.polls.model.Poll import com.nextcloud.talk.polls.viewmodels.PollViewModel import com.nextcloud.talk.polls.viewmodels.PollVoteViewModel import javax.inject.Inject @@ -70,7 +71,7 @@ class PollVoteFragment( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) parentViewModel.viewState.observe(viewLifecycleOwner) { state -> - if (state is PollViewModel.PollOpenState) { + if (state is PollViewModel.PollUnvotedState) { val poll = state.poll binding.radioGroup.removeAllViews() poll.options?.map { option -> @@ -79,6 +80,9 @@ class PollVoteFragment( radioButton.id = index binding.radioGroup.addView(radioButton) } + } else if (state is PollViewModel.PollVotedState && state.poll.resultMode == Poll.RESULT_MODE_HIDDEN) { + Log.d(TAG, "show vote screen also for resultMode hidden poll when already voted") + // TODO: other text for submit button } } diff --git a/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollResultsViewModel.kt b/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollResultsViewModel.kt new file mode 100644 index 000000000..63bf30339 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollResultsViewModel.kt @@ -0,0 +1,50 @@ +/* + * Nextcloud Talk application + * + * @author Álvaro Brey + * Copyright (C) 2022 Álvaro Brey + * Copyright (C) 2022 Nextcloud GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.polls.viewmodels + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.nextcloud.talk.polls.repositories.PollRepository +import io.reactivex.disposables.Disposable +import javax.inject.Inject + +class PollResultsViewModel @Inject constructor(private val repository: PollRepository) : ViewModel() { + + sealed interface ViewState + object InitialState : ViewState + + private val _viewState: MutableLiveData = MutableLiveData(InitialState) + val viewState: LiveData + get() = _viewState + + private var disposable: Disposable? = null + + override fun onCleared() { + super.onCleared() + disposable?.dispose() + } + + companion object { + private val TAG = PollResultsViewModel::class.java.simpleName + } +} diff --git a/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollViewModel.kt b/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollViewModel.kt index e1d0f65ae..6bbb67b42 100644 --- a/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/polls/viewmodels/PollViewModel.kt @@ -28,11 +28,12 @@ class PollViewModel @Inject constructor(private val repository: PollRepository) private lateinit var roomToken: String private lateinit var pollId: String + private var editPoll: Boolean = false + sealed interface ViewState object InitialState : ViewState - open class PollOpenState(val poll: Poll) : ViewState + open class PollUnvotedState(val poll: Poll) : ViewState open class PollVotedState(val poll: Poll) : ViewState - open class PollClosedState(val poll: Poll) : ViewState private val _viewState: MutableLiveData = MutableLiveData(InitialState) val viewState: LiveData @@ -51,6 +52,11 @@ class PollViewModel @Inject constructor(private val repository: PollRepository) loadPoll() // TODO load other view } + fun edit() { + editPoll = true + loadPoll() + } + private fun loadPoll() { repository.getPoll(roomToken, pollId) ?.doOnSubscribe { disposable = it } @@ -79,11 +85,13 @@ class PollViewModel @Inject constructor(private val repository: PollRepository) } override fun onComplete() { - // TODO check attributes and decide if poll is open/closed/selfvoted... - - when (poll.status) { - Poll.STATUS_OPEN -> _viewState.value = PollOpenState(poll) - Poll.STATUS_CLOSED -> _viewState.value = PollClosedState(poll) + if (editPoll) { + _viewState.value = PollUnvotedState(poll) + editPoll = false + } else if (poll.votedSelf.isNullOrEmpty()) { + _viewState.value = PollUnvotedState(poll) + } else { + _viewState.value = PollVotedState(poll) } } } diff --git a/app/src/main/res/layout/dialog_poll_main.xml b/app/src/main/res/layout/dialog_poll_main.xml index ec88db28f..c03afd25d 100644 --- a/app/src/main/res/layout/dialog_poll_main.xml +++ b/app/src/main/res/layout/dialog_poll_main.xml @@ -38,9 +38,8 @@ diff --git a/app/src/main/res/layout/dialog_poll_results.xml b/app/src/main/res/layout/dialog_poll_results.xml new file mode 100644 index 000000000..77a5d7430 --- /dev/null +++ b/app/src/main/res/layout/dialog_poll_results.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_poll_vote.xml b/app/src/main/res/layout/dialog_poll_vote.xml index 102353541..060e9ea1c 100644 --- a/app/src/main/res/layout/dialog_poll_vote.xml +++ b/app/src/main/res/layout/dialog_poll_vote.xml @@ -28,9 +28,9 @@ android:layout_height="wrap_content" app:layout_constraintStart_toStartOf="parent" android:layout_width="wrap_content" - app:layout_constraintTop_toTopOf="parent"> - - + app:layout_constraintTop_toTopOf="parent" + tools:layout_height="400dp" + tools:layout_width="match_parent"> + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 131b6bb5f..b0299a8f5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -534,6 +534,9 @@ Start typing to search … No search results + + Poll results - %1$s votes + Attachments All