Home: continue room list rework.

This commit is contained in:
ganfra 2019-05-17 17:07:02 +02:00 committed by Benoit Marty
parent c0fd06fd2d
commit eb2344a43f
12 changed files with 165 additions and 82 deletions

View file

@ -17,9 +17,9 @@
package im.vector.riotredesign.core.platform package im.vector.riotredesign.core.platform
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import im.vector.riotredesign.R import im.vector.riotredesign.R
import kotlinx.android.synthetic.main.view_state.view.* import kotlinx.android.synthetic.main.view_state.view.*
@ -30,7 +30,7 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
sealed class State { sealed class State {
object Content : State() object Content : State()
object Loading : State() object Loading : State()
data class Empty(val message: CharSequence? = null) : State() data class Empty(val title: CharSequence? = null, val image: Drawable? = null, val message: CharSequence? = null) : State()
data class Error(val message: CharSequence? = null) : State() data class Error(val message: CharSequence? = null) : State()
} }
@ -52,7 +52,7 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
init { init {
View.inflate(context, R.layout.view_state, this) View.inflate(context, R.layout.view_state, this)
layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) layoutParams = LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
errorRetryView.setOnClickListener { errorRetryView.setOnClickListener {
eventCallback?.onRetryClicked() eventCallback?.onRetryClicked()
} }
@ -74,16 +74,18 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
emptyView.visibility = View.INVISIBLE emptyView.visibility = View.INVISIBLE
contentView?.visibility = View.INVISIBLE contentView?.visibility = View.INVISIBLE
} }
is StateView.State.Empty -> { is StateView.State.Empty -> {
progressBar.visibility = View.INVISIBLE progressBar.visibility = View.INVISIBLE
errorView.visibility = View.INVISIBLE errorView.visibility = View.INVISIBLE
emptyView.visibility = View.VISIBLE emptyView.visibility = View.VISIBLE
emptyImageView.setImageDrawable(newState.image)
emptyMessageView.text = newState.message emptyMessageView.text = newState.message
emptyTitleView.text = newState.title
if (contentView != null) { if (contentView != null) {
contentView!!.visibility = View.INVISIBLE contentView!!.visibility = View.INVISIBLE
} }
} }
is StateView.State.Error -> { is StateView.State.Error -> {
progressBar.visibility = View.INVISIBLE progressBar.visibility = View.INVISIBLE
errorView.visibility = View.VISIBLE errorView.visibility = View.VISIBLE
emptyView.visibility = View.INVISIBLE emptyView.visibility = View.INVISIBLE

View file

@ -85,12 +85,16 @@ class GroupListViewModel(initialState: GroupListViewState,
session session
.rx().liveGroupSummaries() .rx().liveGroupSummaries()
.map { .map {
val myUser = session.getUser(session.sessionParams.credentials.userId) if (it.isEmpty()) {
val allCommunityGroup = GroupSummary( it
groupId = ALL_COMMUNITIES_GROUP_ID, } else {
displayName = "All Communities", val myUser = session.getUser(session.sessionParams.credentials.userId)
avatarUrl = myUser?.avatarUrl ?: "") val allCommunityGroup = GroupSummary(
listOf(allCommunityGroup) + it groupId = ALL_COMMUNITIES_GROUP_ID,
displayName = "All Communities",
avatarUrl = myUser?.avatarUrl ?: "")
listOf(allCommunityGroup) + it
}
} }
.execute { async -> .execute { async ->
val newSelectedGroup = selectedGroup ?: async()?.firstOrNull() val newSelectedGroup = selectedGroup ?: async()?.firstOrNull()

View file

@ -0,0 +1,32 @@
/*
* 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.riotredesign.features.home.room.list
import androidx.recyclerview.widget.DefaultItemAnimator
private const val ANIM_DURATION_IN_MILLIS = 100L
class RoomListAnimator : DefaultItemAnimator() {
init {
addDuration = ANIM_DURATION_IN_MILLIS
removeDuration = ANIM_DURATION_IN_MILLIS
moveDuration = 0
changeDuration = 0
}
}

View file

@ -91,6 +91,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Callback {
val layoutManager = LinearLayoutManager(context) val layoutManager = LinearLayoutManager(context)
val stateRestorer = LayoutManagerStateRestorer(layoutManager).register() val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
epoxyRecyclerView.layoutManager = layoutManager epoxyRecyclerView.layoutManager = layoutManager
epoxyRecyclerView.itemAnimator = RoomListAnimator()
roomController.callback = this roomController.callback = this
roomController.addModelBuildListener { it.dispatchTo(stateRestorer) } roomController.addModelBuildListener { it.dispatchTo(stateRestorer) }
stateView.contentView = epoxyRecyclerView stateView.contentView = epoxyRecyclerView
@ -98,22 +99,40 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Callback {
} }
private fun renderState(state: RoomListViewState) { private fun renderState(state: RoomListViewState) {
when (state.asyncRooms) { when (state.asyncFilteredRooms) {
is Incomplete -> renderLoading() is Incomplete -> renderLoading()
is Success -> renderSuccess(state) is Success -> renderSuccess(state)
is Fail -> renderFailure(state.asyncRooms.error) is Fail -> renderFailure(state.asyncFilteredRooms.error)
} }
} }
private fun renderSuccess(state: RoomListViewState) { private fun renderSuccess(state: RoomListViewState) {
if (state.asyncRooms().isNullOrEmpty()) { val allRooms = state.asyncRooms()
stateView.state = StateView.State.Empty(getString(R.string.room_list_empty)) val filteredRooms = state.asyncFilteredRooms()
if (filteredRooms.isNullOrEmpty()) {
renderEmptyState(allRooms)
} else { } else {
stateView.state = StateView.State.Content stateView.state = StateView.State.Content
} }
roomController.setData(state) roomController.setData(state)
} }
private fun renderEmptyState(allRooms: List<RoomSummary>?) {
val hasNoRoom = allRooms.isNullOrEmpty()
val emptyState = when (roomListParams.displayMode) {
DisplayMode.HOME -> {
if (hasNoRoom) {
StateView.State.Empty(getString(R.string.room_list_catchup_welcome_title), null, getString(R.string.room_list_catchup_welcome_body))
} else {
StateView.State.Empty(getString(R.string.room_list_catchup_empty_title), null, getString(R.string.room_list_catchup_empty_body))
}
}
DisplayMode.PEOPLE -> StateView.State.Empty()
DisplayMode.ROOMS -> StateView.State.Empty()
}
stateView.state = emptyState
}
private fun renderLoading() { private fun renderLoading() {
stateView.state = StateView.State.Loading stateView.state = StateView.State.Loading
} }

View file

@ -87,6 +87,12 @@ class RoomListViewModel(initialState: RoomListViewState,
private fun observeRoomSummaries() { private fun observeRoomSummaries() {
homeRoomListObservableSource
.observe()
.execute { asyncRooms ->
copy(asyncRooms = asyncRooms)
}
homeRoomListObservableSource homeRoomListObservableSource
.observe() .observe()
.flatMapSingle { .flatMapSingle {
@ -96,7 +102,7 @@ class RoomListViewModel(initialState: RoomListViewState,
} }
.map { buildRoomSummaries(it) } .map { buildRoomSummaries(it) }
.execute { async -> .execute { async ->
copy(asyncRooms = async) copy(asyncFilteredRooms = async)
} }
} }

View file

@ -17,15 +17,12 @@
package im.vector.riotredesign.features.home.room.list package im.vector.riotredesign.features.home.room.list
import androidx.annotation.StringRes import androidx.annotation.StringRes
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotredesign.R import im.vector.riotredesign.R
data class RoomListViewState( data class RoomListViewState(
val displayMode: RoomListFragment.DisplayMode, val displayMode: RoomListFragment.DisplayMode,
val asyncRooms: Async<RoomSummaries> = Uninitialized,
val isInviteExpanded: Boolean = true, val isInviteExpanded: Boolean = true,
val isFavouriteRoomsExpanded: Boolean = true, val isFavouriteRoomsExpanded: Boolean = true,
val isDirectRoomsExpanded: Boolean = true, val isDirectRoomsExpanded: Boolean = true,
@ -71,5 +68,5 @@ enum class RoomCategory(@StringRes val titleRes: Int) {
} }
fun RoomSummaries?.isNullOrEmpty(): Boolean { fun RoomSummaries?.isNullOrEmpty(): Boolean {
return this == null || isEmpty() return this == null || this.values.flatten().isEmpty()
} }

View file

@ -31,7 +31,7 @@ class RoomSummaryController(private val stringProvider: StringProvider,
var callback: Callback? = null var callback: Callback? = null
override fun buildModels(viewState: RoomListViewState) { override fun buildModels(viewState: RoomListViewState) {
val roomSummaries = viewState.asyncRooms() val roomSummaries = viewState.asyncFilteredRooms()
roomSummaries?.forEach { (category, summaries) -> roomSummaries?.forEach { (category, summaries) ->
if (summaries.isEmpty()) { if (summaries.isEmpty()) {
return@forEach return@forEach

View file

@ -46,11 +46,13 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
holder.titleView.text = roomName holder.titleView.text = roomName
holder.lastEventTimeView.text = lastEventTime holder.lastEventTimeView.text = lastEventTime
holder.lastEventView.text = lastFormattedEvent holder.lastEventView.text = lastFormattedEvent
holder.unreadCounterBadgeView.render(unreadCount, showHighlighted)
AvatarRenderer.render(avatarUrl, roomId, roomName.toString(), holder.avatarImageView) AvatarRenderer.render(avatarUrl, roomId, roomName.toString(), holder.avatarImageView)
} }
class Holder : VectorEpoxyHolder() { class Holder : VectorEpoxyHolder() {
val titleView by bind<TextView>(R.id.roomNameView) val titleView by bind<TextView>(R.id.roomNameView)
val unreadCounterBadgeView by bind<UnreadCounterBadgeView>(R.id.roomUnreadCounterBadgeView)
val lastEventView by bind<TextView>(R.id.roomLastEventView) val lastEventView by bind<TextView>(R.id.roomLastEventView)
val lastEventTimeView by bind<TextView>(R.id.roomLastEventTimeView) val lastEventTimeView by bind<TextView>(R.id.roomLastEventTimeView)
val avatarImageView by bind<ImageView>(R.id.roomAvatarImageView) val avatarImageView by bind<ImageView>(R.id.roomAvatarImageView)

View file

@ -9,12 +9,12 @@
android:background="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:paddingBottom="16dp"
android:paddingTop="16dp"
android:paddingStart="8dp" android:paddingStart="8dp"
android:paddingLeft="8dp" android:paddingLeft="8dp"
android:paddingTop="8dp"
android:paddingEnd="16dp" android:paddingEnd="16dp"
android:paddingRight="16dp"> android:paddingRight="16dp"
android:paddingBottom="8dp">
<ImageView <ImageView
android:id="@+id/roomAvatarImageView" android:id="@+id/roomAvatarImageView"
@ -29,30 +29,41 @@
<TextView <TextView
android:id="@+id/roomNameView" android:id="@+id/roomNameView"
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="8dp"
android:duplicateParentState="true" android:duplicateParentState="true"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/black_87" android:textColor="@color/black_87"
android:textSize="14sp" android:textSize="14sp"
app:layout_constraintEnd_toStartOf="@+id/roomLastEventTimeView" app:layout_constrainedWidth="true"
app:layout_constraintEnd_toStartOf="@+id/roomUnreadCounterBadgeView"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@id/roomAvatarImageView" app:layout_constraintStart_toEndOf="@id/roomAvatarImageView"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="@+id/roomAvatarImageView"
tools:text="@tools:sample/full_names" /> tools:text="@tools:sample/full_names" />
<TextView
android:id="@+id/roomLastEventView" <im.vector.riotredesign.features.home.room.list.UnreadCounterBadgeView
android:layout_width="0dp" android:id="@+id/roomUnreadCounterBadgeView"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textColor="@color/black_38" android:layout_marginRight="8dp"
android:textSize="14sp" android:gravity="center"
android:maxLines="2" android:minWidth="16dp"
android:ellipsize="end" android:minHeight="16dp"
app:layout_constraintEnd_toEndOf="parent" android:paddingLeft="4dp"
app:layout_constraintStart_toStartOf="@+id/roomNameView" android:paddingRight="4dp"
app:layout_constraintTop_toBottomOf="@+id/roomNameView" android:textColor="@android:color/white"
tools:text="@tools:sample/lorem/random" /> android:textSize="10sp"
app:layout_constraintEnd_toStartOf="@+id/roomLastEventTimeView"
app:layout_constraintStart_toEndOf="@+id/roomNameView"
app:layout_constraintTop_toTopOf="@+id/roomAvatarImageView"
tools:background="@drawable/bg_unread_highlight"
tools:text="4" />
<TextView <TextView
android:id="@+id/roomLastEventTimeView" android:id="@+id/roomLastEventTimeView"
@ -63,9 +74,23 @@
android:textColor="@color/black_38" android:textColor="@color/black_38"
android:textSize="12sp" android:textSize="12sp"
app:layout_constraintBaseline_toBaselineOf="@id/messageMemberNameView" app:layout_constraintBaseline_toBaselineOf="@id/messageMemberNameView"
app:layout_constraintTop_toTopOf="@+id/roomAvatarImageView"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/messageMemberNameView"
tools:text="@tools:sample/date/hhmm" /> tools:text="@tools:sample/date/hhmm" />
<TextView
android:id="@+id/roomLastEventView"
android:layout_width="0dp"
android:layout_height="34dp"
android:ellipsize="end"
android:maxLines="2"
android:textColor="@color/black_38"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/roomNameView"
app:layout_constraintTop_toBottomOf="@+id/roomNameView"
tools:text="@tools:sample/lorem/random" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout 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" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/roomCategoryRootView" android:id="@+id/roomCategoryRootView"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -9,6 +8,7 @@
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="16dp" android:paddingStart="16dp"
android:paddingLeft="16dp" android:paddingLeft="16dp"
android:paddingTop="8dp" android:paddingTop="8dp"
@ -19,49 +19,32 @@
<TextView <TextView
android:id="@+id/roomCategoryTitleView" android:id="@+id/roomCategoryTitleView"
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="0dp" android:layout_height="wrap_content"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:layout_marginRight="8dp" android:layout_marginRight="8dp"
android:drawableStart="@drawable/ic_expand_more_white" android:drawableStart="@drawable/ic_expand_more_white"
android:drawableLeft="@drawable/ic_expand_more_white" android:drawableLeft="@drawable/ic_expand_more_white"
android:drawableTint="@color/bluey_grey_two" android:drawableTint="@color/bluey_grey_two"
android:ellipsize="end"
android:gravity="center_vertical" android:gravity="center_vertical"
android:maxLines="1"
android:textColor="@color/bluey_grey_two" android:textColor="@color/bluey_grey_two"
app:layout_constraintBottom_toBottomOf="parent" tools:text="@string/room_recents_favourites" />
app:layout_constraintEnd_toStartOf="@+id/roomCategoryUnreadCounterBadgeView"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/direct_chats_header" />
<im.vector.riotredesign.features.home.room.list.UnreadCounterBadgeView <im.vector.riotredesign.features.home.room.list.UnreadCounterBadgeView
android:id="@+id/roomCategoryUnreadCounterBadgeView" android:id="@+id/roomCategoryUnreadCounterBadgeView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center" android:gravity="center"
android:minWidth="24dp" android:minWidth="16dp"
android:minHeight="24dp" android:minHeight="16dp"
android:paddingLeft="4dp"
android:paddingRight="4dp" android:paddingRight="4dp"
android:paddingLeft="4dp"
android:textSize="10sp"
android:textColor="@android:color/white" android:textColor="@android:color/white"
android:textSize="12sp" tools:background="@drawable/bg_unread_highlight"
app:layout_constraintBottom_toBottomOf="parent" tools:text="24" />
app:layout_constraintEnd_toStartOf="@+id/roomCategoryAddButton"
app:layout_constraintTop_toTopOf="parent"
tools:background="@drawable/bg_unread_highlight"
tools:text="4" />
<ImageView
android:id="@+id/roomCategoryAddButton"
android:layout_width="40dp"
android:layout_height="40dp"
android:scaleType="centerInside"
android:src="@drawable/ic_add_circle_white"
android:tint="@color/bluey_grey_two"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </LinearLayout>

View file

@ -2,8 +2,7 @@
<merge xmlns:android="http://schemas.android.com/apk/res/android" <merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:minHeight="40dp"
android:padding="8dp" android:padding="8dp"
tools:parentTag="android.widget.FrameLayout"> tools:parentTag="android.widget.FrameLayout">
@ -11,13 +10,14 @@
android:id="@+id/progressBar" android:id="@+id/progressBar"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" /> android:layout_gravity="center" />
<LinearLayout <LinearLayout
android:id="@+id/errorView" android:id="@+id/errorView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical" android:orientation="vertical"
android:padding="8dp"> android:padding="8dp">
@ -48,9 +48,26 @@
android:id="@+id/emptyView" android:id="@+id/emptyView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical" android:orientation="vertical"
android:padding="8dp"> android:padding="8dp">
<TextView
android:id="@+id/emptyTitleView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginBottom="40dp"
android:gravity="center"
android:textColor="@android:color/black"
android:textSize="16sp" />
<ImageView
android:id="@+id/emptyImageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="40dp" />
<TextView <TextView
android:id="@+id/emptyMessageView" android:id="@+id/emptyMessageView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -60,14 +77,6 @@
android:textColor="@android:color/black" android:textColor="@android:color/black"
android:textSize="16sp" /> android:textSize="16sp" />
<ImageView
android:id="@+id/emptyImageView"
android:layout_width="190dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="8dp" />
</LinearLayout> </LinearLayout>
</merge> </merge>

View file

@ -7,6 +7,10 @@
<string name="send_you_invite">"Sent you an invitation"</string> <string name="send_you_invite">"Sent you an invitation"</string>
<string name="invited_by">Invited by %s</string> <string name="invited_by">Invited by %s</string>
<string name="room_list_catchup_empty_title">Youre all caught up!</string>
<string name="room_list_catchup_empty_body">You have no more unread messages</string>
<string name="room_list_catchup_welcome_title">Welcome home!</string>
<string name="room_list_catchup_welcome_body">Catch up on unread messages here</string>
<string name="title_activity_emoji_reaction_picker">Reactions</string> <string name="title_activity_emoji_reaction_picker">Reactions</string>
<string name="reactions_agree">Agree</string> <string name="reactions_agree">Agree</string>