Fix empty states for spaces

This commit is contained in:
Valere 2021-05-14 13:10:13 +02:00
parent 201f4c342a
commit 0d0b6a8810
11 changed files with 308 additions and 19 deletions

View File

@ -0,0 +1,92 @@
/*
* Copyright 2019 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.core.ui.list
import android.content.res.ColorStateList
import android.view.View
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.core.view.isVisible
import androidx.core.widget.ImageViewCompat
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.extensions.setTextOrHide
/**
* A generic list item to display when there is no results, with an optional CTA
*/
@EpoxyModelClass(layout = R.layout.item_generic_empty_state)
abstract class GenericEmptyWithActionItem : VectorEpoxyModel<GenericEmptyWithActionItem.Holder>() {
class Action(var title: String) {
var perform: Runnable? = null
}
@EpoxyAttribute
var title: CharSequence? = null
@EpoxyAttribute
var description: CharSequence? = null
@EpoxyAttribute
@DrawableRes
var iconRes: Int = -1
@EpoxyAttribute
@ColorInt
var iconTint: Int? = null
@EpoxyAttribute
var buttonAction: Action? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.titleText.setTextOrHide(title)
holder.descriptionText.setTextOrHide(description)
if (iconRes != -1) {
holder.imageView.setImageResource(iconRes)
holder.imageView.isVisible = true
if (iconTint != null) {
ImageViewCompat.setImageTintList(holder.imageView, ColorStateList.valueOf(iconTint!!))
} else {
ImageViewCompat.setImageTintList(holder.imageView, null)
}
} else {
holder.imageView.isVisible = false
}
holder.actionButton.setTextOrHide(buttonAction?.title)
holder.actionButton.setOnClickListener {
buttonAction?.perform?.run()
}
}
class Holder : VectorEpoxyHolder() {
val root by bind<View>(R.id.item_generic_root)
val titleText by bind<TextView>(R.id.emptyItemTitleView)
val descriptionText by bind<TextView>(R.id.emptyItemMessageView)
val imageView by bind<ImageView>(R.id.emptyItemImageView)
val actionButton by bind<Button>(R.id.emptyItemButton)
}
}

View File

@ -26,7 +26,8 @@ import im.vector.app.core.epoxy.loadingItem
import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.core.ui.list.GenericEmptyWithActionItem
import im.vector.app.core.ui.list.genericEmptyWithActionItem
import im.vector.app.core.ui.list.genericPillItem import im.vector.app.core.ui.list.genericPillItem
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.list.spaceChildInfoItem import im.vector.app.features.home.room.list.spaceChildInfoItem
@ -50,6 +51,7 @@ class SpaceDirectoryController @Inject constructor(
fun onSpaceChildClick(spaceChildInfo: SpaceChildInfo) fun onSpaceChildClick(spaceChildInfo: SpaceChildInfo)
fun onRoomClick(spaceChildInfo: SpaceChildInfo) fun onRoomClick(spaceChildInfo: SpaceChildInfo)
fun retry() fun retry()
fun addExistingRooms(spaceId: String)
} }
var listener: InteractionListener? = null var listener: InteractionListener? = null
@ -97,9 +99,23 @@ class SpaceDirectoryController @Inject constructor(
?: emptyList() ?: emptyList()
if (flattenChildInfo.isEmpty()) { if (flattenChildInfo.isEmpty()) {
genericFooterItem { genericEmptyWithActionItem {
id("empty_footer") id("empty_res")
host.stringProvider.getString(R.string.no_result_placeholder) title(host.stringProvider.getString(R.string.this_space_has_no_rooms))
iconRes(R.drawable.ic_empty_icon_room)
iconTint(host.colorProvider.getColorFromAttribute(R.attr.riotx_reaction_background_on))
apply {
if (data?.canAddRooms == true) {
description(host.stringProvider.getString(R.string.this_space_has_no_rooms_admin))
val action = GenericEmptyWithActionItem.Action(host.stringProvider.getString(R.string.space_add_existing_rooms))
action.perform = Runnable {
host.listener?.addExistingRooms(data.spaceId)
}
buttonAction(action)
} else {
description(host.stringProvider.getString(R.string.this_space_has_no_rooms_not_admin))
}
}
} }
} else { } else {
flattenChildInfo.forEach { info -> flattenChildInfo.forEach { info ->

View File

@ -19,6 +19,8 @@ package im.vector.app.features.spaces.explore
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.activityViewModel
@ -26,9 +28,12 @@ import com.airbnb.mvrx.withState
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.configureWith
import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.platform.OnBackPressed import im.vector.app.core.platform.OnBackPressed
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentRoomDirectoryPickerBinding import im.vector.app.databinding.FragmentRoomDirectoryPickerBinding
import im.vector.app.features.spaces.manage.ManageType
import im.vector.app.features.spaces.manage.SpaceManageActivity
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
import javax.inject.Inject import javax.inject.Inject
@ -44,6 +49,8 @@ class SpaceDirectoryFragment @Inject constructor(
SpaceDirectoryController.InteractionListener, SpaceDirectoryController.InteractionListener,
OnBackPressed { OnBackPressed {
override fun getMenuRes() = R.menu.menu_space_directory
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) =
FragmentRoomDirectoryPickerBinding.inflate(layoutInflater, container, false) FragmentRoomDirectoryPickerBinding.inflate(layoutInflater, container, false)
@ -60,6 +67,10 @@ class SpaceDirectoryFragment @Inject constructor(
} }
epoxyController.listener = this epoxyController.listener = this
views.roomDirectoryPickerList.configureWith(epoxyController) views.roomDirectoryPickerList.configureWith(epoxyController)
viewModel.selectSubscribe(this, SpaceDirectoryState::canAddRooms) {
invalidateOptionsMenu()
}
} }
override fun onDestroyView() { override fun onDestroyView() {
@ -77,6 +88,32 @@ class SpaceDirectoryFragment @Inject constructor(
views.toolbar.title = title views.toolbar.title = title
} }
override fun onPrepareOptionsMenu(menu: Menu) = withState(viewModel) { state ->
menu.findItem(R.id.spaceAddRoom)?.let {
it.isVisible = state.canAddRooms
}
menu.findItem(R.id.spaceCreateRoom)?.let {
it.isVisible = false // Not yet implemented
}
super.onPrepareOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.spaceAddRoom -> {
withState(viewModel) { state ->
addExistingRooms(state.spaceId)
}
return true
}
R.id.spaceCreateRoom -> {
// not implemented yet
return true
}
}
return super.onOptionsItemSelected(item)
}
override fun onButtonClick(spaceChildInfo: SpaceChildInfo) { override fun onButtonClick(spaceChildInfo: SpaceChildInfo) {
viewModel.handle(SpaceDirectoryViewAction.JoinOrOpen(spaceChildInfo)) viewModel.handle(SpaceDirectoryViewAction.JoinOrOpen(spaceChildInfo))
} }
@ -97,6 +134,14 @@ class SpaceDirectoryFragment @Inject constructor(
override fun retry() { override fun retry() {
viewModel.handle(SpaceDirectoryViewAction.Retry) viewModel.handle(SpaceDirectoryViewAction.Retry)
} }
private val addExistingRoomActivityResult = registerStartForActivityResult { activityResult ->
viewModel.handle(SpaceDirectoryViewAction.Retry)
}
override fun addExistingRooms(spaceId: String) {
addExistingRoomActivityResult.launch(SpaceManageActivity.newIntent(requireContext(), spaceId, ManageType.AddRooms))
}
// override fun navigateToRoom(roomId: String) { // override fun navigateToRoom(roomId: String) {
// viewModel.handle(SpaceDirectoryViewAction.NavigateToRoom(roomId)) // viewModel.handle(SpaceDirectoryViewAction.NavigateToRoom(roomId))
// } // }

View File

@ -36,7 +36,8 @@ data class SpaceDirectoryState(
// Set of joined roomId / spaces, // Set of joined roomId / spaces,
val joinedRoomsIds: Set<String> = emptySet(), val joinedRoomsIds: Set<String> = emptySet(),
// keys are room alias or roomId // keys are room alias or roomId
val changeMembershipStates: Map<String, ChangeMembershipState> = emptyMap() val changeMembershipStates: Map<String, ChangeMembershipState> = emptyMap(),
val canAddRooms: Boolean = false
) : MvRxState { ) : MvRxState {
constructor(args: SpaceDirectoryArgs) : this( constructor(args: SpaceDirectoryArgs) : this(
spaceId = args.spaceId spaceId = args.spaceId

View File

@ -28,12 +28,15 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.model.RoomType
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.rx
import timber.log.Timber import timber.log.Timber
@ -70,6 +73,23 @@ class SpaceDirectoryViewModel @AssistedInject constructor(
refreshFromApi() refreshFromApi()
observeJoinedRooms() observeJoinedRooms()
observeMembershipChanges() observeMembershipChanges()
observePermissions()
}
private fun observePermissions() {
val room = session.getRoom(initialState.spaceId) ?: return
val powerLevelsContentLive = PowerLevelsObservableFactory(room).createObservable()
powerLevelsContentLive
.subscribe {
val powerLevelsHelper = PowerLevelsHelper(it)
setState {
copy(canAddRooms = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true,
EventType.STATE_SPACE_CHILD))
}
}
.disposeOnClear()
} }
private fun refreshFromApi() { private fun refreshFromApi() {

View File

@ -19,9 +19,12 @@ package im.vector.app.features.spaces.manage
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete import com.airbnb.mvrx.Incomplete
import im.vector.app.R
import im.vector.app.core.epoxy.errorWithRetryItem import im.vector.app.core.epoxy.errorWithRetryItem
import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.epoxy.loadingItem
import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.genericFooterItem
import im.vector.app.core.utils.DebouncedClickListener import im.vector.app.core.utils.DebouncedClickListener
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.model.RoomType
@ -31,7 +34,8 @@ import javax.inject.Inject
class SpaceManageRoomsController @Inject constructor( class SpaceManageRoomsController @Inject constructor(
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val errorFormatter: ErrorFormatter private val errorFormatter: ErrorFormatter,
private val stringProvider: StringProvider
) : TypedEpoxyController<SpaceManageRoomViewState>() { ) : TypedEpoxyController<SpaceManageRoomViewState>() {
interface Listener { interface Listener {
@ -67,17 +71,24 @@ class SpaceManageRoomsController @Inject constructor(
matchFilter.filter = data.currentFilter matchFilter.filter = data.currentFilter
val filteredResult = directChildren.filter { matchFilter.test(it) } val filteredResult = directChildren.filter { matchFilter.test(it) }
filteredResult.forEach { childInfo -> if (filteredResult.isEmpty()) {
roomManageSelectionItem { genericFooterItem {
id(childInfo.childRoomId) id("empty_result")
matrixItem(childInfo.toMatrixItem()) text(host.stringProvider.getString(R.string.no_result_placeholder))
avatarRenderer(host.avatarRenderer) }
suggested(childInfo.suggested ?: false) } else {
space(childInfo.roomType == RoomType.SPACE) filteredResult.forEach { childInfo ->
selected(data.selectedRooms.contains(childInfo.childRoomId)) roomManageSelectionItem {
itemClickListener(DebouncedClickListener({ id(childInfo.childRoomId)
host.listener?.toggleSelection(childInfo) matrixItem(childInfo.toMatrixItem())
})) avatarRenderer(host.avatarRenderer)
suggested(childInfo.suggested ?: false)
space(childInfo.roomType == RoomType.SPACE)
selected(data.selectedRooms.contains(childInfo.childRoomId))
itemClickListener(DebouncedClickListener({
host.listener?.toggleSelection(childInfo)
}))
}
} }
} }
} }

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="M21.5187,26.2723H25.8404L26.3357,21.6964H22.014L21.5187,26.2723Z"
android:fillColor="#C1C6CD"/>
<path
android:pathData="M44,24C44,35.0457 35.0457,44 24,44C12.9543,44 4,35.0457 4,24C4,12.9543 12.9543,4 24,4C35.0457,4 44,12.9543 44,24ZM21.0505,12.0116C22.1487,12.1305 22.9425,13.1171 22.8237,14.2152L22.4469,17.6964H26.7686L27.192,13.7848C27.3109,12.6866 28.2974,11.8928 29.3956,12.0116C30.4938,12.1305 31.2876,13.1171 31.1688,14.2152L30.792,17.6964H32.6C33.7046,17.6964 34.6,18.5918 34.6,19.6964C34.6,20.801 33.7046,21.6964 32.6,21.6964H30.3591L29.8638,26.2723H32.6C33.7046,26.2723 34.6,27.1677 34.6,28.2723C34.6,29.3769 33.7046,30.2723 32.6,30.2723H29.4308L29.0041,34.2152C28.8852,35.3134 27.8986,36.1072 26.8005,35.9884C25.7023,35.8695 24.9084,34.8829 25.0273,33.7848L25.4075,30.2723H21.0857L20.659,34.2152C20.5401,35.3134 19.5535,36.1072 18.4554,35.9884C17.3572,35.8695 16.5633,34.8829 16.6822,33.7848L17.0624,30.2723H15C13.8954,30.2723 13,29.3769 13,28.2723C13,27.1677 13.8954,26.2723 15,26.2723H17.4953L17.9906,21.6964H15.8784C14.7739,21.6964 13.8784,20.801 13.8784,19.6964C13.8784,18.5918 14.7739,17.6964 15.8784,17.6964H18.4235L18.8469,13.7848C18.9658,12.6866 19.9524,11.8928 21.0505,12.0116Z"
android:fillColor="#C1C6CD"
android:fillType="evenOdd"/>
</vector>

View File

@ -5,7 +5,7 @@
android:id="@+id/coordinatorLayout" android:id="@+id/coordinatorLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?riotx_header_panel_background"> android:background="?riotx_background">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<ImageView
android:id="@+id/emptyItemImageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginBottom="16dp"
android:tint="?riotx_reaction_background_off"
app:layout_constraintBottom_toTopOf="@id/emptyItemTitleView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_empty_space_explore" />
<TextView
android:id="@+id/emptyItemTitleView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="30dp"
android:gravity="center"
android:textColor="?riotx_text_primary"
android:textSize="15sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@id/emptyItemMessageView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/emptyItemImageView"
app:layout_constraintVertical_chainStyle="packed"
tools:text="@string/this_space_has_no_rooms" />
<TextView
android:id="@+id/emptyItemMessageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="20dp"
android:ellipsize="end"
android:gravity="center"
android:maxWidth="300dp"
android:maxLines="10"
android:textColor="?riotx_text_secondary"
android:textSize="14sp"
app:layout_constraintBottom_toTopOf="@+id/emptyItemButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/emptyItemTitleView"
tools:text="@string/this_space_has_no_rooms_admin" />
<com.google.android.material.button.MaterialButton
android:id="@+id/emptyItemButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="16dp"
android:minWidth="190dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/emptyItemMessageView"
tools:text="@string/space_add_existing_rooms" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/spaceAddRoom"
android:title="@string/space_add_existing_rooms"
app:showAsAction="never" />
<item
android:id="@+id/spaceCreateRoom"
android:title="@string/create_new_room"
app:iconTint="?attr/colorAccent"
app:showAsAction="never" />
</menu>

View File

@ -3356,6 +3356,7 @@
<string name="space_add_existing_rooms">Add existing rooms and space</string> <string name="space_add_existing_rooms">Add existing rooms and space</string>
<string name="space_add_rooms">Add rooms</string>
<string name="spaces_beta_welcome_to_spaces">Welcome to Spaces!</string> <string name="spaces_beta_welcome_to_spaces">Welcome to Spaces!</string>
<string name="spaces_beta_welcome_to_spaces_desc">Spaces are a new way to group rooms and people.</string> <string name="spaces_beta_welcome_to_spaces_desc">Spaces are a new way to group rooms and people.</string>
<string name="you_are_invited">You are invited</string> <string name="you_are_invited">You are invited</string>
@ -3377,5 +3378,10 @@
<string name="labs_space_show_orphan_in_home">Experimental Space - Only show orphans in Home</string> <string name="labs_space_show_orphan_in_home">Experimental Space - Only show orphans in Home</string>
<string name="spaces_feeling_experimental_subspace">Feeling experimental?\nYou can add existing spaces to a space.</string> <string name="spaces_feeling_experimental_subspace">Feeling experimental?\nYou can add existing spaces to a space.</string>
<string name="spaces_no_server_support_title">It looks like your homeserver does not support Spaces yet</string> <string name="spaces_no_server_support_title">It looks like your homeserver does not support Spaces yet</string>
<string name="spaces_no_server_support_description">Please contact your homserver admin for further information</string> <string name="spaces_no_server_support_description">Please contact your homeserver admin for further information</string>
<string name="this_space_has_no_rooms">This space has no rooms</string>
<string name="this_space_has_no_rooms_not_admin">Some rooms may be hidden because theyre private and you need an invite.\nYou dont have permission to add rooms.</string>
<string name="this_space_has_no_rooms_admin">Some rooms may be hidden because theyre private and you need an invite.</string>
</resources> </resources>