wip: view poll results

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
This commit is contained in:
Marcel Hibbe 2022-06-14 11:32:19 +02:00 committed by Andy Scherzinger (Rebase PR Action)
parent 7d8aebe234
commit 5a070f5e1b
18 changed files with 434 additions and 26 deletions

View File

@ -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)

View File

@ -0,0 +1,8 @@
package com.nextcloud.talk.polls.adapters
class PollResultItem(
val pollOption: String,
val pollPercent: Int
// val voters....
)

View File

@ -0,0 +1,5 @@
package com.nextcloud.talk.polls.adapters
interface PollResultItemClickListener {
fun onClick(pollResultItem: PollResultItem)
}

View File

@ -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
}
}

View File

@ -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<PollResultViewHolder>() {
internal var list: MutableList<PollResultItem> = ArrayList<PollResultItem>()
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
}
}

View File

@ -4,7 +4,7 @@ data class Poll(
val id: String,
val question: String?,
val options: List<String>?,
val votes: List<Int>?,
val votes: Map<String, Int>?,
val actorType: String?,
val actorId: String?,
val actorDisplayName: String?,

View File

@ -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",

View File

@ -37,7 +37,7 @@ data class PollResponse(
var options: ArrayList<String>? = null,
@JsonField(name = ["votes"])
var votes: ArrayList<Int>? = null,
var votes: Map<String, Int>? = null,
@JsonField(name = ["actorType"])
var actorType: String? = null,

View File

@ -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
*/

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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..")
}
}

View File

@ -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
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<ViewState> = MutableLiveData(InitialState)
val viewState: LiveData<ViewState>
get() = _viewState
private var disposable: Disposable? = null
override fun onCleared() {
super.onCleared()
disposable?.dispose()
}
companion object {
private val TAG = PollResultsViewModel::class.java.simpleName
}
}

View File

@ -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<ViewState> = MutableLiveData(InitialState)
val viewState: LiveData<ViewState>
@ -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)
}
}
}

View File

@ -38,9 +38,8 @@
<androidx.emoji.widget.EmojiTextView
android:id="@+id/message_poll_title"
android:layout_width="257dp"
android:layout_height="55dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:textAlignment="viewStart"
android:textStyle="bold"
app:layout_constraintStart_toEndOf="@id/message_poll_icon"
app:layout_constraintTop_toTopOf="@+id/message_poll_icon"
@ -50,6 +49,7 @@
android:id="@+id/message_poll_content_fragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="@id/message_poll_title"
tools:layout_height="400dp" />

View File

@ -0,0 +1,62 @@
<!--
Nextcloud Android client application
@author Marcel Hibbe
Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License version 2,
as published by the Free Software Foundation.
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 <http://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools"
android:padding="@dimen/standard_padding"
tools:background="@color/white">
<LinearLayout
android:id="@+id/poll_results_list_wrapper"
android:layout_width="match_parent"
android:layout_height="288dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/poll_results_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:listitem="@layout/poll_result_item" />
</LinearLayout>
<TextView
android:id="@+id/poll_amount_voters"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:textColor="@color/low_emphasis_text"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/poll_results_list_wrapper"
tools:text="Poll results - 93 votes" />
<com.google.android.material.button.MaterialButton
android:id="@+id/edit_vote_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/edit"
android:theme="@style/Button.Primary"
app:cornerRadius="@dimen/button_corner_radius"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/poll_results_list_wrapper" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -28,9 +28,9 @@
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
android:layout_width="wrap_content"
app:layout_constraintTop_toTopOf="parent">
</RadioGroup>
app:layout_constraintTop_toTopOf="parent"
tools:layout_height="400dp"
tools:layout_width="match_parent"></RadioGroup>
<com.google.android.material.button.MaterialButton
android:id="@+id/submitVote"

View File

@ -0,0 +1,33 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/standard_padding"
tools:background="@color/white">
<TextView
android:id="@+id/poll_option_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Option Number One" />
<TextView
android:id="@+id/poll_option_percent_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="25 %" />
<ProgressBar
android:id="@+id/poll_option_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="@+id/poll_option_text"
app:layout_constraintTop_toBottomOf="@+id/poll_option_text"
style="?android:attr/progressBarStyleHorizontal" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -534,6 +534,9 @@
<string name="message_search_begin_typing">Start typing to search …</string>
<string name="message_search_begin_empty">No search results</string>
<!-- Polls -->
<string name="polls_amount_voters">Poll results - %1$s votes</string>
<string name="title_attachments">Attachments</string>
<string name="reactions_tab_all">All</string>