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 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="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>
|
||||||
|
|
|
@ -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?"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
|
@ -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() }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
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")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
* 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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">
|
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>
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue