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 @@ + + + +