App layout Home screen empty states (#7007)
1
changelog.d/6835.feature
Normal file
@ -0,0 +1 @@
|
||||
[App Layout] New empty states for home screen
|
@ -3244,4 +3244,14 @@
|
||||
<item quantity="other">Consider signing out from old sessions (%1$d days or more) that you don’t use anymore.</item>
|
||||
</plurals>
|
||||
|
||||
<!-- Note to translators: %s will be replaces with selected space name -->
|
||||
<string name="home_empty_space_no_rooms_title">%s\nis looking a little empty.</string>
|
||||
<!-- Note to translators: for RTL languages, Spaces will be at the bottom left. Please translate "bottom-left" instead of "bottom-right". Thanks!-->
|
||||
<string name="home_empty_space_no_rooms_message">Spaces are a new way to group rooms and people. Add an existing room, or create a new one, using the bottom-right button.</string>
|
||||
<!-- Note to translators: %s will be replaces with current user displayname -->
|
||||
<string name="home_empty_no_rooms_title">Welcome to ${app_name},\n%s.</string>
|
||||
<string name="home_empty_no_rooms_message">The all-in-one secure chat app for teams, friends and organisations. Create a chat, or join an existing room, to get started.</string>
|
||||
<string name="home_empty_no_unreads_title">Nothing to report.</string>
|
||||
<string name="home_empty_no_unreads_message">This is where your unread messages will show up, when you have some.</string>
|
||||
|
||||
</resources>
|
||||
|
@ -21,6 +21,7 @@ import android.graphics.drawable.Drawable
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import androidx.core.view.isVisible
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.updateConstraintSet
|
||||
@ -36,7 +37,8 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
|
||||
val title: CharSequence? = null,
|
||||
val image: Drawable? = null,
|
||||
val isBigImage: Boolean = false,
|
||||
val message: CharSequence? = null
|
||||
val message: CharSequence? = null,
|
||||
val imageScaleType: ImageView.ScaleType? = ImageView.ScaleType.FIT_CENTER,
|
||||
) : State()
|
||||
|
||||
data class Error(val message: CharSequence? = null) : State()
|
||||
@ -79,6 +81,7 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
|
||||
is State.Content -> Unit
|
||||
is State.Loading -> Unit
|
||||
is State.Empty -> {
|
||||
views.emptyImageView.scaleType = newState.imageScaleType
|
||||
views.emptyImageView.setImageDrawable(newState.image)
|
||||
views.emptyView.updateConstraintSet {
|
||||
it.constrainPercentHeight(R.id.emptyImageView, if (newState.isBigImage) 0.5f else 0.1f)
|
||||
|
@ -199,11 +199,17 @@ class HomeRoomListFragment :
|
||||
).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
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
package im.vector.app.features.home.room.list.home
|
||||
|
||||
import android.widget.ImageView
|
||||
import androidx.lifecycle.map
|
||||
import androidx.paging.PagedList
|
||||
import arrow.core.toOption
|
||||
@ -23,11 +24,14 @@ import com.airbnb.mvrx.MavericksViewModelFactory
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.R
|
||||
import im.vector.app.SpaceStateHandler
|
||||
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
||||
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||
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 kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
@ -52,6 +56,7 @@ import org.matrix.android.sdk.api.session.room.RoomSortOrder
|
||||
import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
|
||||
import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
|
||||
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
|
||||
import org.matrix.android.sdk.api.session.room.state.isPublic
|
||||
@ -63,6 +68,8 @@ class HomeRoomListViewModel @AssistedInject constructor(
|
||||
private val session: Session,
|
||||
private val spaceStateHandler: SpaceStateHandler,
|
||||
private val preferencesStore: HomeLayoutPreferencesStore,
|
||||
private val stringProvider: StringProvider,
|
||||
private val drawableProvider: DrawableProvider,
|
||||
) : VectorViewModel<HomeRoomListViewState, HomeRoomListAction, HomeRoomListViewEvents>(initialState) {
|
||||
|
||||
@AssistedFactory
|
||||
@ -82,6 +89,10 @@ class HomeRoomListViewModel @AssistedInject constructor(
|
||||
private val _sections = MutableSharedFlow<Set<HomeRoomSection>>(replay = 1)
|
||||
val sections = _sections.asSharedFlow()
|
||||
|
||||
private var currentFilter: HomeRoomFilter = HomeRoomFilter.ALL
|
||||
private val _emptyStateFlow = MutableSharedFlow<Optional<StateView.State.Empty>>(replay = 1)
|
||||
val emptyStateFlow = _emptyStateFlow.asSharedFlow()
|
||||
|
||||
private var filteredPagedRoomSummariesLive: UpdatableLivePageResult? = null
|
||||
|
||||
init {
|
||||
@ -109,6 +120,7 @@ class HomeRoomListViewModel @AssistedInject constructor(
|
||||
}
|
||||
newSections.add(getFilteredRoomsSection())
|
||||
|
||||
emitEmptyState()
|
||||
_sections.emit(newSections)
|
||||
|
||||
setState {
|
||||
@ -171,6 +183,7 @@ class HomeRoomListViewModel @AssistedInject constructor(
|
||||
liveResults.queryParams = liveResults.queryParams.copy(
|
||||
spaceFilter = selectedSpace?.roomId.toActiveSpaceOrNoFilter()
|
||||
)
|
||||
emitEmptyState()
|
||||
}.launchIn(viewModelScope)
|
||||
|
||||
return HomeRoomSection.RoomSummaryData(
|
||||
@ -179,6 +192,13 @@ class HomeRoomListViewModel @AssistedInject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private fun emitEmptyState() {
|
||||
viewModelScope.launch {
|
||||
val emptyState = getEmptyStateData(currentFilter, spaceStateHandler.getCurrentSpace())
|
||||
_emptyStateFlow.emit(Optional.from(emptyState))
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFiltersDataFlow(): SharedFlow<Optional<List<HomeRoomFilter>>> {
|
||||
val flow = MutableSharedFlow<Optional<List<HomeRoomFilter>>>(replay = 1)
|
||||
|
||||
@ -250,6 +270,38 @@ class HomeRoomListViewModel @AssistedInject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun getEmptyStateData(filter: HomeRoomFilter, selectedSpace: RoomSummary?): StateView.State.Empty? {
|
||||
return when (filter) {
|
||||
HomeRoomFilter.ALL ->
|
||||
if (selectedSpace != null) {
|
||||
StateView.State.Empty(
|
||||
title = stringProvider.getString(R.string.home_empty_space_no_rooms_title, selectedSpace.displayName),
|
||||
message = stringProvider.getString(R.string.home_empty_space_no_rooms_message),
|
||||
image = drawableProvider.getDrawable(R.drawable.ill_empty_space),
|
||||
isBigImage = true
|
||||
)
|
||||
} else {
|
||||
val userName = session.userService().getUser(session.myUserId)?.displayName ?: ""
|
||||
StateView.State.Empty(
|
||||
title = stringProvider.getString(R.string.home_empty_no_rooms_title, userName),
|
||||
message = stringProvider.getString(R.string.home_empty_no_rooms_message),
|
||||
image = drawableProvider.getDrawable(R.drawable.ill_empty_all_chats),
|
||||
isBigImage = true
|
||||
)
|
||||
}
|
||||
HomeRoomFilter.UNREADS ->
|
||||
StateView.State.Empty(
|
||||
title = stringProvider.getString(R.string.home_empty_no_unreads_title),
|
||||
message = stringProvider.getString(R.string.home_empty_no_unreads_message),
|
||||
image = drawableProvider.getDrawable(R.drawable.ill_empty_unreads),
|
||||
isBigImage = true,
|
||||
imageScaleType = ImageView.ScaleType.CENTER_INSIDE
|
||||
)
|
||||
else ->
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun handle(action: HomeRoomListAction) {
|
||||
when (action) {
|
||||
is HomeRoomListAction.SelectRoom -> handleSelectRoom(action)
|
||||
@ -261,9 +313,12 @@ class HomeRoomListViewModel @AssistedInject constructor(
|
||||
}
|
||||
|
||||
private fun handleChangeRoomFilter(action: HomeRoomListAction.ChangeRoomFilter) {
|
||||
currentFilter = action.filter
|
||||
filteredPagedRoomSummariesLive?.let { liveResults ->
|
||||
liveResults.queryParams = getFilteredQueryParams(action.filter, liveResults.queryParams)
|
||||
}
|
||||
|
||||
emitEmptyState()
|
||||
}
|
||||
|
||||
fun isPublicRoom(roomId: String): Boolean {
|
||||
|
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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 com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.app.core.platform.StateView
|
||||
|
||||
@EpoxyModelClass
|
||||
abstract class RoomListEmptyItem : VectorEpoxyModel<RoomListEmptyItem.Holder>(R.layout.item_state_view) {
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var emptyData: StateView.State.Empty
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
holder.stateView.state = emptyData
|
||||
}
|
||||
|
||||
class Holder : VectorEpoxyHolder() {
|
||||
val stateView by bind<StateView>(R.id.stateView)
|
||||
}
|
||||
}
|
@ -18,11 +18,13 @@ package im.vector.app.features.home.room.list.home.filter
|
||||
|
||||
import com.airbnb.epoxy.EpoxyModel
|
||||
import com.airbnb.epoxy.paging.PagedListEpoxyController
|
||||
import im.vector.app.core.platform.StateView
|
||||
import im.vector.app.core.utils.createUIHandler
|
||||
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
|
||||
|
||||
@ -44,6 +46,8 @@ class HomeFilteredRoomsController(
|
||||
var onFilterChanged: ((HomeRoomFilter) -> Unit)? = null
|
||||
|
||||
private var filtersData: List<HomeRoomFilter>? = null
|
||||
private var emptyStateData: StateView.State.Empty? = null
|
||||
private var currentState: StateView.State = StateView.State.Content
|
||||
|
||||
override fun addModels(models: List<EpoxyModel<*>>) {
|
||||
val host = this
|
||||
@ -54,14 +58,29 @@ class HomeFilteredRoomsController(
|
||||
onFilterChangedListener(host.onFilterChanged)
|
||||
}
|
||||
}
|
||||
super.addModels(models)
|
||||
|
||||
if (models.isEmpty() && emptyStateData != null) {
|
||||
emptyStateData?.let { emptyState ->
|
||||
roomListEmptyItem {
|
||||
id("state_item")
|
||||
emptyData(emptyState)
|
||||
}
|
||||
currentState = emptyState
|
||||
}
|
||||
} else {
|
||||
currentState = StateView.State.Content
|
||||
super.addModels(models)
|
||||
}
|
||||
}
|
||||
|
||||
fun submitEmptyStateData(state: StateView.State.Empty?) {
|
||||
this.emptyStateData = state
|
||||
}
|
||||
|
||||
fun submitFiltersData(data: List<HomeRoomFilter>?) {
|
||||
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)
|
||||
|
BIN
vector/src/main/res/drawable-hdpi/ill_empty_all_chats.webp
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
vector/src/main/res/drawable-hdpi/ill_empty_space.webp
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
vector/src/main/res/drawable-hdpi/ill_empty_unreads.webp
Normal file
After Width: | Height: | Size: 794 B |
BIN
vector/src/main/res/drawable-mdpi/ill_empty_all_chats.webp
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
vector/src/main/res/drawable-mdpi/ill_empty_space.webp
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
vector/src/main/res/drawable-mdpi/ill_empty_unreads.webp
Normal file
After Width: | Height: | Size: 556 B |
BIN
vector/src/main/res/drawable-xhdpi/ill_empty_all_chats.webp
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
vector/src/main/res/drawable-xhdpi/ill_empty_space.webp
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
vector/src/main/res/drawable-xhdpi/ill_empty_unreads.webp
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
vector/src/main/res/drawable-xxhdpi/ill_empty_all_chats.webp
Normal file
After Width: | Height: | Size: 8.7 KiB |
BIN
vector/src/main/res/drawable-xxhdpi/ill_empty_space.webp
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
vector/src/main/res/drawable-xxhdpi/ill_empty_unreads.webp
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
vector/src/main/res/drawable-xxxhdpi/ill_empty_all_chats.webp
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
vector/src/main/res/drawable-xxxhdpi/ill_empty_space.webp
Normal file
After Width: | Height: | Size: 6.2 KiB |
BIN
vector/src/main/res/drawable-xxxhdpi/ill_empty_unreads.webp
Normal file
After Width: | Height: | Size: 2.0 KiB |
8
vector/src/main/res/layout/item_state_view.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<im.vector.app.core.platform.StateView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/stateView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:colorBackground">
|
||||
|
||||
</im.vector.app.core.platform.StateView>
|