new app layout's room list overhaul (#7101)

This commit is contained in:
Nikita Fedrunov 2022-09-12 16:47:32 +02:00 committed by GitHub
parent e37344a059
commit da83a85f74
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 371 additions and 409 deletions

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.app.features.home.room.list.home.filter package im.vector.app.features.home.room.list.home
import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.paging.PagedListEpoxyController import com.airbnb.epoxy.paging.PagedListEpoxyController
@ -24,11 +24,11 @@ import im.vector.app.features.home.RoomListDisplayMode
import im.vector.app.features.home.room.list.RoomListListener import im.vector.app.features.home.room.list.RoomListListener
import im.vector.app.features.home.room.list.RoomSummaryItemFactory import im.vector.app.features.home.room.list.RoomSummaryItemFactory
import im.vector.app.features.home.room.list.RoomSummaryItemPlaceHolder_ import im.vector.app.features.home.room.list.RoomSummaryItemPlaceHolder_
import im.vector.app.features.home.room.list.home.roomListEmptyItem
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
import javax.inject.Inject
class HomeFilteredRoomsController( class HomeFilteredRoomsController @Inject constructor(
private val roomSummaryItemFactory: RoomSummaryItemFactory, private val roomSummaryItemFactory: RoomSummaryItemFactory,
) : PagedListEpoxyController<RoomSummary>( ) : PagedListEpoxyController<RoomSummary>(
// Important it must match the PageList builder notify Looper // Important it must match the PageList builder notify Looper
@ -43,22 +43,11 @@ class HomeFilteredRoomsController(
} }
var listener: RoomListListener? = null var listener: RoomListListener? = null
var onFilterChanged: ((HomeRoomFilter) -> Unit)? = null
private var filtersData: List<HomeRoomFilter>? = null
private var emptyStateData: StateView.State.Empty? = null private var emptyStateData: StateView.State.Empty? = null
private var currentState: StateView.State = StateView.State.Content private var currentState: StateView.State = StateView.State.Content
override fun addModels(models: List<EpoxyModel<*>>) { override fun addModels(models: List<EpoxyModel<*>>) {
val host = this
if (host.filtersData != null) {
roomFilterHeaderItem {
id("filter_header")
filtersData(host.filtersData)
onFilterChangedListener(host.onFilterChanged)
}
}
if (models.isEmpty() && emptyStateData != null) { if (models.isEmpty() && emptyStateData != null) {
emptyStateData?.let { emptyState -> emptyStateData?.let { emptyState ->
roomListEmptyItem { roomListEmptyItem {
@ -77,10 +66,6 @@ class HomeFilteredRoomsController(
this.emptyStateData = state this.emptyStateData = state
} }
fun submitFiltersData(data: List<HomeRoomFilter>?) {
this.filtersData = data
requestForcedModelBuild()
}
override fun buildItemModel(currentPosition: Int, item: RoomSummary?): EpoxyModel<*> { override fun buildItemModel(currentPosition: Int, item: RoomSummary?): EpoxyModel<*> {
item ?: return RoomSummaryItemPlaceHolder_().apply { id(currentPosition) } item ?: return RoomSummaryItemPlaceHolder_().apply { id(currentPosition) }
return roomSummaryItemFactory.create(item, roomChangeMembershipStates.orEmpty(), emptySet(), RoomListDisplayMode.ROOMS, listener) return roomSummaryItemFactory.create(item, roomChangeMembershipStates.orEmpty(), emptySet(), RoomListDisplayMode.ROOMS, listener)

View file

@ -17,7 +17,7 @@
package im.vector.app.features.home.room.list.home package im.vector.app.features.home.room.list.home
import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.core.platform.VectorViewModelAction
import im.vector.app.features.home.room.list.home.filter.HomeRoomFilter import im.vector.app.features.home.room.list.home.header.HomeRoomFilter
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState

View file

@ -25,7 +25,6 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyControllerAdapter
import com.airbnb.epoxy.OnModelBuildFinishedListener import com.airbnb.epoxy.OnModelBuildFinishedListener
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
@ -41,15 +40,12 @@ import im.vector.app.databinding.FragmentRoomListBinding
import im.vector.app.features.analytics.plan.ViewRoom import im.vector.app.features.analytics.plan.ViewRoom
import im.vector.app.features.home.room.list.RoomListAnimator import im.vector.app.features.home.room.list.RoomListAnimator
import im.vector.app.features.home.room.list.RoomListListener import im.vector.app.features.home.room.list.RoomListListener
import im.vector.app.features.home.room.list.RoomSummaryItemFactory
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomSheet import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomSheet
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedAction import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedAction
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel
import im.vector.app.features.home.room.list.home.filter.HomeFilteredRoomsController import im.vector.app.features.home.room.list.home.header.HomeRoomFilter
import im.vector.app.features.home.room.list.home.filter.HomeRoomFilter import im.vector.app.features.home.room.list.home.header.HomeRoomsHeadersController
import im.vector.app.features.home.room.list.home.invites.InvitesActivity import im.vector.app.features.home.room.list.home.invites.InvitesActivity
import im.vector.app.features.home.room.list.home.invites.InvitesCounterController
import im.vector.app.features.home.room.list.home.recent.RecentRoomCarouselController
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
@ -63,10 +59,9 @@ class HomeRoomListFragment :
VectorBaseFragment<FragmentRoomListBinding>(), VectorBaseFragment<FragmentRoomListBinding>(),
RoomListListener { RoomListListener {
@Inject lateinit var roomSummaryItemFactory: RoomSummaryItemFactory
@Inject lateinit var userPreferencesProvider: UserPreferencesProvider @Inject lateinit var userPreferencesProvider: UserPreferencesProvider
@Inject lateinit var recentRoomCarouselController: RecentRoomCarouselController @Inject lateinit var headersController: HomeRoomsHeadersController
@Inject lateinit var invitesCounterController: InvitesCounterController @Inject lateinit var roomsController: HomeFilteredRoomsController
private val roomListViewModel: HomeRoomListViewModel by fragmentViewModel() private val roomListViewModel: HomeRoomListViewModel by fragmentViewModel()
private lateinit var sharedQuickActionsViewModel: RoomListQuickActionsSharedActionViewModel private lateinit var sharedQuickActionsViewModel: RoomListQuickActionsSharedActionViewModel
@ -143,10 +138,25 @@ class HomeRoomListFragment :
modelBuildListener = OnModelBuildFinishedListener { it.dispatchTo(stateRestorer) } modelBuildListener = OnModelBuildFinishedListener { it.dispatchTo(stateRestorer) }
roomListViewModel.sections.onEach { sections -> roomListViewModel.onEach(HomeRoomListViewState::headersData) {
setUpAdapters(sections) headersController.submitData(it)
}
roomListViewModel.onEach(HomeRoomListViewState::roomsLivePagedList) { roomsListLive ->
roomsListLive?.observe(viewLifecycleOwner) { roomsList ->
roomsController.submitList(roomsList)
if (roomsList.isEmpty()) {
roomsController.requestForcedModelBuild()
}
}
}
roomListViewModel.emptyStateFlow.onEach { emptyStateOptional ->
roomsController.submitEmptyStateData(emptyStateOptional.getOrNull())
}.launchIn(lifecycleScope) }.launchIn(lifecycleScope)
setUpAdapters()
views.roomListView.adapter = concatAdapter views.roomListView.adapter = concatAdapter
// we need to force scroll when recents/filter tabs are added to make them visible // we need to force scroll when recents/filter tabs are added to make them visible
@ -163,13 +173,20 @@ class HomeRoomListFragment :
views.stateView.state = state.state views.stateView.state = state.state
} }
private fun setUpAdapters(sections: Set<HomeRoomSection>) { private fun setUpAdapters() {
concatAdapter.adapters.forEach { val headersAdapter = headersController.also { controller ->
concatAdapter.removeAdapter(it) controller.invitesClickListener = ::onInvitesCounterClicked
} controller.onFilterChangedListener = ::onRoomFilterChanged
sections.forEach { controller.recentsRoomListener = this
concatAdapter.addAdapter(getAdapterForData(it)) }.adapter
}
val roomsAdapter = roomsController
.also { controller ->
controller.listener = this
}.adapter
concatAdapter.addAdapter(headersAdapter)
concatAdapter.addAdapter(roomsAdapter)
} }
private fun promptLeaveRoom(roomId: String) { private fun promptLeaveRoom(roomId: String) {
@ -191,43 +208,6 @@ class HomeRoomListFragment :
.show() .show()
} }
private fun getAdapterForData(section: HomeRoomSection): EpoxyControllerAdapter {
return when (section) {
is HomeRoomSection.RoomSummaryData -> {
HomeFilteredRoomsController(
roomSummaryItemFactory,
).also { controller ->
controller.listener = this
controller.onFilterChanged = ::onRoomFilterChanged
roomListViewModel.emptyStateFlow.onEach { emptyStateOptional ->
controller.submitEmptyStateData(emptyStateOptional.getOrNull())
}.launchIn(lifecycleScope)
section.filtersData.onEach {
controller.submitFiltersData(it.getOrNull())
}.launchIn(lifecycleScope)
section.list.observe(viewLifecycleOwner) { list ->
controller.submitList(list)
if (list.isEmpty()) {
controller.requestForcedModelBuild()
}
}
}.adapter
}
is HomeRoomSection.RecentRoomsData -> recentRoomCarouselController.also { controller ->
controller.listener = this
section.list.observe(viewLifecycleOwner) { list ->
controller.submitList(list)
}
}.adapter
is HomeRoomSection.InvitesCountData -> invitesCounterController.also { controller ->
controller.clickListener = ::onInvitesCounterClicked
section.count.observe(viewLifecycleOwner) { count ->
controller.submitData(count)
}
}.adapter
}
}
private fun onInvitesCounterClicked() { private fun onInvitesCounterClicked() {
startActivity(Intent(activity, InvitesActivity::class.java)) startActivity(Intent(activity, InvitesActivity::class.java))
} }
@ -247,8 +227,13 @@ class HomeRoomListFragment :
override fun onDestroyView() { override fun onDestroyView() {
views.roomListView.cleanup() views.roomListView.cleanup()
recentRoomCarouselController.listener = null
invitesCounterController.clickListener = null headersController.recentsRoomListener = null
headersController.invitesClickListener = null
headersController.onFilterChangedListener = null
roomsController.listener = null
super.onDestroyView() super.onDestroyView()
} }
@ -266,21 +251,13 @@ class HomeRoomListFragment :
return true return true
} }
override fun onRejectRoomInvitation(room: RoomSummary) { override fun onRejectRoomInvitation(room: RoomSummary) = Unit
TODO("Not yet implemented")
}
override fun onAcceptRoomInvitation(room: RoomSummary) { override fun onAcceptRoomInvitation(room: RoomSummary) = Unit
TODO("Not yet implemented")
}
override fun onJoinSuggestedRoom(room: SpaceChildInfo) { override fun onJoinSuggestedRoom(room: SpaceChildInfo) = Unit
TODO("Not yet implemented")
}
override fun onSuggestedRoomClicked(room: SpaceChildInfo) { override fun onSuggestedRoomClicked(room: SpaceChildInfo) = Unit
TODO("Not yet implemented")
}
// endregion // endregion
} }

View file

@ -17,8 +17,8 @@
package im.vector.app.features.home.room.list.home package im.vector.app.features.home.room.list.home
import android.widget.ImageView import android.widget.ImageView
import androidx.lifecycle.map
import androidx.paging.PagedList import androidx.paging.PagedList
import arrow.core.Option
import arrow.core.toOption import arrow.core.toOption
import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted import dagger.assisted.Assisted
@ -32,22 +32,22 @@ import im.vector.app.core.platform.StateView
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.DrawableProvider import im.vector.app.core.resources.DrawableProvider
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.room.list.home.filter.HomeRoomFilter import im.vector.app.features.home.room.list.home.header.HomeRoomFilter
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.query.RoomCategoryFilter import org.matrix.android.sdk.api.query.RoomCategoryFilter
import org.matrix.android.sdk.api.query.RoomTagQueryFilter import org.matrix.android.sdk.api.query.RoomTagQueryFilter
import org.matrix.android.sdk.api.query.toActiveSpaceOrNoFilter import org.matrix.android.sdk.api.query.toActiveSpaceOrNoFilter
@ -80,6 +80,7 @@ class HomeRoomListViewModel @AssistedInject constructor(
companion object : MavericksViewModelFactory<HomeRoomListViewModel, HomeRoomListViewState> by hiltMavericksViewModelFactory() companion object : MavericksViewModelFactory<HomeRoomListViewModel, HomeRoomListViewState> by hiltMavericksViewModelFactory()
private var roomsFlow: Flow<Option<RoomSummary>>? = null
private val pagedListConfig = PagedList.Config.Builder() private val pagedListConfig = PagedList.Config.Builder()
.setPageSize(10) .setPageSize(10)
.setInitialLoadSizeHint(20) .setInitialLoadSizeHint(20)
@ -87,9 +88,6 @@ class HomeRoomListViewModel @AssistedInject constructor(
.setPrefetchDistance(10) .setPrefetchDistance(10)
.build() .build()
private val _sections = MutableSharedFlow<Set<HomeRoomSection>>(replay = 1)
val sections = _sections.asSharedFlow()
private var currentFilter: HomeRoomFilter = HomeRoomFilter.ALL private var currentFilter: HomeRoomFilter = HomeRoomFilter.ALL
private val _emptyStateFlow = MutableSharedFlow<Optional<StateView.State.Empty>>(replay = 1) private val _emptyStateFlow = MutableSharedFlow<Optional<StateView.State.Empty>>(replay = 1)
val emptyStateFlow = _emptyStateFlow.asSharedFlow() val emptyStateFlow = _emptyStateFlow.asSharedFlow()
@ -97,118 +95,77 @@ class HomeRoomListViewModel @AssistedInject constructor(
private var filteredPagedRoomSummariesLive: UpdatableLivePageResult? = null private var filteredPagedRoomSummariesLive: UpdatableLivePageResult? = null
init { init {
configureSections() observeOrderPreferences()
observePreferences() observeInvites()
observeRecents()
observeFilterTabs()
observeRooms()
} }
private fun observePreferences() { private fun observeInvites() {
preferencesStore.areRecentsEnabledFlow.onEach { session.flow()
configureSections() .liveRoomSummaries(
}.launchIn(viewModelScope) roomSummaryQueryParams {
memberships = listOf(Membership.INVITE)
preferencesStore.isAZOrderingEnabledFlow.onEach { },
configureSections() RoomSortOrder.ACTIVITY
}.launchIn(viewModelScope) ).onEach { list ->
} setState { copy(headersData = headersData.copy(invitesCount = list.size)) }
private fun configureSections() = viewModelScope.launch {
val newSections = mutableSetOf<HomeRoomSection>()
newSections.add(getInvitesCountSection())
val areSettingsEnabled = preferencesStore.areRecentsEnabledFlow.first()
if (areSettingsEnabled) {
newSections.add(getRecentRoomsSection())
}
newSections.add(getFilteredRoomsSection())
emitEmptyState()
_sections.emit(newSections)
setState {
copy(state = StateView.State.Content)
}
}
private fun getRecentRoomsSection(): HomeRoomSection {
val liveList = session.roomService()
.getBreadcrumbsLive(roomSummaryQueryParams {
displayName = QueryStringValue.NoCondition
memberships = listOf(Membership.JOIN)
})
return HomeRoomSection.RecentRoomsData(
list = liveList
)
}
private fun getInvitesCountSection(): HomeRoomSection.InvitesCountData {
val builder = RoomSummaryQueryParams.Builder().also {
it.memberships = listOf(Membership.INVITE)
}
val liveCount = session.roomService().getRoomSummariesLive(
builder.build(),
RoomSortOrder.ACTIVITY
).map { it.count() }
return HomeRoomSection.InvitesCountData(liveCount)
}
private suspend fun getFilteredRoomsSection(): HomeRoomSection.RoomSummaryData {
val builder = RoomSummaryQueryParams.Builder().also {
it.memberships = listOf(Membership.JOIN)
}
val params = getFilteredQueryParams(HomeRoomFilter.ALL, builder.build())
val sortOrder = if (preferencesStore.isAZOrderingEnabledFlow.first()) {
RoomSortOrder.NAME
} else {
RoomSortOrder.ACTIVITY
}
val liveResults = session.roomService().getFilteredPagedRoomSummariesLive(
params,
pagedListConfig,
sortOrder
).also {
this.filteredPagedRoomSummariesLive = it
}
spaceStateHandler.getSelectedSpaceFlow()
.distinctUntilChanged()
.onStart {
emit(spaceStateHandler.getCurrentSpace().toOption())
}
.onEach { selectedSpaceOption ->
val selectedSpace = selectedSpaceOption.orNull()
liveResults.queryParams = liveResults.queryParams.copy(
spaceFilter = selectedSpace?.roomId.toActiveSpaceOrNoFilter()
)
emitEmptyState()
}.launchIn(viewModelScope) }.launchIn(viewModelScope)
return HomeRoomSection.RoomSummaryData(
list = liveResults.livePagedList,
filtersData = getFiltersDataFlow()
)
} }
private fun emitEmptyState() { private fun observeRecents() {
viewModelScope.launch { preferencesStore.areRecentsEnabledFlow
val emptyState = getEmptyStateData(currentFilter, spaceStateHandler.getCurrentSpace()) .distinctUntilChanged()
_emptyStateFlow.emit(Optional.from(emptyState)) .flatMapLatest { areEnabled ->
if (areEnabled) {
session.flow()
.liveBreadcrumbs(roomSummaryQueryParams {
memberships = listOf(Membership.JOIN)
})
.map { Optional.from(it) }
} else {
flow { emit(Optional.empty()) }
}.onEach { listOptional ->
setState { copy(headersData = headersData.copy(recents = listOptional.getOrNull())) }
}
}.launchIn(viewModelScope)
}
private fun observeFilterTabs() {
preferencesStore.areFiltersEnabledFlow
.distinctUntilChanged()
.flatMapLatest { areEnabled ->
if (areEnabled) {
getFilterTabsFlow()
} else {
flow { emit(Optional.empty()) }
}.onEach { filtersOptional ->
setState {
validateCurrentFilter(filtersOptional.getOrNull())
copy(
headersData = headersData.copy(
filtersList = filtersOptional.getOrNull(),
currentFilter = currentFilter
)
)
}
}
}.launchIn(viewModelScope)
}
private fun validateCurrentFilter(filtersList: List<HomeRoomFilter>?) {
if (filtersList?.contains(currentFilter) != true) {
handleChangeRoomFilter(HomeRoomFilter.ALL)
} }
} }
private fun getFiltersDataFlow(): SharedFlow<Optional<List<HomeRoomFilter>>> { private fun getFilterTabsFlow(): Flow<Optional<MutableList<HomeRoomFilter>>> {
val flow = MutableSharedFlow<Optional<List<HomeRoomFilter>>>(replay = 1)
val spaceFLow = spaceStateHandler.getSelectedSpaceFlow() val spaceFLow = spaceStateHandler.getSelectedSpaceFlow()
.distinctUntilChanged() .distinctUntilChanged()
.onStart { .onStart {
emit(spaceStateHandler.getCurrentSpace().toOption()) emit(spaceStateHandler.getCurrentSpace().toOption())
} }
val favouritesFlow = val favouritesFlow =
spaceFLow.flatMapLatest { selectedSpace -> spaceFLow.flatMapLatest { selectedSpace ->
session.flow() session.flow()
@ -236,31 +193,76 @@ class HomeRoomListViewModel @AssistedInject constructor(
.map { it.isNotEmpty() } .map { it.isNotEmpty() }
.distinctUntilChanged() .distinctUntilChanged()
combine(favouritesFlow, dmsFLow, preferencesStore.areFiltersEnabledFlow) { hasFavourite, hasDm, areFiltersEnabled -> return combine(favouritesFlow, dmsFLow) { hasFavourite, hasDm ->
Triple(hasFavourite, hasDm, areFiltersEnabled) hasFavourite to hasDm
}.onEach { (hasFavourite, hasDm, areFiltersEnabled) -> }.map { (hasFavourite, hasDm) ->
if (areFiltersEnabled) { val filtersData = mutableListOf(
val filtersData = mutableListOf( HomeRoomFilter.ALL,
HomeRoomFilter.ALL, HomeRoomFilter.UNREADS
HomeRoomFilter.UNREADS )
if (hasFavourite) {
filtersData.add(
HomeRoomFilter.FAVOURITES
) )
if (hasFavourite) {
filtersData.add(
HomeRoomFilter.FAVOURITES
)
}
if (hasDm) {
filtersData.add(
HomeRoomFilter.PEOPlE
)
}
flow.emit(Optional.from(filtersData))
} else {
flow.emit(Optional.empty())
} }
}.launchIn(viewModelScope) if (hasDm) {
filtersData.add(
HomeRoomFilter.PEOPlE
)
}
Optional.from(filtersData)
}
}
return flow private fun observeRooms() = viewModelScope.launch {
val builder = RoomSummaryQueryParams.Builder().also {
it.memberships = listOf(Membership.JOIN)
}
val params = getFilteredQueryParams(currentFilter, builder.build())
val sortOrder = if (preferencesStore.isAZOrderingEnabledFlow.first()) {
RoomSortOrder.NAME
} else {
RoomSortOrder.ACTIVITY
}
val liveResults = session.roomService().getFilteredPagedRoomSummariesLive(
params,
pagedListConfig,
sortOrder
).also {
filteredPagedRoomSummariesLive = it
}
spaceStateHandler.getSelectedSpaceFlow()
.distinctUntilChanged()
.onStart {
emit(spaceStateHandler.getCurrentSpace().toOption())
}
.onEach { selectedSpaceOption ->
val selectedSpace = selectedSpaceOption.orNull()
liveResults.queryParams = liveResults.queryParams.copy(
spaceFilter = selectedSpace?.roomId.toActiveSpaceOrNoFilter()
)
emitEmptyState()
}
.also { roomsFlow = it }
.launchIn(viewModelScope)
setState { copy(roomsLivePagedList = liveResults.livePagedList) }
}
private fun observeOrderPreferences() {
preferencesStore.isAZOrderingEnabledFlow.onEach {
observeRooms()
}.launchIn(viewModelScope)
}
private fun emitEmptyState() {
viewModelScope.launch {
val emptyState = getEmptyStateData(currentFilter, spaceStateHandler.getCurrentSpace())
_emptyStateFlow.emit(Optional.from(emptyState))
}
} }
private fun getFilteredQueryParams(filter: HomeRoomFilter, currentParams: RoomSummaryQueryParams): RoomSummaryQueryParams { private fun getFilteredQueryParams(filter: HomeRoomFilter, currentParams: RoomSummaryQueryParams): RoomSummaryQueryParams {
@ -323,16 +325,20 @@ class HomeRoomListViewModel @AssistedInject constructor(
is HomeRoomListAction.LeaveRoom -> handleLeaveRoom(action) is HomeRoomListAction.LeaveRoom -> handleLeaveRoom(action)
is HomeRoomListAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action) is HomeRoomListAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action)
is HomeRoomListAction.ToggleTag -> handleToggleTag(action) is HomeRoomListAction.ToggleTag -> handleToggleTag(action)
is HomeRoomListAction.ChangeRoomFilter -> handleChangeRoomFilter(action) is HomeRoomListAction.ChangeRoomFilter -> handleChangeRoomFilter(action.filter)
} }
} }
private fun handleChangeRoomFilter(action: HomeRoomListAction.ChangeRoomFilter) { private fun handleChangeRoomFilter(newFilter: HomeRoomFilter) {
currentFilter = action.filter if (currentFilter == newFilter) {
return
}
currentFilter = newFilter
filteredPagedRoomSummariesLive?.let { liveResults -> filteredPagedRoomSummariesLive?.let { liveResults ->
liveResults.queryParams = getFilteredQueryParams(action.filter, liveResults.queryParams) liveResults.queryParams = getFilteredQueryParams(currentFilter, liveResults.queryParams)
} }
setState { copy(headersData = headersData.copy(currentFilter = currentFilter)) }
emitEmptyState() emitEmptyState()
} }

View file

@ -16,9 +16,15 @@
package im.vector.app.features.home.room.list.home package im.vector.app.features.home.room.list.home
import androidx.lifecycle.LiveData
import androidx.paging.PagedList
import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.MavericksState
import im.vector.app.core.platform.StateView import im.vector.app.core.platform.StateView
import im.vector.app.features.home.room.list.home.header.RoomsHeadersData
import org.matrix.android.sdk.api.session.room.model.RoomSummary
data class HomeRoomListViewState( data class HomeRoomListViewState(
val state: StateView.State = StateView.State.Loading val state: StateView.State = StateView.State.Content,
val headersData: RoomsHeadersData = RoomsHeadersData(),
val roomsLivePagedList: LiveData<PagedList<RoomSummary>>? = null
) : MavericksState ) : MavericksState

View file

@ -1,39 +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.home.room.list.home
import androidx.lifecycle.LiveData
import androidx.paging.PagedList
import im.vector.app.features.home.room.list.home.filter.HomeRoomFilter
import kotlinx.coroutines.flow.SharedFlow
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.util.Optional
sealed class HomeRoomSection {
data class RoomSummaryData(
val list: LiveData<PagedList<RoomSummary>>,
val filtersData: SharedFlow<Optional<List<HomeRoomFilter>>>,
) : HomeRoomSection()
data class RecentRoomsData(
val list: LiveData<List<RoomSummary>>
) : HomeRoomSection()
data class InvitesCountData(
val count: LiveData<Int>
) : HomeRoomSection()
}

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.app.features.home.room.list.home.filter package im.vector.app.features.home.room.list.home.header
import androidx.annotation.StringRes import androidx.annotation.StringRes
import im.vector.app.R import im.vector.app.R

View file

@ -0,0 +1,138 @@
/*
* 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.home.room.list.home.header
import android.content.res.Resources
import android.util.TypedValue
import com.airbnb.epoxy.Carousel
import com.airbnb.epoxy.CarouselModelBuilder
import com.airbnb.epoxy.EpoxyController
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.carousel
import com.google.android.material.color.MaterialColors
import im.vector.app.R
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.list.RoomListListener
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
class HomeRoomsHeadersController @Inject constructor(
val stringProvider: StringProvider,
private val avatarRenderer: AvatarRenderer,
resources: Resources,
) : EpoxyController() {
private var data: RoomsHeadersData = RoomsHeadersData()
var onFilterChangedListener: ((HomeRoomFilter) -> Unit)? = null
var recentsRoomListener: RoomListListener? = null
var invitesClickListener: (() -> Unit)? = null
private val recentsHPadding = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
4f,
resources.displayMetrics
).toInt()
private val recentsTopPadding = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
12f,
resources.displayMetrics
).toInt()
override fun buildModels() {
val host = this
if (data.invitesCount != 0) {
addInviteCounter(host.invitesClickListener, data.invitesCount)
}
data.recents?.let {
addRecents(host, it)
}
host.data.filtersList?.let {
addRoomFilterHeaderItem(host.onFilterChangedListener, it, host.data.currentFilter)
}
}
private fun addInviteCounter(invitesClickListener: (() -> Unit)?, invitesCount: Int) {
inviteCounterItem {
id("invites_counter")
invitesCount(invitesCount)
listener { invitesClickListener?.invoke() }
}
}
private fun addRecents(host: HomeRoomsHeadersController, recents: List<RoomSummary>) {
carousel {
id("recents_carousel")
padding(
Carousel.Padding(
host.recentsHPadding,
host.recentsTopPadding,
host.recentsHPadding,
0,
0,
)
)
onBind { _, view, _ ->
val colorSurface = MaterialColors.getColor(view, R.attr.vctr_toolbar_background)
view.setBackgroundColor(colorSurface)
}
withModelsFrom(recents) { roomSummary ->
val onClick = host.recentsRoomListener?.let { it::onRoomClicked }
val onLongClick = host.recentsRoomListener?.let { it::onRoomLongClicked }
RecentRoomItem_()
.id(roomSummary.roomId)
.avatarRenderer(host.avatarRenderer)
.matrixItem(roomSummary.toMatrixItem())
.unreadNotificationCount(roomSummary.notificationCount)
.showHighlighted(roomSummary.highlightCount > 0)
.itemLongClickListener { _ -> onLongClick?.invoke(roomSummary) ?: false }
.itemClickListener { onClick?.invoke(roomSummary) }
}
}
}
private fun addRoomFilterHeaderItem(
filterChangedListener: ((HomeRoomFilter) -> Unit)?,
filtersList: List<HomeRoomFilter>,
currentFilter: HomeRoomFilter?,
) {
roomFilterHeaderItem {
id("filter_header")
filtersData(filtersList)
selectedFilter(currentFilter)
onFilterChangedListener(filterChangedListener)
}
}
fun submitData(data: RoomsHeadersData) {
this.data = data
requestModelBuild()
}
}
private inline fun <T> CarouselModelBuilder.withModelsFrom(
items: List<T>,
modelBuilder: (T) -> EpoxyModel<*>
) {
models(items.map { modelBuilder(it) })
}

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.app.features.home.room.list.home.invites package im.vector.app.features.home.room.list.home.header
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.app.features.home.room.list.home.recent package im.vector.app.features.home.room.list.home.header
import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants
import android.view.View import android.view.View

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.app.features.home.room.list.home.filter package im.vector.app.features.home.room.list.home.header
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
@ -32,13 +32,20 @@ abstract class RoomFilterHeaderItem : VectorEpoxyModel<RoomFilterHeaderItem.Hold
@EpoxyAttribute @EpoxyAttribute
var filtersData: List<HomeRoomFilter>? = null var filtersData: List<HomeRoomFilter>? = null
@EpoxyAttribute
var selectedFilter: HomeRoomFilter? = null
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
with(holder.tabLayout) { with(holder.tabLayout) {
removeAllTabs() removeAllTabs()
clearOnTabSelectedListeners()
filtersData?.forEach { filter -> filtersData?.forEach { filter ->
addTab(newTab().setText(filter.titleRes).setTag(filter)) addTab(
newTab().setText(filter.titleRes).setTag(filter),
filter == (selectedFilter ?: HomeRoomFilter.ALL)
)
} }
addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {

View file

@ -0,0 +1,26 @@
/*
* 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.home.room.list.home.header
import org.matrix.android.sdk.api.session.room.model.RoomSummary
data class RoomsHeadersData(
val invitesCount: Int = 0,
val filtersList: List<HomeRoomFilter>? = null,
val currentFilter: HomeRoomFilter? = null,
val recents: List<RoomSummary>? = null
)

View file

@ -1,45 +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.home.room.list.home.invites
import com.airbnb.epoxy.EpoxyController
import im.vector.app.core.resources.StringProvider
import javax.inject.Inject
class InvitesCounterController @Inject constructor(
val stringProvider: StringProvider
) : EpoxyController() {
private var count = 0
var clickListener: (() -> Unit)? = null
override fun buildModels() {
val host = this
if (count != 0) {
inviteCounterItem {
id("invites_counter")
invitesCount(host.count)
listener { host.clickListener?.invoke() }
}
}
}
fun submitData(count: Int?) {
this.count = count ?: 0
requestModelBuild()
}
}

View file

@ -1,99 +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.home.room.list.home.recent
import android.content.res.Resources
import android.util.TypedValue
import com.airbnb.epoxy.Carousel
import com.airbnb.epoxy.CarouselModelBuilder
import com.airbnb.epoxy.EpoxyController
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.carousel
import com.google.android.material.color.MaterialColors
import im.vector.app.R
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.list.RoomListListener
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
class RecentRoomCarouselController @Inject constructor(
private val avatarRenderer: AvatarRenderer,
private val resources: Resources,
) : EpoxyController() {
private var data: List<RoomSummary>? = null
var listener: RoomListListener? = null
private val hPadding = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
4f,
resources.displayMetrics
).toInt()
private val topPadding = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
12f,
resources.displayMetrics
).toInt()
fun submitList(recentList: List<RoomSummary>) {
this.data = recentList
requestModelBuild()
}
override fun buildModels() {
val host = this
data?.let { data ->
carousel {
id("recents_carousel")
padding(Carousel.Padding(
host.hPadding,
host.topPadding,
host.hPadding,
0,
0,
)
)
onBind { _, view, _ ->
val colorSurface = MaterialColors.getColor(view, R.attr.vctr_toolbar_background)
view.setBackgroundColor(colorSurface)
}
withModelsFrom(data) { roomSummary ->
val onClick = host.listener?.let { it::onRoomClicked }
val onLongClick = host.listener?.let { it::onRoomLongClicked }
RecentRoomItem_()
.id(roomSummary.roomId)
.avatarRenderer(host.avatarRenderer)
.matrixItem(roomSummary.toMatrixItem())
.unreadNotificationCount(roomSummary.notificationCount)
.showHighlighted(roomSummary.highlightCount > 0)
.itemLongClickListener { _ -> onLongClick?.invoke(roomSummary) ?: false }
.itemClickListener { onClick?.invoke(roomSummary) }
}
}
}
}
}
private inline fun <T> CarouselModelBuilder.withModelsFrom(
items: List<T>,
modelBuilder: (T) -> EpoxyModel<*>
) {
models(items.map { modelBuilder(it) })
}