Merge pull request #7886 from vector-im/feature/mna/past-polls-ui
[Poll] Render past polls list of a room (PSG-1029)
This commit is contained in:
commit
f856142cdc
|
@ -1 +1,2 @@
|
|||
[Poll] Render active polls list of a room
|
||||
[Poll] Render past polls list of a room
|
||||
|
|
|
@ -3193,6 +3193,8 @@
|
|||
<string name="closed_poll_option_description">Results are only revealed when you end the poll</string>
|
||||
<string name="room_polls_active">Active polls</string>
|
||||
<string name="room_polls_active_no_item">There are no active polls in this room</string>
|
||||
<string name="room_polls_ended">Past polls</string>
|
||||
<string name="room_polls_ended_no_item">There are no past polls in this room</string>
|
||||
|
||||
<!-- Location -->
|
||||
<string name="location_activity_title_static_sharing">Share location</string>
|
||||
|
|
|
@ -16,50 +16,99 @@
|
|||
|
||||
package im.vector.app.features.roomprofile.polls
|
||||
|
||||
import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
|
||||
class GetPollsUseCase @Inject constructor() {
|
||||
|
||||
fun execute(filter: RoomPollsFilterType): Flow<List<PollSummary>> {
|
||||
fun execute(): Flow<List<PollSummary>> {
|
||||
// TODO unmock and add unit tests
|
||||
return when (filter) {
|
||||
RoomPollsFilterType.ACTIVE -> getActivePolls()
|
||||
RoomPollsFilterType.ENDED -> emptyFlow()
|
||||
}.map { it.sortedByDescending { poll -> poll.creationTimestamp } }
|
||||
return flowOf(getActivePolls() + getEndedPolls())
|
||||
.map { it.sortedByDescending { poll -> poll.creationTimestamp } }
|
||||
}
|
||||
|
||||
private fun getActivePolls(): Flow<List<PollSummary.ActivePoll>> {
|
||||
return flowOf(
|
||||
listOf(
|
||||
PollSummary.ActivePoll(
|
||||
id = "id1",
|
||||
// 2022/06/28 UTC+1
|
||||
creationTimestamp = 1656367200000,
|
||||
title = "Which charity would you like to support?"
|
||||
private fun getActivePolls(): List<PollSummary.ActivePoll> {
|
||||
return listOf(
|
||||
PollSummary.ActivePoll(
|
||||
id = "id1",
|
||||
// 2022/06/28 UTC+1
|
||||
creationTimestamp = 1656367200000,
|
||||
title = "Which charity would you like to support?"
|
||||
),
|
||||
PollSummary.ActivePoll(
|
||||
id = "id2",
|
||||
// 2022/06/26 UTC+1
|
||||
creationTimestamp = 1656194400000,
|
||||
title = "Which sport should the pupils do this year?"
|
||||
),
|
||||
PollSummary.ActivePoll(
|
||||
id = "id3",
|
||||
// 2022/06/24 UTC+1
|
||||
creationTimestamp = 1656021600000,
|
||||
title = "What type of food should we have at the party?"
|
||||
),
|
||||
PollSummary.ActivePoll(
|
||||
id = "id4",
|
||||
// 2022/06/22 UTC+1
|
||||
creationTimestamp = 1655848800000,
|
||||
title = "What film should we show at the end of the year party?"
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getEndedPolls(): List<PollSummary.EndedPoll> {
|
||||
return listOf(
|
||||
PollSummary.EndedPoll(
|
||||
id = "id1-ended",
|
||||
// 2022/06/28 UTC+1
|
||||
creationTimestamp = 1656367200000,
|
||||
title = "Which charity would you like to support?",
|
||||
totalVotes = 22,
|
||||
winnerOptions = listOf(
|
||||
PollOptionViewState.PollEnded(
|
||||
optionId = "id1",
|
||||
optionAnswer = "Cancer research",
|
||||
voteCount = 13,
|
||||
votePercentage = 13 / 22.0,
|
||||
isWinner = true,
|
||||
)
|
||||
),
|
||||
PollSummary.ActivePoll(
|
||||
id = "id2",
|
||||
// 2022/06/26 UTC+1
|
||||
creationTimestamp = 1656194400000,
|
||||
title = "Which sport should the pupils do this year?"
|
||||
),
|
||||
PollSummary.EndedPoll(
|
||||
id = "id2-ended",
|
||||
// 2022/06/26 UTC+1
|
||||
creationTimestamp = 1656194400000,
|
||||
title = "Where should we do the offsite?",
|
||||
totalVotes = 92,
|
||||
winnerOptions = listOf(
|
||||
PollOptionViewState.PollEnded(
|
||||
optionId = "id1",
|
||||
optionAnswer = "Hawaii",
|
||||
voteCount = 43,
|
||||
votePercentage = 43 / 92.0,
|
||||
isWinner = true,
|
||||
)
|
||||
),
|
||||
PollSummary.ActivePoll(
|
||||
id = "id3",
|
||||
// 2022/06/24 UTC+1
|
||||
creationTimestamp = 1656021600000,
|
||||
title = "What type of food should we have at the party?"
|
||||
),
|
||||
PollSummary.EndedPoll(
|
||||
id = "id3-ended",
|
||||
// 2022/06/24 UTC+1
|
||||
creationTimestamp = 1656021600000,
|
||||
title = "What type of food should we have at the party?",
|
||||
totalVotes = 22,
|
||||
winnerOptions = listOf(
|
||||
PollOptionViewState.PollEnded(
|
||||
optionId = "id1",
|
||||
optionAnswer = "Brazilian",
|
||||
voteCount = 13,
|
||||
votePercentage = 13 / 22.0,
|
||||
isWinner = true,
|
||||
)
|
||||
),
|
||||
PollSummary.ActivePoll(
|
||||
id = "id4",
|
||||
// 2022/06/22 UTC+1
|
||||
creationTimestamp = 1655848800000,
|
||||
title = "What film should we show at the end of the year party?"
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,10 +16,24 @@
|
|||
|
||||
package im.vector.app.features.roomprofile.polls
|
||||
|
||||
import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState
|
||||
|
||||
sealed interface PollSummary {
|
||||
val id: String
|
||||
val creationTimestamp: Long
|
||||
val title: String
|
||||
|
||||
data class ActivePoll(
|
||||
val id: String,
|
||||
val creationTimestamp: Long,
|
||||
val title: String,
|
||||
override val id: String,
|
||||
override val creationTimestamp: Long,
|
||||
override val title: String,
|
||||
) : PollSummary
|
||||
|
||||
data class EndedPoll(
|
||||
override val id: String,
|
||||
override val creationTimestamp: Long,
|
||||
override val title: String,
|
||||
val totalVotes: Int,
|
||||
val winnerOptions: List<PollOptionViewState.PollEnded>,
|
||||
) : PollSummary
|
||||
}
|
||||
|
|
|
@ -18,6 +18,4 @@ package im.vector.app.features.roomprofile.polls
|
|||
|
||||
import im.vector.app.core.platform.VectorViewModelAction
|
||||
|
||||
sealed interface RoomPollsAction : VectorViewModelAction {
|
||||
data class SetFilter(val filter: RoomPollsFilterType) : RoomPollsAction
|
||||
}
|
||||
sealed interface RoomPollsAction : VectorViewModelAction
|
||||
|
|
|
@ -66,7 +66,8 @@ class RoomPollsFragment : VectorBaseFragment<FragmentRoomPollsBinding>() {
|
|||
|
||||
tabLayoutMediator = TabLayoutMediator(views.roomPollsTabs, views.roomPollsViewPager) { tab, position ->
|
||||
when (position) {
|
||||
0 -> tab.text = getString(R.string.room_polls_active)
|
||||
RoomPollsType.ACTIVE.ordinal -> tab.text = getString(R.string.room_polls_active)
|
||||
RoomPollsType.ENDED.ordinal -> tab.text = getString(R.string.room_polls_ended)
|
||||
}
|
||||
}.also { it.attach() }
|
||||
}
|
||||
|
|
|
@ -19,15 +19,20 @@ package im.vector.app.features.roomprofile.polls
|
|||
import androidx.fragment.app.Fragment
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import im.vector.app.features.roomprofile.polls.active.RoomActivePollsFragment
|
||||
import im.vector.app.features.roomprofile.polls.ended.RoomEndedPollsFragment
|
||||
|
||||
class RoomPollsPagerAdapter(
|
||||
private val fragment: Fragment
|
||||
) : FragmentStateAdapter(fragment) {
|
||||
|
||||
override fun getItemCount() = 1
|
||||
override fun getItemCount() = RoomPollsType.values().size
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return instantiateFragment(RoomActivePollsFragment::class.java.name)
|
||||
return when (position) {
|
||||
RoomPollsType.ACTIVE.ordinal -> instantiateFragment(RoomActivePollsFragment::class.java.name)
|
||||
RoomPollsType.ENDED.ordinal -> instantiateFragment(RoomEndedPollsFragment::class.java.name)
|
||||
else -> throw IllegalArgumentException("position should be between 0 and ${itemCount - 1}, while it was $position")
|
||||
}
|
||||
}
|
||||
|
||||
private fun instantiateFragment(fragmentName: String): Fragment {
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
package im.vector.app.features.roomprofile.polls
|
||||
|
||||
enum class RoomPollsFilterType {
|
||||
enum class RoomPollsType {
|
||||
ACTIVE,
|
||||
ENDED,
|
||||
}
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
package im.vector.app.features.roomprofile.polls
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.airbnb.mvrx.MavericksViewModelFactory
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
|
@ -24,7 +23,6 @@ import dagger.assisted.AssistedInject
|
|||
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
||||
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
|
@ -40,24 +38,17 @@ class RoomPollsViewModel @AssistedInject constructor(
|
|||
|
||||
companion object : MavericksViewModelFactory<RoomPollsViewModel, RoomPollsViewState> by hiltMavericksViewModelFactory()
|
||||
|
||||
@VisibleForTesting
|
||||
var pollsCollectionJob: Job? = null
|
||||
|
||||
override fun handle(action: RoomPollsAction) {
|
||||
when (action) {
|
||||
is RoomPollsAction.SetFilter -> handleSetFilter(action.filter)
|
||||
}
|
||||
init {
|
||||
observePolls()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
pollsCollectionJob = null
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
private fun handleSetFilter(filter: RoomPollsFilterType) {
|
||||
pollsCollectionJob?.cancel()
|
||||
pollsCollectionJob = getPollsUseCase.execute(filter)
|
||||
private fun observePolls() {
|
||||
getPollsUseCase.execute()
|
||||
.onEach { setState { copy(polls = it) } }
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
override fun handle(action: RoomPollsAction) {
|
||||
// do nothing for now
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.roomprofile.polls.active
|
||||
|
||||
import com.airbnb.epoxy.TypedEpoxyController
|
||||
import im.vector.app.core.date.DateFormatKind
|
||||
import im.vector.app.core.date.VectorDateFormatter
|
||||
import im.vector.app.features.roomprofile.polls.PollSummary
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomActivePollsController @Inject constructor(
|
||||
val dateFormatter: VectorDateFormatter,
|
||||
) : TypedEpoxyController<List<PollSummary.ActivePoll>>() {
|
||||
|
||||
interface Listener {
|
||||
fun onPollClicked(pollId: String)
|
||||
}
|
||||
|
||||
var listener: Listener? = null
|
||||
|
||||
override fun buildModels(data: List<PollSummary.ActivePoll>?) {
|
||||
if (data.isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val host = this
|
||||
for (poll in data) {
|
||||
activePollItem {
|
||||
id(poll.id)
|
||||
formattedDate(host.dateFormatter.format(poll.creationTimestamp, DateFormatKind.TIMELINE_DAY_DIVIDER))
|
||||
title(poll.title)
|
||||
clickListener {
|
||||
host.listener?.onPollClicked(poll.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,77 +16,19 @@
|
|||
|
||||
package im.vector.app.features.roomprofile.polls.active
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.mvrx.parentFragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.cleanup
|
||||
import im.vector.app.core.extensions.configureWith
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.databinding.FragmentRoomPollsListBinding
|
||||
import im.vector.app.features.roomprofile.polls.PollSummary
|
||||
import im.vector.app.features.roomprofile.polls.RoomPollsAction
|
||||
import im.vector.app.features.roomprofile.polls.RoomPollsFilterType
|
||||
import im.vector.app.features.roomprofile.polls.RoomPollsViewModel
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import im.vector.app.features.roomprofile.polls.RoomPollsType
|
||||
import im.vector.app.features.roomprofile.polls.list.RoomPollsListFragment
|
||||
|
||||
@AndroidEntryPoint
|
||||
class RoomActivePollsFragment :
|
||||
VectorBaseFragment<FragmentRoomPollsListBinding>(),
|
||||
RoomActivePollsController.Listener {
|
||||
class RoomActivePollsFragment : RoomPollsListFragment() {
|
||||
|
||||
@Inject
|
||||
lateinit var roomActivePollsController: RoomActivePollsController
|
||||
|
||||
private val viewModel: RoomPollsViewModel by parentFragmentViewModel(RoomPollsViewModel::class)
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentRoomPollsListBinding {
|
||||
return FragmentRoomPollsListBinding.inflate(inflater, container, false)
|
||||
override fun getEmptyListTitle(): String {
|
||||
return getString(R.string.room_polls_active_no_item)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setupList()
|
||||
}
|
||||
|
||||
private fun setupList() {
|
||||
roomActivePollsController.listener = this
|
||||
views.roomPollsList.configureWith(roomActivePollsController)
|
||||
views.roomPollsEmptyTitle.text = getString(R.string.room_polls_active_no_item)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
cleanUpList()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun cleanUpList() {
|
||||
views.roomPollsList.cleanup()
|
||||
roomActivePollsController.listener = null
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.handle(RoomPollsAction.SetFilter(RoomPollsFilterType.ACTIVE))
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(viewModel) { viewState ->
|
||||
renderList(viewState.polls.filterIsInstance(PollSummary.ActivePoll::class.java))
|
||||
}
|
||||
|
||||
private fun renderList(polls: List<PollSummary.ActivePoll>) {
|
||||
roomActivePollsController.setData(polls)
|
||||
views.roomPollsEmptyTitle.isVisible = polls.isEmpty()
|
||||
}
|
||||
|
||||
override fun onPollClicked(pollId: String) {
|
||||
// TODO navigate to details
|
||||
Timber.d("poll with id $pollId clicked")
|
||||
override fun getRoomPollsType(): RoomPollsType {
|
||||
return RoomPollsType.ACTIVE
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.roomprofile.polls.ended
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import im.vector.app.R
|
||||
import im.vector.app.features.roomprofile.polls.RoomPollsType
|
||||
import im.vector.app.features.roomprofile.polls.list.RoomPollsListFragment
|
||||
|
||||
@AndroidEntryPoint
|
||||
class RoomEndedPollsFragment : RoomPollsListFragment() {
|
||||
|
||||
override fun getEmptyListTitle(): String {
|
||||
return getString(R.string.room_polls_ended_no_item)
|
||||
}
|
||||
|
||||
override fun getRoomPollsType(): RoomPollsType {
|
||||
return RoomPollsType.ENDED
|
||||
}
|
||||
}
|
|
@ -14,9 +14,11 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.roomprofile.polls.active
|
||||
package im.vector.app.features.roomprofile.polls.list
|
||||
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.app.R
|
||||
|
@ -24,9 +26,12 @@ import im.vector.app.core.epoxy.ClickListener
|
|||
import im.vector.app.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.app.core.epoxy.onClick
|
||||
import im.vector.app.core.extensions.setTextOrHide
|
||||
import im.vector.app.features.home.room.detail.timeline.item.PollOptionView
|
||||
import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState
|
||||
|
||||
@EpoxyModelClass
|
||||
abstract class ActivePollItem : VectorEpoxyModel<ActivePollItem.Holder>(R.layout.item_poll) {
|
||||
abstract class RoomPollItem : VectorEpoxyModel<RoomPollItem.Holder>(R.layout.item_poll) {
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var formattedDate: String
|
||||
|
@ -34,6 +39,12 @@ abstract class ActivePollItem : VectorEpoxyModel<ActivePollItem.Holder>(R.layout
|
|||
@EpoxyAttribute
|
||||
lateinit var title: String
|
||||
|
||||
@EpoxyAttribute
|
||||
var winnerOptions: List<PollOptionViewState.PollEnded> = emptyList()
|
||||
|
||||
@EpoxyAttribute
|
||||
var totalVotesStatus: String? = null
|
||||
|
||||
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
|
||||
var clickListener: ClickListener? = null
|
||||
|
||||
|
@ -42,10 +53,20 @@ abstract class ActivePollItem : VectorEpoxyModel<ActivePollItem.Holder>(R.layout
|
|||
holder.view.onClick(clickListener)
|
||||
holder.date.text = formattedDate
|
||||
holder.title.text = title
|
||||
holder.winnerOptions.removeAllViews()
|
||||
holder.winnerOptions.isVisible = winnerOptions.isNotEmpty()
|
||||
for (winnerOption in winnerOptions) {
|
||||
val optionView = PollOptionView(holder.view.context)
|
||||
holder.winnerOptions.addView(optionView)
|
||||
optionView.render(winnerOption)
|
||||
}
|
||||
holder.totalVotes.setTextOrHide(totalVotesStatus)
|
||||
}
|
||||
|
||||
class Holder : VectorEpoxyHolder() {
|
||||
val date by bind<TextView>(R.id.pollActiveDate)
|
||||
val title by bind<TextView>(R.id.pollActiveTitle)
|
||||
val date by bind<TextView>(R.id.pollDate)
|
||||
val title by bind<TextView>(R.id.pollTitle)
|
||||
val winnerOptions by bind<LinearLayout>(R.id.pollWinnerOptionsContainer)
|
||||
val totalVotes by bind<TextView>(R.id.pollTotalVotes)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.roomprofile.polls.list
|
||||
|
||||
import com.airbnb.epoxy.TypedEpoxyController
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.date.DateFormatKind
|
||||
import im.vector.app.core.date.VectorDateFormatter
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.features.roomprofile.polls.PollSummary
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomPollsController @Inject constructor(
|
||||
val dateFormatter: VectorDateFormatter,
|
||||
val stringProvider: StringProvider,
|
||||
) : TypedEpoxyController<List<PollSummary>>() {
|
||||
|
||||
interface Listener {
|
||||
fun onPollClicked(pollId: String)
|
||||
}
|
||||
|
||||
var listener: Listener? = null
|
||||
|
||||
override fun buildModels(data: List<PollSummary>?) {
|
||||
if (data.isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
for (poll in data) {
|
||||
when (poll) {
|
||||
is PollSummary.ActivePoll -> buildActivePollItem(poll)
|
||||
is PollSummary.EndedPoll -> buildEndedPollItem(poll)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildActivePollItem(poll: PollSummary.ActivePoll) {
|
||||
val host = this
|
||||
roomPollItem {
|
||||
id(poll.id)
|
||||
formattedDate(host.dateFormatter.format(poll.creationTimestamp, DateFormatKind.TIMELINE_DAY_DIVIDER))
|
||||
title(poll.title)
|
||||
clickListener {
|
||||
host.listener?.onPollClicked(poll.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildEndedPollItem(poll: PollSummary.EndedPoll) {
|
||||
val host = this
|
||||
roomPollItem {
|
||||
id(poll.id)
|
||||
formattedDate(host.dateFormatter.format(poll.creationTimestamp, DateFormatKind.TIMELINE_DAY_DIVIDER))
|
||||
title(poll.title)
|
||||
winnerOptions(poll.winnerOptions)
|
||||
totalVotesStatus(host.stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, poll.totalVotes, poll.totalVotes))
|
||||
clickListener {
|
||||
host.listener?.onPollClicked(poll.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.roomprofile.polls.list
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.mvrx.parentFragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.app.core.extensions.cleanup
|
||||
import im.vector.app.core.extensions.configureWith
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.databinding.FragmentRoomPollsListBinding
|
||||
import im.vector.app.features.roomprofile.polls.PollSummary
|
||||
import im.vector.app.features.roomprofile.polls.RoomPollsType
|
||||
import im.vector.app.features.roomprofile.polls.RoomPollsViewModel
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
abstract class RoomPollsListFragment :
|
||||
VectorBaseFragment<FragmentRoomPollsListBinding>(),
|
||||
RoomPollsController.Listener {
|
||||
|
||||
@Inject
|
||||
lateinit var roomPollsController: RoomPollsController
|
||||
|
||||
private val viewModel: RoomPollsViewModel by parentFragmentViewModel(RoomPollsViewModel::class)
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentRoomPollsListBinding {
|
||||
return FragmentRoomPollsListBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
setupList()
|
||||
}
|
||||
|
||||
abstract fun getEmptyListTitle(): String
|
||||
|
||||
abstract fun getRoomPollsType(): RoomPollsType
|
||||
|
||||
private fun setupList() {
|
||||
roomPollsController.listener = this
|
||||
views.roomPollsList.configureWith(roomPollsController)
|
||||
views.roomPollsEmptyTitle.text = getEmptyListTitle()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
cleanUpList()
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun cleanUpList() {
|
||||
views.roomPollsList.cleanup()
|
||||
roomPollsController.listener = null
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(viewModel) { viewState ->
|
||||
when (getRoomPollsType()) {
|
||||
RoomPollsType.ACTIVE -> renderList(viewState.polls.filterIsInstance(PollSummary.ActivePoll::class.java))
|
||||
RoomPollsType.ENDED -> renderList(viewState.polls.filterIsInstance(PollSummary.EndedPoll::class.java))
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderList(polls: List<PollSummary>) {
|
||||
roomPollsController.setData(polls)
|
||||
views.roomPollsEmptyTitle.isVisible = polls.isEmpty()
|
||||
}
|
||||
|
||||
override fun onPollClicked(pollId: String) {
|
||||
// TODO navigate to details
|
||||
Timber.d("poll with id $pollId clicked")
|
||||
}
|
||||
}
|
|
@ -7,7 +7,7 @@
|
|||
android:foreground="?selectableItemBackground">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pollActiveDate"
|
||||
android:id="@+id/pollDate"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
|
@ -18,19 +18,19 @@
|
|||
tools:text="28/06/22" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pollActiveIcon"
|
||||
android:id="@+id/pollIcon"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/ic_attachment_poll"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/pollActiveDate"
|
||||
app:layout_constraintTop_toBottomOf="@id/pollDate"
|
||||
app:tint="?vctr_content_secondary"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pollActiveTitle"
|
||||
android:id="@+id/pollTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
|
@ -38,8 +38,32 @@
|
|||
android:textAppearance="@style/TextAppearance.Vector.Subtitle"
|
||||
android:textColor="?vctr_content_primary"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/pollActiveIcon"
|
||||
app:layout_constraintTop_toBottomOf="@id/pollActiveDate"
|
||||
app:layout_constraintStart_toEndOf="@id/pollIcon"
|
||||
app:layout_constraintTop_toBottomOf="@id/pollDate"
|
||||
tools:text="Which sport should the pupils do this year?" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/pollWinnerOptionsContainer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="13dp"
|
||||
android:divider="@drawable/divider_poll_options"
|
||||
android:orientation="vertical"
|
||||
android:showDividers="middle"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/pollTitle" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pollTotalVotes"
|
||||
style="@style/Widget.Vector.TextView.Caption"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/pollWinnerOptionsContainer"
|
||||
tools:text="@sample/poll.json/totalVotes" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
@ -23,7 +23,6 @@ import io.mockk.every
|
|||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.amshove.kluent.shouldNotBeNull
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
|
@ -45,28 +44,22 @@ class RoomPollsViewModelTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `given SetFilter action when handle then useCase is called with given filter and viewState is updated`() {
|
||||
fun `given viewModel when created then polls list is observed and viewState is updated`() {
|
||||
// Given
|
||||
val filter = RoomPollsFilterType.ACTIVE
|
||||
val action = RoomPollsAction.SetFilter(filter = filter)
|
||||
val polls = listOf(givenAPollSummary())
|
||||
every { fakeGetPollsUseCase.execute(any()) } returns flowOf(polls)
|
||||
val viewModel = createViewModel()
|
||||
every { fakeGetPollsUseCase.execute() } returns flowOf(polls)
|
||||
val expectedViewState = initialState.copy(polls = polls)
|
||||
|
||||
// When
|
||||
val viewModel = createViewModel()
|
||||
val viewModelTest = viewModel.test()
|
||||
viewModel.pollsCollectionJob = null
|
||||
viewModel.handle(action)
|
||||
|
||||
// Then
|
||||
viewModelTest
|
||||
.assertLatestState(expectedViewState)
|
||||
.finish()
|
||||
viewModel.pollsCollectionJob.shouldNotBeNull()
|
||||
verify {
|
||||
viewModel.pollsCollectionJob?.cancel()
|
||||
fakeGetPollsUseCase.execute(filter)
|
||||
fakeGetPollsUseCase.execute()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue