diff --git a/changelog.d/6835.feature b/changelog.d/6835.feature
new file mode 100644
index 0000000000..e4e610f7e0
--- /dev/null
+++ b/changelog.d/6835.feature
@@ -0,0 +1 @@
+[App Layout] New empty states for home screen
diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index 6b44377b79..26f26dc7e5 100644
--- a/library/ui-strings/src/main/res/values/strings.xml
+++ b/library/ui-strings/src/main/res/values/strings.xml
@@ -3244,4 +3244,14 @@
- Consider signing out from old sessions (%1$d days or more) that you don’t use anymore.
+
+ %s\nis looking a little empty.
+
+ Spaces are a new way to group rooms and people. Add an existing room, or create a new one, using the bottom-right button.
+
+ Welcome to ${app_name},\n%s.
+ The all-in-one secure chat app for teams, friends and organisations. Create a chat, or join an existing room, to get started.
+ Nothing to report.
+ This is where your unread messages will show up, when you have some.
+
diff --git a/vector/src/main/java/im/vector/app/core/platform/StateView.kt b/vector/src/main/java/im/vector/app/core/platform/StateView.kt
index 6f36787d0c..2fb99c705a 100755
--- a/vector/src/main/java/im/vector/app/core/platform/StateView.kt
+++ b/vector/src/main/java/im/vector/app/core/platform/StateView.kt
@@ -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)
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 32635e3407..edb619cd90 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
@@ -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
}
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 425f309202..021b979b2b 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
@@ -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(initialState) {
@AssistedFactory
@@ -82,6 +89,10 @@ class HomeRoomListViewModel @AssistedInject constructor(
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()
+
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>> {
val flow = MutableSharedFlow>>(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 {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/RoomListEmptyItem.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/RoomListEmptyItem.kt
new file mode 100644
index 0000000000..f7b3262529
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/RoomListEmptyItem.kt
@@ -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(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(R.id.stateView)
+ }
+}
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/filter/HomeFilteredRoomsController.kt
index 2d673bc089..789c9e9985 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/filter/HomeFilteredRoomsController.kt
@@ -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? = null
+ private var emptyStateData: StateView.State.Empty? = null
+ private var currentState: StateView.State = StateView.State.Content
override fun addModels(models: List>) {
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?) {
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/res/drawable-hdpi/ill_empty_all_chats.webp b/vector/src/main/res/drawable-hdpi/ill_empty_all_chats.webp
new file mode 100644
index 0000000000..6f5211b17e
Binary files /dev/null and b/vector/src/main/res/drawable-hdpi/ill_empty_all_chats.webp differ
diff --git a/vector/src/main/res/drawable-hdpi/ill_empty_space.webp b/vector/src/main/res/drawable-hdpi/ill_empty_space.webp
new file mode 100644
index 0000000000..b33fe7937c
Binary files /dev/null and b/vector/src/main/res/drawable-hdpi/ill_empty_space.webp differ
diff --git a/vector/src/main/res/drawable-hdpi/ill_empty_unreads.webp b/vector/src/main/res/drawable-hdpi/ill_empty_unreads.webp
new file mode 100644
index 0000000000..ce94823cc7
Binary files /dev/null and b/vector/src/main/res/drawable-hdpi/ill_empty_unreads.webp differ
diff --git a/vector/src/main/res/drawable-mdpi/ill_empty_all_chats.webp b/vector/src/main/res/drawable-mdpi/ill_empty_all_chats.webp
new file mode 100644
index 0000000000..41c83c6b50
Binary files /dev/null and b/vector/src/main/res/drawable-mdpi/ill_empty_all_chats.webp differ
diff --git a/vector/src/main/res/drawable-mdpi/ill_empty_space.webp b/vector/src/main/res/drawable-mdpi/ill_empty_space.webp
new file mode 100644
index 0000000000..379bf44b63
Binary files /dev/null and b/vector/src/main/res/drawable-mdpi/ill_empty_space.webp differ
diff --git a/vector/src/main/res/drawable-mdpi/ill_empty_unreads.webp b/vector/src/main/res/drawable-mdpi/ill_empty_unreads.webp
new file mode 100644
index 0000000000..4209c0591d
Binary files /dev/null and b/vector/src/main/res/drawable-mdpi/ill_empty_unreads.webp differ
diff --git a/vector/src/main/res/drawable-xhdpi/ill_empty_all_chats.webp b/vector/src/main/res/drawable-xhdpi/ill_empty_all_chats.webp
new file mode 100644
index 0000000000..c8374d1160
Binary files /dev/null and b/vector/src/main/res/drawable-xhdpi/ill_empty_all_chats.webp differ
diff --git a/vector/src/main/res/drawable-xhdpi/ill_empty_space.webp b/vector/src/main/res/drawable-xhdpi/ill_empty_space.webp
new file mode 100644
index 0000000000..c6d83f16c7
Binary files /dev/null and b/vector/src/main/res/drawable-xhdpi/ill_empty_space.webp differ
diff --git a/vector/src/main/res/drawable-xhdpi/ill_empty_unreads.webp b/vector/src/main/res/drawable-xhdpi/ill_empty_unreads.webp
new file mode 100644
index 0000000000..14d2dbdf9a
Binary files /dev/null and b/vector/src/main/res/drawable-xhdpi/ill_empty_unreads.webp differ
diff --git a/vector/src/main/res/drawable-xxhdpi/ill_empty_all_chats.webp b/vector/src/main/res/drawable-xxhdpi/ill_empty_all_chats.webp
new file mode 100644
index 0000000000..fc19311faf
Binary files /dev/null and b/vector/src/main/res/drawable-xxhdpi/ill_empty_all_chats.webp differ
diff --git a/vector/src/main/res/drawable-xxhdpi/ill_empty_space.webp b/vector/src/main/res/drawable-xxhdpi/ill_empty_space.webp
new file mode 100644
index 0000000000..18b26b82ff
Binary files /dev/null and b/vector/src/main/res/drawable-xxhdpi/ill_empty_space.webp differ
diff --git a/vector/src/main/res/drawable-xxhdpi/ill_empty_unreads.webp b/vector/src/main/res/drawable-xxhdpi/ill_empty_unreads.webp
new file mode 100644
index 0000000000..17127018ba
Binary files /dev/null and b/vector/src/main/res/drawable-xxhdpi/ill_empty_unreads.webp differ
diff --git a/vector/src/main/res/drawable-xxxhdpi/ill_empty_all_chats.webp b/vector/src/main/res/drawable-xxxhdpi/ill_empty_all_chats.webp
new file mode 100644
index 0000000000..e020c33543
Binary files /dev/null and b/vector/src/main/res/drawable-xxxhdpi/ill_empty_all_chats.webp differ
diff --git a/vector/src/main/res/drawable-xxxhdpi/ill_empty_space.webp b/vector/src/main/res/drawable-xxxhdpi/ill_empty_space.webp
new file mode 100644
index 0000000000..2c11bbafa0
Binary files /dev/null and b/vector/src/main/res/drawable-xxxhdpi/ill_empty_space.webp differ
diff --git a/vector/src/main/res/drawable-xxxhdpi/ill_empty_unreads.webp b/vector/src/main/res/drawable-xxxhdpi/ill_empty_unreads.webp
new file mode 100644
index 0000000000..278fbfac0b
Binary files /dev/null and b/vector/src/main/res/drawable-xxxhdpi/ill_empty_unreads.webp differ
diff --git a/vector/src/main/res/layout/item_state_view.xml b/vector/src/main/res/layout/item_state_view.xml
new file mode 100644
index 0000000000..3cf2e3e6d2
--- /dev/null
+++ b/vector/src/main/res/layout/item_state_view.xml
@@ -0,0 +1,8 @@
+
+
+
+