From da83a85f74ae05581173ee724abcd3249d9f2a50 Mon Sep 17 00:00:00 2001 From: Nikita Fedrunov <66663241+fedrunov@users.noreply.github.com> Date: Mon, 12 Sep 2022 16:47:32 +0200 Subject: [PATCH] new app layout's room list overhaul (#7101) --- .../HomeFilteredRoomsController.kt | 21 +- .../home/room/list/home/HomeRoomListAction.kt | 2 +- .../room/list/home/HomeRoomListFragment.kt | 115 +++----- .../room/list/home/HomeRoomListViewModel.kt | 270 +++++++++--------- .../room/list/home/HomeRoomListViewState.kt | 8 +- .../home/room/list/home/HomeRoomSection.kt | 39 --- .../home/{filter => header}/HomeRoomFilter.kt | 2 +- .../home/header/HomeRoomsHeadersController.kt | 138 +++++++++ .../{invites => header}/InviteCounterItem.kt | 2 +- .../home/{recent => header}/RecentRoomItem.kt | 2 +- .../RoomFilterHeaderItem.kt | 11 +- .../room/list/home/header/RoomsHeadersData.kt | 26 ++ .../home/invites/InvitesCounterController.kt | 45 --- .../recent/RecentRoomCarouselController.kt | 99 ------- 14 files changed, 371 insertions(+), 409 deletions(-) rename vector/src/main/java/im/vector/app/features/home/room/list/home/{filter => }/HomeFilteredRoomsController.kt (80%) delete mode 100644 vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomSection.kt rename vector/src/main/java/im/vector/app/features/home/room/list/home/{filter => header}/HomeRoomFilter.kt (94%) create mode 100644 vector/src/main/java/im/vector/app/features/home/room/list/home/header/HomeRoomsHeadersController.kt rename vector/src/main/java/im/vector/app/features/home/room/list/home/{invites => header}/InviteCounterItem.kt (96%) rename vector/src/main/java/im/vector/app/features/home/room/list/home/{recent => header}/RecentRoomItem.kt (98%) rename vector/src/main/java/im/vector/app/features/home/room/list/home/{filter => header}/RoomFilterHeaderItem.kt (85%) create mode 100644 vector/src/main/java/im/vector/app/features/home/room/list/home/header/RoomsHeadersData.kt delete mode 100644 vector/src/main/java/im/vector/app/features/home/room/list/home/invites/InvitesCounterController.kt delete mode 100644 vector/src/main/java/im/vector/app/features/home/room/list/home/recent/RecentRoomCarouselController.kt diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/filter/HomeFilteredRoomsController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeFilteredRoomsController.kt similarity index 80% rename from vector/src/main/java/im/vector/app/features/home/room/list/home/filter/HomeFilteredRoomsController.kt rename to vector/src/main/java/im/vector/app/features/home/room/list/home/HomeFilteredRoomsController.kt index 789c9e9985..ae0f9d328f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/filter/HomeFilteredRoomsController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeFilteredRoomsController.kt @@ -14,7 +14,7 @@ * 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.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.RoomSummaryItemFactory 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.model.RoomSummary +import javax.inject.Inject -class HomeFilteredRoomsController( +class HomeFilteredRoomsController @Inject constructor( private val roomSummaryItemFactory: RoomSummaryItemFactory, ) : PagedListEpoxyController( // Important it must match the PageList builder notify Looper @@ -43,22 +43,11 @@ class HomeFilteredRoomsController( } var listener: RoomListListener? = null - var onFilterChanged: ((HomeRoomFilter) -> Unit)? = null - private var filtersData: List? = null private var emptyStateData: StateView.State.Empty? = null private var currentState: StateView.State = StateView.State.Content override fun addModels(models: List>) { - val host = this - if (host.filtersData != null) { - roomFilterHeaderItem { - id("filter_header") - filtersData(host.filtersData) - onFilterChangedListener(host.onFilterChanged) - } - } - if (models.isEmpty() && emptyStateData != null) { emptyStateData?.let { emptyState -> roomListEmptyItem { @@ -77,10 +66,6 @@ class HomeFilteredRoomsController( this.emptyStateData = state } - fun submitFiltersData(data: List?) { - this.filtersData = data - requestForcedModelBuild() - } override fun buildItemModel(currentPosition: Int, item: RoomSummary?): EpoxyModel<*> { item ?: return RoomSummaryItemPlaceHolder_().apply { id(currentPosition) } return roomSummaryItemFactory.create(item, roomChangeMembershipStates.orEmpty(), emptySet(), RoomListDisplayMode.ROOMS, listener) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListAction.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListAction.kt index 6d17792969..b7ade559da 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListAction.kt @@ -17,7 +17,7 @@ package im.vector.app.features.home.room.list.home 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.notification.RoomNotificationState diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListFragment.kt index edb619cd90..a4738a550c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListFragment.kt @@ -25,7 +25,6 @@ import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import com.airbnb.epoxy.EpoxyControllerAdapter import com.airbnb.epoxy.OnModelBuildFinishedListener import com.airbnb.mvrx.fragmentViewModel 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.home.room.list.RoomListAnimator 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.RoomListQuickActionsSharedAction 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.filter.HomeRoomFilter +import im.vector.app.features.home.room.list.home.header.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.InvitesCounterController -import im.vector.app.features.home.room.list.home.recent.RecentRoomCarouselController import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.session.room.model.RoomSummary @@ -63,10 +59,9 @@ class HomeRoomListFragment : VectorBaseFragment(), RoomListListener { - @Inject lateinit var roomSummaryItemFactory: RoomSummaryItemFactory @Inject lateinit var userPreferencesProvider: UserPreferencesProvider - @Inject lateinit var recentRoomCarouselController: RecentRoomCarouselController - @Inject lateinit var invitesCounterController: InvitesCounterController + @Inject lateinit var headersController: HomeRoomsHeadersController + @Inject lateinit var roomsController: HomeFilteredRoomsController private val roomListViewModel: HomeRoomListViewModel by fragmentViewModel() private lateinit var sharedQuickActionsViewModel: RoomListQuickActionsSharedActionViewModel @@ -143,10 +138,25 @@ class HomeRoomListFragment : modelBuildListener = OnModelBuildFinishedListener { it.dispatchTo(stateRestorer) } - roomListViewModel.sections.onEach { sections -> - setUpAdapters(sections) + roomListViewModel.onEach(HomeRoomListViewState::headersData) { + 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) + setUpAdapters() + views.roomListView.adapter = concatAdapter // 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 } - private fun setUpAdapters(sections: Set) { - concatAdapter.adapters.forEach { - concatAdapter.removeAdapter(it) - } - sections.forEach { - concatAdapter.addAdapter(getAdapterForData(it)) - } + private fun setUpAdapters() { + val headersAdapter = headersController.also { controller -> + controller.invitesClickListener = ::onInvitesCounterClicked + controller.onFilterChangedListener = ::onRoomFilterChanged + controller.recentsRoomListener = this + }.adapter + + val roomsAdapter = roomsController + .also { controller -> + controller.listener = this + }.adapter + + concatAdapter.addAdapter(headersAdapter) + concatAdapter.addAdapter(roomsAdapter) } private fun promptLeaveRoom(roomId: String) { @@ -191,43 +208,6 @@ class HomeRoomListFragment : .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() { startActivity(Intent(activity, InvitesActivity::class.java)) } @@ -247,8 +227,13 @@ class HomeRoomListFragment : override fun onDestroyView() { views.roomListView.cleanup() - recentRoomCarouselController.listener = null - invitesCounterController.clickListener = null + + headersController.recentsRoomListener = null + headersController.invitesClickListener = null + headersController.onFilterChangedListener = null + + roomsController.listener = null + super.onDestroyView() } @@ -266,21 +251,13 @@ class HomeRoomListFragment : return true } - override fun onRejectRoomInvitation(room: RoomSummary) { - TODO("Not yet implemented") - } + override fun onRejectRoomInvitation(room: RoomSummary) = Unit - override fun onAcceptRoomInvitation(room: RoomSummary) { - TODO("Not yet implemented") - } + override fun onAcceptRoomInvitation(room: RoomSummary) = Unit - override fun onJoinSuggestedRoom(room: SpaceChildInfo) { - TODO("Not yet implemented") - } + override fun onJoinSuggestedRoom(room: SpaceChildInfo) = Unit - override fun onSuggestedRoomClicked(room: SpaceChildInfo) { - TODO("Not yet implemented") - } + override fun onSuggestedRoomClicked(room: SpaceChildInfo) = Unit // endregion } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewModel.kt index b52c4e0190..83ffc482ad 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewModel.kt @@ -17,8 +17,8 @@ package im.vector.app.features.home.room.list.home import android.widget.ImageView -import androidx.lifecycle.map import androidx.paging.PagedList +import arrow.core.Option import arrow.core.toOption import com.airbnb.mvrx.MavericksViewModelFactory 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.resources.DrawableProvider 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.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch 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.RoomTagQueryFilter import org.matrix.android.sdk.api.query.toActiveSpaceOrNoFilter @@ -80,6 +80,7 @@ class HomeRoomListViewModel @AssistedInject constructor( companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + private var roomsFlow: Flow>? = null private val pagedListConfig = PagedList.Config.Builder() .setPageSize(10) .setInitialLoadSizeHint(20) @@ -87,9 +88,6 @@ class HomeRoomListViewModel @AssistedInject constructor( .setPrefetchDistance(10) .build() - private val _sections = MutableSharedFlow>(replay = 1) - val sections = _sections.asSharedFlow() - private var currentFilter: HomeRoomFilter = HomeRoomFilter.ALL private val _emptyStateFlow = MutableSharedFlow>(replay = 1) val emptyStateFlow = _emptyStateFlow.asSharedFlow() @@ -97,118 +95,77 @@ class HomeRoomListViewModel @AssistedInject constructor( private var filteredPagedRoomSummariesLive: UpdatableLivePageResult? = null init { - configureSections() - observePreferences() + observeOrderPreferences() + observeInvites() + observeRecents() + observeFilterTabs() + observeRooms() } - private fun observePreferences() { - preferencesStore.areRecentsEnabledFlow.onEach { - configureSections() - }.launchIn(viewModelScope) - - preferencesStore.isAZOrderingEnabledFlow.onEach { - configureSections() - }.launchIn(viewModelScope) - } - - private fun configureSections() = viewModelScope.launch { - val newSections = mutableSetOf() - 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() + private fun observeInvites() { + session.flow() + .liveRoomSummaries( + roomSummaryQueryParams { + memberships = listOf(Membership.INVITE) + }, + RoomSortOrder.ACTIVITY + ).onEach { list -> + setState { copy(headersData = headersData.copy(invitesCount = list.size)) } }.launchIn(viewModelScope) - - return HomeRoomSection.RoomSummaryData( - list = liveResults.livePagedList, - filtersData = getFiltersDataFlow() - ) } - private fun emitEmptyState() { - viewModelScope.launch { - val emptyState = getEmptyStateData(currentFilter, spaceStateHandler.getCurrentSpace()) - _emptyStateFlow.emit(Optional.from(emptyState)) + private fun observeRecents() { + preferencesStore.areRecentsEnabledFlow + .distinctUntilChanged() + .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?) { + if (filtersList?.contains(currentFilter) != true) { + handleChangeRoomFilter(HomeRoomFilter.ALL) } } - private fun getFiltersDataFlow(): SharedFlow>> { - val flow = MutableSharedFlow>>(replay = 1) - + private fun getFilterTabsFlow(): Flow>> { val spaceFLow = spaceStateHandler.getSelectedSpaceFlow() .distinctUntilChanged() .onStart { emit(spaceStateHandler.getCurrentSpace().toOption()) } - val favouritesFlow = spaceFLow.flatMapLatest { selectedSpace -> session.flow() @@ -236,31 +193,76 @@ class HomeRoomListViewModel @AssistedInject constructor( .map { it.isNotEmpty() } .distinctUntilChanged() - combine(favouritesFlow, dmsFLow, preferencesStore.areFiltersEnabledFlow) { hasFavourite, hasDm, areFiltersEnabled -> - Triple(hasFavourite, hasDm, areFiltersEnabled) - }.onEach { (hasFavourite, hasDm, areFiltersEnabled) -> - if (areFiltersEnabled) { - val filtersData = mutableListOf( - HomeRoomFilter.ALL, - HomeRoomFilter.UNREADS + return combine(favouritesFlow, dmsFLow) { hasFavourite, hasDm -> + hasFavourite to hasDm + }.map { (hasFavourite, hasDm) -> + val filtersData = mutableListOf( + HomeRoomFilter.ALL, + 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 { @@ -323,16 +325,20 @@ class HomeRoomListViewModel @AssistedInject constructor( is HomeRoomListAction.LeaveRoom -> handleLeaveRoom(action) is HomeRoomListAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action) is HomeRoomListAction.ToggleTag -> handleToggleTag(action) - is HomeRoomListAction.ChangeRoomFilter -> handleChangeRoomFilter(action) + is HomeRoomListAction.ChangeRoomFilter -> handleChangeRoomFilter(action.filter) } } - private fun handleChangeRoomFilter(action: HomeRoomListAction.ChangeRoomFilter) { - currentFilter = action.filter + private fun handleChangeRoomFilter(newFilter: HomeRoomFilter) { + if (currentFilter == newFilter) { + return + } + currentFilter = newFilter filteredPagedRoomSummariesLive?.let { liveResults -> - liveResults.queryParams = getFilteredQueryParams(action.filter, liveResults.queryParams) + liveResults.queryParams = getFilteredQueryParams(currentFilter, liveResults.queryParams) } + setState { copy(headersData = headersData.copy(currentFilter = currentFilter)) } emitEmptyState() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewState.kt index bfcaea22e9..8647054f3d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewState.kt @@ -16,9 +16,15 @@ package im.vector.app.features.home.room.list.home +import androidx.lifecycle.LiveData +import androidx.paging.PagedList import com.airbnb.mvrx.MavericksState 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( - val state: StateView.State = StateView.State.Loading + val state: StateView.State = StateView.State.Content, + val headersData: RoomsHeadersData = RoomsHeadersData(), + val roomsLivePagedList: LiveData>? = null ) : MavericksState diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomSection.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomSection.kt deleted file mode 100644 index 29df594d06..0000000000 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomSection.kt +++ /dev/null @@ -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>, - val filtersData: SharedFlow>>, - ) : HomeRoomSection() - - data class RecentRoomsData( - val list: LiveData> - ) : HomeRoomSection() - - data class InvitesCountData( - val count: LiveData - ) : HomeRoomSection() -} diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/filter/HomeRoomFilter.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/header/HomeRoomFilter.kt similarity index 94% rename from vector/src/main/java/im/vector/app/features/home/room/list/home/filter/HomeRoomFilter.kt rename to vector/src/main/java/im/vector/app/features/home/room/list/home/header/HomeRoomFilter.kt index ce33440238..9bd800a47c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/filter/HomeRoomFilter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/header/HomeRoomFilter.kt @@ -14,7 +14,7 @@ * 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 im.vector.app.R diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/header/HomeRoomsHeadersController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/header/HomeRoomsHeadersController.kt new file mode 100644 index 0000000000..8be9bf12f9 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/header/HomeRoomsHeadersController.kt @@ -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) { + 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, + currentFilter: HomeRoomFilter?, + ) { + roomFilterHeaderItem { + id("filter_header") + filtersData(filtersList) + selectedFilter(currentFilter) + onFilterChangedListener(filterChangedListener) + } + } + + fun submitData(data: RoomsHeadersData) { + this.data = data + requestModelBuild() + } +} + +private inline fun CarouselModelBuilder.withModelsFrom( + items: List, + modelBuilder: (T) -> EpoxyModel<*> +) { + models(items.map { modelBuilder(it) }) +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/invites/InviteCounterItem.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/header/InviteCounterItem.kt similarity index 96% rename from vector/src/main/java/im/vector/app/features/home/room/list/home/invites/InviteCounterItem.kt rename to vector/src/main/java/im/vector/app/features/home/room/list/home/header/InviteCounterItem.kt index 7536bde10a..5c43c86933 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/invites/InviteCounterItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/header/InviteCounterItem.kt @@ -14,7 +14,7 @@ * 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.EpoxyModelClass diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/recent/RecentRoomItem.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/header/RecentRoomItem.kt similarity index 98% rename from vector/src/main/java/im/vector/app/features/home/room/list/home/recent/RecentRoomItem.kt rename to vector/src/main/java/im/vector/app/features/home/room/list/home/header/RecentRoomItem.kt index d7c72ba5ed..9cd28624a5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/recent/RecentRoomItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/header/RecentRoomItem.kt @@ -14,7 +14,7 @@ * 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.View diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/filter/RoomFilterHeaderItem.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/header/RoomFilterHeaderItem.kt similarity index 85% rename from vector/src/main/java/im/vector/app/features/home/room/list/home/filter/RoomFilterHeaderItem.kt rename to vector/src/main/java/im/vector/app/features/home/room/list/home/header/RoomFilterHeaderItem.kt index bbe503806b..ed99b51681 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/filter/RoomFilterHeaderItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/header/RoomFilterHeaderItem.kt @@ -14,7 +14,7 @@ * 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.EpoxyModelClass @@ -32,13 +32,20 @@ abstract class RoomFilterHeaderItem : VectorEpoxyModel? = null + @EpoxyAttribute + var selectedFilter: HomeRoomFilter? = null + override fun bind(holder: Holder) { super.bind(holder) with(holder.tabLayout) { removeAllTabs() + clearOnTabSelectedListeners() 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 { diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/header/RoomsHeadersData.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/header/RoomsHeadersData.kt new file mode 100644 index 0000000000..87e45c99fa --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/header/RoomsHeadersData.kt @@ -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? = null, + val currentFilter: HomeRoomFilter? = null, + val recents: List? = null +) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/invites/InvitesCounterController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/invites/InvitesCounterController.kt deleted file mode 100644 index 82a31d30a9..0000000000 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/invites/InvitesCounterController.kt +++ /dev/null @@ -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() - } -} diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/recent/RecentRoomCarouselController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/recent/RecentRoomCarouselController.kt deleted file mode 100644 index df5ce28da5..0000000000 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/recent/RecentRoomCarouselController.kt +++ /dev/null @@ -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? = 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) { - 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 CarouselModelBuilder.withModelsFrom( - items: List, - modelBuilder: (T) -> EpoxyModel<*> -) { - models(items.map { modelBuilder(it) }) -}