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:
Maxime NATUREL 2023-01-06 16:07:44 +01:00 committed by GitHub
commit f856142cdc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 385 additions and 196 deletions

View File

@ -1 +1,2 @@
[Poll] Render active polls list of a room [Poll] Render active polls list of a room
[Poll] Render past polls list of a room

View File

@ -3193,6 +3193,8 @@
<string name="closed_poll_option_description">Results are only revealed when you end the poll</string> <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">Active polls</string>
<string name="room_polls_active_no_item">There are no active polls in this room</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 --> <!-- Location -->
<string name="location_activity_title_static_sharing">Share location</string> <string name="location_activity_title_static_sharing">Share location</string>

View File

@ -16,50 +16,99 @@
package im.vector.app.features.roomprofile.polls 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.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import javax.inject.Inject import javax.inject.Inject
class GetPollsUseCase @Inject constructor() { class GetPollsUseCase @Inject constructor() {
fun execute(filter: RoomPollsFilterType): Flow<List<PollSummary>> { fun execute(): Flow<List<PollSummary>> {
// TODO unmock and add unit tests // TODO unmock and add unit tests
return when (filter) { return flowOf(getActivePolls() + getEndedPolls())
RoomPollsFilterType.ACTIVE -> getActivePolls() .map { it.sortedByDescending { poll -> poll.creationTimestamp } }
RoomPollsFilterType.ENDED -> emptyFlow()
}.map { it.sortedByDescending { poll -> poll.creationTimestamp } }
} }
private fun getActivePolls(): Flow<List<PollSummary.ActivePoll>> { private fun getActivePolls(): List<PollSummary.ActivePoll> {
return flowOf( return listOf(
listOf( PollSummary.ActivePoll(
PollSummary.ActivePoll( id = "id1",
id = "id1", // 2022/06/28 UTC+1
// 2022/06/28 UTC+1 creationTimestamp = 1656367200000,
creationTimestamp = 1656367200000, title = "Which charity would you like to support?"
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", PollSummary.EndedPoll(
// 2022/06/26 UTC+1 id = "id2-ended",
creationTimestamp = 1656194400000, // 2022/06/26 UTC+1
title = "Which sport should the pupils do this year?" 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", PollSummary.EndedPoll(
// 2022/06/24 UTC+1 id = "id3-ended",
creationTimestamp = 1656021600000, // 2022/06/24 UTC+1
title = "What type of food should we have at the party?" 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?"
),
)
) )
} }
} }

View File

@ -16,10 +16,24 @@
package im.vector.app.features.roomprofile.polls package im.vector.app.features.roomprofile.polls
import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState
sealed interface PollSummary { sealed interface PollSummary {
val id: String
val creationTimestamp: Long
val title: String
data class ActivePoll( data class ActivePoll(
val id: String, override val id: String,
val creationTimestamp: Long, override val creationTimestamp: Long,
val title: String, 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 ) : PollSummary
} }

View File

@ -18,6 +18,4 @@ package im.vector.app.features.roomprofile.polls
import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.core.platform.VectorViewModelAction
sealed interface RoomPollsAction : VectorViewModelAction { sealed interface RoomPollsAction : VectorViewModelAction
data class SetFilter(val filter: RoomPollsFilterType) : RoomPollsAction
}

View File

@ -66,7 +66,8 @@ class RoomPollsFragment : VectorBaseFragment<FragmentRoomPollsBinding>() {
tabLayoutMediator = TabLayoutMediator(views.roomPollsTabs, views.roomPollsViewPager) { tab, position -> tabLayoutMediator = TabLayoutMediator(views.roomPollsTabs, views.roomPollsViewPager) { tab, position ->
when (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() } }.also { it.attach() }
} }

View File

@ -19,15 +19,20 @@ package im.vector.app.features.roomprofile.polls
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import im.vector.app.features.roomprofile.polls.active.RoomActivePollsFragment import im.vector.app.features.roomprofile.polls.active.RoomActivePollsFragment
import im.vector.app.features.roomprofile.polls.ended.RoomEndedPollsFragment
class RoomPollsPagerAdapter( class RoomPollsPagerAdapter(
private val fragment: Fragment private val fragment: Fragment
) : FragmentStateAdapter(fragment) { ) : FragmentStateAdapter(fragment) {
override fun getItemCount() = 1 override fun getItemCount() = RoomPollsType.values().size
override fun createFragment(position: Int): Fragment { 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 { private fun instantiateFragment(fragmentName: String): Fragment {

View File

@ -16,7 +16,7 @@
package im.vector.app.features.roomprofile.polls package im.vector.app.features.roomprofile.polls
enum class RoomPollsFilterType { enum class RoomPollsType {
ACTIVE, ACTIVE,
ENDED, ENDED,
} }

View File

@ -16,7 +16,6 @@
package im.vector.app.features.roomprofile.polls package im.vector.app.features.roomprofile.polls
import androidx.annotation.VisibleForTesting
import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory 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.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -40,24 +38,17 @@ class RoomPollsViewModel @AssistedInject constructor(
companion object : MavericksViewModelFactory<RoomPollsViewModel, RoomPollsViewState> by hiltMavericksViewModelFactory() companion object : MavericksViewModelFactory<RoomPollsViewModel, RoomPollsViewState> by hiltMavericksViewModelFactory()
@VisibleForTesting init {
var pollsCollectionJob: Job? = null observePolls()
override fun handle(action: RoomPollsAction) {
when (action) {
is RoomPollsAction.SetFilter -> handleSetFilter(action.filter)
}
} }
override fun onCleared() { private fun observePolls() {
pollsCollectionJob = null getPollsUseCase.execute()
super.onCleared()
}
private fun handleSetFilter(filter: RoomPollsFilterType) {
pollsCollectionJob?.cancel()
pollsCollectionJob = getPollsUseCase.execute(filter)
.onEach { setState { copy(polls = it) } } .onEach { setState { copy(polls = it) } }
.launchIn(viewModelScope) .launchIn(viewModelScope)
} }
override fun handle(action: RoomPollsAction) {
// do nothing for now
}
} }

View File

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

View File

@ -16,77 +16,19 @@
package im.vector.app.features.roomprofile.polls.active 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 dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.cleanup import im.vector.app.features.roomprofile.polls.RoomPollsType
import im.vector.app.core.extensions.configureWith import im.vector.app.features.roomprofile.polls.list.RoomPollsListFragment
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
@AndroidEntryPoint @AndroidEntryPoint
class RoomActivePollsFragment : class RoomActivePollsFragment : RoomPollsListFragment() {
VectorBaseFragment<FragmentRoomPollsListBinding>(),
RoomActivePollsController.Listener {
@Inject override fun getEmptyListTitle(): String {
lateinit var roomActivePollsController: RoomActivePollsController return getString(R.string.room_polls_active_no_item)
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?) { override fun getRoomPollsType(): RoomPollsType {
super.onViewCreated(view, savedInstanceState) return RoomPollsType.ACTIVE
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")
} }
} }

View File

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

View File

@ -14,9 +14,11 @@
* limitations under the License. * 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 android.widget.TextView
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R 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.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.epoxy.onClick 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 @EpoxyModelClass
abstract class ActivePollItem : VectorEpoxyModel<ActivePollItem.Holder>(R.layout.item_poll) { abstract class RoomPollItem : VectorEpoxyModel<RoomPollItem.Holder>(R.layout.item_poll) {
@EpoxyAttribute @EpoxyAttribute
lateinit var formattedDate: String lateinit var formattedDate: String
@ -34,6 +39,12 @@ abstract class ActivePollItem : VectorEpoxyModel<ActivePollItem.Holder>(R.layout
@EpoxyAttribute @EpoxyAttribute
lateinit var title: String lateinit var title: String
@EpoxyAttribute
var winnerOptions: List<PollOptionViewState.PollEnded> = emptyList()
@EpoxyAttribute
var totalVotesStatus: String? = null
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
var clickListener: ClickListener? = null var clickListener: ClickListener? = null
@ -42,10 +53,20 @@ abstract class ActivePollItem : VectorEpoxyModel<ActivePollItem.Holder>(R.layout
holder.view.onClick(clickListener) holder.view.onClick(clickListener)
holder.date.text = formattedDate holder.date.text = formattedDate
holder.title.text = title 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() { class Holder : VectorEpoxyHolder() {
val date by bind<TextView>(R.id.pollActiveDate) val date by bind<TextView>(R.id.pollDate)
val title by bind<TextView>(R.id.pollActiveTitle) val title by bind<TextView>(R.id.pollTitle)
val winnerOptions by bind<LinearLayout>(R.id.pollWinnerOptionsContainer)
val totalVotes by bind<TextView>(R.id.pollTotalVotes)
} }
} }

View File

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

View File

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

View File

@ -7,7 +7,7 @@
android:foreground="?selectableItemBackground"> android:foreground="?selectableItemBackground">
<TextView <TextView
android:id="@+id/pollActiveDate" android:id="@+id/pollDate"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="32dp" android:layout_marginTop="32dp"
@ -18,19 +18,19 @@
tools:text="28/06/22" /> tools:text="28/06/22" />
<ImageView <ImageView
android:id="@+id/pollActiveIcon" android:id="@+id/pollIcon"
android:layout_width="16dp" android:layout_width="16dp"
android:layout_height="16dp" android:layout_height="16dp"
android:layout_marginTop="12dp" android:layout_marginTop="12dp"
android:importantForAccessibility="no" android:importantForAccessibility="no"
android:src="@drawable/ic_attachment_poll" android:src="@drawable/ic_attachment_poll"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/pollActiveDate" app:layout_constraintTop_toBottomOf="@id/pollDate"
app:tint="?vctr_content_secondary" app:tint="?vctr_content_secondary"
tools:ignore="ContentDescription" /> tools:ignore="ContentDescription" />
<TextView <TextView
android:id="@+id/pollActiveTitle" android:id="@+id/pollTitle"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
@ -38,8 +38,32 @@
android:textAppearance="@style/TextAppearance.Vector.Subtitle" android:textAppearance="@style/TextAppearance.Vector.Subtitle"
android:textColor="?vctr_content_primary" android:textColor="?vctr_content_primary"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/pollActiveIcon" app:layout_constraintStart_toEndOf="@id/pollIcon"
app:layout_constraintTop_toBottomOf="@id/pollActiveDate" app:layout_constraintTop_toBottomOf="@id/pollDate"
tools:text="Which sport should the pupils do this year?" /> 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> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -23,7 +23,6 @@ import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.verify import io.mockk.verify
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import org.amshove.kluent.shouldNotBeNull
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -45,28 +44,22 @@ class RoomPollsViewModelTest {
} }
@Test @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 // Given
val filter = RoomPollsFilterType.ACTIVE
val action = RoomPollsAction.SetFilter(filter = filter)
val polls = listOf(givenAPollSummary()) val polls = listOf(givenAPollSummary())
every { fakeGetPollsUseCase.execute(any()) } returns flowOf(polls) every { fakeGetPollsUseCase.execute() } returns flowOf(polls)
val viewModel = createViewModel()
val expectedViewState = initialState.copy(polls = polls) val expectedViewState = initialState.copy(polls = polls)
// When // When
val viewModel = createViewModel()
val viewModelTest = viewModel.test() val viewModelTest = viewModel.test()
viewModel.pollsCollectionJob = null
viewModel.handle(action)
// Then // Then
viewModelTest viewModelTest
.assertLatestState(expectedViewState) .assertLatestState(expectedViewState)
.finish() .finish()
viewModel.pollsCollectionJob.shouldNotBeNull()
verify { verify {
viewModel.pollsCollectionJob?.cancel() fakeGetPollsUseCase.execute()
fakeGetPollsUseCase.execute(filter)
} }
} }