Mutualizing list fragments and add ended polls tab

This commit is contained in:
Maxime NATUREL 2022-12-30 17:28:57 +01:00
parent 0b535910d6
commit cb45056c1a
12 changed files with 190 additions and 106 deletions

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

@ -17,19 +17,16 @@
package im.vector.app.features.roomprofile.polls package im.vector.app.features.roomprofile.polls
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 getActivePolls()
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(): Flow<List<PollSummary.ActivePoll>> {

View File

@ -22,4 +22,10 @@ sealed interface PollSummary {
val creationTimestamp: Long, val creationTimestamp: Long,
val title: String, val title: String,
) : PollSummary ) : PollSummary
data class EndedPoll(
val id: String,
val creationTimestamp: Long,
val title: String,
) : 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

@ -67,6 +67,7 @@ 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) 0 -> tab.text = getString(R.string.room_polls_active)
1 -> tab.text = getString(R.string.room_polls_ended)
} }
}.also { it.attach() } }.also { it.attach() }
} }

View File

@ -19,15 +19,19 @@ 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() = 2
override fun createFragment(position: Int): Fragment { override fun createFragment(position: Int): Fragment {
return instantiateFragment(RoomActivePollsFragment::class.java.name) return when (position) {
0 -> instantiateFragment(RoomActivePollsFragment::class.java.name)
else -> instantiateFragment(RoomEndedPollsFragment::class.java.name)
}
} }
private fun instantiateFragment(fragmentName: String): Fragment { private fun instantiateFragment(fragmentName: String): Fragment {

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

@ -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.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.RoomPollsFilterType
import im.vector.app.features.roomprofile.polls.RoomPollsViewModel import im.vector.app.features.roomprofile.polls.list.RoomPollsListFragment
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 getRoomPollsFilter(): RoomPollsFilterType {
super.onViewCreated(view, savedInstanceState) return RoomPollsFilterType.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.RoomPollsFilter
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 getRoomPollsFilter(): RoomPollsFilter {
return RoomPollsFilter.ENDED
}
}

View File

@ -14,7 +14,7 @@
* 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.TextView import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
@ -26,7 +26,7 @@ import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.epoxy.onClick import im.vector.app.core.epoxy.onClick
@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

View File

@ -14,7 +14,7 @@
* 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 com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.DateFormatKind
@ -22,9 +22,9 @@ import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.features.roomprofile.polls.PollSummary import im.vector.app.features.roomprofile.polls.PollSummary
import javax.inject.Inject import javax.inject.Inject
class RoomActivePollsController @Inject constructor( class RoomPollsController @Inject constructor(
val dateFormatter: VectorDateFormatter, val dateFormatter: VectorDateFormatter,
) : TypedEpoxyController<List<PollSummary.ActivePoll>>() { ) : TypedEpoxyController<List<PollSummary>>() {
interface Listener { interface Listener {
fun onPollClicked(pollId: String) fun onPollClicked(pollId: String)
@ -32,20 +32,39 @@ class RoomActivePollsController @Inject constructor(
var listener: Listener? = null var listener: Listener? = null
override fun buildModels(data: List<PollSummary.ActivePoll>?) { override fun buildModels(data: List<PollSummary>?) {
if (data.isNullOrEmpty()) { if (data.isNullOrEmpty()) {
return return
} }
val host = this
for (poll in data) { for (poll in data) {
activePollItem { when (poll) {
id(poll.id) is PollSummary.ActivePoll -> buildActivePollItem(poll)
formattedDate(host.dateFormatter.format(poll.creationTimestamp, DateFormatKind.TIMELINE_DAY_DIVIDER)) is PollSummary.EndedPoll -> buildEndedPollItem(poll)
title(poll.title) }
clickListener { }
host.listener?.onPollClicked(poll.id) }
}
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)
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.RoomPollsFilter
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 getRoomPollsFilter(): RoomPollsFilter
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 (getRoomPollsFilter()) {
RoomPollsFilter.ACTIVE -> renderList(viewState.polls.filterIsInstance(PollSummary.ActivePoll::class.java))
RoomPollsFilter.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")
}
}