Merge pull request #3342 from vector-im/feature/bca/spaces_fix_3327

Fix empty states for spaces
This commit is contained in:
Benoit Marty 2021-05-21 14:47:41 +02:00 committed by GitHub
commit dcf76bf6ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 307 additions and 19 deletions

View File

@ -13,6 +13,7 @@ Bugfix 🐛:
- Fix a problem with database migration on nightly builds (#3335)
- Implement a workaround to render <del> and <u> in the timeline (#1817)
- Make sure the SDK can retrieve the secret storage if the system is upgraded (#3304)
- #+ button on lower right when looking at an empty space goes to an empty 'Explore rooms' (#3327)
Translations 🗣:
-

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.resources.ColorProvider
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.features.home.AvatarRenderer
import im.vector.app.features.home.room.list.spaceChildInfoItem
@ -50,6 +51,7 @@ class SpaceDirectoryController @Inject constructor(
fun onSpaceChildClick(spaceChildInfo: SpaceChildInfo)
fun onRoomClick(spaceChildInfo: SpaceChildInfo)
fun retry()
fun addExistingRooms(spaceId: String)
}
var listener: InteractionListener? = null
@ -97,9 +99,23 @@ class SpaceDirectoryController @Inject constructor(
?: emptyList()
if (flattenChildInfo.isEmpty()) {
genericFooterItem {
id("empty_footer")
host.stringProvider.getString(R.string.no_result_placeholder)
genericEmptyWithActionItem {
id("empty_res")
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 {
flattenChildInfo.forEach { info ->

View File

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

View File

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

View File

@ -28,12 +28,15 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
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.RoomType
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.rx.rx
import timber.log.Timber
@ -70,6 +73,23 @@ class SpaceDirectoryViewModel @AssistedInject constructor(
refreshFromApi()
observeJoinedRooms()
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() {

View File

@ -19,9 +19,12 @@ package im.vector.app.features.spaces.manage
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete
import im.vector.app.R
import im.vector.app.core.epoxy.errorWithRetryItem
import im.vector.app.core.epoxy.loadingItem
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.features.home.AvatarRenderer
import org.matrix.android.sdk.api.session.room.model.RoomType
@ -31,7 +34,8 @@ import javax.inject.Inject
class SpaceManageRoomsController @Inject constructor(
private val avatarRenderer: AvatarRenderer,
private val errorFormatter: ErrorFormatter
private val errorFormatter: ErrorFormatter,
private val stringProvider: StringProvider
) : TypedEpoxyController<SpaceManageRoomViewState>() {
interface Listener {
@ -67,17 +71,24 @@ class SpaceManageRoomsController @Inject constructor(
matchFilter.filter = data.currentFilter
val filteredResult = directChildren.filter { matchFilter.test(it) }
filteredResult.forEach { childInfo ->
roomManageSelectionItem {
id(childInfo.childRoomId)
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)
}))
if (filteredResult.isEmpty()) {
genericFooterItem {
id("empty_result")
text(host.stringProvider.getString(R.string.no_result_placeholder))
}
} else {
filteredResult.forEach { childInfo ->
roomManageSelectionItem {
id(childInfo.childRoomId)
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:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?riotx_header_panel_background">
android:background="?riotx_background">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"

View File

@ -0,0 +1,72 @@
<?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:importantForAccessibility="no"
app:layout_constraintBottom_toTopOf="@id/emptyItemTitleView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="?riotx_reaction_background_on"
tools:ignore="MissingPrefix"
tools:src="@drawable/ic_empty_icon_room" />
<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_rooms">Add rooms</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="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="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_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>