Room list: rework invitations

This commit is contained in:
Benoit Marty 2019-07-01 16:29:04 +02:00
parent b25098c52d
commit 07309c90e1
7 changed files with 238 additions and 9 deletions

View File

@ -29,7 +29,7 @@ import im.vector.riotredesign.features.themes.ThemeUtils
/** /**
* Set a text in the TextView, or set visibility to GONE if the text is null * Set a text in the TextView, or set visibility to GONE if the text is null
*/ */
fun TextView.setTextOrHide(newText: String?, hideWhenBlank: Boolean = true) { fun TextView.setTextOrHide(newText: CharSequence?, hideWhenBlank: Boolean = true) {
if (newText == null if (newText == null
|| (newText.isBlank() && hideWhenBlank)) { || (newText.isBlank() && hideWhenBlank)) {
isVisible = false isVisible = false

View File

@ -0,0 +1,64 @@
/*
* 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 android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
import im.vector.riotredesign.core.extensions.setTextOrHide
import im.vector.riotredesign.features.home.AvatarRenderer


@EpoxyModelClass(layout = R.layout.item_room_invitation)
abstract class RoomInvitationItem : VectorEpoxyModel<RoomInvitationItem.Holder>() {

@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute lateinit var roomName: CharSequence
@EpoxyAttribute lateinit var roomId: String
@EpoxyAttribute var secondLine: CharSequence? = null
@EpoxyAttribute var avatarUrl: String? = null
@EpoxyAttribute var listener: (() -> Unit)? = null
@EpoxyAttribute var acceptListener: (() -> Unit)? = null
@EpoxyAttribute var rejectListener: (() -> Unit)? = null


override fun bind(holder: Holder) {
super.bind(holder)
holder.rootView.setOnClickListener { listener?.invoke() }
holder.acceptView.setOnClickListener { acceptListener?.invoke() }
holder.rejectView.setOnClickListener { rejectListener?.invoke() }
holder.titleView.text = roomName
holder.subtitleView.setTextOrHide(secondLine)
avatarRenderer.render(avatarUrl, roomId, roomName.toString(), holder.avatarImageView)
}

class Holder : VectorEpoxyHolder() {
val titleView by bind<TextView>(R.id.roomInvitationNameView)
val subtitleView by bind<TextView>(R.id.roomInvitationSubTitle)
val acceptView by bind<Button>(R.id.roomInvitationAccept)
val rejectView by bind<Button>(R.id.roomInvitationReject)
val avatarImageView by bind<ImageView>(R.id.roomInvitationAvatarImageView)
val rootView by bind<ViewGroup>(R.id.itemRoomInvitationLayout)
}

}

View File

@ -45,7 +45,7 @@ data class RoomListParams(
) : Parcelable ) : Parcelable




class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Callback, OnBackPressed, FabMenuView.Listener { class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, OnBackPressed, FabMenuView.Listener {


enum class DisplayMode(@StringRes val titleRes: Int) { enum class DisplayMode(@StringRes val titleRes: Int) {
HOME(R.string.bottom_action_home), HOME(R.string.bottom_action_home),
@ -135,7 +135,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Callback, O
val stateRestorer = LayoutManagerStateRestorer(layoutManager).register() val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
epoxyRecyclerView.layoutManager = layoutManager epoxyRecyclerView.layoutManager = layoutManager
epoxyRecyclerView.itemAnimator = RoomListAnimator() epoxyRecyclerView.itemAnimator = RoomListAnimator()
roomController.callback = this roomController.listener = this
roomController.addModelBuildListener { it.dispatchTo(stateRestorer) } roomController.addModelBuildListener { it.dispatchTo(stateRestorer) }
stateView.contentView = epoxyRecyclerView stateView.contentView = epoxyRecyclerView
epoxyRecyclerView.setController(roomController) epoxyRecyclerView.setController(roomController)
@ -233,6 +233,14 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Callback, O
roomListViewModel.accept(RoomListActions.SelectRoom(room)) roomListViewModel.accept(RoomListActions.SelectRoom(room))
} }


override fun onAcceptRoomInvitation(room: RoomSummary) {
vectorBaseActivity.notImplemented("Accept room invitation")
}

override fun onRejectRoomInvitation(room: RoomSummary) {
vectorBaseActivity.notImplemented("Reject room invitation")
}

override fun onToggleRoomCategory(roomCategory: RoomCategory) { override fun onToggleRoomCategory(roomCategory: RoomCategory) {
roomListViewModel.accept(RoomListActions.ToggleCategory(roomCategory)) roomListViewModel.accept(RoomListActions.ToggleCategory(roomCategory))
} }

View File

@ -26,7 +26,7 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
private val roomSummaryItemFactory: RoomSummaryItemFactory private val roomSummaryItemFactory: RoomSummaryItemFactory
) : TypedEpoxyController<RoomListViewState>() { ) : TypedEpoxyController<RoomListViewState>() {


var callback: Callback? = null var listener: Listener? = null


override fun buildModels(viewState: RoomListViewState) { override fun buildModels(viewState: RoomListViewState) {
val roomSummaries = viewState.asyncFilteredRooms() val roomSummaries = viewState.asyncFilteredRooms()
@ -36,7 +36,7 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
} else { } else {
val isExpanded = viewState.isCategoryExpanded(category) val isExpanded = viewState.isCategoryExpanded(category)
buildRoomCategory(viewState, summaries, category.titleRes, viewState.isCategoryExpanded(category)) { buildRoomCategory(viewState, summaries, category.titleRes, viewState.isCategoryExpanded(category)) {
callback?.onToggleRoomCategory(category) listener?.onToggleRoomCategory(category)
} }
if (isExpanded) { if (isExpanded) {
buildRoomModels(summaries) buildRoomModels(summaries)
@ -76,14 +76,16 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
private fun buildRoomModels(summaries: List<RoomSummary>) { private fun buildRoomModels(summaries: List<RoomSummary>) {
summaries.forEach { roomSummary -> summaries.forEach { roomSummary ->
roomSummaryItemFactory roomSummaryItemFactory
.create(roomSummary) { callback?.onRoomSelected(it) } .create(roomSummary, listener)
.addTo(this) .addTo(this)
} }
} }


interface Callback { interface Listener {
fun onToggleRoomCategory(roomCategory: RoomCategory) fun onToggleRoomCategory(roomCategory: RoomCategory)
fun onRoomSelected(room: RoomSummary) fun onRoomSelected(room: RoomSummary)
fun onRejectRoomInvitation(room: RoomSummary)
fun onAcceptRoomInvitation(room: RoomSummary)
} }


} }

View File

@ -18,12 +18,15 @@ package im.vector.riotredesign.features.home.room.list


import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
import im.vector.riotredesign.core.extensions.localDateTime import im.vector.riotredesign.core.extensions.localDateTime
import im.vector.riotredesign.core.resources.ColorProvider import im.vector.riotredesign.core.resources.ColorProvider
import im.vector.riotredesign.core.resources.DateProvider import im.vector.riotredesign.core.resources.DateProvider
import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.home.AvatarRenderer import im.vector.riotredesign.features.home.AvatarRenderer
import im.vector.riotredesign.features.home.room.detail.timeline.format.NoticeEventFormatter import im.vector.riotredesign.features.home.room.detail.timeline.format.NoticeEventFormatter
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter
@ -34,9 +37,39 @@ import javax.inject.Inject
class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatter: NoticeEventFormatter, class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatter: NoticeEventFormatter,
private val timelineDateFormatter: TimelineDateFormatter, private val timelineDateFormatter: TimelineDateFormatter,
private val colorProvider: ColorProvider, private val colorProvider: ColorProvider,
private val stringProvider: StringProvider,
private val avatarRenderer: AvatarRenderer) { private val avatarRenderer: AvatarRenderer) {


fun create(roomSummary: RoomSummary, onRoomSelected: (RoomSummary) -> Unit): RoomSummaryItem { fun create(roomSummary: RoomSummary, listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> {
return when (roomSummary.membership) {
Membership.INVITE -> createInvitationItem(roomSummary, listener)
else -> createRoomItem(roomSummary, listener)

}
}

private fun createInvitationItem(roomSummary: RoomSummary, listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> {
val secondLine = if (roomSummary.isDirect) {
roomSummary.latestEvent?.root?.senderId
} else {
roomSummary.latestEvent?.root?.senderId?.let {
stringProvider.getString(R.string.invited_by, it)
}
}

return RoomInvitationItem_()
.id(roomSummary.roomId)
.avatarRenderer(avatarRenderer)
.roomId(roomSummary.roomId)
.secondLine(secondLine)
.acceptListener { listener?.onAcceptRoomInvitation(roomSummary) }
.rejectListener { listener?.onRejectRoomInvitation(roomSummary) }
.roomName(roomSummary.displayName)
.avatarUrl(roomSummary.avatarUrl)
.listener { listener?.onRoomSelected(roomSummary) }
}

private fun createRoomItem(roomSummary: RoomSummary, listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> {
val unreadCount = roomSummary.notificationCount val unreadCount = roomSummary.notificationCount
val showHighlighted = roomSummary.highlightCount > 0 val showHighlighted = roomSummary.highlightCount > 0


@ -84,6 +117,7 @@ class RoomSummaryItemFactory @Inject constructor(private val noticeEventFormatte
.avatarUrl(roomSummary.avatarUrl) .avatarUrl(roomSummary.avatarUrl)
.showHighlighted(showHighlighted) .showHighlighted(showHighlighted)
.unreadCount(unreadCount) .unreadCount(unreadCount)
.listener { onRoomSelected(roomSummary) } .listener { listener?.onRoomSelected(roomSummary) }
} }

} }

View File

@ -0,0 +1,111 @@
<?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:id="@+id/itemRoomInvitationLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?riotx_background"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground">

<ImageView
android:id="@+id/roomInvitationAvatarImageView"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="12dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />

<!-- Margin bottom does not work, so I use space -->
<Space
android:id="@+id/roomInvitationAvatarBottomSpace"
android:layout_width="0dp"
android:layout_height="12dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/roomInvitationAvatarImageView"
tools:layout_marginStart="20dp" />

<TextView
android:id="@+id/roomInvitationNameView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginLeft="@dimen/layout_horizontal_margin"
android:layout_marginTop="12dp"
android:layout_marginEnd="20dp"
android:layout_marginRight="20dp"
android:drawableEnd="@drawable/ic_arrow_right"
android:drawablePadding="8dp"
android:duplicateParentState="true"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?riotx_text_primary"
android:textSize="15sp"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@id/roomInvitationAvatarImageView"
app:layout_constraintTop_toTopOf="parent"
tools:text="@sample/matrix.json/data/displayName" />

<TextView
android:id="@+id/roomInvitationSubTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:ellipsize="end"
android:maxLines="2"
android:textColor="?riotx_text_secondary"
android:textSize="15sp"
app:layout_constraintEnd_toEndOf="@+id/roomInvitationNameView"
app:layout_constraintStart_toStartOf="@+id/roomInvitationNameView"
app:layout_constraintTop_toBottomOf="@+id/roomInvitationNameView"
tools:text="@sample/matrix.json/data/message" />

<!-- Margin bottom does not work, so I use space -->
<Space
android:id="@+id/roomLastEventBottomSpace"
android:layout_width="0dp"
android:layout_height="7dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/roomInvitationSubTitle"
tools:layout_marginStart="120dp" />

<com.google.android.material.button.MaterialButton
android:id="@+id/roomInvitationAccept"
style="@style/VectorButtonStyle"
android:layout_marginTop="8dp"
android:minWidth="122dp"
android:text="@string/accept"
app:layout_constraintEnd_toEndOf="@+id/roomInvitationNameView"
app:layout_constraintTop_toBottomOf="@+id/roomLastEventBottomSpace" />

<com.google.android.material.button.MaterialButton
android:id="@+id/roomInvitationReject"
style="@style/VectorButtonStyleOutlined"
android:layout_marginEnd="@dimen/layout_vertical_margin"
android:minWidth="122dp"
android:text="@string/reject"
android:textAllCaps="true"
android:textColor="?riotx_text_primary"
app:layout_constraintEnd_toStartOf="@+id/roomInvitationAccept"
app:layout_constraintTop_toTopOf="@+id/roomInvitationAccept" />

<View
android:id="@+id/roomInvitationDividerView"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_marginTop="5dp"
android:background="?riotx_header_panel_border_mobile"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/roomInvitationAccept" />

</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -142,6 +142,16 @@
<item name="colorControlHighlight">?colorAccent</item> <item name="colorControlHighlight">?colorAccent</item>
</style> </style>


<style name="VectorButtonStyleOutlined" parent="Widget.MaterialComponents.Button.OutlinedButton">
<item name="android:textStyle">bold</item>
<item name="android:textAllCaps">false</item>
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:background">@null</item>
<!--item name="android:textColor">?colorAccent</item-->
<item name="colorControlHighlight">?colorAccent</item>
</style>

<style name="AlerterButton" parent="Widget.AppCompat.Button.Borderless.Colored"> <style name="AlerterButton" parent="Widget.AppCompat.Button.Borderless.Colored">
<item name="colorAccent">@android:color/white</item> <item name="colorAccent">@android:color/white</item>
<item name="android:textColor">@android:color/white</item> <item name="android:textColor">@android:color/white</item>